td-ai-tools 1.2.8 → 1.2.10

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 (46) hide show
  1. package/package.json +1 -1
  2. package/skills/README.md +1 -1
  3. package/skills/shopify-lint/SKILL.md +32 -14
  4. package/skills/shopify-lint/eslint-plugin-theory/README.md +198 -0
  5. package/skills/shopify-lint/eslint-plugin-theory/bin/lint-js.mjs +126 -0
  6. package/skills/shopify-lint/eslint-plugin-theory/eslint.config.mjs +4 -0
  7. package/skills/shopify-lint/eslint-plugin-theory/package-lock.json +2643 -0
  8. package/skills/shopify-lint/eslint-plugin-theory/package.json +46 -0
  9. package/skills/shopify-lint/eslint-plugin-theory/src/config.ts +98 -0
  10. package/skills/shopify-lint/eslint-plugin-theory/src/index.test.ts +79 -0
  11. package/skills/shopify-lint/eslint-plugin-theory/src/index.ts +50 -0
  12. package/skills/shopify-lint/eslint-plugin-theory/src/integration.test.ts +112 -0
  13. package/skills/shopify-lint/eslint-plugin-theory/src/liquid/mask.test.ts +195 -0
  14. package/skills/shopify-lint/eslint-plugin-theory/src/liquid/mask.ts +126 -0
  15. package/skills/shopify-lint/eslint-plugin-theory/src/processors/liquid.test.ts +32 -0
  16. package/skills/shopify-lint/eslint-plugin-theory/src/processors/liquid.ts +32 -0
  17. package/skills/shopify-lint/eslint-plugin-theory/src/rules/centralize-selectors.test.ts +42 -0
  18. package/skills/shopify-lint/eslint-plugin-theory/src/rules/centralize-selectors.ts +71 -0
  19. package/skills/shopify-lint/eslint-plugin-theory/src/rules/disconnected-callback-cleanup.test.ts +126 -0
  20. package/skills/shopify-lint/eslint-plugin-theory/src/rules/disconnected-callback-cleanup.ts +206 -0
  21. package/skills/shopify-lint/eslint-plugin-theory/src/rules/guarded-custom-element-define.test.ts +67 -0
  22. package/skills/shopify-lint/eslint-plugin-theory/src/rules/guarded-custom-element-define.ts +142 -0
  23. package/skills/shopify-lint/eslint-plugin-theory/src/rules/no-class-selectors.test.ts +69 -0
  24. package/skills/shopify-lint/eslint-plugin-theory/src/rules/no-class-selectors.ts +91 -0
  25. package/skills/shopify-lint/eslint-plugin-theory/src/rules/no-unapproved-imports.test.ts +68 -0
  26. package/skills/shopify-lint/eslint-plugin-theory/src/rules/no-unapproved-imports.ts +97 -0
  27. package/skills/shopify-lint/eslint-plugin-theory/src/rules/optional.ts +181 -0
  28. package/skills/shopify-lint/eslint-plugin-theory/src/rules/require-jsdoc.test.ts +36 -0
  29. package/skills/shopify-lint/eslint-plugin-theory/src/rules/require-jsdoc.ts +85 -0
  30. package/skills/shopify-lint/eslint-plugin-theory/src/utils/ast.ts +82 -0
  31. package/skills/shopify-lint/eslint-plugin-theory/src/utils/selectors.ts +105 -0
  32. package/skills/shopify-lint/eslint-plugin-theory/tsconfig.build.json +9 -0
  33. package/skills/shopify-lint/eslint-plugin-theory/tsconfig.json +14 -0
  34. package/skills/shopify-lint/eslint-plugin-theory/vitest.config.ts +13 -0
  35. package/skills/shopify-lint/hooks/claude-stop.sh +10 -5
  36. package/skills/shopify-lint/hooks/codex-stop.sh +10 -5
  37. package/skills/shopify-lint/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  38. package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
  39. package/skills/shopify-lint/scripts/setup.sh +13 -2
  40. package/skills/shopify-lint/scripts/shopify_lint.py +203 -4
  41. package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
  42. package/skills/shopify-lint/tests/test_shopify_lint.py +449 -0
  43. package/skills/shopify-lint/theme-check-theory/README.md +5 -3
  44. package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.test.ts +42 -0
  45. package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.ts +103 -31
  46. package/skills/visual-regression/scripts/__pycache__/visual_regression.cpython-312.pyc +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.8",
