diff --git a/packages/liquid-html-parser/src/tags/block.test.ts b/packages/liquid-html-parser/src/tags/block.test.ts
index 254398a58..f17dba8e9 100644
--- a/packages/liquid-html-parser/src/tags/block.test.ts
+++ b/packages/liquid-html-parser/src/tags/block.test.ts
@@ -218,6 +218,33 @@ describe('blockTag', () => {
);
});
+ it('parses a canonical app block path', () => {
+ const path =
+ 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000';
+ const result = blockTag.parse('block', parser(`'${path}'`), stubParser);
+ expect(result.name.value).toBe(path);
+ });
+
+ it('rejects malformed app block paths', () => {
+ const uuid = '00000000-0000-4000-8000-000000000000';
+ const invalidPaths = [
+ `shopify://apps/example_app/snippets/example-block/${uuid}`,
+ `shopify://apps//blocks/example-block/${uuid}`,
+ `shopify://apps/example_app/blocks//${uuid}`,
+ 'shopify://apps/example_app/blocks/example-block/not-a-uuid',
+ 'shopify://apps/example_app/blocks/example-block/deadbeef',
+ `shopify://apps/example_app/blocks/example-block/${uuid}/extra`,
+ `shopify://apps-evil/example_app/blocks/example-block/${uuid}`,
+ `shopify://apps/example_app/blocks/example-block/${uuid}\n`,
+ ];
+
+ for (const path of invalidPaths) {
+ expect(() => blockTag.parse('block', parser(`'${path}'`), stubParser)).toThrow(
+ `in 'block' - '${path}' is not a valid block type`,
+ );
+ }
+ });
+
it('rejects markup without a comma before args', () => {
expect(() => blockTag.parse('block', parser("'name' key: value"), stubParser)).toThrow(
"Unexpected token in 'block' tag: key",
diff --git a/packages/liquid-html-parser/src/tags/block.ts b/packages/liquid-html-parser/src/tags/block.ts
index d57ed52e9..e9232d026 100644
--- a/packages/liquid-html-parser/src/tags/block.ts
+++ b/packages/liquid-html-parser/src/tags/block.ts
@@ -5,6 +5,9 @@ import { NodeTypes } from '../types';
import { TagKind, type TagDefinitionBlock, type Parser } from '../tag-definitions';
const BLOCK_TYPE_REGEX = /^_?[a-zA-Z0-9][\w-]*$/;
+// Canonical app block paths include app and block handles followed by a UUID.
+const APP_BLOCK_TYPE_REGEX =
+ /^shopify:\/\/apps\/[A-Za-z0-9][A-Za-z0-9_-]*\/blocks\/[A-Za-z0-9][A-Za-z0-9_-]*\/[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$/;
export const blockTag: TagDefinitionBlock = {
kind: TagKind.Block,
@@ -18,7 +21,7 @@ export const blockTag: TagDefinitionBlock = {
if (name.type !== NodeTypes.String) {
throw new Error("in 'block' - file name must be a string literal");
}
- if (!BLOCK_TYPE_REGEX.test(name.value)) {
+ if (!BLOCK_TYPE_REGEX.test(name.value) && !APP_BLOCK_TYPE_REGEX.test(name.value)) {
throw new Error(`in 'block' - '${name.value}' is not a valid block type`);
}
diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts
index 0858f1b35..55e99a452 100644
--- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts
+++ b/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts
@@ -104,6 +104,14 @@ describe('Module: LiquidHTMLSyntaxError', () => {
expect(offenses).to.be.empty;
});
+ it('should not report canonical inline app block paths', async () => {
+ const sourceCode =
+ "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}";
+
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
+ expect(offenses).to.be.empty;
+ });
+
it('should highligh the error', async () => {
let offenses: Offense[];
let highlights: string[];
diff --git a/packages/theme-check-common/src/checks/liquid-syntax-error/block.ts b/packages/theme-check-common/src/checks/liquid-syntax-error/block.ts
index fec5688e4..426d9614d 100644
--- a/packages/theme-check-common/src/checks/liquid-syntax-error/block.ts
+++ b/packages/theme-check-common/src/checks/liquid-syntax-error/block.ts
@@ -26,11 +26,6 @@ export function checkBlockTag(node: LiquidTag, context: Context): void {
const markup = node.markup as BlockMarkup;
- if (hasInvalidBlockName(markup.name.value)) {
- report(node, context, "Liquid syntax error: in 'block' - Valid syntax: block '[file_name]'");
- return;
- }
-
if (hasInvalidBlockArguments(markup)) {
report(node, context, SYNTAX_ERROR);
return;
@@ -75,10 +70,6 @@ export function checkBlockParserError(error: Error, context: Context, source: st
});
}
-function hasInvalidBlockName(value: string): boolean {
- return value.includes('/') || value.includes('.');
-}
-
function hasInvalidBlockArguments(markup: BlockMarkup): boolean {
return markup.args.some((arg) => {
if (arg.name === 'block.content') return false;
diff --git a/packages/theme-check-common/src/checks/liquid-syntax-error/index.spec.ts b/packages/theme-check-common/src/checks/liquid-syntax-error/index.spec.ts
index c210893d8..0a508d809 100644
--- a/packages/theme-check-common/src/checks/liquid-syntax-error/index.spec.ts
+++ b/packages/theme-check-common/src/checks/liquid-syntax-error/index.spec.ts
@@ -802,6 +802,34 @@ describe('LiquidSyntaxError', () => {
});
});
+ describe('block tags', () => {
+ it('produces no diagnostics for canonical app block paths', async () => {
+ const offenses = await runLiquidCheck(
+ LiquidSyntaxError,
+ "{% block 'shopify://apps/example_app/blocks/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}",
+ 'snippets/test.liquid',
+ NO_DOCSET,
+ );
+
+ expect(offenses).toEqual([]);
+ });
+
+ it('reports LiquidSyntaxError for malformed app block paths', async () => {
+ const offenses = await runLiquidCheck(
+ LiquidSyntaxError,
+ "{% block 'shopify://apps/example_app/snippets/example-block/00000000-0000-4000-8000-000000000000' %}{% endblock %}",
+ 'snippets/test.liquid',
+ NO_DOCSET,
+ );
+
+ expect(offenses).toHaveLength(1);
+ expect(offenses[0]).toMatchObject({
+ check: 'LiquidSyntaxError',
+ message: "Syntax error in 'block' tag",
+ });
+ });
+ });
+
describe('block and partial parser errors', () => {
it('locates line-mode block tags inside liquid blocks', () => {
expect(liquidLineTagLocation("{% liquid\n block 'hero'\n%}", 'block')).toEqual([12, 24]);