td-ai-tools 1.2.5 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/skills/README.md +1 -1
  3. package/skills/shopify-lint/SKILL.md +4 -4
  4. package/skills/shopify-lint/agents/openai.yaml +1 -1
  5. package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
  6. package/skills/shopify-lint/scripts/setup.sh +19 -9
  7. package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
  8. package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +16 -1
  9. package/skills/shopify-lint/theme-check-theory/README.md +95 -18
  10. package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +16 -1
  11. package/skills/shopify-lint/theme-check-theory/package-lock.json +2 -2
  12. package/skills/shopify-lint/theme-check-theory/package.json +1 -1
  13. package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.test.ts +87 -0
  14. package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.ts +161 -0
  15. package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.test.ts +147 -0
  16. package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.ts +243 -0
  17. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metafield.test.ts +76 -0
  18. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metafield.ts +49 -0
  19. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metaobject.test.ts +77 -0
  20. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metaobject.ts +42 -0
  21. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-nullable-setting.test.ts +301 -0
  22. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-nullable-setting.ts +129 -0
  23. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.ts +1 -1
  24. package/skills/shopify-lint/theme-check-theory/src/index.test.ts +17 -4
  25. package/skills/shopify-lint/theme-check-theory/src/index.ts +20 -6
  26. package/skills/shopify-lint/theme-check-theory/src/utils/null-guard.ts +79 -0
  27. package/skills/shopify-lint/theme-check-theory/src/utils/nullable-output-check.ts +90 -0
  28. package/skills/visual-regression/scripts/__pycache__/visual_regression.cpython-312.pyc +0 -0
  29. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.test.ts +0 -198
  30. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.ts +0 -143
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.5",
3
+ "version": "1.2.8",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {
package/skills/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  - `pull-request-statamic`: Generates GitHub pull request descriptions for Statamic and Laravel development by analyzing git diffs and…
17
17
  - `record-changes`: Update `docs/changes.md` by summarizing the current branch against the primary development branch.
18
18
  - `shopify-cli`: Shopify CLI workflows for theme development.
19
- - `shopify-lint`: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on
19
+ - `shopify-lint`: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks, including warnings for hard-coded
20
20
  - `stylesheet-migration`: Migrate Shopify Liquid `{% stylesheet %}` blocks into theme CSS assets using bundled Python scripts.
21
21
  - `td-js-vanilla-rules`: Theory Digital vanilla JavaScript standards for Shopify theme work.
22
22
  - `td-review`: Run parallel code review agents on a PR (including TD theme compliance) and produce a synthesized findings…
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: shopify-lint
3
- version: 1.1.0
4
- description: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on offenses in files modified on the current Git branch. Use when Codex needs to lint a Shopify theme, validate branch-scoped Liquid or theme changes, enforce Theory theme rules, or avoid surfacing pre-existing Theme Check offenses from untouched files.
3
+ version: 1.3.2
4
+ description: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks, including warnings for hard-coded storefront text and script or style tags in Liquid, while reporting and failing only on offenses in files modified on the current Git branch. Use when Codex needs to lint a Shopify theme, validate branch-scoped Liquid or theme changes, enforce Theory theme rules, or avoid surfacing pre-existing Theme Check offenses from untouched files.
5
5
  ---
6
6
 
7
7
  # Shopify Lint
@@ -17,7 +17,7 @@ bash .agents/skills/shopify-lint/scripts/setup.sh
17
17
  Installing with `td-ai-tools install --setup shopify-lint` runs this automatically. The script:
18
18
 
19
19
  1. Runs `npm install && npm run build` inside `theme-check-theory/`, producing `dist/index.js` — the CommonJS entry point the root `.theme-check.yml` requires.
20
- 2. Writes a `.theme-check.yml` at the project root wiring in the bundled checks. If one already exists it is left untouched; ensure its `require:` list includes `./.agents/skills/shopify-lint/theme-check-theory`.
20
+ 2. Writes a `.theme-check.yml` at the project root wiring in the bundled checks. Setup replaces any existing file with the skill's current configuration so installs and updates cannot retain stale or incomplete settings.
21
21
  3. Through the installer's shared project-hook setup, installs matching Claude and Codex Stop hooks and merges their registrations into `.claude/settings.json` and `.codex/hooks.json` without replacing unrelated settings or hooks. Re-running setup updates the Shopify Lint hook in place instead of duplicating it.
22
22
 
23
23
  The Stop hooks run this skill's branch-scoped lint before handoff, return modified-file offenses to the agent, and allow handoff after three unsuccessful fix rounds. They require `jq`, `python3`, and Shopify CLI to be available when the hook runs. Directly running `scripts/setup.sh` performs the package build and Theme Check configuration; use `td-ai-tools install --setup` or `td-ai-tools update --setup` to apply the shared hook manifest as well.
@@ -32,7 +32,7 @@ Run the bundled Python script from the Shopify theme root:
32
32
  python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path .
33
33
  ```
34
34
 
35
- The script runs `shopify theme check --output json`, identifies files changed since the current branch's merge-base, adds staged, unstaged, and untracked files, and emits only reports whose paths are in that set. The root `.theme-check.yml` directly requires the bundled `theme-check-theory` package from this skill directory.
35
+ The script runs `shopify theme check --output json`, identifies files changed since the current branch's merge-base, adds staged, unstaged, and untracked files, and emits only reports whose paths are in that set. The root `.theme-check.yml` directly requires the bundled `theme-check-theory` package from this skill directory. `DisallowedScriptOrStyleTag` reports inline executable `<script>` and all `<style>` tags in modified Liquid files while allowing external-source and JSON-data scripts. `HardcodedText` reports rendered hard-coded copy, except content inside Liquid `stylesheet`, `javascript`, and `schema` tags.
36
36
 
37
37
  ## Workflow
38
38
 
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Shopify Lint"
3
- short_description: "Lint modified theme files with Theory checks"
3
+ short_description: "Lint modified Shopify theme files"
4
4
  default_prompt: "Use $shopify-lint to run Theme Check for files modified on my current branch."
@@ -21,7 +21,7 @@ cd "$PKG_DIR"
21
21
  npm install
22
22
  npm run build
23
23
 
24
- # 2. Generate the project-root .theme-check.yml.
24
+ # 2. Write the project-root .theme-check.yml.
25
25
  #
26
26
  # The installed layout is <project>/<.agents|.claude>/skills/shopify-lint, so
27
27
  # the project root is three levels above the skill directory. Guard against
@@ -37,12 +37,6 @@ fi
37
37
  PROJECT_ROOT="$(dirname "$TARGET_DIR")"
38
38
  THEME_CHECK_FILE="$PROJECT_ROOT/.theme-check.yml"
39
39
 
40
- if [[ -f "$THEME_CHECK_FILE" ]]; then
41
- echo "shopify-lint setup: $THEME_CHECK_FILE already exists; leaving it unchanged."
42
- echo " Ensure its 'require:' list includes ./.agents/skills/shopify-lint/theme-check-theory"
43
- exit 0
44
- fi
45
-
46
40
  cat > "$THEME_CHECK_FILE" <<'YAML'
47
41
  extends:
48
42
  - theme-check:recommended
@@ -50,13 +44,29 @@ extends:
50
44
  require:
51
45
  - ./.agents/skills/shopify-lint/theme-check-theory
52
46
 
47
+ DisallowedScriptOrStyleTag:
48
+ enabled: true
49
+ severity: warning
50
+
51
+ HardcodedText:
52
+ enabled: true
53
+ severity: warning
54
+
53
55
  UnusedSectionSettings:
56
+ enabled: false
57
+ severity: warning
58
+
59
+ UnguardedNullableSetting:
60
+ enabled: true
61
+ severity: warning
62
+
63
+ UnguardedMetafield:
54
64
  enabled: true
55
65
  severity: warning
56
66
 
57
- UnguardedTextSetting:
67
+ UnguardedMetaobject:
58
68
  enabled: true
59
69
  severity: warning
60
70
  YAML
61
71
 
62
- echo "shopify-lint setup: wrote $THEME_CHECK_FILE"
72
+ echo "shopify-lint setup: wrote $THEME_CHECK_FILE (replacing any existing configuration)"
@@ -4,11 +4,26 @@ extends:
4
4
  require:
5
5
  - ./node_modules/theme-check-theory
6
6
 
7
+ DisallowedScriptOrStyleTag:
8
+ enabled: true
9
+ severity: warning
10
+
11
+ HardcodedText:
12
+ enabled: true
13
+ severity: warning
14
+
7
15
  UnusedSectionSettings:
16
+ enabled: false
17
+ severity: warning
18
+
19
+ UnguardedNullableSetting:
8
20
  enabled: true
9
21
  severity: warning
10
22
 
11
- UnguardedTextSetting:
23
+ UnguardedMetafield:
12
24
  enabled: true
13
25
  severity: warning
14
26
 
27
+ UnguardedMetaobject:
28
+ enabled: true
29
+ severity: warning
@@ -11,8 +11,37 @@ integrated into Shopify CLI. It is not compatible with the archived Ruby
11
11
 
12
12
  | Code | Severity | What it catches |
13
13
  | --- | --- | --- |
14
+ | `DisallowedScriptOrStyleTag` | warning | An inline executable `<script>` or any `<style>` tag in a Liquid file. |
15
+ | `HardcodedText` | warning | Rendered hard-coded storefront copy that should use a dynamic content source. |
14
16
  | `UnusedSectionSettings` | warning | A setting declared in `{% schema %}` that is never referenced in the file. |
15
- | `UnguardedTextSetting` | warning | A text-like setting output with `{{ }}` and no presence guard (`{% if %}`/`{% unless %}`) or `\| default`. |
17
+ | `UnguardedNullableSetting` | warning | A potentially blank setting output with `{{ }}` and no presence guard (`{% if %}`/`{% unless %}`) or `\| default`. |
18
+ | `UnguardedMetafield` | warning | A metafield output with no presence guard or `\| default`. |
19
+ | `UnguardedMetaobject` | warning | A metaobject entry output with no presence guard or `\| default`. |
20
+
21
+ ### DisallowedScriptOrStyleTag
22
+
23
+ HTML `<script>` and `<style>` tags keep JavaScript and CSS embedded in Liquid
24
+ files. Move their contents to theme assets or the appropriate Shopify Liquid
25
+ tags instead. Scripts with a non-empty `src` attribute and scripts declared as
26
+ JSON data, including `application/json` and `application/ld+json`, are allowed.
27
+ HTML comments and similarly named elements such as `<script-template>` are
28
+ ignored.
29
+
30
+ ### HardcodedText
31
+
32
+ Rendered storefront copy should come from a section setting, translation key,
33
+ metafield, or metaobject. The check reports text nodes, direct quoted output
34
+ with `{{ }}` or `{% echo %}`, quoted `default` filter fallbacks, hard-coded
35
+ `alt`, `title`, `placeholder`, and ARIA text attributes, and labels in the
36
+ `value` attribute of button-like inputs. It ignores technical attributes such
37
+ as classes and IDs, Liquid logic strings, comments, punctuation-only fragments,
38
+ and text made entirely of HTML character references.
39
+
40
+ Content inside `{% stylesheet %}`, `{% javascript %}`, and `{% schema %}` is
41
+ ignored. A quoted translation key passed through `t` or `translate` is also
42
+ accepted. Short alphabetic tokens and copyright years remain reportable because
43
+ the check cannot reliably distinguish user-facing copy from currency codes,
44
+ units, or other intentional literals.
16
45
 
17
46
  ### UnusedSectionSettings
18
47
 
@@ -25,31 +54,62 @@ If a file uses dynamic access such as `section.settings[key]`, or passes an
25
54
  entire settings object elsewhere, the check cannot prove which setting is
26
55
  unused. It therefore stays silent for the whole file to avoid false positives.
27
56
 
28
- ### UnguardedTextSetting
57
+ This check is disabled by default. Enable it explicitly when a theme's section
58
+ settings are expected to be fully traceable within each file:
29
59
 
30
- Text-like settings render as an empty string when a merchant leaves them blank,
31
- so outputting them unguarded is a recurring bug source. A setting is considered
32
- handled if, anywhere in the file, it is either tested in an `if`, `unless`,
33
- `elsif`, or `case` conditional, or output with a `default` filter.
60
+ ```yaml
61
+ UnusedSectionSettings:
62
+ enabled: true
63
+ severity: warning
64
+ ```
65
+
66
+ ### UnguardedNullableSetting
34
67
 
35
- Version 1 is deliberately coarse and per-file. It favors near-zero false
36
- positives over catching a setting guarded in one branch and output raw in
37
- another.
68
+ Many settings can return an empty, `blank`, or `nil` value when a merchant
69
+ leaves them unset or their selected resource is no longer available. Outputting
70
+ them unguarded is a recurring bug source. Each output is evaluated independently.
71
+ It is considered guarded when its active `if`, `unless`, `elsif`, or `case`
72
+ branch tests that setting, or when that output uses a `default` filter. A guard
73
+ or default on one invocation does not suppress offenses for unguarded
74
+ invocations elsewhere in the file.
38
75
 
39
- The setting types considered text-like are configurable with
40
- `textualSettingTypes`. The defaults are `text`, `textarea`, `richtext`,
41
- `inline_richtext`, `html`, `liquid`, and `url`.
76
+ The setting types checked are configurable with the `nullableSettingTypes`
77
+ option. The defaults are `number`, `text`, `textarea`, `article`,
78
+ `article_list`, `blog`, `collection`, `collection_list`, `image_picker`,
79
+ `link_list`, `metaobject`, `metaobject_list`, `page`, `product`,
80
+ `product_list`, `richtext`, `inline_richtext`, `html`, `liquid`, `url`,
81
+ `video`, and `video_url`. Color-related settings are excluded because an unset
82
+ color can resolve to a non-null fallback such as `transparent`.
42
83
 
43
84
  ```yaml
44
- UnguardedTextSetting:
85
+ UnguardedNullableSetting:
45
86
  enabled: true
46
87
  severity: warning
47
- textualSettingTypes:
88
+ nullableSettingTypes:
48
89
  - text
49
90
  - textarea
50
91
  - image_picker
51
92
  ```
52
93
 
94
+ ### UnguardedMetafield and UnguardedMetaobject
95
+
96
+ [Metafields](https://shopify.dev/docs/api/liquid/objects/metafield) are
97
+ identified from the static `resource.metafields.namespace.key` access path,
98
+ and [metaobjects](https://shopify.dev/docs/api/liquid/objects/metaobject) from
99
+ `metaobjects.type.handle`. Outputting the object itself or any descendant, such
100
+ as a metafield's `value` or a metaobject's field, requires a guard on the same
101
+ object or a `default` filter on that output.
102
+
103
+ Both dot and static square-bracket notation are supported. Dynamic namespace,
104
+ key, type, and handle lookups are ignored because the selected object cannot be
105
+ identified statically. The singular `metaobject` drop available inside a
106
+ metaobject template is also ignored because the template is already rendering
107
+ that entry.
108
+
109
+ All three nullability checks share the same branch and `default`-filter
110
+ analysis. A guard applies only inside its active `if`, `unless`, `elsif`, or
111
+ `case` branch and only to the matching setting, metafield, or metaobject.
112
+
53
113
  ## Usage in a project
54
114
 
55
115
  Install the module as a development dependency:
@@ -67,11 +127,27 @@ extends:
67
127
  require:
68
128
  - ./node_modules/theme-check-theory
69
129
 
130
+ DisallowedScriptOrStyleTag:
131
+ enabled: true
132
+ severity: warning
133
+
134
+ HardcodedText:
135
+ enabled: true
136
+ severity: warning
137
+
70
138
  UnusedSectionSettings:
139
+ enabled: false
140
+ severity: warning
141
+
142
+ UnguardedNullableSetting:
71
143
  enabled: true
72
144
  severity: warning
73
145
 
74
- UnguardedTextSetting:
146
+ UnguardedMetafield:
147
+ enabled: true
148
+ severity: warning
149
+
150
+ UnguardedMetaobject:
75
151
  enabled: true
76
152
  severity: warning
77
153
  ```
@@ -118,6 +194,7 @@ real Theme Check parser and traversal rather than a mocked AST.
118
194
  4. Add it to `configs/recommended.yml`.
119
195
 
120
196
  Shared schema parsing and setting-reference helpers live in `src/utils/ast.ts`.
121
- Their node shapes are verified against `@shopify/liquid-html-parser` 2.9.x;
122
- run the tests when upgrading that dependency.
123
-
197
+ Null-guard analysis and nullable-object check construction live in
198
+ `src/utils/null-guard.ts` and `src/utils/nullable-output-check.ts`. Their node
199
+ shapes are verified against `@shopify/liquid-html-parser` 2.9.x; run the tests
200
+ when upgrading that dependency.
@@ -1,8 +1,23 @@
1
+ DisallowedScriptOrStyleTag:
2
+ enabled: true
3
+ severity: warning
4
+
5
+ HardcodedText:
6
+ enabled: true
7
+ severity: warning
8
+
1
9
  UnusedSectionSettings:
10
+ enabled: false
11
+ severity: warning
12
+
13
+ UnguardedNullableSetting:
2
14
  enabled: true
3
15
  severity: warning
4
16
 
5
- UnguardedTextSetting:
17
+ UnguardedMetafield:
6
18
  enabled: true
7
19
  severity: warning
8
20
 
21
+ UnguardedMetaobject:
22
+ enabled: true
23
+ severity: warning
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "theme-check-theory",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "theme-check-theory",
9
- "version": "0.1.0",
9
+ "version": "0.2.1",
10
10
  "license": "UNLICENSED",
11
11
  "dependencies": {
12
12
  "@shopify/liquid-html-parser": "2.9.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theme-check-theory",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Theory Digital's custom Shopify Theme Check rules",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,87 @@
1
+ import {
2
+ runLiquidCheck,
3
+ } from '@shopify/theme-check-common/dist/test/test-helper';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { DisallowedScriptOrStyleTag } from './disallowed-script-or-style-tag';
7
+
8
+ describe('DisallowedScriptOrStyleTag', () => {
9
+ it('reports script and style tags', async () => {
10
+ const source = `
11
+ <script>window.example = true;</script>
12
+ <style>.example { color: red; }</style>
13
+ `;
14
+
15
+ const offenses = await runLiquidCheck(DisallowedScriptOrStyleTag, source);
16
+
17
+ expect(offenses).toHaveLength(2);
18
+ expect(offenses.map(({ message }) => message)).toEqual([
19
+ 'Do not include <script> tags in Liquid files',
20
+ 'Do not include <style> tags in Liquid files',
21
+ ]);
22
+ expect(
23
+ offenses.every(({ check }) => check === 'DisallowedScriptOrStyleTag'),
24
+ ).toBe(true);
25
+ });
26
+
27
+ it('reports case variants and self-closing tags', async () => {
28
+ const source = `
29
+ <SCRIPT type="module">window.example = true;</SCRIPT>
30
+ <STYLE />
31
+ `;
32
+
33
+ const offenses = await runLiquidCheck(DisallowedScriptOrStyleTag, source);
34
+
35
+ expect(offenses).toHaveLength(2);
36
+ expect(offenses.map(({ message }) => message)).toEqual([
37
+ 'Do not include <script> tags in Liquid files',
38
+ 'Do not include <style> tags in Liquid files',
39
+ ]);
40
+ });
41
+
42
+ it('ignores scripts with an external source', async () => {
43
+ const source = `
44
+ <script src="{{ 'theme.js' | asset_url }}" defer></script>
45
+ <SCRIPT SRC="https://cdn.example.com/widget.js"></SCRIPT>
46
+ `;
47
+
48
+ expect(
49
+ await runLiquidCheck(DisallowedScriptOrStyleTag, source),
50
+ ).toEqual([]);
51
+ });
52
+
53
+ it('ignores scripts containing JSON data', async () => {
54
+ const source = `
55
+ <script type="application/ld+json">{"@type":"Organization"}</script>
56
+ <script type="application/json">{"enabled":true}</script>
57
+ `;
58
+
59
+ expect(
60
+ await runLiquidCheck(DisallowedScriptOrStyleTag, source),
61
+ ).toEqual([]);
62
+ });
63
+
64
+ it('reports executable scripts with an empty src', async () => {
65
+ const source = `
66
+ <script src="">window.example = true;</script>
67
+ <script type="text/javascript">window.example = true;</script>
68
+ `;
69
+
70
+ expect(
71
+ await runLiquidCheck(DisallowedScriptOrStyleTag, source),
72
+ ).toHaveLength(2);
73
+ });
74
+
75
+ it('ignores tags in comments and similarly named elements', async () => {
76
+ const source = `
77
+ <!-- <script></script> -->
78
+ {% comment %}<style></style>{% endcomment %}
79
+ <script-template></script-template>
80
+ <div style="color: red"></div>
81
+ `;
82
+
83
+ expect(
84
+ await runLiquidCheck(DisallowedScriptOrStyleTag, source),
85
+ ).toEqual([]);
86
+ });
87
+ });
@@ -0,0 +1,161 @@
1
+ import {
2
+ AttributeNode,
3
+ HtmlElement,
4
+ HtmlRawNode,
5
+ HtmlSelfClosingElement,
6
+ NodeTypes,
7
+ } from '@shopify/liquid-html-parser';
8
+ import {
9
+ LiquidCheckDefinition,
10
+ Severity,
11
+ SourceCodeType,
12
+ } from '@shopify/theme-check-common';
13
+
14
+ const DISALLOWED_TAGS = new Set(['script', 'style']);
15
+
16
+ type NamedHtmlNode = HtmlElement | HtmlSelfClosingElement;
17
+ type ScriptNode = HtmlRawNode | NamedHtmlNode;
18
+ type ValuedAttribute = Extract<AttributeNode, { value: unknown }>;
19
+
20
+ function isValuedAttribute(
21
+ attribute: AttributeNode,
22
+ ): attribute is ValuedAttribute {
23
+ return (
24
+ attribute.type === NodeTypes.AttrDoubleQuoted ||
25
+ attribute.type === NodeTypes.AttrSingleQuoted ||
26
+ attribute.type === NodeTypes.AttrUnquoted
27
+ );
28
+ }
29
+
30
+ function staticAttributeName(attribute: ValuedAttribute): string | null {
31
+ if (
32
+ attribute.name.length !== 1 ||
33
+ attribute.name[0].type !== NodeTypes.TextNode
34
+ ) {
35
+ return null;
36
+ }
37
+
38
+ return attribute.name[0].value.toLowerCase();
39
+ }
40
+
41
+ function staticAttributeValue(
42
+ node: ScriptNode,
43
+ attributeName: string,
44
+ ): string | null {
45
+ const attribute = node.attributes.find(
46
+ (candidate) =>
47
+ isValuedAttribute(candidate) &&
48
+ staticAttributeName(candidate) === attributeName,
49
+ );
50
+ if (
51
+ !attribute ||
52
+ !isValuedAttribute(attribute) ||
53
+ !attribute.value.every(({ type }) => type === NodeTypes.TextNode)
54
+ ) {
55
+ return null;
56
+ }
57
+
58
+ return attribute.value
59
+ .map((part) => ('value' in part ? part.value : ''))
60
+ .join('')
61
+ .trim()
62
+ .toLowerCase();
63
+ }
64
+
65
+ function hasExternalSource(node: ScriptNode): boolean {
66
+ return node.attributes.some(
67
+ (attribute) =>
68
+ isValuedAttribute(attribute) &&
69
+ staticAttributeName(attribute) === 'src' &&
70
+ attribute.value.length > 0,
71
+ );
72
+ }
73
+
74
+ function containsJsonData(node: ScriptNode): boolean {
75
+ const type = staticAttributeValue(node, 'type');
76
+ if (!type) return false;
77
+
78
+ const mimeType = type.split(';', 1)[0].trim();
79
+ return (
80
+ mimeType.endsWith('/json') ||
81
+ mimeType.endsWith('+json') ||
82
+ mimeType === 'importmap' ||
83
+ mimeType === 'speculationrules'
84
+ );
85
+ }
86
+
87
+ function isAllowedScript(node: ScriptNode): boolean {
88
+ return hasExternalSource(node) || containsJsonData(node);
89
+ }
90
+
91
+ function staticTagName(node: NamedHtmlNode): string | null {
92
+ if (
93
+ node.name.length !== 1 ||
94
+ node.name[0].type !== NodeTypes.TextNode
95
+ ) {
96
+ return null;
97
+ }
98
+
99
+ return node.name[0].value.toLowerCase();
100
+ }
101
+
102
+ export const DisallowedScriptOrStyleTag: LiquidCheckDefinition = {
103
+ meta: {
104
+ code: 'DisallowedScriptOrStyleTag',
105
+ name: 'Prevent script and style tags in Liquid',
106
+ docs: {
107
+ description:
108
+ 'Reports inline executable script and style tags in Liquid files.',
109
+ recommended: true,
110
+ },
111
+ type: SourceCodeType.LiquidHtml,
112
+ severity: Severity.WARNING,
113
+ schema: {},
114
+ },
115
+ create(context) {
116
+ function report(tagName: string, startIndex: number, endIndex: number) {
117
+ if (!DISALLOWED_TAGS.has(tagName)) return;
118
+
119
+ context.report({
120
+ message: `Do not include <${tagName}> tags in Liquid files`,
121
+ startIndex,
122
+ endIndex,
123
+ });
124
+ }
125
+
126
+ function checkNamedNode(node: NamedHtmlNode) {
127
+ const tagName = staticTagName(node);
128
+ if (!tagName) return;
129
+ if (tagName === 'script' && isAllowedScript(node)) return;
130
+
131
+ report(
132
+ tagName,
133
+ node.blockStartPosition.start,
134
+ node.blockStartPosition.end,
135
+ );
136
+ }
137
+
138
+ return {
139
+ async HtmlRawNode(node) {
140
+ if (
141
+ node.name.toLowerCase() === 'script' &&
142
+ isAllowedScript(node)
143
+ ) {
144
+ return;
145
+ }
146
+
147
+ report(
148
+ node.name.toLowerCase(),
149
+ node.blockStartPosition.start,
150
+ node.blockStartPosition.end,
151
+ );
152
+ },
153
+ async HtmlElement(node) {
154
+ checkNamedNode(node);
155
+ },
156
+ async HtmlSelfClosingElement(node) {
157
+ checkNamedNode(node);
158
+ },
159
+ };
160
+ },
161
+ };