3
+ "version": "1.2.10",
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, including warnings for hard-coded
19
+ - `shopify-lint`: Run Shopify CLI Theme Check and Theory Digital's bundled Liquid and JavaScript rules together, linting
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.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.
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.
5
5
  ---
6
6
 
7
7
  # Shopify Lint
8
8
 
9
9
  ## Setup
10
10
 
11
- The bundled `theme-check-theory` custom-check package ships as TypeScript source only; its `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) 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):
12
12
 
13
13
  ```bash
14
14
  bash .agents/skills/shopify-lint/scripts/setup.sh
@@ -17,12 +17,15 @@ 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. Setup replaces any existing file with the skill's current configuration so installs and updates cannot retain stale or incomplete settings.
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.
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.
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.
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.
24
25
 
25
- Keep these dependencies inside `theme-check-theory/node_modules`; do not install Node dependencies at the theme root.
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
+
28
+ Keep these dependencies inside each bundled package's `node_modules`; do not install Node dependencies at the theme root.
26
29
 
27
30
  ## Usage
28
31
 
@@ -32,15 +35,28 @@ Run the bundled Python script from the Shopify theme root:
32
35
  python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path .
33
36
  ```
34
37
 
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.
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:
39
+
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.
41
+ - **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.
42
+
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.
44
+
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.
46
+
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.
48
+
49
+ `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
+
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.
36
52
 
37
53
  ## Workflow
38
54
 
39
- 1. Confirm `shopify` and `python3` are available.
40
- 2. Run the bundled script instead of calling `shopify theme check` directly.
41
- 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, or JSON-processing error.
42
- 4. Fix reported issues and rerun until the command passes. Do not fix offenses in untouched files unless the user expands the scope.
43
- 5. Keep custom-check dependencies inside `theme-check-theory/node_modules`. Do not install Node dependencies at the theme root.
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.
57
+ 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
+ 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.
44
60
 
45
61
  The default base is `origin/HEAD`, then `origin/main`, `main`, `origin/master`, or `master`. Override it when needed:
46
62
 
@@ -48,6 +64,8 @@ The default base is `origin/HEAD`, then `origin/main`, `main`, `origin/master`,
48
64
  python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path . --base-ref origin/develop
