td-ai-tools 1.2.1 → 1.2.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 (23) hide show
  1. package/package.json +1 -1
  2. package/skills/README.md +1 -0
  3. package/skills/shopify-lint/SKILL.md +50 -0
  4. package/skills/shopify-lint/agents/openai.yaml +4 -0
  5. package/skills/shopify-lint/scripts/setup.sh +60 -0
  6. package/skills/shopify-lint/scripts/shopify_lint.py +259 -0
  7. package/skills/shopify-lint/tests/test_shopify_lint.py +137 -0
  8. package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +14 -0
  9. package/skills/shopify-lint/theme-check-theory/README.md +123 -0
  10. package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +8 -0
  11. package/skills/shopify-lint/theme-check-theory/package-lock.json +1947 -0
  12. package/skills/shopify-lint/theme-check-theory/package.json +44 -0
  13. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.test.ts +198 -0
  14. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.ts +143 -0
  15. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.test.ts +137 -0
  16. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.ts +64 -0
  17. package/skills/shopify-lint/theme-check-theory/src/index.test.ts +20 -0
  18. package/skills/shopify-lint/theme-check-theory/src/index.ts +11 -0
  19. package/skills/shopify-lint/theme-check-theory/src/test-utils.ts +31 -0
  20. package/skills/shopify-lint/theme-check-theory/src/utils/ast.ts +126 -0
  21. package/skills/shopify-lint/theme-check-theory/tsconfig.build.json +10 -0
  22. package/skills/shopify-lint/theme-check-theory/tsconfig.json +15 -0
  23. package/skills/shopify-lint/theme-check-theory/vitest.config.ts +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {
package/skills/README.md CHANGED
@@ -18,6 +18,7 @@
18
18
  - `pull-request-statamic`: Generates GitHub pull request descriptions for Statamic and Laravel development by analyzing git diffs and…
19
19
  - `record-changes`: Update `docs/changes.md` by summarizing the current branch against the primary development branch.
20
20
  - `shopify-cli`: Shopify CLI workflows for theme development.
21
+ - `shopify-lint`: 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.
21
22
  - `stylesheet-migration`: Migrate Shopify Liquid `{% stylesheet %}` blocks into theme CSS assets using bundled Python scripts.
22
23
  - `td-js-vanilla-rules`: Theory Digital vanilla JavaScript standards for Shopify theme work.
23
24
  - `td-review`: Run parallel code review agents on a PR (including TD theme compliance) and produce a synthesized findings…
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: shopify-lint
3
+ version: 1.0.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.
5
+ ---
6
+
7
+ # Shopify Lint
8
+
9
+ ## Setup
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):
12
+
13
+ ```bash
14
+ bash .agents/skills/shopify-lint/scripts/setup.sh
15
+ ```
16
+
17
+ Installing with `td-ai-tools install --setup shopify-lint` runs this automatically. The script:
18
+
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`.
21
+
22
+ Keep these dependencies inside `theme-check-theory/node_modules`; do not install Node dependencies at the theme root.
23
+
24
+ ## Usage
25
+
26
+ Run the bundled Python script from the Shopify theme root:
27
+
28
+ ```bash
29
+ python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path .
30
+ ```
31
+
32
+ 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.
33
+
34
+ ## Workflow
35
+
36
+ 1. Confirm `shopify` and `python3` are available.
37
+ 2. Run the bundled script instead of calling `shopify theme check` directly.
38
+ 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.
39
+ 4. Fix reported issues and rerun until the command passes. Do not fix offenses in untouched files unless the user expands the scope.
40
+ 5. Keep custom-check dependencies inside `theme-check-theory/node_modules`. Do not install Node dependencies at the theme root.
41
+
42
+ The default base is `origin/HEAD`, then `origin/main`, `main`, `origin/master`, or `master`. Override it when needed:
43
+
44
+ ```bash
45
+ python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path . --base-ref origin/develop
46
+ ```
47
+
48
+ 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.
49
+
50
+ Do not use Theme Check auto-correction through this workflow because it can modify untouched files before filtering.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Shopify Lint"
3
+ short_description: "Lint modified theme files with Theory checks"
4
+ default_prompt: "Use $shopify-lint to run Theme Check for files modified on my current branch."
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # shopify-lint setup
5
+ #
6
+ # Run automatically by the installer (`--setup`) from the installed skill
7
+ # directory, or manually with `bash scripts/setup.sh`. It:
8
+ # 1. Builds the bundled `theme-check-theory` custom-check package
9
+ # (its node_modules and dist/ are intentionally not committed).
10
+ # 2. Writes a `.theme-check.yml` at the project root that wires the bundled
11
+ # checks into Shopify CLI Theme Check.
12
+
13
+ SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
14
+ PKG_DIR="$SKILL_DIR/theme-check-theory"
15
+
16
+ # 1. Build the bundled custom-check package.
17
+ echo "shopify-lint setup: building theme-check-theory in $PKG_DIR"
18
+ cd "$PKG_DIR"
19
+ npm install
20
+ npm run build
21
+
22
+ # 2. Generate the project-root .theme-check.yml.
23
+ #
24
+ # The installed layout is <project>/<.agents|.claude>/skills/shopify-lint, so
25
+ # the project root is three levels above the skill directory. Guard against
26
+ # running outside that layout (e.g. from the catalog repo) to avoid writing the
27
+ # config into an unexpected directory.
28
+ TARGET_DIR="$(cd "$SKILL_DIR/../.." && pwd)"
29
+ TARGET_BASE="$(basename "$TARGET_DIR")"
30
+ if [[ "$TARGET_BASE" != ".agents" && "$TARGET_BASE" != ".claude" ]]; then
31
+ echo "shopify-lint setup: unrecognized install layout ($TARGET_DIR); skipping .theme-check.yml generation." >&2
32
+ exit 0
33
+ fi
34
+
35
+ PROJECT_ROOT="$(dirname "$TARGET_DIR")"
36
+ THEME_CHECK_FILE="$PROJECT_ROOT/.theme-check.yml"
37
+
38
+ if [[ -f "$THEME_CHECK_FILE" ]]; then
39
+ echo "shopify-lint setup: $THEME_CHECK_FILE already exists; leaving it unchanged."
40
+ echo " Ensure its 'require:' list includes ./.agents/skills/shopify-lint/theme-check-theory"
41
+ exit 0
42
+ fi
43
+
44
+ cat > "$THEME_CHECK_FILE" <<'YAML'
45
+ extends:
46
+ - theme-check:recommended
47
+
48
+ require:
49
+ - ./.agents/skills/shopify-lint/theme-check-theory
50
+
51
+ UnusedSectionSettings:
52
+ enabled: true
53
+ severity: warning
54
+
55
+ UnguardedTextSetting:
56
+ enabled: true
57
+ severity: warning
58
+ YAML
59
+
60
+ echo "shopify-lint setup: wrote $THEME_CHECK_FILE"
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env python3
2
+ """Run Shopify Theme Check and report offenses only for branch-modified files."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+
15
+ SEVERITY_RANK = {"error": 0, "warning": 1, "info": 2}
16
+
17
+
18
+ class ShopifyLintError(RuntimeError):
19
+ """Raised when Git or Shopify CLI cannot produce filterable results."""
20
+
21
+
22
+ def run_command(command: list[str], cwd: Path) -> subprocess.CompletedProcess[str]:
23
+ return subprocess.run(
24
+ command,
25
+ cwd=cwd,
26
+ capture_output=True,
27
+ text=True,
28
+ check=False,
29
+ )
30
+
31
+
32
+ def git_output(repo_root: Path, *arguments: str) -> str:
33
+ result = run_command(["git", *arguments], repo_root)
34
+ if result.returncode != 0:
35
+ detail = result.stderr.strip() or result.stdout.strip()
36
+ raise ShopifyLintError(f"Git command failed: {detail}")
37
+ return result.stdout
38
+
39
+
40
+ def find_repo_root(theme_path: Path) -> Path:
41
+ result = run_command(["git", "rev-parse", "--show-toplevel"], theme_path)
42
+ if result.returncode != 0:
43
+ raise ShopifyLintError("Theme path must be inside a Git repository.")
44
+ return Path(result.stdout.strip()).resolve()
45
+
46
+
47
+ def discover_base_ref(repo_root: Path) -> str:
48
+ configured = os.environ.get("SHOPIFY_LINT_BASE_REF")
49
+ if configured:
50
+ return configured
51
+
52
+ remote_head = run_command(
53
+ ["git", "symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
54
+ repo_root,
55
+ )
56
+ candidates = [remote_head.stdout.strip(), "origin/main", "main", "origin/master", "master"]
57
+ for candidate in candidates:
58
+ if not candidate:
59
+ continue
60
+ result = run_command(
61
+ ["git", "rev-parse", "--verify", "--quiet", f"{candidate}^{{commit}}"],
62
+ repo_root,
63
+ )
64
+ if result.returncode == 0:
65
+ return candidate
66
+
67
+ raise ShopifyLintError(
68
+ "Could not determine the base branch. Pass --base-ref or set "
69
+ "SHOPIFY_LINT_BASE_REF."
70
+ )
71
+
72
+
73
+ def parse_nul_paths(output: str) -> set[str]:
74
+ return {path for path in output.split("\0") if path}
75
+
76
+
77
+ def collect_changed_files(repo_root: Path, base_ref: str) -> set[str]:
78
+ merge_base = git_output(repo_root, "merge-base", "HEAD", base_ref).strip()
79
+ committed = git_output(
80
+ repo_root,
81
+ "diff",
82
+ "--name-only",
83
+ "--diff-filter=ACMR",
84
+ "-z",
85
+ f"{merge_base}..HEAD",
86
+ )
87
+ working_tree = git_output(
88
+ repo_root,
89
+ "diff",
90
+ "--name-only",
91
+ "--diff-filter=ACMR",
92
+ "-z",
93
+ "HEAD",
94
+ )
95
+ untracked = git_output(
96
+ repo_root,
97
+ "ls-files",
98
+ "--others",
99
+ "--exclude-standard",
100
+ "-z",
101
+ )
102
+ return parse_nul_paths(committed) | parse_nul_paths(working_tree) | parse_nul_paths(
103
+ untracked
104
+ )
105
+
106
+
107
+ def run_theme_check(
108
+ theme_path: Path, fail_level: str, config: Path | None
109
+ ) -> list[dict[str, Any]]:
110
+ command = [
111
+ "shopify",
112
+ "theme",
113
+ "check",
114
+ "--output",
115
+ "json",
116
+ "--path",
117
+ str(theme_path),
118
+ "--fail-level",
119
+ fail_level,
120
+ ]
121
+ if config:
122
+ command.extend(["--config", str(config)])
123
+
124
+ try:
125
+ result = subprocess.run(
126
+ command,
127
+ capture_output=True,
128
+ text=True,
129
+ check=False,
130
+ )
131
+ except FileNotFoundError as error:
132
+ raise ShopifyLintError("Shopify CLI is not installed or is not on PATH.") from error
133
+
134
+ try:
135
+ reports = json.loads(result.stdout)
136
+ except json.JSONDecodeError as error:
137
+ detail = result.stderr.strip() or result.stdout.strip() or "no output"
138
+ raise ShopifyLintError(f"Shopify Theme Check failed: {detail}") from error
139
+
140
+ if not isinstance(reports, list):
141
+ raise ShopifyLintError("Shopify Theme Check returned an unexpected JSON shape.")
142
+ return reports
143
+
144
+
145
+ def repo_relative_path(path: str, repo_root: Path) -> str | None:
146
+ candidate = Path(path)
147
+ if not candidate.is_absolute():
148
+ candidate = repo_root / candidate
149
+ try:
150
+ return candidate.resolve().relative_to(repo_root.resolve()).as_posix()
151
+ except ValueError:
152
+ return None
153
+
154
+
155
+ def filter_reports(
156
+ reports: list[dict[str, Any]], changed_files: set[str], repo_root: Path
157
+ ) -> list[dict[str, Any]]:
158
+ return [
159
+ report
160
+ for report in reports
161
+ if repo_relative_path(str(report.get("path", "")), repo_root) in changed_files
162
+ ]
163
+
164
+
165
+ def normalized_reports(
166
+ reports: list[dict[str, Any]], repo_root: Path
167
+ ) -> list[dict[str, Any]]:
168
+ normalized = []
169
+ for report in reports:
170
+ item = dict(report)
171
+ item["path"] = repo_relative_path(str(report.get("path", "")), repo_root)
172
+ normalized.append(item)
173
+ return normalized
174
+
175
+
176
+ def render_json(reports: list[dict[str, Any]], repo_root: Path) -> str:
177
+ return json.dumps(normalized_reports(reports, repo_root), indent=2)
178
+
179
+
180
+ def render_text(reports: list[dict[str, Any]], repo_root: Path) -> str:
181
+ if not reports:
182
+ return "No Theme Check offenses found in files modified on this branch."
183
+
184
+ lines = []
185
+ offense_count = 0
186
+ for report in normalized_reports(reports, repo_root):
187
+ lines.append(str(report["path"]))
188
+ for offense in report.get("offenses", []):
189
+ offense_count += 1
190
+ severity = str(offense.get("severity", "unknown")).upper()
191
+ check = offense.get("check", "UnknownCheck")
192
+ row = offense.get("start_row", 0)
193
+ column = offense.get("start_column", 0)
194
+ message = offense.get("message", "")
195
+ lines.append(f" {severity} {check} {row}:{column} {message}".rstrip())
196
+ lines.append("")
197
+
198
+ file_label = "file" if len(reports) == 1 else "files"
199
+ offense_label = "offense" if offense_count == 1 else "offenses"
200
+ lines.append(
201
+ f"Theme Check found {offense_count} {offense_label} in "
202
+ f"{len(reports)} modified {file_label}."
203
+ )
204
+ return "\n".join(lines)
205
+
206
+
207
+ def exit_code_for(reports: list[dict[str, Any]], fail_level: str) -> int:
208
+ threshold = SEVERITY_RANK[fail_level]
209
+ for report in reports:
210
+ for offense in report.get("offenses", []):
211
+ severity = str(offense.get("severity", "info")).lower()
212
+ if SEVERITY_RANK.get(severity, 0) <= threshold:
213
+ return 1
214
+ return 0
215
+
216
+
217
+ def build_parser() -> argparse.ArgumentParser:
218
+ parser = argparse.ArgumentParser(
219
+ description="Run Shopify Theme Check for files modified on the current branch."
220
+ )
221
+ parser.add_argument("--path", type=Path, default=Path.cwd(), help="Theme root path.")
222
+ parser.add_argument("--base-ref", help="Base branch or commit used for branch changes.")
223
+ parser.add_argument(
224
+ "--fail-level",
225
+ choices=tuple(SEVERITY_RANK),
226
+ default="error",
227
+ help="Minimum filtered severity that returns exit code 1.",
228
+ )
229
+ parser.add_argument("--config", type=Path, help="Theme Check config path.")
230
+ parser.add_argument(
231
+ "--format", choices=("text", "json"), default="text", help="Filtered output format."
232
+ )
233
+ return parser
234
+
235
+
236
+ def main() -> int:
237
+ arguments = build_parser().parse_args()
238
+ theme_path = arguments.path.resolve()
239
+ try:
240
+ repo_root = find_repo_root(theme_path)
241
+ base_ref = arguments.base_ref or discover_base_ref(repo_root)
242
+ changed_files = collect_changed_files(repo_root, base_ref)
243
+ reports = run_theme_check(theme_path, arguments.fail_level, arguments.config)
244
+ filtered = filter_reports(reports, changed_files, repo_root)
245
+ except ShopifyLintError as error:
246
+ print(f"shopify-lint: {error}", file=sys.stderr)
247
+ return 2
248
+
249
+ output = (
250
+ render_json(filtered, repo_root)
251
+ if arguments.format == "json"
252
+ else render_text(filtered, repo_root)
253
+ )
254
+ print(output)
255
+ return exit_code_for(filtered, arguments.fail_level)
256
+
257
+
258
+ if __name__ == "__main__":
259
+ raise SystemExit(main())
@@ -0,0 +1,137 @@
1
+ """Tests for the branch-scoped Shopify Theme Check runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+
14
+ SKILL_ROOT = Path(__file__).resolve().parents[1]
15
+ sys.path.insert(0, str(SKILL_ROOT / "scripts"))
16
+
17
+ import shopify_lint # noqa: E402
18
+
19
+
20
+ class ChangedFilesTests(unittest.TestCase):
21
+ def run_git(self, root: Path, *args: str) -> None:
22
+ subprocess.run(["git", *args], cwd=root, check=True, capture_output=True, text=True)
23
+
24
+ def test_collects_committed_staged_unstaged_and_untracked_files(self) -> None:
25
+ with tempfile.TemporaryDirectory() as directory:
26
+ root = Path(directory)
27
+ self.run_git(root, "init", "-q", "-b", "main")
28
+ self.run_git(root, "config", "user.email", "test@example.com")
29
+ self.run_git(root, "config", "user.name", "Test User")
30
+
31
+ for name in ("committed.liquid", "staged.liquid", "unstaged.liquid"):
32
+ (root / name).write_text("initial\n")
33
+ self.run_git(root, "add", ".")
34
+ self.run_git(root, "commit", "-qm", "initial")
35
+ self.run_git(root, "switch", "-qc", "feature")
36
+
37
+ (root / "committed.liquid").write_text("branch change\n")
38
+ self.run_git(root, "add", "committed.liquid")
39
+ self.run_git(root, "commit", "-qm", "branch change")
40
+ (root / "staged.liquid").write_text("staged change\n")
41
+ self.run_git(root, "add", "staged.liquid")
42
+ (root / "unstaged.liquid").write_text("unstaged change\n")
43
+ (root / "untracked.liquid").write_text("untracked\n")
44
+
45
+ changed = shopify_lint.collect_changed_files(root, "main")
46
+
47
+ self.assertEqual(
48
+ changed,
49
+ {
50
+ "committed.liquid",
51
+ "staged.liquid",
52
+ "unstaged.liquid",
53
+ "untracked.liquid",
54
+ },
55
+ )
56
+
57
+
58
+ class ThemeCheckTests(unittest.TestCase):
59
+ def test_runs_shopify_cli_with_json_output(self) -> None:
60
+ completed = subprocess.CompletedProcess([], 1, stdout="[]", stderr="")
61
+
62
+ with patch.object(shopify_lint.subprocess, "run", return_value=completed) as run:
63
+ result = shopify_lint.run_theme_check(Path("/theme"), "warning", None)
64
+
65
+ self.assertEqual(result, [])
66
+ run.assert_called_once_with(
67
+ [
68
+ "shopify",
69
+ "theme",
70
+ "check",
71
+ "--output",
72
+ "json",
73
+ "--path",
74
+ "/theme",
75
+ "--fail-level",
76
+ "warning",
77
+ ],
78
+ capture_output=True,
79
+ text=True,
80
+ check=False,
81
+ )
82
+
83
+ def test_filters_reports_to_changed_repo_relative_paths(self) -> None:
84
+ root = Path("/repo")
85
+ reports = [
86
+ {
87
+ "path": "/repo/sections/td-changed.liquid",
88
+ "offenses": [{"check": "ChangedCheck", "severity": "error"}],
89
+ },
90
+ {
91
+ "path": "/repo/sections/unchanged.liquid",
92
+ "offenses": [{"check": "IgnoredCheck", "severity": "error"}],
93
+ },
94
+ ]
95
+
96
+ filtered = shopify_lint.filter_reports(
97
+ reports, {"sections/td-changed.liquid"}, root
98
+ )
99
+
100
+ self.assertEqual(filtered, [reports[0]])
101
+
102
+ def test_json_output_contains_only_changed_file_reports(self) -> None:
103
+ reports = [
104
+ {
105
+ "path": "/repo/sections/td-changed.liquid",
106
+ "offenses": [{"check": "LiquidHTMLSyntaxError", "severity": "error"}],
107
+ }
108
+ ]
109
+
110
+ rendered = shopify_lint.render_json(reports, Path("/repo"))
111
+
112
+ self.assertEqual(
113
+ json.loads(rendered),
114
+ [
115
+ {
116
+ "path": "sections/td-changed.liquid",
117
+ "offenses": [
118
+ {"check": "LiquidHTMLSyntaxError", "severity": "error"}
119
+ ],
120
+ }
121
+ ],
122
+ )
123
+
124
+ def test_fail_level_is_recomputed_from_filtered_offenses(self) -> None:
125
+ warning_report = [
126
+ {
127
+ "path": "changed.liquid",
128
+ "offenses": [{"check": "RemoteAsset", "severity": "warning"}],
129
+ }
130
+ ]
131
+
132
+ self.assertEqual(shopify_lint.exit_code_for(warning_report, "error"), 0)
133
+ self.assertEqual(shopify_lint.exit_code_for(warning_report, "warning"), 1)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ unittest.main()
@@ -0,0 +1,14 @@
1
+ extends:
2
+ - theme-check:recommended
3
+
4
+ require:
5
+ - ./node_modules/theme-check-theory
6
+
7
+ UnusedSectionSettings:
8
+ enabled: true
9
+ severity: warning
10
+
11
+ UnguardedTextSetting:
12
+ enabled: true
13
+ severity: warning
14
+
@@ -0,0 +1,123 @@
1
+ # theme-check-theory
2
+
3
+ Theory Digital's custom [Shopify Theme Check](https://shopify.dev/docs/storefronts/themes/tools/theme-check)
4
+ rules — house standards enforced on top of `theme-check:recommended`.
5
+
6
+ Built for the TypeScript Theme Check engine (`@shopify/theme-check-common` v3)
7
+ integrated into Shopify CLI. It is not compatible with the archived Ruby
8
+ `Shopify/theme-check` project.
9
+
10
+ ## Checks
11
+
12
+ | Code | Severity | What it catches |
13
+ | --- | --- | --- |
14
+ | `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`. |
16
+
17
+ ### UnusedSectionSettings
18
+
19
+ This single-file check collects every `id` under `settings` and
20
+ `blocks[].settings` in the schema, then collects static
21
+ `section.settings.<id>` and `block.settings.<id>` references in the Liquid. It
22
+ reports every declared setting that has no matching reference.
23
+
24
+ If a file uses dynamic access such as `section.settings[key]`, or passes an
25
+ entire settings object elsewhere, the check cannot prove which setting is
26
+ unused. It therefore stays silent for the whole file to avoid false positives.
27
+
28
+ ### UnguardedTextSetting
29
+
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.
34
+
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.
38
+
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`.
42
+
43
+ ```yaml
44
+ UnguardedTextSetting:
45
+ enabled: true
46
+ severity: warning
47
+ textualSettingTypes:
48
+ - text
49
+ - textarea
50
+ - image_picker
51
+ ```
52
+
53
+ ## Usage in a project
54
+
55
+ Install the module as a development dependency:
56
+
57
+ ```bash
58
+ npm install --save-dev theme-check-theory
59
+ ```
60
+
61
+ Reference its CommonJS entry point from the project's `.theme-check.yml`:
62
+
63
+ ```yaml
64
+ extends:
65
+ - theme-check:recommended
66
+
67
+ require:
68
+ - ./node_modules/theme-check-theory
69
+
70
+ UnusedSectionSettings:
71
+ enabled: true
72
+ severity: warning
73
+
74
+ UnguardedTextSetting:
75
+ enabled: true
76
+ severity: warning
77
+ ```
78
+
79
+ See [`.theme-check.example.yml`](.theme-check.example.yml) for a copyable
80
+ configuration. The custom-check-only settings are also available in
81
+ [`configs/recommended.yml`](configs/recommended.yml).
82
+
83
+ Run Theme Check through Shopify CLI:
84
+
85
+ ```bash
86
+ shopify theme check
87
+ ```
88
+
89
+ ## CI
90
+
91
+ Start new checks at `warning` or `info` so they surface without blocking.
92
+ Review the findings across existing themes, then promote them to `error` once
93
+ the themes are clean.
94
+
95
+ ```yaml
96
+ # .github/workflows/theme-check.yml
97
+ - run: npm ci
98
+ - run: npx shopify theme check --fail-level error
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ npm install
105
+ npm test
106
+ npm run build
107
+ ```
108
+
109
+ Tests use `runLiquidCheck` and `check` from
110
+ `@shopify/theme-check-common/dist/test/test-helper`, so Vitest exercises the
111
+ real Theme Check parser and traversal rather than a mocked AST.
112
+
113
+ ## Adding a check
114
+
115
+ 1. Create `src/checks/<name>.ts` exporting a `LiquidCheckDefinition`.
116
+ 2. Add it to the `checks` array in `src/index.ts`.
117
+ 3. Add `src/checks/<name>.test.ts` with true-positive and false-positive cases.
118
+ 4. Add it to `configs/recommended.yml`.
119
+
120
+ 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
+
@@ -0,0 +1,8 @@
1
+ UnusedSectionSettings:
2
+ enabled: true
3
+ severity: warning
4
+
5
+ UnguardedTextSetting:
6
+ enabled: true
7
+ severity: warning
8
+