td-ai-tools 1.2.11 → 1.3.2

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 (41) hide show
  1. package/package.json +1 -1
  2. package/skills/README.md +2 -2
  3. package/skills/shopify-lint/SKILL.md +26 -18
  4. package/skills/shopify-lint/eslint-plugin-theory/README.md +25 -8
  5. package/skills/shopify-lint/eslint-plugin-theory/src/liquid/mask.test.ts +41 -0
  6. package/skills/shopify-lint/eslint-plugin-theory/src/liquid/mask.ts +41 -2
  7. package/skills/shopify-lint/hooks/claude-stop.sh +2 -3
  8. package/skills/shopify-lint/hooks/codex-stop.sh +2 -3
  9. package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
  10. package/skills/shopify-lint/scripts/setup.sh +21 -2
  11. package/skills/shopify-lint/scripts/shopify_lint.py +179 -30
  12. package/skills/shopify-lint/stylelint-config-theory/.stylelintignore +9 -0
  13. package/skills/shopify-lint/stylelint-config-theory/README.md +144 -0
  14. package/skills/shopify-lint/stylelint-config-theory/bin/lint-css.mjs +206 -0
  15. package/skills/shopify-lint/stylelint-config-theory/package-lock.json +3110 -0
  16. package/skills/shopify-lint/stylelint-config-theory/package.json +47 -0
  17. package/skills/shopify-lint/stylelint-config-theory/src/build.test.ts +31 -0
  18. package/skills/shopify-lint/stylelint-config-theory/src/config.test.ts +150 -0
  19. package/skills/shopify-lint/stylelint-config-theory/src/config.ts +109 -0
  20. package/skills/shopify-lint/stylelint-config-theory/src/index.ts +15 -0
  21. package/skills/shopify-lint/stylelint-config-theory/src/integration.test.ts +187 -0
  22. package/skills/shopify-lint/stylelint-config-theory/src/liquid/mask.test.ts +300 -0
  23. package/skills/shopify-lint/stylelint-config-theory/src/liquid/mask.ts +203 -0
  24. package/skills/shopify-lint/stylelint-config-theory/src/types/shared-configs.d.ts +26 -0
  25. package/skills/shopify-lint/stylelint-config-theory/tsconfig.build.json +9 -0
  26. package/skills/shopify-lint/stylelint-config-theory/tsconfig.json +14 -0
  27. package/skills/shopify-lint/stylelint-config-theory/vitest.config.ts +11 -0
  28. package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
  29. package/skills/shopify-lint/tests/test_shopify_lint.py +483 -8
  30. package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +7 -0
  31. package/skills/shopify-lint/theme-check-theory/README.md +48 -0
  32. package/skills/shopify-pre-pr/SKILL.md +129 -0
  33. package/skills/shopify-pre-pr/agents/openai.yaml +4 -0
  34. package/skills/shopify-pre-pr/references/td-change-delimiters.md +88 -0
  35. package/skills/shopify-pre-pr/scripts/__pycache__/test_vendor_change_audit.cpython-312.pyc +0 -0
  36. package/skills/shopify-pre-pr/scripts/__pycache__/vendor_change_audit.cpython-312.pyc +0 -0
  37. package/skills/shopify-pre-pr/scripts/test_vendor_change_audit.py +424 -0
  38. package/skills/shopify-pre-pr/scripts/vendor_change_audit.py +540 -0
  39. package/skills/record-changes/SKILL.md +0 -76
  40. package/skills/record-changes/agents/openai.yaml +0 -4
  41. /package/skills/{record-changes → shopify-pre-pr}/scripts/branch_diff_context.py +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.11",
