td-ai-tools 1.2.5 → 1.2.6
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 +17 -1
- 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 +89 -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 +54 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/disallowed-script-or-style-tag.ts +77 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.test.ts +102 -0
- package/skills/shopify-lint/theme-check-theory/src/checks/hardcoded-text.ts +171 -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.0
|
|
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. If one already exists it is left untouched; ensure its `require:` list includes `./.agents/skills/shopify-lint/theme-check-theory
|
|
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` and both `DisallowedScriptOrStyleTag` and `HardcodedText` are enabled.
|
|
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 HTML `<script>` and `<style>` tags in modified Liquid files. `HardcodedText` reports rendered hard-coded copy, except content inside Liquid `stylesheet`, `javascript`, and `schema` tags.
|
|
36
36
|
|
|
37
37
|
## Workflow
|
|
38
38
|
|
|
Binary file
|
|
@@ -50,11 +50,27 @@ extends:
|
|
|
50
50
|
require:
|
|
51
51
|
- ./.agents/skills/shopify-lint/theme-check-theory
|
|
52
52
|
|
|
53
|
+
DisallowedScriptOrStyleTag:
|
|
54
|
+
enabled: true
|
|
55
|
+
severity: warning
|
|
56
|
+
|
|
57
|
+
HardcodedText:
|
|
58
|
+
enabled: true
|
|
59
|
+
severity: warning
|
|
60
|
+
|
|
53
61
|
UnusedSectionSettings:
|
|
62
|
+
enabled: false
|
|
63
|
+
severity: warning
|
|
64
|
+
|
|
65
|
+
UnguardedNullableSetting:
|
|
66
|
+
enabled: true
|
|
67
|
+
severity: warning
|
|
68
|
+
|
|
69
|
+
UnguardedMetafield:
|
|
54
70
|
enabled: true
|
|
55
71
|
severity: warning
|
|
56
72
|
|
|
57
|
-
|
|
73
|
+
UnguardedMetaobject:
|
|
58
74
|
enabled: true
|
|
59
75
|
severity: warning
|
|
60
76
|
YAML
|
|
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,31 @@ 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 HTML `<script>` or `<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. HTML comments and similarly named elements such as
|
|
26
|
+
`<script-template>` are ignored.
|
|
27
|
+
|
|
28
|
+
### HardcodedText
|
|
29
|
+
|
|
30
|
+
Rendered storefront copy should come from a section setting, translation key,
|
|
31
|
+
metafield, or metaobject. The check reports text nodes, direct quoted output
|
|
32
|
+
with `{{ }}` or `{% echo %}`, and hard-coded `alt`, `title`, `placeholder`, and
|
|
33
|
+
ARIA text attributes. It ignores technical attributes such as classes and IDs,
|
|
34
|
+
Liquid logic strings, comments, and punctuation-only fragments.
|
|
35
|
+
|
|
36
|
+
Content inside `{% stylesheet %}`, `{% javascript %}`, and `{% schema %}` is
|
|
37
|
+
ignored. A quoted translation key passed through `t` or `translate` is also
|
|
38
|
+
accepted.
|
|
16
39
|
|
|
17
40
|
### UnusedSectionSettings
|
|
18
41
|
|
|
@@ -25,31 +48,62 @@ If a file uses dynamic access such as `section.settings[key]`, or passes an
|
|
|
25
48
|
entire settings object elsewhere, the check cannot prove which setting is
|
|
26
49
|
unused. It therefore stays silent for the whole file to avoid false positives.
|
|
27
50
|
|
|
28
|
-
|
|
51
|
+
This check is disabled by default. Enable it explicitly when a theme's section
|
|
52
|
+
settings are expected to be fully traceable within each file:
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
UnusedSectionSettings:
|
|
56
|
+
enabled: true
|
|
57
|
+
severity: warning
|
|
58
|
+
```
|
|
29
59
|
|
|
30
|
-
|
|
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.
|
|
60
|
+
### UnguardedNullableSetting
|
|
34
61
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
Many settings can return an empty, `blank`, or `nil` value when a merchant
|
|
63
|
+
leaves them unset or their selected resource is no longer available. Outputting
|
|
64
|
+
them unguarded is a recurring bug source. Each output is evaluated independently.
|
|
65
|
+
It is considered guarded when its active `if`, `unless`, `elsif`, or `case`
|
|
66
|
+
branch tests that setting, or when that output uses a `default` filter. A guard
|
|
67
|
+
or default on one invocation does not suppress offenses for unguarded
|
|
68
|
+
invocations elsewhere in the file.
|
|
38
69
|
|
|
39
|
-
The setting types
|
|
40
|
-
|
|
41
|
-
`
|
|
70
|
+
The setting types checked are configurable with the `nullableSettingTypes`
|
|
71
|
+
option. The defaults are `number`, `text`, `textarea`, `article`,
|
|
72
|
+
`article_list`, `blog`, `collection`, `collection_list`, `image_picker`,
|
|
73
|
+
`link_list`, `metaobject`, `metaobject_list`, `page`, `product`,
|
|
74
|
+
`product_list`, `richtext`, `inline_richtext`, `html`, `liquid`, `url`,
|
|
75
|
+
`video`, and `video_url`. Color-related settings are excluded because an unset
|
|
76
|
+
color can resolve to a non-null fallback such as `transparent`.
|
|
42
77
|
|
|
43
78
|
```yaml
|
|
44
|
-
|
|
79
|
+
UnguardedNullableSetting:
|
|
45
80
|
enabled: true
|
|
46
81
|
severity: warning
|
|
47
|
-
|
|
82
|
+
nullableSettingTypes:
|
|
48
83
|
- text
|
|
49
84
|
- textarea
|
|
50
85
|
- image_picker
|
|
51
86
|
```
|
|
52
87
|
|
|
88
|
+
### UnguardedMetafield and UnguardedMetaobject
|
|
89
|
+
|
|
90
|
+
[Metafields](https://shopify.dev/docs/api/liquid/objects/metafield) are
|
|
91
|
+
identified from the static `resource.metafields.namespace.key` access path,
|
|
92
|
+
and [metaobjects](https://shopify.dev/docs/api/liquid/objects/metaobject) from
|
|
93
|
+
`metaobjects.type.handle`. Outputting the object itself or any descendant, such
|
|
94
|
+
as a metafield's `value` or a metaobject's field, requires a guard on the same
|
|
95
|
+
object or a `default` filter on that output.
|
|
96
|
+
|
|
97
|
+
Both dot and static square-bracket notation are supported. Dynamic namespace,
|
|
98
|
+
key, type, and handle lookups are ignored because the selected object cannot be
|
|
99
|
+
identified statically. The singular `metaobject` drop available inside a
|
|
100
|
+
metaobject template is also ignored because the template is already rendering
|
|
101
|
+
that entry.
|
|
102
|
+
|
|
103
|
+
All three nullability checks share the same branch and `default`-filter
|
|
104
|
+
analysis. A guard applies only inside its active `if`, `unless`, `elsif`, or
|
|
105
|
+
`case` branch and only to the matching setting, metafield, or metaobject.
|
|
106
|
+
|
|
53
107
|
## Usage in a project
|
|
54
108
|
|
|
55
109
|
Install the module as a development dependency:
|
|
@@ -67,11 +121,27 @@ extends:
|
|
|
67
121
|
require:
|
|
68
122
|
- ./node_modules/theme-check-theory
|
|
69
123
|
|
|
124
|
+
DisallowedScriptOrStyleTag:
|
|
125
|
+
enabled: true
|
|
126
|
+
severity: warning
|
|
127
|
+
|
|
128
|
+
HardcodedText:
|
|
129
|
+
enabled: true
|
|
130
|
+
severity: warning
|
|
131
|
+
|
|
70
132
|
UnusedSectionSettings:
|
|
133
|
+
enabled: false
|
|
134
|
+
severity: warning
|
|
135
|
+
|
|
136
|
+
UnguardedNullableSetting:
|
|
137
|
+
enabled: true
|
|
138
|
+
severity: warning
|
|
139
|
+
|
|
140
|
+
UnguardedMetafield:
|
|
71
141
|
enabled: true
|
|
72
142
|
severity: warning
|
|
73
143
|
|
|
74
|
-
|
|
144
|
+
UnguardedMetaobject:
|
|
75
145
|
enabled: true
|
|
76
146
|
severity: warning
|
|
77
147
|
```
|
|
@@ -118,6 +188,7 @@ real Theme Check parser and traversal rather than a mocked AST.
|
|
|
118
188
|
4. Add it to `configs/recommended.yml`.
|
|
119
189
|
|
|
120
190
|
Shared schema parsing and setting-reference helpers live in `src/utils/ast.ts`.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
191
|
+
Null-guard analysis and nullable-object check construction live in
|
|
192
|
+
`src/utils/null-guard.ts` and `src/utils/nullable-output-check.ts`. Their node
|
|
193
|
+
shapes are verified against `@shopify/liquid-html-parser` 2.9.x; run the tests
|
|
194
|
+
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.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "theme-check-theory",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.2.0",
|
|
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,54 @@
|
|
|
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 src="{{ 'theme.js' | asset_url }}" defer></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="application/json">{}</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 tags in comments and similarly named elements', async () => {
|
|
43
|
+
const source = `
|
|
44
|
+
<!-- <script></script> -->
|
|
45
|
+
{% comment %}<style></style>{% endcomment %}
|
|
46
|
+
<script-template></script-template>
|
|
47
|
+
<div style="color: red"></div>
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
expect(
|
|
51
|
+
await runLiquidCheck(DisallowedScriptOrStyleTag, source),
|
|
52
|
+
).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HtmlElement,
|
|
3
|
+
HtmlSelfClosingElement,
|
|
4
|
+
NodeTypes,
|
|
5
|
+
} from '@shopify/liquid-html-parser';
|
|
6
|
+
import {
|
|
7
|
+
LiquidCheckDefinition,
|
|
8
|
+
Severity,
|
|
9
|
+
SourceCodeType,
|
|
10
|
+
} from '@shopify/theme-check-common';
|
|
11
|
+
|
|
12
|
+
const DISALLOWED_TAGS = new Set(['script', 'style']);
|
|
13
|
+
|
|
14
|
+
type NamedHtmlNode = HtmlElement | HtmlSelfClosingElement;
|
|
15
|
+
|
|
16
|
+
function staticTagName(node: NamedHtmlNode): string | null {
|
|
17
|
+
if (
|
|
18
|
+
node.name.length !== 1 ||
|
|
19
|
+
node.name[0].type !== NodeTypes.TextNode
|
|
20
|
+
) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return node.name[0].value.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DisallowedScriptOrStyleTag: LiquidCheckDefinition = {
|
|
28
|
+
meta: {
|
|
29
|
+
code: 'DisallowedScriptOrStyleTag',
|
|
30
|
+
name: 'Prevent script and style tags in Liquid',
|
|
31
|
+
docs: {
|
|
32
|
+
description: 'Reports HTML script and style tags in Liquid files.',
|
|
33
|
+
recommended: true,
|
|
34
|
+
},
|
|
35
|
+
type: SourceCodeType.LiquidHtml,
|
|
36
|
+
severity: Severity.WARNING,
|
|
37
|
+
schema: {},
|
|
38
|
+
},
|
|
39
|
+
create(context) {
|
|
40
|
+
function report(tagName: string, startIndex: number, endIndex: number) {
|
|
41
|
+
if (!DISALLOWED_TAGS.has(tagName)) return;
|
|
42
|
+
|
|
43
|
+
context.report({
|
|
44
|
+
message: `Do not include <${tagName}> tags in Liquid files`,
|
|
45
|
+
startIndex,
|
|
46
|
+
endIndex,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function checkNamedNode(node: NamedHtmlNode) {
|
|
51
|
+
const tagName = staticTagName(node);
|
|
52
|
+
if (!tagName) return;
|
|
53
|
+
|
|
54
|
+
report(
|
|
55
|
+
tagName,
|
|
56
|
+
node.blockStartPosition.start,
|
|
57
|
+
node.blockStartPosition.end,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
async HtmlRawNode(node) {
|
|
63
|
+
report(
|
|
64
|
+
node.name.toLowerCase(),
|
|
65
|
+
node.blockStartPosition.start,
|
|
66
|
+
node.blockStartPosition.end,
|
|
67
|
+
);
|
|
68
|
+
},
|
|
69
|
+
async HtmlElement(node) {
|
|
70
|
+
checkNamedNode(node);
|
|
71
|
+
},
|
|
72
|
+
async HtmlSelfClosingElement(node) {
|
|
73
|
+
checkNamedNode(node);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runLiquidCheck,
|
|
3
|
+
} from '@shopify/theme-check-common/dist/test/test-helper';
|
|
4
|
+
import { describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { HardcodedText } from './hardcoded-text';
|
|
7
|
+
|
|
8
|
+
describe('HardcodedText', () => {
|
|
9
|
+
it('reports rendered text and direct string output', async () => {
|
|
10
|
+
const source = `
|
|
11
|
+
Welcome to our store
|
|
12
|
+
<h2>Featured products</h2>
|
|
13
|
+
{{ 'Limited edition' }}
|
|
14
|
+
{% echo "Shop now" %}
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
const offenses = await runLiquidCheck(HardcodedText, source);
|
|
18
|
+
|
|
19
|
+
expect(offenses).toHaveLength(4);
|
|
20
|
+
expect(
|
|
21
|
+
offenses.every(({ check }) => check === 'HardcodedText'),
|
|
22
|
+
).toBe(true);
|
|
23
|
+
expect(
|
|
24
|
+
offenses.every(
|
|
25
|
+
({ message }) =>
|
|
26
|
+
message ===
|
|
27
|
+
'Replace hard-coded text with a section setting, translation key, metafield, or metaobject',
|
|
28
|
+
),
|
|
29
|
+
).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('reports hard-coded text in common user-facing attributes', async () => {
|
|
33
|
+
const source = `
|
|
34
|
+
<img src="{{ image | image_url }}" alt="Product photo">
|
|
35
|
+
<button title='Close dialog' aria-label="Close">×</button>
|
|
36
|
+
<input placeholder="Email address">
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
const offenses = await runLiquidCheck(HardcodedText, source);
|
|
40
|
+
|
|
41
|
+
expect(offenses).toHaveLength(4);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('reports static fragments in otherwise dynamic rendered text', async () => {
|
|
45
|
+
const source = `
|
|
46
|
+
<p>Welcome, {{ customer.name }}!</p>
|
|
47
|
+
<img alt="Photo of {{ product.title }}" src="{{ product.featured_image | image_url }}">
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
const offenses = await runLiquidCheck(HardcodedText, source);
|
|
51
|
+
|
|
52
|
+
expect(offenses).toHaveLength(2);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('accepts settings, translation keys, metafields, and metaobjects', async () => {
|
|
56
|
+
const source = `
|
|
57
|
+
<h2>{{ section.settings.heading }}</h2>
|
|
58
|
+
<p>{{ 'general.welcome' | t }}</p>
|
|
59
|
+
<p>{{ "general.subtitle" | translate }}</p>
|
|
60
|
+
<p>{{ product.metafields.content.description }}</p>
|
|
61
|
+
<p>{{ metaobjects.testimonials.homepage.quote }}</p>
|
|
62
|
+
<img alt="{{ section.settings.image_alt }}" src="{{ section.settings.image | image_url }}">
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
expect(await runLiquidCheck(HardcodedText, source)).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it.each([
|
|
69
|
+
[
|
|
70
|
+
'stylesheet',
|
|
71
|
+
'{% stylesheet %}.card::after { content: "Hard coded"; }{% endstylesheet %}',
|
|
72
|
+
],
|
|
73
|
+
[
|
|
74
|
+
'javascript',
|
|
75
|
+
'{% javascript %}console.log("Hard coded");{% endjavascript %}',
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
'schema',
|
|
79
|
+
'{% schema %}{"name":"Hard coded","settings":[]}{% endschema %}',
|
|
80
|
+
],
|
|
81
|
+
])('ignores text inside the %s raw tag', async (_, source) => {
|
|
82
|
+
expect(await runLiquidCheck(HardcodedText, source)).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('ignores comments, technical attributes, punctuation, and Liquid logic strings', async () => {
|
|
86
|
+
const source = `
|
|
87
|
+
<!-- Hard coded HTML comment -->
|
|
88
|
+
{% comment %}Hard coded Liquid comment{% endcomment %}
|
|
89
|
+
<div class="hard-coded" id="hard-coded" data-label="Hard coded">—</div>
|
|
90
|
+
{% assign state = 'active' %}
|
|
91
|
+
{% if request.page_type == 'product' %}{{ product.title }}{% endif %}
|
|
92
|
+
`;
|
|
93
|
+
|
|
94
|
+
expect(await runLiquidCheck(HardcodedText, source)).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('still reports rendered text inside other raw tags', async () => {
|
|
98
|
+
const source = '{% raw %}Hard coded raw output{% endraw %}';
|
|
99
|
+
|
|
100
|
+
expect(await runLiquidCheck(HardcodedText, source)).toHaveLength(1);
|
|
101
|
+
});
|
|
102
|
+
});
|