td-ai-tools 1.2.0 → 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.
- package/package.json +1 -1
- package/skills/README.md +3 -0
- package/skills/client-overview/SKILL.md +93 -0
- package/skills/client-overview/agents/openai.yaml +4 -0
- package/skills/client-overview/scripts/branch_client_overview_context.py +317 -0
- package/skills/debugging-ios-webkit/SKILL.md +60 -0
- package/skills/debugging-ios-webkit/references/device.md +62 -0
- package/skills/debugging-ios-webkit/references/playwright.md +60 -0
- package/skills/debugging-ios-webkit/references/simulator.md +76 -0
- package/skills/debugging-ios-webkit/scripts/device_eval.py +62 -0
- package/skills/debugging-ios-webkit/scripts/device_experiment.py +99 -0
- package/skills/debugging-ios-webkit/scripts/device_snapshot.py +62 -0
- package/skills/record-changes/SKILL.md +3 -3
- package/skills/shopify-lint/SKILL.md +50 -0
- package/skills/shopify-lint/agents/openai.yaml +4 -0
- package/skills/shopify-lint/scripts/setup.sh +60 -0
- package/skills/shopify-lint/scripts/shopify_lint.py +259 -0
- package/skills/shopify-lint/tests/test_shopify_lint.py +137 -0
- package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +14 -0
- package/skills/shopify-lint/theme-check-theory/README.md +123 -0
- package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +8 -0
- package/skills/shopify-lint/theme-check-theory/package-lock.json +1947 -0
- package/skills/shopify-lint/theme-check-theory/package.json +44 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.test.ts +198 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.ts +143 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.test.ts +137 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.ts +64 -0
- package/skills/shopify-lint/theme-check-theory/src/index.test.ts +20 -0
- package/skills/shopify-lint/theme-check-theory/src/index.ts +11 -0
- package/skills/shopify-lint/theme-check-theory/src/test-utils.ts +31 -0
- package/skills/shopify-lint/theme-check-theory/src/utils/ast.ts +126 -0
- package/skills/shopify-lint/theme-check-theory/tsconfig.build.json +10 -0
- package/skills/shopify-lint/theme-check-theory/tsconfig.json +15 -0
- package/skills/shopify-lint/theme-check-theory/vitest.config.ts +11 -0
|
@@ -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,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
|
+
|