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]);