td-ai-tools 1.2.5 → 1.2.8
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 +1 -1
- package/skills/shopify-lint/SKILL.md +4 -4
- package/skills/shopify-lint/agents/openai.yaml +1 -1
- package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/scripts/setup.sh +19 -9
- package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +16 -1
- package/skills/shopify-lint/theme-check-theory/README.md +95 -18
- package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +16 -1
- package/skills/shopify-lint/theme-check-theory/package-lock.json +2 -2
- package/skills/shopify-lint/theme-check-theory/package.json +1 -1
- package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.test.ts +87 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.ts +161 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.test.ts +147 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.ts +243 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metafield.test.ts +76 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metafield.ts +49 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metaobject.test.ts +77 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-metaobject.ts +42 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-nullable-setting.test.ts +301 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-nullable-setting.ts +129 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.ts +1 -1
- package/skills/shopify-lint/theme-check-theory/src/index.test.ts +17 -4
- package/skills/shopify-lint/theme-check-theory/src/index.ts +20 -6
- package/skills/shopify-lint/theme-check-theory/src/utils/null-guard.ts +79 -0
- package/skills/shopify-lint/theme-check-theory/src/utils/nullable-output-check.ts +90 -0
- package/skills/visual-regression/scripts/__pycache__/visual_regression.cpython-312.pyc +0 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.test.ts +0 -198
- package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.ts +0 -143
package/package.json
CHANGED
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
|
|
19
|
+
- `shopify-lint`: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks, including warnings for hard-coded…
|
|
20
20
|
- `stylesheet-migration`: Migrate Shopify Liquid `{% stylesheet %}` blocks into theme CSS assets using bundled Python scripts.
|
|
21
21
|
- `td-js-vanilla-rules`: Theory Digital vanilla JavaScript standards for Shopify theme work.
|
|
22
22
|
- `td-review`: Run parallel code review agents on a PR (including TD theme compliance) and produce a synthesized findings…
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: shopify-lint
|
|
3
|
-
version: 1.
|
|
4
|
-
description: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on offenses in files modified on the current Git branch. Use when Codex needs to lint a Shopify theme, validate branch-scoped Liquid or theme changes, enforce Theory theme rules, or avoid surfacing pre-existing Theme Check offenses from untouched files.
|
|
3
|
+
version: 1.3.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.
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Shopify Lint
|
|
@@ -17,7 +17,7 @@ bash .agents/skills/shopify-lint/scripts/setup.sh
|
|
|
17
17
|
Installing with `td-ai-tools install --setup shopify-lint` runs this automatically. The script:
|
|
18
18
|
|
|
19
19
|
1. Runs `npm install && npm run build` inside `theme-check-theory/`, producing `dist/index.js` — the CommonJS entry point the root `.theme-check.yml` requires.
|
|
20
|
-
2. Writes a `.theme-check.yml` at the project root wiring in the bundled checks.
|
|
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
21
|
3. Through the installer's shared project-hook setup, installs matching Claude and Codex Stop hooks and merges their registrations into `.claude/settings.json` and `.codex/hooks.json` without replacing unrelated settings or hooks. Re-running setup updates the Shopify Lint hook in place instead of duplicating it.
|
|
22
22
|
|
|
23
23
|
The Stop hooks run this skill's branch-scoped lint before handoff, return modified-file offenses to the agent, and allow handoff after three unsuccessful fix rounds. They require `jq`, `python3`, and Shopify CLI to be available when the hook runs. Directly running `scripts/setup.sh` performs the package build and Theme Check configuration; use `td-ai-tools install --setup` or `td-ai-tools update --setup` to apply the shared hook manifest as well.
|
|
@@ -32,7 +32,7 @@ Run the bundled Python script from the Shopify theme root:
|
|
|
32
32
|
python3 .agents/skills/shopify-lint/scripts/shopify_lint.py --path .
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The script runs `shopify theme check --output json`, identifies files changed since the current branch's merge-base, adds staged, unstaged, and untracked files, and emits only reports whose paths are in that set. The root `.theme-check.yml` directly requires the bundled `theme-check-theory` package from this skill directory.
|
|
35
|
+
The script runs `shopify theme check --output json`, identifies files changed since the current branch's merge-base, adds staged, unstaged, and untracked files, and emits only reports whose paths are in that set. The root `.theme-check.yml` directly requires the bundled `theme-check-theory` package from this skill directory. `DisallowedScriptOrStyleTag` reports 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.
|
|
36
36
|
|
|
37
37
|
## Workflow
|
|
38
38
|
|
|
Binary file
|
|
@@ -21,7 +21,7 @@ cd "$PKG_DIR"
|
|
|
21
21
|
npm install
|
|
22
22
|
npm run build
|
|
23
23
|
|
|
24
|
-
# 2.
|
|
24
|
+
# 2. Write the project-root .theme-check.yml.
|
|
25
25
|
#
|
|
26
26
|
# The installed layout is <project>/<.agents|.claude>/skills/shopify-lint, so
|
|
27
27
|
# the project root is three levels above the skill directory. Guard against
|
|
@@ -37,12 +37,6 @@ fi
|
|
|
37
37
|
PROJECT_ROOT="$(dirname "$TARGET_DIR")"
|
|
38
38
|
THEME_CHECK_FILE="$PROJECT_ROOT/.theme-check.yml"
|
|
39
39
|
|
|
40
|
-
if [[ -f "$THEME_CHECK_FILE" ]]; then
|
|
41
|
-
echo "shopify-lint setup: $THEME_CHECK_FILE already exists; leaving it unchanged."
|
|
42
|
-
echo " Ensure its 'require:' list includes ./.agents/skills/shopify-lint/theme-check-theory"
|
|
43
|
-
exit 0
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
40
|
cat > "$THEME_CHECK_FILE" <<'YAML'
|
|
47
41
|
extends:
|
|
48
42
|
- theme-check:recommended
|
|
@@ -50,13 +44,29 @@ extends:
|
|
|
50
44
|
require:
|
|
51
45
|
- ./.agents/skills/shopify-lint/theme-check-theory
|
|
52
46
|
|
|
47
|
+
DisallowedScriptOrStyleTag:
|
|
48
|
+
enabled: true
|
|
49
|
+
severity: warning
|
|
50
|
+
|
|
51
|
+
HardcodedText:
|
|
52
|
+
enabled: true
|
|
53
|
+
severity: warning
|
|
54
|
+
|
|
53
55
|
UnusedSectionSettings:
|
|
56
|
+
enabled: false
|
|
57
|
+
severity: warning
|
|
58
|
+
|
|
59
|
+
UnguardedNullableSetting:
|
|
60
|
+
enabled: true
|
|
61
|
+
severity: warning
|
|
62
|
+
|
|
63
|
+
UnguardedMetafield:
|
|
54
64
|
enabled: true
|
|
55
65
|
severity: warning
|
|
56
66
|
|
|
57
|
-
|
|
67
|
+
UnguardedMetaobject:
|
|
58
68
|
enabled: true
|
|
59
69
|
severity: warning
|
|
60
70
|
YAML
|
|
61
71
|
|
|
62
|
-
echo "shopify-lint setup: wrote $THEME_CHECK_FILE"
|
|
72
|
+
echo "shopify-lint setup: wrote $THEME_CHECK_FILE (replacing any existing configuration)"
|
|
Binary file
|
|
@@ -4,11 +4,26 @@ extends:
|
|
|
4
4
|
require:
|
|
5
5
|
- ./node_modules/theme-check-theory
|
|
6
6
|
|
|
7
|
+
DisallowedScriptOrStyleTag:
|
|
8
|
+
enabled: true
|
|
9
|
+
severity: warning
|
|
10
|
+
|
|
11
|
+
HardcodedText:
|
|
12
|
+
enabled: true
|
|
13
|
+
severity: warning
|
|
14
|
+
|
|
7
15
|
UnusedSectionSettings:
|
|
16
|
+
enabled: false
|
|
17
|
+
severity: warning
|
|
18
|
+
|
|
19
|
+
UnguardedNullableSetting:
|
|
8
20
|
enabled: true
|
|
9
21
|
severity: warning
|
|
10
22
|
|
|
11
|
-
|
|
23
|
+
UnguardedMetafield:
|
|
12
24
|
enabled: true
|
|
13
25
|
severity: warning
|
|
14
26
|
|
|
27
|
+
UnguardedMetaobject:
|
|
28
|
+
enabled: true
|
|
29
|
+
severity: warning
|
|
@@ -11,8 +11,37 @@ integrated into Shopify CLI. It is not compatible with the archived Ruby
|
|
|
11
11
|
|
|
12
12
|
| Code | Severity | What it catches |
|
|
13
13
|
| --- | --- | --- |
|
|
14
|
+
| `DisallowedScriptOrStyleTag` | warning | An inline executable `<script>` or any `<style>` tag in a Liquid file. |
|
|
15
|
+
| `HardcodedText` | warning | Rendered hard-coded storefront copy that should use a dynamic content source. |
|
|
14
16
|
| `UnusedSectionSettings` | warning | A setting declared in `{% schema %}` that is never referenced in the file. |
|
|
15
|
-
| `
|
|
17
|
+
| `UnguardedNullableSetting` | warning | A potentially blank setting output with `{{ }}` and no presence guard (`{% if %}`/`{% unless %}`) or `\| default`. |
|
|
18
|
+
| `UnguardedMetafield` | warning | A metafield output with no presence guard or `\| default`. |
|
|
19
|
+
| `UnguardedMetaobject` | warning | A metaobject entry output with no presence guard or `\| default`. |
|
|
20
|
+
|
|
21
|
+
### DisallowedScriptOrStyleTag
|
|
22
|
+
|
|
23
|
+
HTML `<script>` and `<style>` tags keep JavaScript and CSS embedded in Liquid
|
|
24
|
+
files. Move their contents to theme assets or the appropriate Shopify Liquid
|
|
25
|
+
tags instead. Scripts with a non-empty `src` attribute and scripts declared as
|
|
26
|
+
JSON data, including `application/json` and `application/ld+json`, are allowed.
|
|
27
|
+
HTML comments and similarly named elements such as `<script-template>` are
|
|
28
|
+
ignored.
|
|
29
|
+
|
|
30
|
+
### HardcodedText
|
|
31
|
+
|
|
32
|
+
Rendered storefront copy should come from a section setting, translation key,
|
|
33
|
+
metafield, or metaobject. The check reports text nodes, direct quoted output
|
|
34
|
+
with `{{ }}` or `{% echo %}`, quoted `default` filter fallbacks, hard-coded
|
|
35
|
+
`alt`, `title`, `placeholder`, and ARIA text attributes, and labels in the
|
|
36
|
+
`value` attribute of button-like inputs. It ignores technical attributes such
|
|
37
|
+
as classes and IDs, Liquid logic strings, comments, punctuation-only fragments,
|
|
38
|
+
and text made entirely of HTML character references.
|
|
39
|
+
|
|
40
|
+
Content inside `{% stylesheet %}`, `{% javascript %}`, and `{% schema %}` is
|
|
41
|
+
ignored. A quoted translation key passed through `t` or `translate` is also
|
|
42
|
+
accepted. Short alphabetic tokens and copyright years remain reportable because
|
|
43
|
+
the check cannot reliably distinguish user-facing copy from currency codes,
|
|
44
|
+
units, or other intentional literals.
|
|
16
45
|
|
|
17
46
|
### UnusedSectionSettings
|
|
18
47
|
|
|
@@ -25,31 +54,62 @@ If a file uses dynamic access such as `section.settings[key]`, or passes an
|
|
|
25
54
|
entire settings object elsewhere, the check cannot prove which setting is
|
|
26
55
|
unused. It therefore stays silent for the whole file to avoid false positives.
|
|
27
56
|
|
|
28
|
-
|
|
57
|
+
This check is disabled by default. Enable it explicitly when a theme's section
|
|
58
|
+
settings are expected to be fully traceable within each file:
|
|
29
59
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
60
|
+
```yaml
|
|
61
|
+
UnusedSectionSettings:
|
|
62
|
+
enabled: true
|
|
63
|
+
severity: warning
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### UnguardedNullableSetting
|
|
34
67
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
68
|
+
Many settings can return an empty, `blank`, or `nil` value when a merchant
|
|
69
|
+
leaves them unset or their selected resource is no longer available. Outputting
|
|
70
|
+
them unguarded is a recurring bug source. Each output is evaluated independently.
|
|
71
|
+
It is considered guarded when its active `if`, `unless`, `elsif`, or `case`
|
|
72
|
+
branch tests that setting, or when that output uses a `default` filter. A guard
|
|
73
|
+
or default on one invocation does not suppress offenses for unguarded
|
|
74
|
+
invocations elsewhere in the file.
|
|
38
75
|
|
|
39
|
-
The setting types
|
|
40
|
-
|
|
41
|
-
`
|
|
76
|
+
The setting types checked are configurable with the `nullableSettingTypes`
|
|
77
|
+
option. The defaults are `number`, `text`, `textarea`, `article`,
|
|
78
|
+
`article_list`, `blog`, `collection`, `collection_list`, `image_picker`,
|
|
79
|
+
`link_list`, `metaobject`, `metaobject_list`, `page`, `product`,
|
|
80
|
+
`product_list`, `richtext`, `inline_richtext`, `html`, `liquid`, `url`,
|
|
81
|
+
`video`, and `video_url`. Color-related settings are excluded because an unset
|
|
82
|
+
color can resolve to a non-null fallback such as `transparent`.
|
|
42
83
|
|
|
43
84
|
```yaml
|
|
44
|
-
|
|
85
|
+
UnguardedNullableSetting:
|
|
45
86
|
enabled: true
|
|
46
87
|
severity: warning
|
|
47
|
-
|
|
88
|
+
nullableSettingTypes:
|
|
48
89
|
- text
|
|
49
90
|
- textarea
|
|
50
91
|
- image_picker
|
|
51
92
|
```
|
|
52
93
|
|
|
94
|
+
### UnguardedMetafield and UnguardedMetaobject
|
|
95
|
+
|
|
96
|
+
[Metafields](https://shopify.dev/docs/api/liquid/objects/metafield) are
|
|
97
|
+
identified from the static `resource.metafields.namespace.key` access path,
|
|
98
|
+
and [metaobjects](https://shopify.dev/docs/api/liquid/objects/metaobject) from
|
|
99
|
+
`metaobjects.type.handle`. Outputting the object itself or any descendant, such
|
|
100
|
+
as a metafield's `value` or a metaobject's field, requires a guard on the same
|
|
101
|
+
object or a `default` filter on that output.
|
|
102
|
+
|
|
103
|
+
Both dot and static square-bracket notation are supported. Dynamic namespace,
|
|
104
|
+
key, type, and handle lookups are ignored because the selected object cannot be
|
|
105
|
+
identified statically. The singular `metaobject` drop available inside a
|
|
106
|
+
metaobject template is also ignored because the template is already rendering
|
|
107
|
+
that entry.
|
|
108
|
+
|
|
109
|
+
All three nullability checks share the same branch and `default`-filter
|
|
110
|
+
analysis. A guard applies only inside its active `if`, `unless`, `elsif`, or
|
|
111
|
+
`case` branch and only to the matching setting, metafield, or metaobject.
|
|
112
|
+
|
|
53
113
|
## Usage in a project
|
|
54
114
|
|
|
55
115
|
Install the module as a development dependency:
|
|
@@ -67,11 +127,27 @@ extends:
|
|
|
67
127
|
require:
|
|
68
128
|
- ./node_modules/theme-check-theory
|
|
69
129
|
|
|
130
|
+
DisallowedScriptOrStyleTag:
|
|
131
|
+
enabled: true
|
|
132
|
+
severity: warning
|
|
133
|
+
|
|
134
|
+
HardcodedText:
|
|
135
|
+
enabled: true
|
|
136
|
+
severity: warning
|
|
137
|
+
|
|
70
138
|
UnusedSectionSettings:
|
|
139
|
+
enabled: false
|
|
140
|
+
severity: warning
|
|
141
|
+
|
|
142
|
+
UnguardedNullableSetting:
|
|
71
143
|
enabled: true
|
|
72
144
|
severity: warning
|
|
73
145
|
|
|
74
|
-
|
|
146
|
+
UnguardedMetafield:
|
|
147
|
+
enabled: true
|
|
148
|
+
severity: warning
|
|
149
|
+
|
|
150
|
+
UnguardedMetaobject:
|
|
75
151
|
enabled: true
|
|
76
152
|
severity: warning
|
|
77
153
|
```
|
|
@@ -118,6 +194,7 @@ real Theme Check parser and traversal rather than a mocked AST.
|
|
|
118
194
|
4. Add it to `configs/recommended.yml`.
|
|
119
195
|
|
|
120
196
|
Shared schema parsing and setting-reference helpers live in `src/utils/ast.ts`.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
197
|
+
Null-guard analysis and nullable-object check construction live in
|
|
198
|
+
`src/utils/null-guard.ts` and `src/utils/nullable-output-check.ts`. Their node
|
|
199
|
+
shapes are verified against `@shopify/liquid-html-parser` 2.9.x; run the tests
|
|
200
|
+
when upgrading that dependency.
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
DisallowedScriptOrStyleTag:
|
|
2
|
+
enabled: true
|
|
3
|
+
severity: warning
|
|
4
|
+
|
|
5
|
+
HardcodedText:
|
|
6
|
+
enabled: true
|
|
7
|
+
severity: warning
|
|
8
|
+
|
|
1
9
|
UnusedSectionSettings:
|
|
10
|
+
enabled: false
|
|
11
|
+
severity: warning
|
|
12
|
+
|
|
13
|
+
UnguardedNullableSetting:
|
|
2
14
|
enabled: true
|
|
3
15
|
severity: warning
|
|
4
16
|
|
|
5
|
-
|
|
17
|
+
UnguardedMetafield:
|
|
6
18
|
enabled: true
|
|
7
19
|
severity: warning
|
|
8
20
|
|
|
21
|
+
UnguardedMetaobject:
|
|
22
|
+
enabled: true
|
|
23
|
+
severity: warning
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "theme-check-theory",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "theme-check-theory",
|
|
9
|
-
"version": "0.1
|
|
9
|
+
"version": "0.2.1",
|
|
10
10
|
"license": "UNLICENSED",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@shopify/liquid-html-parser": "2.9.2",
|
package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.test.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runLiquidCheck,
|
|
3
|
+
} from '@shopify/theme-check-common/dist/test/test-helper';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { DisallowedScriptOrStyleTag } from './disallowed-script-or-style-tag';
|
|
7
|
+
|
|
8
|
+
describe('DisallowedScriptOrStyleTag', () => {
|
|
9
|
+
it('reports script and style tags', async () => {
|
|
10
|
+
const source = `
|
|
11
|
+
<script>window.example = true;</script>
|
|
12
|
+
<style>.example { color: red; }</style>
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const offenses = await runLiquidCheck(DisallowedScriptOrStyleTag, source);
|
|
16
|
+
|
|
17
|
+
expect(offenses).toHaveLength(2);
|
|
18
|
+
expect(offenses.map(({ message }) => message)).toEqual([
|
|
19
|
+
'Do not include <script> tags in Liquid files',
|
|
20
|
+
'Do not include <style> tags in Liquid files',
|
|
21
|
+
]);
|
|
22
|
+
expect(
|
|
23
|
+
offenses.every(({ check }) => check === 'DisallowedScriptOrStyleTag'),
|
|
24
|
+
).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('reports case variants and self-closing tags', async () => {
|
|
28
|
+
const source = `
|
|
29
|
+
<SCRIPT type="module">window.example = true;</SCRIPT>
|
|
30
|
+
<STYLE />
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
const offenses = await runLiquidCheck(DisallowedScriptOrStyleTag, source);
|
|
34
|
+
|
|
35
|
+
expect(offenses).toHaveLength(2);
|
|
36
|
+
expect(offenses.map(({ message }) => message)).toEqual([
|
|
37
|
+
'Do not include <script> tags in Liquid files',
|
|
38
|
+
'Do not include <style> tags in Liquid files',
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('ignores scripts with an external source', async () => {
|
|
43
|
+
const source = `
|
|
44
|
+
<script src="{{ 'theme.js' | asset_url }}" defer></script>
|
|
45
|
+
<SCRIPT SRC="https://cdn.example.com/widget.js"></SCRIPT>
|
|
46
|
+
`;
|
|
47
|
+
|
|
48
|
+
expect(
|
|
49
|
+
await runLiquidCheck(DisallowedScriptOrStyleTag, source),
|
|
50
|
+
).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('ignores scripts containing JSON data', async () => {
|
|
54
|
+
const source = `
|
|
55
|
+
<script type="application/ld+json">{"@type":"Organization"}</script>
|
|
56
|
+
<script type="application/json">{"enabled":true}</script>
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
await runLiquidCheck(DisallowedScriptOrStyleTag, source),
|
|
61
|
+
).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('reports executable scripts with an empty src', async () => {
|
|
65
|
+
const source = `
|
|
66
|
+
<script src="">window.example = true;</script>
|
|
67
|
+
<script type="text/javascript">window.example = true;</script>
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
await runLiquidCheck(DisallowedScriptOrStyleTag, source),
|
|
72
|
+
).toHaveLength(2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('ignores tags in comments and similarly named elements', async () => {
|
|
76
|
+
const source = `
|
|
77
|
+
<!-- <script></script> -->
|
|
78
|
+
{% comment %}<style></style>{% endcomment %}
|
|
79
|
+
<script-template></script-template>
|
|
80
|
+
<div style="color: red"></div>
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
await runLiquidCheck(DisallowedScriptOrStyleTag, source),
|
|
85
|
+
).toEqual([]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AttributeNode,
|
|
3
|
+
HtmlElement,
|
|
4
|
+
HtmlRawNode,
|
|
5
|
+
HtmlSelfClosingElement,
|
|
6
|
+
NodeTypes,
|
|
7
|
+
} from '@shopify/liquid-html-parser';
|
|
8
|
+
import {
|
|
9
|
+
LiquidCheckDefinition,
|
|
10
|
+
Severity,
|
|
11
|
+
SourceCodeType,
|
|
12
|
+
} from '@shopify/theme-check-common';
|
|
13
|
+
|
|
14
|
+
const DISALLOWED_TAGS = new Set(['script', 'style']);
|
|
15
|
+
|
|
16
|
+
type NamedHtmlNode = HtmlElement | HtmlSelfClosingElement;
|
|
17
|
+
type ScriptNode = HtmlRawNode | NamedHtmlNode;
|
|
18
|
+
type ValuedAttribute = Extract<AttributeNode, { value: unknown }>;
|
|
19
|
+
|
|
20
|
+
function isValuedAttribute(
|
|
21
|
+
attribute: AttributeNode,
|
|
22
|
+
): attribute is ValuedAttribute {
|
|
23
|
+
return (
|
|
24
|
+
attribute.type === NodeTypes.AttrDoubleQuoted ||
|
|
25
|
+
attribute.type === NodeTypes.AttrSingleQuoted ||
|
|
26
|
+
attribute.type === NodeTypes.AttrUnquoted
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function staticAttributeName(attribute: ValuedAttribute): string | null {
|
|
31
|
+
if (
|
|
32
|
+
attribute.name.length !== 1 ||
|
|
33
|
+
attribute.name[0].type !== NodeTypes.TextNode
|
|
34
|
+
) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return attribute.name[0].value.toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function staticAttributeValue(
|
|
42
|
+
node: ScriptNode,
|
|
43
|
+
attributeName: string,
|
|
44
|
+
): string | null {
|
|
45
|
+
const attribute = node.attributes.find(
|
|
46
|
+
(candidate) =>
|
|
47
|
+
isValuedAttribute(candidate) &&
|
|
48
|
+
staticAttributeName(candidate) === attributeName,
|
|
49
|
+
);
|
|
50
|
+
if (
|
|
51
|
+
!attribute ||
|
|
52
|
+
!isValuedAttribute(attribute) ||
|
|
53
|
+
!attribute.value.every(({ type }) => type === NodeTypes.TextNode)
|
|
54
|
+
) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return attribute.value
|
|
59
|
+
.map((part) => ('value' in part ? part.value : ''))
|
|
60
|
+
.join('')
|
|
61
|
+
.trim()
|
|
62
|
+
.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasExternalSource(node: ScriptNode): boolean {
|
|
66
|
+
return node.attributes.some(
|
|
67
|
+
(attribute) =>
|
|
68
|
+
isValuedAttribute(attribute) &&
|
|
69
|
+
staticAttributeName(attribute) === 'src' &&
|
|
70
|
+
attribute.value.length > 0,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function containsJsonData(node: ScriptNode): boolean {
|
|
75
|
+
const type = staticAttributeValue(node, 'type');
|
|
76
|
+
if (!type) return false;
|
|
77
|
+
|
|
78
|
+
const mimeType = type.split(';', 1)[0].trim();
|
|
79
|
+
return (
|
|
80
|
+
mimeType.endsWith('/json') ||
|
|
81
|
+
mimeType.endsWith('+json') ||
|
|
82
|
+
mimeType === 'importmap' ||
|
|
83
|
+
mimeType === 'speculationrules'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isAllowedScript(node: ScriptNode): boolean {
|
|
88
|
+
return hasExternalSource(node) || containsJsonData(node);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function staticTagName(node: NamedHtmlNode): string | null {
|
|
92
|
+
if (
|
|
93
|
+
node.name.length !== 1 ||
|
|
94
|
+
node.name[0].type !== NodeTypes.TextNode
|
|
95
|
+
) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return node.name[0].value.toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const DisallowedScriptOrStyleTag: LiquidCheckDefinition = {
|
|
103
|
+
meta: {
|
|
104
|
+
code: 'DisallowedScriptOrStyleTag',
|
|
105
|
+
name: 'Prevent script and style tags in Liquid',
|
|
106
|
+
docs: {
|
|
107
|
+
description:
|
|
108
|
+
'Reports inline executable script and style tags in Liquid files.',
|
|
109
|
+
recommended: true,
|
|
110
|
+
},
|
|
111
|
+
type: SourceCodeType.LiquidHtml,
|
|
112
|
+
severity: Severity.WARNING,
|
|
113
|
+
schema: {},
|
|
114
|
+
},
|
|
115
|
+
create(context) {
|
|
116
|
+
function report(tagName: string, startIndex: number, endIndex: number) {
|
|
117
|
+
if (!DISALLOWED_TAGS.has(tagName)) return;
|
|
118
|
+
|
|
119
|
+
context.report({
|
|
120
|
+
message: `Do not include <${tagName}> tags in Liquid files`,
|
|
121
|
+
startIndex,
|
|
122
|
+
endIndex,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function checkNamedNode(node: NamedHtmlNode) {
|
|
127
|
+
const tagName = staticTagName(node);
|
|
128
|
+
if (!tagName) return;
|
|
129
|
+
if (tagName === 'script' && isAllowedScript(node)) return;
|
|
130
|
+
|
|
131
|
+
report(
|
|
132
|
+
tagName,
|
|
133
|
+
node.blockStartPosition.start,
|
|
134
|
+
node.blockStartPosition.end,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
async HtmlRawNode(node) {
|
|
140
|
+
if (
|
|
141
|
+
node.name.toLowerCase() === 'script' &&
|
|
142
|
+
isAllowedScript(node)
|
|
143
|
+
) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
report(
|
|
148
|
+
node.name.toLowerCase(),
|
|
149
|
+
node.blockStartPosition.start,
|
|
150
|
+
node.blockStartPosition.end,
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
async HtmlElement(node) {
|
|
154
|
+
checkNamedNode(node);
|
|
155
|
+
},
|
|
156
|
+
async HtmlSelfClosingElement(node) {
|
|
157
|
+
checkNamedNode(node);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
};
|