td-ai-tools 1.2.5 → 1.2.6

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 +17 -1
  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 +89 -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 +54 -0
  14. package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.ts +77 -0
  15. package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.test.ts +102 -0
  16. package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.ts +171 -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.6",
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.0
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. If one already exists it is left untouched; ensure its `require:` list includes `./.agents/skills/shopify-lint/theme-check-theory` and both `DisallowedScriptOrStyleTag` and `HardcodedText` are enabled.
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 HTML `<script>` and `<style>` tags in modified Liquid files. `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."
@@ -50,11 +50,27 @@ extends:
50
50
  require:
51
51
  - ./.agents/skills/shopify-lint/theme-check-theory
52
52
 
53
+ DisallowedScriptOrStyleTag:
54
+ enabled: true
55
+ severity: warning
56
+
57
+ HardcodedText:
58
+ enabled: true
59
+ severity: warning
60
+
53
61
  UnusedSectionSettings:
62
+ enabled: false
63
+ severity: warning
64
+
65
+ UnguardedNullableSetting:
66
+ enabled: true
67
+ severity: warning
68
+
69
+ UnguardedMetafield:
54
70
  enabled: true
55
71
  severity: warning
56
72
 
57
- UnguardedTextSetting:
73
+ UnguardedMetaobject:
58
74
  enabled: true
59
75
  severity: warning
60
76
  YAML
@@ -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,31 @@ 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 HTML `<script>` or `<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. HTML comments and similarly named elements such as
26
+ `<script-template>` are ignored.
27
+
28
+ ### HardcodedText
29
+
30
+ Rendered storefront copy should come from a section setting, translation key,
31
+ metafield, or metaobject. The check reports text nodes, direct quoted output
32
+ with `{{ }}` or `{% echo %}`, and hard-coded `alt`, `title`, `placeholder`, and
33
+ ARIA text attributes. It ignores technical attributes such as classes and IDs,
34
+ Liquid logic strings, comments, and punctuation-only fragments.
35
+
36
+ Content inside `{% stylesheet %}`, `{% javascript %}`, and `{% schema %}` is
37
+ ignored. A quoted translation key passed through `t` or `translate` is also
38
+ accepted.
16
39
 
17
40
  ### UnusedSectionSettings
18
41
 
@@ -25,31 +48,62 @@ If a file uses dynamic access such as `section.settings[key]`, or passes an
25
48
  entire settings object elsewhere, the check cannot prove which setting is
26
49
  unused. It therefore stays silent for the whole file to avoid false positives.
27
50
 
28
- ### UnguardedTextSetting
51
+ This check is disabled by default. Enable it explicitly when a theme's section
52
+ settings are expected to be fully traceable within each file:
53
+
54
+ ```yaml
55
+ UnusedSectionSettings:
56
+ enabled: true
57
+ severity: warning
58
+ ```
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
+ ### UnguardedNullableSetting
34
61
 
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.
62
+ Many settings can return an empty, `blank`, or `nil` value when a merchant
63
+ leaves them unset or their selected resource is no longer available. Outputting
64
+ them unguarded is a recurring bug source. Each output is evaluated independently.
65
+ It is considered guarded when its active `if`, `unless`, `elsif`, or `case`
66
+ branch tests that setting, or when that output uses a `default` filter. A guard
67
+ or default on one invocation does not suppress offenses for unguarded
68
+ invocations elsewhere in the file.
38
69
 
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`.
70
+ The setting types checked are configurable with the `nullableSettingTypes`
71
+ option. The defaults are `number`, `text`, `textarea`, `article`,
72
+ `article_list`, `blog`, `collection`, `collection_list`, `image_picker`,
73
+ `link_list`, `metaobject`, `metaobject_list`, `page`, `product`,
74
+ `product_list`, `richtext`, `inline_richtext`, `html`, `liquid`, `url`,
75
+ `video`, and `video_url`. Color-related settings are excluded because an unset
76
+ color can resolve to a non-null fallback such as `transparent`.
42
77
 
43
78
  ```yaml
44
- UnguardedTextSetting:
79
+ UnguardedNullableSetting:
45
80
  enabled: true
46
81
  severity: warning
47
- textualSettingTypes:
82
+ nullableSettingTypes:
48
83
  - text
49
84
  - textarea
50
85
  - image_picker