3
+ "version": "1.3.2",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {
package/skills/README.md CHANGED
@@ -14,9 +14,9 @@
14
14
  - `pr-solver`: Resolve GitHub pull request feedback by querying unresolved review conversations with the GitHub GraphQL API…
15
15
  - `pull-request`: Generates well-structured GitHub pull request descriptions for Shopify theme development teams by analyzing…
16
16
  - `pull-request-statamic`: Generates GitHub pull request descriptions for Statamic and Laravel development by analyzing git diffs and…
17
- - `record-changes`: Update `docs/changes.md` by summarizing the current branch against the primary development branch.
18
17
  - `shopify-cli`: Shopify CLI workflows for theme development.
19
- - `shopify-lint`: Run Shopify CLI Theme Check and Theory Digital's bundled Liquid and JavaScript rules together, linting…
18
+ - `shopify-lint`: Run Shopify CLI Theme Check and Theory Digital's bundled Liquid, JavaScript, and CSS rules together, linting…
19
+ - `shopify-pre-pr`: Prepare a Shopify theme branch for pull request.
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,14 +1,14 @@
1
1
  ---
2
2
  name: shopify-lint
3
- version: 1.4.0
4
- description: Run Shopify CLI Theme Check and Theory Digital's bundled Liquid and JavaScript rules together, linting modified .js files and the JavaScript inside Liquid javascript blocks, 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, JavaScript, or theme changes, enforce Theory theme and vanilla JavaScript rules, or avoid surfacing pre-existing offenses from untouched files.
3
+ version: 1.6.0
4
+ description: Run Shopify CLI Theme Check and Theory Digital's bundled Liquid, JavaScript, and CSS rules together, linting modified .js and .css files plus the JavaScript and CSS inside Liquid javascript and stylesheet blocks, 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, JavaScript, CSS, or theme changes, enforce Theory theme, vanilla JavaScript, and stylesheet rules, flag Liquid written inside stylesheet or javascript blocks where Shopify never renders it, or avoid surfacing pre-existing offenses from untouched files.
5
5
  ---
6
6
 
7
7
  # Shopify Lint
8
8
 
9
9
  ## Setup
10
10
 
11
- The bundled `theme-check-theory` (Liquid checks) and `eslint-plugin-theory` (JavaScript rules) packages ship as TypeScript source only; their `node_modules` and compiled `dist/` are not committed. Run setup once before the first run (and after upgrading the skill):
11
+ The bundled `theme-check-theory` (Liquid checks), `eslint-plugin-theory` (JavaScript rules), and `stylelint-config-theory` (CSS rules) packages ship as TypeScript source only; their `node_modules` and compiled `dist/` are not committed. Run setup once before the first run (and after upgrading the skill):
12
12
 
13
13
  ```bash
14
14
  bash .agents/skills/shopify-lint/scripts/setup.sh
@@ -18,15 +18,18 @@ Installing with `td-ai-tools install --setup shopify-lint` runs this automatical
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
20
  2. Runs `npm install && npm run build` inside `eslint-plugin-theory/`, producing the `dist/` that the JavaScript lint runner imports.
21
- 3. 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.
22
- 4. 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.
21
+ 3. Runs `npm install && npm run build` inside `stylelint-config-theory/`, producing the `dist/` that the CSS lint runner imports.
22
+ 4. 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.
23
+ 5. 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.
23
24
 
24
- No ESLint configuration is written to the project root, and the theme's own ESLint config is never consulted — the branch lint is a gate, so a theme must not be able to silently disable it. Override the rules per project with `SHOPIFY_LINT_ESLINT_CONFIG=<path>` pointing at a module that exports a `createConfig` function.
25
+ No ESLint or Stylelint configuration is written to the project root, and the theme's own ESLint and Stylelint configs are never consulted — the branch lint is a gate, so a theme must not be able to silently disable it. A project `.stylelintignore` is ignored for the same reason. Override the rules per project with `SHOPIFY_LINT_ESLINT_CONFIG=<path>` or `SHOPIFY_LINT_STYLELINT_CONFIG=<path>`, each pointing at a module that exports a `createConfig` function.
25
26
 
26
- The Stop hooks run this skill's branch-scoped lint before handoff at `--fail-level warning`, so **every reported offense blocks handoff, warnings included** — both the Theory Liquid checks and the Theory JavaScript rules. Only advisory `info` offenses do not block; that severity is reserved for JavaScript parse errors inside Liquid, which masking cannot always avoid and the agent cannot reliably fix. The hooks return modified-file offenses to the agent and allow handoff after three unsuccessful fix rounds. They require `jq`, `python3`, `node`, and Shopify CLI to be available when the hook runs. Directly running `scripts/setup.sh` performs the package builds and Theme Check configuration; use `td-ai-tools install --setup` or `td-ai-tools update --setup` to apply the shared hook manifest as well.
27
+ The Stop hooks run this skill's branch-scoped lint before handoff at `--fail-level warning`, so **every reported offense blocks handoff, warnings included** — the Theory Liquid checks, the Theory JavaScript rules, and the CSS rules alike. Only advisory `info` offenses do not block, and nothing the bundled linters report is graded that way. The hooks return modified-file offenses to the agent and allow handoff after three unsuccessful fix rounds. They require `jq`, `python3`, `node`, and Shopify CLI to be available when the hook runs. Directly running `scripts/setup.sh` performs the package builds and Theme Check configuration; use `td-ai-tools install --setup` or `td-ai-tools update --setup` to apply the shared hook manifest as well.
27
28
 
28
29
  Keep these dependencies inside each bundled package's `node_modules`; do not install Node dependencies at the theme root.
29
30
 
31
+ Stylelint requires Node **>= 20.19**, which is the skill's effective floor.
32
+
30
33
  ## Usage
31
34
 
32
35
  Run the bundled Python script from the Shopify theme root:
@@ -35,28 +38,33 @@ Run the bundled Python script from the Shopify theme root:
35
38
  python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path .
36
39
  ```
37
40
 
38
- The script identifies files changed since the current branch's merge-base, adds staged, unstaged, and untracked files, then runs two linters over that set and merges their results into one report:
41
+ The script identifies files changed since the current branch's merge-base, adds staged, unstaged, and untracked files, then runs three linters over that set and merges their results into one report:
39
42
 
40
- - **Theme Check** — `shopify theme check --output json`. 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.
43
+ - **Theme Check** — `shopify theme check --output json`. 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. The generated `.theme-check.yml` also pins the upstream `StaticStylesheetAndJavascriptTags` to `error`, so Liquid written inside a `{% stylesheet %}` or `{% javascript %}` block always fails the lint.
41
44
  - **JavaScript** — ESLint's recommended rules plus the bundled `eslint-plugin-theory`, over modified `.js` and `.mjs` files *and* the JavaScript inside `{% javascript %}` blocks of modified `.liquid` files.
45
+ - **CSS** — `stylelint-config-standard` through the bundled `stylelint-config-theory`, over modified `.css` files *and* the CSS inside `{% stylesheet %}` blocks of modified `.liquid` files.
42
46
 
43
- Standalone JavaScript is linted only when its **filename** contains `td-` (matched case-insensitively, anywhere in the name, so `td-globals.js` and `theme-td-utils.js` both qualify while `vendor.js` does not). This keeps vendored and merchant-authored assets from being reported against Theory conventions they were never written to. The match is on the filename alone — a `td-` directory does not opt its contents in — and `*.min.js` is always skipped.
47
+ Standalone JavaScript and CSS are linted only when the **filename** contains `td-` (matched case-insensitively, anywhere in the name, so `td-globals.js` and `component-td-card.css` both qualify while `vendor.js` and `theme.css` do not). This keeps vendored and merchant-authored assets from being reported against Theory conventions they were never written to. The match is on the filename alone — a `td-` directory does not opt its contents in — and `*.min.js` and `*.min.css` are always skipped.
44
48
 
45
- The filter does **not** apply to Liquid: `{% javascript %}` blocks are linted in every modified `.liquid` file regardless of its name, because that JavaScript is Theory's either way.
49
+ The filter does **not** apply to Liquid: `{% javascript %}` and `{% stylesheet %}` blocks are linted in every modified `.liquid` file regardless of its name, because that code is Theory's either way. A `{% stylesheet 'scss' %}` block is skipped, because its body is Sass rather than CSS, as is any block commented out with `{% comment %}` or `{% raw %}`.
46
50
 
47
- Offenses in a Liquid block are reported at their real line and column in the `.liquid` file. Check codes say where they came from: `PascalCase` is Theme Check, `theory/…` is a Theory JavaScript rule, `eslint/…` is an upstream ESLint rule.
51
+ Offenses in a Liquid block are reported at their real line and column in the `.liquid` file. Check codes say where they came from: `PascalCase` is Theme Check, `theory/…` is a Theory JavaScript rule, `eslint/…` is an upstream ESLint rule, `stylelint/…` is an upstream Stylelint rule.
48
52
 
49
53
  `theory/guarded-custom-element-define` and `theory/no-unapproved-imports` are errors, as are genuine ESLint correctness failures; the convention rules (`theory/disconnected-callback-cleanup`, `theory/no-class-selectors`, `theory/centralize-selectors`, `theory/require-jsdoc`) are warnings. `theory/require-jsdoc` requires named classes and functions to have JSDoc comments, including typed `@param` and `@returns` tags. The severity distinction only affects `--fail-level`: the Stop hooks run at `--fail-level warning`, so warnings block handoff too. See `eslint-plugin-theory/README.md` for every rule and its blind spots.
50
54
 
51
- Liquid interpolation is masked out before the JavaScript is parsed. Control flow that straddles a JavaScript syntax boundary can defeat that, so parse errors inside `.liquid` are reported at `info` visible, but never failing a run that cannot reasonably be fixed.
55
+ For CSS, everything `stylelint-config-recommended` turns on is an error unknown properties and at-rules, duplicate declarations, invalid values, empty blocks and the notational conventions `stylelint-config-standard` adds on top are warnings. The naming-pattern rules (`selector-class-pattern`, `custom-property-pattern`, and their siblings) ship disabled because their kebab-case defaults reject Theory's BEM-style class names, and `no-descending-specificity` is demoted to a warning. Stylelint no longer checks formatting at all — its stylistic rules were removed in v15 and v16. See `stylelint-config-theory/README.md` for the full policy.
56
+
57
+ Liquid does not belong inside a `{% stylesheet %}` or `{% javascript %}` block: Shopify concatenates their contents into the compiled asset bundles without rendering them, so interpolation there is served to the browser as literal text. `StaticStylesheetAndJavascriptTags` reports every Liquid construct in one — output tags, control flow, and `{% comment %}`, which is not a comment there either — as an error at its real position. `{% style %}` and `{% script %}`, which Shopify does render, are untouched. Its one false positive is host-language nesting the Liquid parser reads as an output tag, such as `if (a) {{ b(); }}`; the JavaScript and CSS masks treat that as code and keep linting it.
58
+
59
+ Interpolation is still masked out before the JavaScript or CSS is parsed — an output tag becomes a `0` padded with blanks — so a block that still holds Liquid reports its real CSS and JavaScript offenses alongside the Liquid ones instead of collapsing into a single parse error. Because that Liquid is itself an offense, nothing is relaxed to accommodate the substitution: a `{% stylesheet %}` block is graded exactly as a standalone `.css` file is, and a parse error inside `.liquid` is a plain error. Expect a masked value such as `margin: {{ s.gap }}px` to draw a CSS offense as well as the Liquid one; both are fixed by taking the Liquid out.
52
60
 
53
61
  ## Workflow
54
62
 
55
- 1. Confirm `shopify`, `python3`, and `node` (>= 20) are available.
56
- 2. Run the bundled script instead of calling `shopify theme check` or `eslint` directly.
63
+ 1. Confirm `shopify`, `python3`, and `node` (>= 20.19) are available.
64
+ 2. Run the bundled script instead of calling `shopify theme check`, `eslint`, or `stylelint` directly.
57
65
  3. Treat exit code `0` as no failing offenses in modified files, `1` as filtered offenses at or above the fail level, and `2` as a Git, CLI, Node, or JSON-processing error.
58
66
  4. Fix every reported issue, warnings included, and rerun until the command passes. Run with `--fail-level warning` to match what the Stop hooks enforce. Do not fix offenses in untouched files unless the user expands the scope.
59
- 5. Keep bundled-package dependencies inside `theme-check-theory/node_modules` and `eslint-plugin-theory/node_modules`. Do not install Node dependencies at the theme root.
67
+ 5. Keep bundled-package dependencies inside `theme-check-theory/node_modules`, `eslint-plugin-theory/node_modules`, and `stylelint-config-theory/node_modules`. Do not install Node dependencies at the theme root.
60
68
 
61
69
  The default base is `origin/HEAD`, then `origin/main`, `main`, `origin/master`, or `master`. Override it when needed:
62
70
 
@@ -64,8 +72,8 @@ The default base is `origin/HEAD`, then `origin/main`, `main`, `origin/master`,
64
72
  python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path . --base-ref origin/develop
65
73
  ```
66
74
 
67
- Set `SHOPIFY_LINT_BASE_REF` for the same override in automation. Use `--format json` for machine-readable filtered output. The CLI still defaults to `--fail-level error`; the Stop hooks pass `--fail-level warning` explicitly, and `--fail-level info` additionally fails on advisory Liquid parse errors. When passing `--config <path>`, preserve the bundled package's `require` entry or the Theory checks will not load.
75
+ Set `SHOPIFY_LINT_BASE_REF` for the same override in automation. Use `--format json` for machine-readable filtered output. The CLI still defaults to `--fail-level error`; the Stop hooks pass `--fail-level warning` explicitly, and `--fail-level info` additionally fails on any advisory offense a Theme Check configuration grades that way. When passing `--config <path>`, preserve the bundled package's `require` entry or the Theory checks will not load.
68
76
 
69
- Pass `--skip-js` (or set `SHOPIFY_LINT_SKIP_JS=1`) to report Theme Check offenses only — useful when the JavaScript toolchain is unavailable. Node is not spawned at all when no `.js`, `.mjs`, or `.liquid` file changed.
77
+ Pass `--skip-js` (or set `SHOPIFY_LINT_SKIP_JS=1`) and `--skip-css` (or `SHOPIFY_LINT_SKIP_CSS=1`) to drop those linters — useful when the Node toolchain is unavailable. Passing both reports Theme Check offenses only. Node is not spawned for JavaScript when no `.js`, `.mjs`, or `.liquid` file changed, nor for CSS when no `.css` or `.liquid` file changed.
70
78
 
71
79
  Do not use Theme Check auto-correction through this workflow because it can modify untouched files before filtering.
@@ -159,20 +159,37 @@ being a literal, it introduces no identifier for `no-undef` to fire on.
159
159
  block into one bundle, so identifiers defined in another file are legitimately
160
160
  global.
161
161
 
162
- A file with no `{% javascript %}` block produces no blocks at all and is
163
- skipped.
162
+ A file with no `{% javascript %}` block produces no blocks at all and is skipped,
163
+ as is a file whose every block sits inside a `{% comment %}` or `{% raw %}`
164
+ region — a commented-out block is not code the storefront runs.
164
165
 
165
- **Known limit.** Liquid control flow that straddles a JavaScript syntax
166
- boundary cannot mask into valid JavaScript:
166
+ ### Liquid in a block is an offense, not a case to accommodate
167
+
168
+ Shopify does not render Liquid inside a `{% javascript %}` tag: the block is
169
+ concatenated into the theme's compiled JavaScript verbatim, so anything the mask
170
+ blanks out is already broken theme code. Theme Check's
171
+ `StaticStylesheetAndJavascriptTags` reports it as an **error**, at its exact
172
+ position in the `.liquid` file — see `theme-check-theory/README.md`. Masking it
173
+ anyway is what lets the author see the Liquid offense and the JavaScript offenses
174
+ in one round, rather than a parse error at the first `{{` standing in for
175
+ everything.
176
+
177
+ One case diverges: `if (a) {{ b(); }}` is valid JavaScript that the Liquid parser
178
+ reads as an output tag. The mask treats anything between `{{` and `}}` carrying a
179
+ brace or a semicolon as JavaScript and keeps linting it; the Theme Check offense
180
+ on that line is a false positive.
181
+
182
+ **Known limit.** Liquid control flow that straddles a JavaScript syntax boundary
183
+ cannot mask into valid JavaScript:
167
184
 
168
185
  ```liquid
169
186
  {% if a %}function f() {{% else %}function g() {{% endif %}}
170
187
  ```
171
188
 
172
- That produces a genuine parse error. The branch runner reports parse errors
173
- found in `.liquid` at `info` severity, with a note that the position may be
174
- approximate, so a masking limitation can never fail a run the author has no
175
- reasonable way to fix. Parse errors in standalone `.js` files remain errors.
189
+ That produces a genuine parse error, reported as an `error` like any other. It
190
+ used to be demoted to `info`, on the grounds that the author had no reasonable
191
+ way to fix it; the Liquid offense above now names the fix take the Liquid out
192
+ of the block.
176
193
 
177
194
  Note also that a rule reporting "at the top of the program" reports at line 1
178
195
  of the `.liquid` file rather than at the block.
@@ -187,6 +187,47 @@ describe('maskLiquid', () => {
187
187
  expect(masked).toContain('const b = 2;');
188
188
  });
189
189
 
190
+ it('skips a block that is commented out', () => {
191
+ const source = ['{% comment %}', '{% javascript %}', 'run();', '{% endjavascript %}', '{% endcomment %}', ''].join(
192
+ '\n',
193
+ );
194
+ expect(maskLiquid(source)).toBeNull();
195
+ });
196
+
197
+ it('skips a block inside a raw tag', () => {
198
+ const source = ['{% raw %}', '{% javascript %}', 'run();', '{% endjavascript %}', '{% endraw %}', ''].join('\n');
199
+ expect(maskLiquid(source)).toBeNull();
200
+ });
201
+
202
+ it('still lints a live block alongside a commented-out one', () => {
203
+ const masked =
204
+ maskLiquid(
205
+ [
206
+ '{% comment %}',
207
+ '{% javascript %}',
208
+ 'old();',
209
+ '{% endjavascript %}',
210
+ '{% endcomment %}',
211
+ '{% javascript %}',
212
+ 'live();',
213
+ '{% endjavascript %}',
214
+ '',
215
+ ].join('\n'),
216
+ ) ?? '';
217
+
218
+ expect(masked).toContain('live();');
219
+ expect(masked).not.toContain('old();');
220
+ });
221
+
222
+ it('keeps a block that only contains a comment tag', () => {
223
+ // The comment is inside the block, not the other way around: this is live
224
+ // JavaScript, and the comment tag itself is reported by Theme Check.
225
+ const masked = maskLiquid('{% javascript %}{% comment %}note{% endcomment %}run();{% endjavascript %}') ?? '';
226
+
227
+ expect(masked).toContain('run();');
228
+ expect(masked).not.toContain('note');
229
+ });
230
+
190
231
  it('blanks Liquid control flow inside the block', () => {
191
232
  const masked = maskLiquid('{% javascript %}{% if a %}run();{% endif %}{% endjavascript %}');
192
233
  expect(masked).toContain('run();');
@@ -7,6 +7,13 @@
7
7
  * already correct for the original `.liquid` file. That identity mapping is
8
8
  * the whole point: the ESLint processor can then remap offenses with no
9
9
  * arithmetic at all.
10
+ *
11
+ * Shopify does not render Liquid inside a `{% javascript %}` tag, so anything
12
+ * masked out of a block body is already broken theme code, and Theme Check's
13
+ * `StaticStylesheetAndJavascriptTags` reports it as an error. Masking it anyway
14
+ * is what lets the author see the Liquid offense and the JavaScript offenses in
15
+ * the same round, instead of a parse error at the first `{{` standing in for
16
+ * both.
10
17
  */
11
18
 
12
19
  /** Matches a `{% javascript %}` … `{% endjavascript %}` pair, capturing the body. */
@@ -25,6 +32,10 @@ const LIQUID_TAG = /\{%[\s\S]*?%\}/g;
25
32
  * Characters that never appear inside a Liquid output tag but do appear in the
26
33
  * JavaScript constructs that can be mistaken for one — most importantly a
27
34
  * nested block, `if (a) {{ b(); }}`.
35
+ *
36
+ * Theme Check's `StaticStylesheetAndJavascriptTags` applies no such guard, so it
37
+ * does report that construct as Liquid. Keeping the guard here means the block
38
+ * is still linted as the JavaScript it is.
28
39
  */
29
40
  const NOT_LIQUID_OUTPUT = /[{};]/;
30
41
 
@@ -51,6 +62,26 @@ function overlaps(intervals: Interval[], start: number, end: number): boolean {
51
62
  return intervals.some((interval) => start < interval.end && end > interval.start);
52
63
  }
53
64
 
65
+ /**
66
+ * Whether an offset falls inside one of the intervals.
67
+ *
68
+ * Used to decide whether a `{% javascript %}` tag is itself commented out.
69
+ * Containment, not overlap, is the question: a block that *contains* a
70
+ * `{% comment %}` is still live JavaScript, and testing for overlap would throw
71
+ * it away.
72
+ */
73
+ function contains(intervals: Interval[], offset: number): boolean {
74
+ return intervals.some((interval) => offset >= interval.start && offset < interval.end);
75
+ }
76
+
77
+ /** Every `{% comment %}` / `{% raw %}` region in the source, in order. */
78
+ function rawContentRegions(source: string): Interval[] {
79
+ return [...source.matchAll(RAW_CONTENT_TAG)].map((match) => ({
80
+ start: match.index,
81
+ end: match.index + match[0].length,
82
+ }));
83
+ }
84
+
54
85
  /**
55
86
  * Mask the Liquid constructs inside one `{% javascript %}` body, in place.
56
87
  *
@@ -101,8 +132,11 @@ function maskLiquidInBody(source: string, out: string[], start: number, end: num
101
132
  * Reduce a Liquid source to its `{% javascript %}` content, preserving every
102
133
  * character position.
103
134
  *
104
- * Returns `null` when the file contains no complete `{% javascript %}` block,
105
- * which lets the processor skip the file entirely.
135
+ * Returns `null` when the file contains no complete `{% javascript %}` block
136
+ * that is live code, which lets the processor skip the file entirely. A block
137
+ * that sits inside a `{% comment %}` or `{% raw %}` region does not count:
138
+ * commented-out blocks are common mid-migration, and linting one would report
139
+ * offenses in code the storefront never runs.
106
140
  */
107
141
  export function maskLiquid(source: string): string | null {
108
142
  // Indexed per UTF-16 code unit, not per code point, so that offsets stay
@@ -112,9 +146,14 @@ export function maskLiquid(source: string): string | null {
112
146
  out[index] = source[index] === '\n' ? '\n' : ' ';
113
147
  }
114
148
 
149
+ const rawRegions = rawContentRegions(source);
115
150
  let found = false;
116
151
 
117
152
  for (const match of source.matchAll(JAVASCRIPT_BLOCK)) {
153
+ if (contains(rawRegions, match.index)) {
154
+ // A commented-out or raw-quoted block is not live JavaScript.
155
+ continue;
156
+ }
118
157
  found = true;
119
158
  const bodyStart = match.index + match[1].length;
120
159
  const bodyEnd = bodyStart + match[2].length;
@@ -4,9 +4,8 @@
4
4
  # offenses in files modified on the current branch.
5
5
  #
6
6
  # Runs at --fail-level warning so every reported offense blocks, not just
7
- # errors. Advisory `info` offenses stay non-blocking: they are only used for
8
- # JavaScript parse errors inside Liquid, where masking interpolation out cannot
9
- # always yield parseable code and the agent has no reliable fix.
7
+ # errors. Advisory `info` offenses stay non-blocking; nothing the linters report
8
+ # is graded that way today.
10
9
  set -u
11
10
 
12
11
  input=$(cat)
@@ -4,9 +4,8 @@
4
4
  # branch so Codex can fix them before trying to stop again.
5
5
  #
6
6
  # Runs at --fail-level warning so every reported offense blocks, not just
7
- # errors. Advisory `info` offenses stay non-blocking: they are only used for
8
- # JavaScript parse errors inside Liquid, where masking interpolation out cannot
9
- # always yield parseable code and the agent has no reliable fix.
7
+ # errors. Advisory `info` offenses stay non-blocking; nothing the linters report
8
+ # is graded that way today.
10
9
  set -u
11
10
 
12
11
  input=$(cat)
@@ -5,8 +5,9 @@ set -euo pipefail
5
5
  #
6
6
  # Run automatically by the installer (`--setup`) from the installed skill
7
7
  # directory, or manually with `bash scripts/setup.sh`. It:
8
- # 1. Builds the bundled `theme-check-theory` custom-check package and the
9
- # bundled `eslint-plugin-theory` JavaScript rules (their node_modules and
8
+ # 1. Builds the bundled `theme-check-theory` custom-check package, the
9
+ # bundled `eslint-plugin-theory` JavaScript rules, and the bundled
10
+ # `stylelint-config-theory` CSS configuration (their node_modules and
10
11
  # dist/ are intentionally not committed).
11
12
  # 2. Writes a `.theme-check.yml` at the project root that wires the bundled
12
13
  # checks into Shopify CLI Theme Check.
@@ -16,6 +17,7 @@ set -euo pipefail
16
17
  SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
17
18
  PKG_DIR="$SKILL_DIR/theme-check-theory"
18
19
  ESLINT_PKG_DIR="$SKILL_DIR/eslint-plugin-theory"
20
+ STYLELINT_PKG_DIR="$SKILL_DIR/stylelint-config-theory"
19
21
 
20
22
  # 1. Build the bundled custom-check package.
21
23
  echo "shopify-lint setup: building theme-check-theory in $PKG_DIR"
@@ -32,6 +34,16 @@ cd "$ESLINT_PKG_DIR"
32
34
  npm install --no-audit --no-fund
33
35
  npm run build
34
36
 
37
+ # 1c. Build the bundled Stylelint configuration used to lint modified CSS.
38
+ #
39
+ # Like 1b, this runs before the install-layout guard below. No configuration is
40
+ # written to the project root for it: the branch lint is a gate, so a theme must
41
+ # not be able to silently disable it.
42
+ echo "shopify-lint setup: building stylelint-config-theory in $STYLELINT_PKG_DIR"
43
+ cd "$STYLELINT_PKG_DIR"
44
+ npm install --no-audit --no-fund
45
+ npm run build
46
+
35
47
  # 2. Write the project-root .theme-check.yml.
36
48
  #
37
49
  # The installed layout is <project>/<.agents|.claude>/skills/shopify-lint, so
@@ -63,6 +75,13 @@ HardcodedText:
63
75
  enabled: true
64
76
  severity: warning
65
77
 
78
+ # Upstream check, pinned here so the branch lint always fails on it: Shopify
79
+ # serves `{% stylesheet %}` and `{% javascript %}` bodies verbatim, so Liquid
80
+ # written inside one ships to the browser as literal text.
81
+ StaticStylesheetAndJavascriptTags:
82
+ enabled: true
83
+ severity: error
84
+
66
85
  UnusedSectionSettings:
67
86
  enabled: false
68
87
  severity: warning