From 850441dd61d1fb69c4a0a1707bd83d05bda52f24 Mon Sep 17 00:00:00 2001 From: Francis Beaudoin Date: Tue, 8 Sep 2026 18:13:07 -0400 Subject: [PATCH] Allow static asset script URLs in app doctor --- .changeset/quiet-assets-repair.md | 5 +++++ .../services/app-doctor-engine/rules/liquid-rules.ts | 9 +++++++++ .../app-doctor-engine/tests/rule-analysis.test.ts | 10 +++++++++- 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .changeset/quiet-assets-repair.md diff --git a/.changeset/quiet-assets-repair.md b/.changeset/quiet-assets-repair.md new file mode 100644 index 00000000000..b2fc82bd864 --- /dev/null +++ b/.changeset/quiet-assets-repair.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Avoid flagging static theme asset script tags in app doctor. diff --git a/packages/app/src/cli/services/app-doctor-engine/rules/liquid-rules.ts b/packages/app/src/cli/services/app-doctor-engine/rules/liquid-rules.ts index 9a3840c7b0c..22865b74c1f 100644 --- a/packages/app/src/cli/services/app-doctor-engine/rules/liquid-rules.ts +++ b/packages/app/src/cli/services/app-doctor-engine/rules/liquid-rules.ts @@ -66,6 +66,7 @@ function liquidExecutableContextVisitor(file: SourceFile) { const context = liquidOutputContext(ancestors) if (context !== 'javascript' && context !== 'executable_attribute') return undefined if (context === 'javascript' && filterNames(node).includes('json')) return undefined + if (isStaticScriptAssetUrl(node, ancestors)) return undefined return [ makeLiquidIssue( 'UNSAFE_INNERHTML', @@ -118,6 +119,14 @@ function attributeName(attribute: AttributeNode): string { return attribute.name.map((part) => ('value' in part && typeof part.value === 'string' ? part.value : '')).join('') } +function isStaticScriptAssetUrl(node: LiquidVariableOutput, ancestors: LiquidHtmlNode[]): boolean { + const script = ancestors.find((ancestor) => ancestor.type === 'HtmlRawNode' && ancestor.name === 'script') + const attribute = ancestors.find((ancestor): ancestor is AttributeNode => ancestor.type.startsWith('Attr')) + if (!script || !attribute || attributeName(attribute).toLowerCase() !== 'src') return false + + return /^(['"])(?:\\.|(?!\1)[^\\\n])+\1\s*\|\s*asset_url\s*$/.test(outputSource(node).trim()) +} + function outputSource(node: LiquidVariableOutput): string { return typeof node.markup === 'string' ? node.markup : node.markup.rawSource } diff --git a/packages/app/src/cli/services/app-doctor-engine/tests/rule-analysis.test.ts b/packages/app/src/cli/services/app-doctor-engine/tests/rule-analysis.test.ts index 9ffd21b8913..8c5eb680125 100644 --- a/packages/app/src/cli/services/app-doctor-engine/tests/rule-analysis.test.ts +++ b/packages/app/src/cli/services/app-doctor-engine/tests/rule-analysis.test.ts @@ -707,8 +707,16 @@ describe('Liquid public AST analysis', () => { ]) expect(ordinary.issues).toEqual([]) + const staticAsset = scanLiquidSecurity([ + source('', 'extensions/theme/blocks/chat.liquid'), + ]) + expect(staticAsset.issues).toEqual([]) + const script = scanLiquidSecurity([ - source('', 'extensions/theme/blocks/script.liquid'), + source( + '', + 'extensions/theme/blocks/script.liquid', + ), ]) expect(script.issues.map((finding) => finding.id)).toEqual(['LIQUID_UNSAFE_RENDER', 'UNSAFE_INNERHTML']) })