51
86
  ```
52
87
 
88
+ ### UnguardedMetafield and UnguardedMetaobject
89
+
90
+ [Metafields](https://shopify.dev/docs/api/liquid/objects/metafield) are
91
+ identified from the static `resource.metafields.namespace.key` access path,
92
+ and [metaobjects](https://shopify.dev/docs/api/liquid/objects/metaobject) from
93
+ `metaobjects.type.handle`. Outputting the object itself or any descendant, such
94
+ as a metafield's `value` or a metaobject's field, requires a guard on the same
95
+ object or a `default` filter on that output.
96
+
97
+ Both dot and static square-bracket notation are supported. Dynamic namespace,
98
+ key, type, and handle lookups are ignored because the selected object cannot be
99
+ identified statically. The singular `metaobject` drop available inside a
100
+ metaobject template is also ignored because the template is already rendering
101
+ that entry.
102
+
103
+ All three nullability checks share the same branch and `default`-filter
104
+ analysis. A guard applies only inside its active `if`, `unless`, `elsif`, or
105
+ `case` branch and only to the matching setting, metafield, or metaobject.
106
+
53
107
  ## Usage in a project
54
108
 
55
109
  Install the module as a development dependency:
@@ -67,11 +121,27 @@ extends:
67
121
  require:
68
122
  - ./node_modules/theme-check-theory
69
123
 
124
+ DisallowedScriptOrStyleTag:
125
+ enabled: true
126
+ severity: warning
127
+
128
+ HardcodedText:
129
+ enabled: true
130
+ severity: warning
131
+
70
132
  UnusedSectionSettings:
133
+ enabled: false
134
+ severity: warning
135
+
136
+ UnguardedNullableSetting:
137
+ enabled: true
138
+ severity: warning
139
+
140
+ UnguardedMetafield:
71
141
  enabled: true
72
142
  severity: warning
73
143
 
74
- UnguardedTextSetting:
144
+ UnguardedMetaobject:
75
145
  enabled: true
76
146
  severity: warning
77
147
  ```
@@ -118,6 +188,7 @@ real Theme Check parser and traversal rather than a mocked AST.
118
188
  4. Add it to `configs/recommended.yml`.
119
189
 
120
190
  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
-
191
+ Null-guard analysis and nullable-object check construction live in
192
+ `src/utils/null-guard.ts` and `src/utils/nullable-output-check.ts`. Their node
193
+ shapes are verified against `@shopify/liquid-html-parser` 2.9.x; run the tests
194
+ 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.0",
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.0",
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.0",
4
4
  "description": "Theory Digital's custom Shopify Theme Check rules",