49
65
  ```
50
66
 
51
- Set `SHOPIFY_LINT_BASE_REF` for the same override in automation. Use `--format json` for machine-readable filtered output and `--fail-level warning` or `--fail-level info` for stricter runs. When passing `--config <path>`, preserve the bundled package's `require` entry or the Theory checks will not load.
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.
68
+
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.
52
70
 
53
71
  Do not use Theme Check auto-correction through this workflow because it can modify untouched files before filtering.
@@ -0,0 +1,198 @@
1
+ # eslint-plugin-theory
2
+
3
+ Theory Digital's custom ESLint rules for Shopify theme JavaScript, plus a
4
+ processor that lints the JavaScript inside Liquid `{% javascript %}` blocks.
5
+
6
+ It is bundled with the `shopify-lint` skill and is normally run through that
7
+ skill's branch-scoped runner rather than directly. Like `theme-check-theory`,
8
+ it ships as TypeScript source: `npm install && npm run build` produces the
9
+ `dist/` that `bin/lint-js.mjs` imports.
10
+
11
+ ## Rules
12
+
13
+ | Rule | Default | Catches |
14
+ | --- | --- | --- |
15
+ | `theory/guarded-custom-element-define` | **error** | `customElements.define` without a `customElements.get` guard |
16
+ | `theory/no-unapproved-imports` | **error** | Third-party JavaScript outside the allowlist (Swiper, Embla) |
17
+ | `theory/disconnected-callback-cleanup` | warning | Listeners, timers, and observers never torn down |
18
+ | `theory/no-class-selectors` | warning | Classes used as JavaScript behaviour hooks |
19
+ | `theory/centralize-selectors` | warning | The same selector literal repeated inline |
20
+ | `theory/require-jsdoc` | warning | Named classes and functions without JSDoc documentation and explicit types |
21
+ | `theory/respect-reduced-motion` | off | Motion with no `prefers-reduced-motion` check |
22
+ | `theory/aria-expanded-sync` | off | Disclosure state toggled without `aria-expanded` |
23
+ | `theory/escape-to-close` | off | Dismissible components that ignore Escape |
24
+ | `theory/require-block-select-events` | off | Block-aware components missing the editor hooks |
25
+
26
+ Errors fail the branch lint at its default `--fail-level error`; warnings are
27
+ reported but advisory, matching how the bundled Theme Check rules behave.
28
+
29
+ ### theory/guarded-custom-element-define
30
+
31
+ Re-registering a tag name throws, and that exception takes out every later
32
+ script on the page. Shopify renders section files more than once — in the theme
33
+ editor, and through section rendering — so an unguarded define is a real defect.
34
+
35
+ Accepted guards, all matched against the same tag expression as the define
36
+ (a string literal and a constant holding one are both handled):
37
+
38
+ ```js
39
+ if (!customElements.get('td-hero')) { customElements.define('td-hero', TdHero); }
40
+ customElements.get('td-hero') || customElements.define('td-hero', TdHero);
41
+ if (customElements.get('td-hero')) return; // earlier in the same scope
42
+ ```
43
+
44
+ **Blind spot:** a guard whose tag expression is written differently from the
45
+ define's (`customElements.get(TAG)` guarding `define('td-hero', …)`) is not
46
+ recognised and will be reported.
47
+
48
+ ### theory/no-unapproved-imports
49
+
50
+ Theory themes are vanilla JavaScript; Swiper and Embla are the sanctioned
51
+ libraries. Relative and absolute specifiers are always allowed — they are the
52
+ theme's own files. Covers `import`, dynamic `import()`, `export … from`, and
53
+ `require()`.
54
+
55
+ The default allowlist is `['swiper', 'embla-carousel*']`. A trailing `*`
56
+ matches a family of sibling packages, which Embla needs because it ships each
57
+ plugin as its own package (`embla-carousel-autoplay`, `embla-carousel-fade`, …)
58
+ rather than as subpaths of one package the way Swiper does. Scoping is
59
+ respected: `@someone/embla-carousel-extra` is a different package and is still
60
+ reported.
61
+
62
+ Extend the allowlist per project with `{ allow: ['gsap'] }`, or
63
+ `{ allow: ['@acme/*'] }` for a family.
64
+
65
+ **Blind spot:** a non-literal specifier (`import(name)`) cannot be resolved
66
+ statically and is skipped.
67
+
68
+ ### theory/disconnected-callback-cleanup
69
+
70
+ Applies to custom-element-shaped classes only: those extending something
71
+ ending in `Element`, or defining a `connectedCallback`.
72
+
73
+ Matching is by **category**, never by the identity of a particular listener or
74
+ handle — identity matching is what makes this class of rule unusable in
75
+ practice. Registering in a helper method counts, and a single
76
+ `AbortController.abort()` satisfies the listener category wholesale.
77
+
78
+ | Registered | Satisfied by |
79
+ | --- | --- |
80
+ | `addEventListener` (without `{ once: true }`) | `removeEventListener`, or any `.abort()` |
81
+ | `setInterval` | `clearInterval` |
82
+ | `setTimeout` (only when the handle is stored) | `clearTimeout` |
83
+ | `requestAnimationFrame` (only when the handle is stored) | `cancelAnimationFrame` |
84
+ | `new *Observer` | `.disconnect()` or `.unobserve()` |
85
+
86
+ **Blind spot:** it verifies that a category is torn down, not that *every*
87
+ instance of it is. A component removing one of two listeners passes.
88
+
89
+ ### theory/no-class-selectors
90
+
91
+ Merchants edit markup, so a class rename in the theme editor silently breaks
92
+ JavaScript that queried it. Behaviour hooks belong on `data-td-*` attributes;
93
+ classes are for visual state via `classList`.
94
+
95
+ Checks literal selectors passed to `querySelector`, `querySelectorAll`,
96
+ `closest`, and `matches`, always flags `getElementsByClassName`, and looks
97
+ inside objects named `SELECTORS`/`selectors` so moving a class selector into a
98
+ constant does not hide it. The selector is scanned rather than regex-matched,
99
+ so a dot inside an attribute value (`[data-src=".jpg"]`) or a CSS-escaped dot
100
+ is not mistaken for a class.
101
+
102
+ Record an approved exception with a comment on or above the line, as the
103
+ ruleset's exception policy requires:
104
+
105
+ ```js
106
+ /* td-selector-exception: Swiper owns this markup */
107
+ this.querySelector('.swiper-slide');
108
+ ```
109
+
110
+ **Blind spot:** selectors built at runtime are skipped entirely.
111
+
112
+ ### theory/centralize-selectors
113
+
114
+ Reports a selector literal used inline `threshold` times or more (default `2`),
115
+ excluding the first use. The threshold is what makes the rule shippable: a
116
+ selector used once is perfectly readable, and flagging it would be noise. A
117
+ call that takes a reference to a constant is already centralized and never
118
+ counted.
119
+
120
+ ### theory/require-jsdoc
121
+
122
+ Requires a JSDoc comment immediately above every named class, function,
123
+ function-valued variable, and non-constructor class method. Function comments
124
+ must include a typed `@param {Type}` for every parameter and a typed
125
+ `@returns {Type}` (use `{void}` when the function has no return value).
126
+
127
+ Anonymous callbacks are intentionally excluded: documenting event handlers and
128
+ array callbacks inline would add noise without improving their public contract.
129
+
130
+ ### Opt-in rules
131
+
132
+ The four default-off rules each encode a real Theory standard that is not
133
+ reliably observable from the JavaScript AST: motion frequently lives in CSS,
134
+ ARIA is often set by a separate render step, Escape is often handled by a
135
+ parent or natively by `<dialog>`, and "block-dependent" is a property of the
136
+ Liquid template rather than the script. Enable them deliberately, per project,
137
+ through `SHOPIFY_LINT_ESLINT_CONFIG`.
138
+
139
+ ## Liquid handling
140
+
141
+ `.liquid` files run through the `theory/liquid` processor. Rather than
142
+ extracting each `{% javascript %}` block, it returns the **whole file with
143
+ every non-JavaScript character replaced by a space**, newlines untouched.
144
+ Positions are therefore unchanged, and remapping offenses back to the `.liquid`
145
+ file is the identity function — there is no offset arithmetic to get wrong.
146
+
147
+ Inside a block:
148
+
149
+ | Construct | Becomes |
150
+ | --- | --- |
151
+ | `{% comment %}` / `{% raw %}` bodies | spaces |
152
+ | `{{ … }}` | `0` followed by spaces |
153
+ | `{% … %}` | spaces |
154
+
155
+ `0` stands in for a Liquid output because it is valid wherever an output can
156
+ appear — a value, an argument, a property key, a template substitution — and,
157
+ being a literal, it introduces no identifier for `no-undef` to fire on.
158
+ `no-undef` is off for Liquid blocks anyway: Shopify concatenates every theme
159
+ block into one bundle, so identifiers defined in another file are legitimately
160
+ global.
161
+
162
+ A file with no `{% javascript %}` block produces no blocks at all and is
163
+ skipped.
164
+
165
+ **Known limit.** Liquid control flow that straddles a JavaScript syntax
166
+ boundary cannot mask into valid JavaScript:
167
+
168
+ ```liquid
169
+ {% if a %}function f() {{% else %}function g() {{% endif %}}
170
+ ```
171
+
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.
176
+
177
+ Note also that a rule reporting "at the top of the program" reports at line 1
178
+ of the `.liquid` file rather than at the block.
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ npm install
184
+ npm run build
185
+ npm test
186
+ ```
187
+
188
+ ## Adding a rule
189
+
190
+ 1. Create `src/rules/<name>.ts` exporting a `Rule.RuleModule`.
191
+ 2. Register it in the `rules` map in `src/index.ts`.
192
+ 3. Add `src/rules/<name>.test.ts` with `RuleTester` cases. Include *valid*
193
+ cases for every false positive you can think of — those are the cases that
194
+ decide whether the rule is usable.
195
+ 4. Give it a severity in `THEORY_RULES` (or `THEORY_OPTIONAL_RULES`) in
196
+ `src/config.ts`. `src/index.test.ts` fails if a registered rule has no
197
+ configured severity.
198
+ 5. Document it in the table and in a section above, including its blind spots.
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Lint runner spawned by scripts/shopify_lint.py.
4
+ *
5
+ * Reads `{"cwd": "<theme root>", "files": ["<absolute path>", ...]}` on stdin
6
+ * and writes ESLint's native results array to stdout.
7
+ *
8
+ * Exit codes:
9
+ * 0 results were produced (however many problems they contain)
10
+ * 1 the lint could not run
11
+ *
12
+ * Lint problems never change the exit code — the branch runner owns the
13
+ * fail-level policy.
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { pathToFileURL } from 'node:url';
18
+
19
+ const MODULE_SYNTAX = /^\s*(?:import|export)\s/m;
20
+
21
+ function fail(message) {
22
+ process.stderr.write(`theory js lint: ${message}\n`);
23
+ process.exit(1);
24
+ }
25
+
26
+ async function readStdin() {
27
+ const chunks = [];
28
+ for await (const chunk of process.stdin) {
29
+ chunks.push(chunk);
30
+ }
31
+ return Buffer.concat(chunks).toString('utf8');
32
+ }
33
+
34
+ /**
35
+ * Theme assets are usually plain scripts, but some themes ship ES modules.
36
+ * Parsing a script as a module (or the reverse) produces spurious syntax
37
+ * errors, so files are partitioned and linted by two ESLint instances.
38
+ */
39
+ function usesModuleSyntax(file) {
40
+ if (file.endsWith('.mjs')) {
41
+ return true;
42
+ }
43
+ if (!file.endsWith('.js')) {
44
+ return false;
45
+ }
46
+ try {
47
+ return MODULE_SYNTAX.test(readFileSync(file, 'utf8'));
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+
53
+ async function loadConfig(createConfig) {
54
+ const override = process.env.SHOPIFY_LINT_ESLINT_CONFIG;
55
+ if (!override) {
56
+ return createConfig;
57
+ }
58
+ const imported = await import(pathToFileURL(override).href);
59
+ const custom = imported.createConfig ?? imported.default;
60
+ if (typeof custom !== 'function') {
61
+ fail(`config at ${override} must export a createConfig function or a default function.`);
62
+ }
63
+ return custom;
64
+ }
65
+
66
+ async function main() {
67
+ let payload;
68
+ try {
69
+ payload = JSON.parse(await readStdin());
70
+ } catch (error) {
71
+ fail(`could not parse the request on stdin: ${error.message}`);
72
+ }
73
+
74
+ const files = Array.isArray(payload?.files) ? payload.files : [];
75
+ if (files.length === 0) {
76
+ process.stdout.write('[]');
77
+ return;
78
+ }
79
+
80
+ let ESLint;
81
+ let createConfig;
82
+ try {
83
+ ({ ESLint } = await import('eslint'));
84
+ ({ createConfig } = await import('../dist/config.js'));
85
+ } catch (error) {
86
+ fail(
87
+ 'dependencies are not installed or the package is not built. ' +
88
+ `Run: bash <skill>/scripts/setup.sh (${error.message})`,
89
+ );
90
+ }
91
+
92
+ const buildConfig = await loadConfig(createConfig);
93
+ const cwd = payload.cwd ?? process.cwd();
94
+
95
+ const moduleFiles = [];
96
+ const scriptFiles = [];
97
+ for (const file of files) {
98
+ (usesModuleSyntax(file) ? moduleFiles : scriptFiles).push(file);
99
+ }
100
+
101
+ const results = [];
102
+ for (const [sourceType, group] of [
103
+ ['script', scriptFiles],
104
+ ['module', moduleFiles],
105
+ ]) {
106
+ if (group.length === 0) {
107
+ continue;
108
+ }
109
+ const eslint = new ESLint({
110
+ cwd,
111
+ // Never consult the theme's own ESLint configuration: the branch lint is
112
+ // a gate, so a theme must not be able to silently disable it.
113
+ overrideConfigFile: true,
114
+ overrideConfig: buildConfig({ sourceType }),
115
+ errorOnUnmatchedPattern: false,
116
+ warnIgnored: false,
117
+ });
118
+ results.push(...(await eslint.lintFiles(group)));
119
+ }
120
+
121
+ process.stdout.write(JSON.stringify(results));
122
+ }
123
+
124
+ main().catch((error) => {
125
+ fail(error?.stack ?? String(error));
126
+ });
@@ -0,0 +1,4 @@
1
+ // Flat config entry point for editors and for `npx eslint` inside a theme.
2
+ // The branch runner (bin/lint-js.mjs) imports `createConfig` directly instead,
3
+ // so that it can select the source type per file.
4
+ export { default } from './dist/config.js';