5
5
  "type": "commonjs",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,54 @@
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 src="{{ 'theme.js' | asset_url }}" defer></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="application/json">{}</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 tags in comments and similarly named elements', async () => {
43
+ const source = `
44
+ <!-- <script></script> -->
45
+ {% comment %}<style></style>{% endcomment %}
46
+ <script-template></script-template>
47
+ <div style="color: red"></div>
48
+ `;
49
+
50
+ expect(
51
+ await runLiquidCheck(DisallowedScriptOrStyleTag, source),
52
+ ).toEqual([]);
53
+ });
54
+ });
@@ -0,0 +1,77 @@
1
+ import {
2
+ HtmlElement,
3
+ HtmlSelfClosingElement,
4
+ NodeTypes,
5
+ } from '@shopify/liquid-html-parser';
6
+ import {
7
+ LiquidCheckDefinition,
8
+ Severity,
9
+ SourceCodeType,
10
+ } from '@shopify/theme-check-common';
11
+
12
+ const DISALLOWED_TAGS = new Set(['script', 'style']);
13
+
14
+ type NamedHtmlNode = HtmlElement | HtmlSelfClosingElement;
15
+
16
+ function staticTagName(node: NamedHtmlNode): string | null {
17
+ if (
18
+ node.name.length !== 1 ||
19
+ node.name[0].type !== NodeTypes.TextNode
20
+ ) {
21
+ return null;
22
+ }
23
+
24
+ return node.name[0].value.toLowerCase();
25
+ }
26
+
27
+ export const DisallowedScriptOrStyleTag: LiquidCheckDefinition = {
28
+ meta: {
29
+ code: 'DisallowedScriptOrStyleTag',
30
+ name: 'Prevent script and style tags in Liquid',
31
+ docs: {
32
+ description: 'Reports HTML script and style tags in Liquid files.',
33
+ recommended: true,
34
+ },
35
+ type: SourceCodeType.LiquidHtml,
36
+ severity: Severity.WARNING,
37
+ schema: {},
38
+ },
39
+ create(context) {
40
+ function report(tagName: string, startIndex: number, endIndex: number) {
41
+ if (!DISALLOWED_TAGS.has(tagName)) return;
42
+
43
+ context.report({
44
+ message: `Do not include <${tagName}> tags in Liquid files`,
45
+ startIndex,
46
+ endIndex,
47
+ });
48
+ }
49
+
50
+ function checkNamedNode(node: NamedHtmlNode) {
51
+ const tagName = staticTagName(node);
52
+ if (!tagName) return;
53
+
54
+ report(
55
+ tagName,
56
+ node.blockStartPosition.start,
57
+ node.blockStartPosition.end,
58
+ );
59
+ }
60
+
61
+ return {
62
+ async HtmlRawNode(node) {
63
+ report(
64
+ node.name.toLowerCase(),
65
+ node.blockStartPosition.start,
66
+ node.blockStartPosition.end,
67
+ );
68
+ },
69
+ async HtmlElement(node) {
70
+ checkNamedNode(node);
71
+ },
72
+ async HtmlSelfClosingElement(node) {
73
+ checkNamedNode(node);
74
+ },
75
+ };
76
+ },
77
+ };
@@ -0,0 +1,102 @@
1
+ import {
2
+ runLiquidCheck,
3
+ } from '@shopify/theme-check-common/dist/test/test-helper';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ import { HardcodedText } from './hardcoded-text';
7
+
8
+ describe('HardcodedText', () => {
9
+ it('reports rendered text and direct string output', async () => {
10
+ const source = `
11
+ Welcome to our store
12
+ <h2>Featured products</h2>
13
+ {{ 'Limited edition' }}
14
+ {% echo "Shop now" %}
15
+ `;
16
+
17
+ const offenses = await runLiquidCheck(HardcodedText, source);
18
+
19
+ expect(offenses).toHaveLength(4);
20
+ expect(
21
+ offenses.every(({ check }) => check === 'HardcodedText'),
22
+ ).toBe(true);
23
+ expect(
24
+ offenses.every(
25
+ ({ message }) =>
26
+ message ===
27
+ 'Replace hard-coded text with a section setting, translation key, metafield, or metaobject',
28
+ ),
29
+ ).toBe(true);
30
+ });
31
+
32
+ it('reports hard-coded text in common user-facing attributes', async () => {
33
+ const source = `
34
+ <img src="{{ image | image_url }}" alt="Product photo">
35
+ <button title='Close dialog' aria-label="Close">×</button>
36
+ <input placeholder="Email address">
37
+ `;
38
+
39
+ const offenses = await runLiquidCheck(HardcodedText, source);
40
+
41
+ expect(offenses).toHaveLength(4);
42
+ });
43
+
44
+ it('reports static fragments in otherwise dynamic rendered text', async () => {
45
+ const source = `
46
+ <p>Welcome, {{ customer.name }}!</p>
47
+ <img alt="Photo of {{ product.title }}" src="{{ product.featured_image | image_url }}">
48
+ `;
49
+
50
+ const offenses = await runLiquidCheck(HardcodedText, source);
51
+
52
+ expect(offenses).toHaveLength(2);
53
+ });
54
+
55
+ it('accepts settings, translation keys, metafields, and metaobjects', async () => {
56
+ const source = `
57
+ <h2>{{ section.settings.heading }}</h2>
58
+ <p>{{ 'general.welcome' | t }}</p>
59
+ <p>{{ "general.subtitle" | translate }}</p>
60
+ <p>{{ product.metafields.content.description }}</p>
61
+ <p>{{ metaobjects.testimonials.homepage.quote }}</p>
62
+ <img alt="{{ section.settings.image_alt }}" src="{{ section.settings.image | image_url }}">
63
+ `;
64
+
65
+ expect(await runLiquidCheck(HardcodedText, source)).toEqual([]);
66
+ });
67
+
68
+ it.each([
69
+ [
70
+ 'stylesheet',
71
+ '{% stylesheet %}.card::after { content: "Hard coded"; }{% endstylesheet %}',
72
+ ],
73
+ [
74
+ 'javascript',
75
+ '{% javascript %}console.log("Hard coded");{% endjavascript %}',
76
+ ],
77
+ [
78
+ 'schema',
79
+ '{% schema %}{"name":"Hard coded","settings":[]}{% endschema %}',
80
+ ],
81
+ ])('ignores text inside the %s raw tag', async (_, source) => {
82
+ expect(await runLiquidCheck(HardcodedText, source)).toEqual([]);
83
+ });
84
+
85
+ it('ignores comments, technical attributes, punctuation, and Liquid logic strings', async () => {
86
+ const source = `
87
+ <!-- Hard coded HTML comment -->
88
+ {% comment %}Hard coded Liquid comment{% endcomment %}
89
+ <div class="hard-coded" id="hard-coded" data-label="Hard coded">—</div>
90
+ {% assign state = 'active' %}
91
+ {% if request.page_type == 'product' %}{{ product.title }}{% endif %}
92
+ `;
93
+
94
+ expect(await runLiquidCheck(HardcodedText, source)).toEqual([]);
95
+ });
96
+
97
+ it('still reports rendered text inside other raw tags', async () => {
98
+ const source = '{% raw %}Hard coded raw output{% endraw %}';
99
+
100
+ expect(await runLiquidCheck(HardcodedText, source)).toHaveLength(1);
101
+ });
102
+ });