td-ai-tools 1.2.11 → 1.3.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 +2 -2
- package/skills/shopify-lint/SKILL.md +26 -18
- package/skills/shopify-lint/eslint-plugin-theory/README.md +25 -8
- package/skills/shopify-lint/eslint-plugin-theory/src/liquid/mask.test.ts +41 -0
- package/skills/shopify-lint/eslint-plugin-theory/src/liquid/mask.ts +41 -2
- package/skills/shopify-lint/hooks/claude-stop.sh +2 -3
- package/skills/shopify-lint/hooks/codex-stop.sh +2 -3
- package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/scripts/setup.sh +21 -2
- package/skills/shopify-lint/scripts/shopify_lint.py +179 -30
- package/skills/shopify-lint/stylelint-config-theory/.stylelintignore +9 -0
- package/skills/shopify-lint/stylelint-config-theory/README.md +144 -0
- package/skills/shopify-lint/stylelint-config-theory/bin/lint-css.mjs +206 -0
- package/skills/shopify-lint/stylelint-config-theory/package-lock.json +3110 -0
- package/skills/shopify-lint/stylelint-config-theory/package.json +47 -0
- package/skills/shopify-lint/stylelint-config-theory/src/build.test.ts +31 -0
- package/skills/shopify-lint/stylelint-config-theory/src/config.test.ts +150 -0
- package/skills/shopify-lint/stylelint-config-theory/src/config.ts +109 -0
- package/skills/shopify-lint/stylelint-config-theory/src/index.ts +15 -0
- package/skills/shopify-lint/stylelint-config-theory/src/integration.test.ts +187 -0
- package/skills/shopify-lint/stylelint-config-theory/src/liquid/mask.test.ts +300 -0
- package/skills/shopify-lint/stylelint-config-theory/src/liquid/mask.ts +203 -0
- package/skills/shopify-lint/stylelint-config-theory/src/types/shared-configs.d.ts +26 -0
- package/skills/shopify-lint/stylelint-config-theory/tsconfig.build.json +9 -0
- package/skills/shopify-lint/stylelint-config-theory/tsconfig.json +14 -0
- package/skills/shopify-lint/stylelint-config-theory/vitest.config.ts +11 -0
- package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/tests/test_shopify_lint.py +483 -8
- package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +7 -0
- package/skills/shopify-lint/theme-check-theory/README.md +48 -0
- package/skills/shopify-pre-pr/SKILL.md +129 -0
- package/skills/shopify-pre-pr/agents/openai.yaml +4 -0
- package/skills/shopify-pre-pr/references/td-change-delimiters.md +88 -0
- package/skills/shopify-pre-pr/scripts/__pycache__/test_vendor_change_audit.cpython-312.pyc +0 -0
- package/skills/shopify-pre-pr/scripts/__pycache__/vendor_change_audit.cpython-312.pyc +0 -0
- package/skills/shopify-pre-pr/scripts/test_vendor_change_audit.py +424 -0
- package/skills/shopify-pre-pr/scripts/vendor_change_audit.py +540 -0
- package/skills/record-changes/SKILL.md +0 -76
- package/skills/record-changes/agents/openai.yaml +0 -4
- /package/skills/{record-changes → shopify-pre-pr}/scripts/branch_diff_context.py +0 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stylelint-config-theory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Theory Digital's Stylelint configuration for Shopify theme CSS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./dist/index.js",
|
|
10
|
+
"./config": "./dist/config.js"
|
|
11
|
+
},
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"bin",
|
|
16
|
+
".stylelintignore",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc -p tsconfig.build.json",
|
|
21
|
+
"prepack": "npm run build",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"test:watch": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"shopify",
|
|
27
|
+
"stylelint",
|
|
28
|
+
"stylelint-config",
|
|
29
|
+
"liquid",
|
|
30
|
+
"css",
|
|
31
|
+
"lint"
|
|
32
|
+
],
|
|
33
|
+
"license": "UNLICENSED",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20.19.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"stylelint": "17.14.1",
|
|
39
|
+
"stylelint-config-recommended": "18.0.0",
|
|
40
|
+
"stylelint-config-standard": "40.0.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "24.0.15",
|
|
44
|
+
"typescript": "5.8.3",
|
|
45
|
+
"vitest": "3.2.7"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const run = promisify(execFile);
|
|
9
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
|
|
11
|
+
describe('the published build', () => {
|
|
12
|
+
// `scripts/setup.sh` runs `npm run build` for this package, so a type error
|
|
13
|
+
// here breaks setup for every consumer of the shopify-lint skill.
|
|
14
|
+
it('typechecks under the build config', async () => {
|
|
15
|
+
const result = await run('npx', ['tsc', '-p', 'tsconfig.build.json', '--noEmit'], {
|
|
16
|
+
cwd: packageRoot,
|
|
17
|
+
}).catch((error: { stdout?: string; stderr?: string }) => error);
|
|
18
|
+
|
|
19
|
+
expect([result.stdout, result.stderr].filter(Boolean).join('\n')).toBe('');
|
|
20
|
+
}, 120_000);
|
|
21
|
+
|
|
22
|
+
// bin/lint-css.mjs imports from dist/, so the entry points it needs must
|
|
23
|
+
// survive any reshuffle of the source tree.
|
|
24
|
+
it('exposes the entry points the runner imports', async () => {
|
|
25
|
+
const config = await import(resolve(packageRoot, 'dist/config.js'));
|
|
26
|
+
const index = await import(resolve(packageRoot, 'dist/index.js'));
|
|
27
|
+
|
|
28
|
+
expect(typeof config.createConfig).toBe('function');
|
|
29
|
+
expect(typeof index.maskLiquidStylesheet).toBe('function');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import recommended from 'stylelint-config-recommended';
|
|
2
|
+
import standard from 'stylelint-config-standard';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { createConfig } from './config.js';
|
|
6
|
+
|
|
7
|
+
const RECOMMENDED_RULES = Object.keys(recommended.rules ?? {});
|
|
8
|
+
const STANDARD_ONLY_RULES = Object.keys(standard.rules ?? {}).filter(
|
|
9
|
+
(name) => !RECOMMENDED_RULES.includes(name),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
/** The severity Stylelint would resolve for a rule, given the config. */
|
|
13
|
+
function severityOf(config: ReturnType<typeof createConfig>, name: string): string | undefined {
|
|
14
|
+
const settings = config.rules?.[name];
|
|
15
|
+
if (settings === null || settings === undefined) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const secondary = Array.isArray(settings) ? settings[1] : undefined;
|
|
19
|
+
const explicit = (secondary as { severity?: string } | undefined)?.severity;
|
|
20
|
+
return explicit ?? (config.defaultSeverity as string | undefined);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('createConfig', () => {
|
|
24
|
+
it('covers every rule from both shared configs', () => {
|
|
25
|
+
const config = createConfig();
|
|
26
|
+
|
|
27
|
+
for (const name of [...RECOMMENDED_RULES, ...STANDARD_ONLY_RULES]) {
|
|
28
|
+
expect(config.rules).toHaveProperty(name);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('adds no rules the shared configs do not define', () => {
|
|
33
|
+
const known = new Set([...RECOMMENDED_RULES, ...STANDARD_ONLY_RULES]);
|
|
34
|
+
|
|
35
|
+
expect(Object.keys(createConfig().rules ?? {}).filter((name) => !known.has(name))).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('sanity-checks the split the severity policy depends on', () => {
|
|
39
|
+
// If upstream ever reshapes these configs, the grading below is meaningless
|
|
40
|
+
// without this holding.
|
|
41
|
+
expect(RECOMMENDED_RULES.length).toBeGreaterThan(30);
|
|
42
|
+
expect(STANDARD_ONLY_RULES.length).toBeGreaterThan(30);
|
|
43
|
+
expect(RECOMMENDED_RULES.filter((name) => STANDARD_ONLY_RULES.includes(name))).toEqual([]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('grades the correctness layer as errors', () => {
|
|
47
|
+
const config = createConfig();
|
|
48
|
+
const overridden = new Set(['no-descending-specificity']);
|
|
49
|
+
|
|
50
|
+
for (const name of RECOMMENDED_RULES) {
|
|
51
|
+
if (overridden.has(name)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
expect(severityOf(config, name), name).toBe('error');
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('grades the convention layer as warnings', () => {
|
|
59
|
+
const config = createConfig();
|
|
60
|
+
|
|
61
|
+
for (const name of STANDARD_ONLY_RULES) {
|
|
62
|
+
const severity = severityOf(config, name);
|
|
63
|
+
// `undefined` means the rule is deliberately disabled by an override.
|
|
64
|
+
if (severity === undefined) {
|
|
65
|
+
expect(config.rules?.[name]).toBeNull();
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
expect(severity, name).toBe('warning');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('defaults unspecified severities to warning', () => {
|
|
73
|
+
expect(createConfig().defaultSeverity).toBe('warning');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('preserves the options of a promoted rule', () => {
|
|
77
|
+
// `selector-type-no-unknown` ships as `[true, { ignore: ['custom-elements'] }]`
|
|
78
|
+
// and must keep that option, or every `<product-card>` would be reported.
|
|
79
|
+
expect(createConfig().rules?.['selector-type-no-unknown']).toEqual([
|
|
80
|
+
true,
|
|
81
|
+
{ ignore: ['custom-elements'], severity: 'error' },
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('widens a bare primary value when promoting it', () => {
|
|
86
|
+
expect(createConfig().rules?.['block-no-empty']).toEqual([true, { severity: 'error' }]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('keeps a message function intact', () => {
|
|
90
|
+
// A promoted rule with a `message` function must keep it callable: the
|
|
91
|
+
// config is used in-process and must never be JSON round-tripped.
|
|
92
|
+
const settings = createConfig().rules?.['custom-media-pattern'];
|
|
93
|
+
const secondary = Array.isArray(settings) ? (settings[1] as { message?: unknown }) : undefined;
|
|
94
|
+
|
|
95
|
+
expect(typeof secondary?.message).toBe('function');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('disables the naming-pattern rules pending a Theory convention', () => {
|
|
99
|
+
const config = createConfig();
|
|
100
|
+
|
|
101
|
+
for (const name of [
|
|
102
|
+
'selector-class-pattern',
|
|
103
|
+
'selector-id-pattern',
|
|
104
|
+
'custom-property-pattern',
|
|
105
|
+
'keyframes-name-pattern',
|
|
106
|
+
]) {
|
|
107
|
+
expect(config.rules?.[name], name).toBeNull();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('demotes no-descending-specificity to a warning', () => {
|
|
112
|
+
expect(severityOf(createConfig(), 'no-descending-specificity')).toBe('warning');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('keeps the value-shape rules enabled', () => {
|
|
116
|
+
// These were once disabled for `{% stylesheet %}` blocks, where a masked
|
|
117
|
+
// Liquid output stands in as a value. Liquid there is now reported as an
|
|
118
|
+
// error by Theme Check's `StaticStylesheetAndJavascriptTags`, so there is
|
|
119
|
+
// no correct theme
|
|
120
|
+
// code left for the relaxation to protect, and a `.liquid` block is graded
|
|
121
|
+
// exactly as a `.css` file is.
|
|
122
|
+
const config = createConfig();
|
|
123
|
+
|
|
124
|
+
for (const name of [
|
|
125
|
+
'declaration-property-value-no-unknown',
|
|
126
|
+
'declaration-property-value-keyword-no-deprecated',
|
|
127
|
+
'at-rule-prelude-no-invalid',
|
|
128
|
+
'media-query-no-invalid',
|
|
129
|
+
'media-feature-name-value-no-unknown',
|
|
130
|
+
'syntax-string-no-invalid',
|
|
131
|
+
'function-calc-no-unspaced-operator',
|
|
132
|
+
'no-empty-source',
|
|
133
|
+
]) {
|
|
134
|
+
expect(severityOf(config, name), name).toBe('error');
|
|
135
|
+
}
|
|
136
|
+
expect(severityOf(config, 'function-url-quotes')).toBe('warning');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('takes no options, so every source is graded the same way', () => {
|
|
140
|
+
// The signature is the contract a `SHOPIFY_LINT_STYLELINT_CONFIG` override
|
|
141
|
+
// has to match; an argument passed by an older caller must not change the
|
|
142
|
+
// result.
|
|
143
|
+
expect(createConfig.length).toBe(0);
|
|
144
|
+
expect(
|
|
145
|
+
(createConfig as (options?: unknown) => ReturnType<typeof createConfig>)({
|
|
146
|
+
source: 'liquid',
|
|
147
|
+
}),
|
|
148
|
+
).toEqual(createConfig());
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import recommended from 'stylelint-config-recommended';
|
|
2
|
+
import standard from 'stylelint-config-standard';
|
|
3
|
+
import type { Config } from 'stylelint';
|
|
4
|
+
|
|
5
|
+
type RuleSettings = Config['rules'] extends Record<string, infer Value> | undefined
|
|
6
|
+
? Value
|
|
7
|
+
: never;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Severities for the upstream Stylelint rules.
|
|
11
|
+
*
|
|
12
|
+
* The two shared configs split cleanly along the line this skill already draws
|
|
13
|
+
* for JavaScript in `eslint-plugin-theory/src/config.ts`: rules whose violation
|
|
14
|
+
* is a real defect are errors and so fail the branch lint at the default
|
|
15
|
+
* `--fail-level error`, while the convention rules are warnings — reported but
|
|
16
|
+
* advisory, matching how the bundled Theme Check rules behave.
|
|
17
|
+
*
|
|
18
|
+
* `stylelint-config-recommended` is the correctness layer: unknown properties
|
|
19
|
+
* and at-rules, duplicate declarations, invalid values, empty blocks. Those are
|
|
20
|
+
* errors. `stylelint-config-standard` adds notational conventions on top —
|
|
21
|
+
* `color-function-notation`, `length-zero-no-unit`, the kebab-case patterns —
|
|
22
|
+
* and those are warnings.
|
|
23
|
+
*
|
|
24
|
+
* Deriving the split from `Object.keys(recommended.rules)` rather than listing
|
|
25
|
+
* it here means it tracks upstream: a rule promoted into the recommended config
|
|
26
|
+
* becomes an error on the next dependency bump without anyone editing this file.
|
|
27
|
+
*/
|
|
28
|
+
const CORRECTNESS_RULES = new Set(Object.keys(recommended.rules ?? {}));
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Deliberate departures from the shared configs, applied to every source.
|
|
32
|
+
*
|
|
33
|
+
* Anything set to `null` here is shipped disabled and documented in the README
|
|
34
|
+
* as opt-in, mirroring `THEORY_OPTIONAL_RULES` on the JavaScript side.
|
|
35
|
+
*/
|
|
36
|
+
const THEORY_OVERRIDES: Record<string, RuleSettings> = {
|
|
37
|
+
// Standard's default pattern is `^([a-z][a-z0-9]*)(-[a-z0-9]+)*$`, which
|
|
38
|
+
// rejects the BEM-ish names Theory writes (`td-card__title--active`).
|
|
39
|
+
// Re-enable with a Theory-specific pattern once that naming is written down.
|
|
40
|
+
'selector-class-pattern': null,
|
|
41
|
+
'selector-id-pattern': null,
|
|
42
|
+
'custom-property-pattern': null,
|
|
43
|
+
'keyframes-name-pattern': null,
|
|
44
|
+
// In the recommended config, so it would otherwise be an error. Too noisy on
|
|
45
|
+
// real theme CSS to block a handoff over.
|
|
46
|
+
'no-descending-specificity': [true, { severity: 'warning' }],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Stamp a severity onto one rule's settings without losing its options.
|
|
51
|
+
*
|
|
52
|
+
* Several `stylelint-config-standard` rules carry a `message` **function** in
|
|
53
|
+
* their secondary options, so the secondary object has to be merged into rather
|
|
54
|
+
* than replaced. A rule whose primary value is not already an array is widened
|
|
55
|
+
* into the `[primary, secondary]` form, which is how Stylelint expects a
|
|
56
|
+
* severity to be expressed.
|
|
57
|
+
*/
|
|
58
|
+
function withSeverity(settings: RuleSettings, severity: 'error' | 'warning'): RuleSettings {
|
|
59
|
+
if (settings === null || settings === undefined) {
|
|
60
|
+
return settings;
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(settings)) {
|
|
63
|
+
const [primary, secondary] = settings;
|
|
64
|
+
if (primary === null) {
|
|
65
|
+
return settings;
|
|
66
|
+
}
|
|
67
|
+
return [primary, { ...(secondary as object | undefined), severity }];
|
|
68
|
+
}
|
|
69
|
+
return [settings, { severity }];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Build the configuration.
|
|
74
|
+
*
|
|
75
|
+
* One config serves both standalone `.css` and the CSS masked out of a
|
|
76
|
+
* `{% stylesheet %}` block. There used to be a relaxed variant for Liquid,
|
|
77
|
+
* because a masked interpolation stands in as a value and the rules that
|
|
78
|
+
* inspect the shape of a value fired on it. Liquid inside `{% stylesheet %}` is
|
|
79
|
+
* now an offense in its own right — Theme Check's
|
|
80
|
+
* `StaticStylesheetAndJavascriptTags` reports it as an error — so a value built
|
|
81
|
+
* out of a placeholder can only appear in a block that is already failing the
|
|
82
|
+
* lint, and there is nothing left for the relaxation to protect.
|
|
83
|
+
*
|
|
84
|
+
* It takes no options, and is still a function so that a project override
|
|
85
|
+
* passed through `SHOPIFY_LINT_STYLELINT_CONFIG` keeps the same shape.
|
|
86
|
+
*/
|
|
87
|
+
export function createConfig(): Config {
|
|
88
|
+
const merged: Record<string, RuleSettings> = {
|
|
89
|
+
...(recommended.rules as Record<string, RuleSettings>),
|
|
90
|
+
...(standard.rules as Record<string, RuleSettings>),
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const rules: Record<string, RuleSettings> = {};
|
|
94
|
+
for (const [name, settings] of Object.entries(merged)) {
|
|
95
|
+
rules[name] = CORRECTNESS_RULES.has(name) ? withSeverity(settings, 'error') : settings;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
Object.assign(rules, THEORY_OVERRIDES);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
// Everything the standard config adds on top of the correctness layer is a
|
|
102
|
+
// convention, so warning is the right floor; the correctness rules are
|
|
103
|
+
// promoted to error individually above.
|
|
104
|
+
defaultSeverity: 'warning',
|
|
105
|
+
rules,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default createConfig();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theory Digital's Stylelint configuration for Shopify theme CSS.
|
|
3
|
+
*
|
|
4
|
+
* The package ships no custom rules yet: the branch lint runs the upstream
|
|
5
|
+
* `stylelint-config-standard` set, graded by `./config.js`. When Theory CSS
|
|
6
|
+
* conventions are written down, add a `src/rules/` directory and a `plugin`
|
|
7
|
+
* export here — `createConfig` is the only place the wiring would change, and
|
|
8
|
+
* the runner and the Python orchestrator would not need touching. See README.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { maskLiquidStylesheet } from './liquid/mask.js';
|
|
12
|
+
|
|
13
|
+
// `createConfig` deliberately lives in ./config.js and is not re-exported here,
|
|
14
|
+
// matching eslint-plugin-theory: once this module gains a plugin object that
|
|
15
|
+
// config.ts imports, re-exporting it would make that a cycle.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import stylelint from 'stylelint';
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createConfig } from './config.js';
|
|
8
|
+
import { maskLiquidStylesheet } from './liquid/mask.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lint a source the way `bin/lint-css.mjs` does, so these tests exercise the
|
|
12
|
+
* real Stylelint pipeline rather than the config object alone.
|
|
13
|
+
*/
|
|
14
|
+
async function lint(
|
|
15
|
+
code: string,
|
|
16
|
+
codeFilename: string,
|
|
17
|
+
cwd: string,
|
|
18
|
+
): Promise<stylelint.Warning[]> {
|
|
19
|
+
const result = await stylelint.lint({
|
|
20
|
+
code,
|
|
21
|
+
codeFilename,
|
|
22
|
+
config: createConfig(),
|
|
23
|
+
cwd,
|
|
24
|
+
ignorePath: join(import.meta.dirname, '..', '.stylelintignore'),
|
|
25
|
+
disableDefaultIgnores: true,
|
|
26
|
+
quietDeprecationWarnings: true,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(result.results).toHaveLength(1);
|
|
30
|
+
expect(result.results[0]!.ignored).toBeFalsy();
|
|
31
|
+
expect(result.results[0]!.invalidOptionWarnings).toEqual([]);
|
|
32
|
+
|
|
33
|
+
return result.results[0]!.warnings;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function find(warnings: stylelint.Warning[], rule: string): stylelint.Warning | undefined {
|
|
37
|
+
return warnings.find((warning) => warning.rule === rule);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('linting a standalone CSS asset', () => {
|
|
41
|
+
let cwd: string;
|
|
42
|
+
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
cwd = await mkdtemp(join(tmpdir(), 'stylelint-config-theory-'));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(async () => {
|
|
48
|
+
await rm(cwd, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('grades a correctness offense as an error and a convention offense as a warning', async () => {
|
|
52
|
+
const warnings = await lint(
|
|
53
|
+
'.td-card {\n margin: 0px;\n}\n.td-empty {}\n',
|
|
54
|
+
join(cwd, 'assets/component-td-card.css'),
|
|
55
|
+
cwd,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(find(warnings, 'block-no-empty')?.severity).toBe('error');
|
|
59
|
+
expect(find(warnings, 'length-zero-no-unit')?.severity).toBe('warning');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('reports a CSS syntax error as an error', async () => {
|
|
63
|
+
const warnings = await lint(
|
|
64
|
+
'.td-card { color: red;\n',
|
|
65
|
+
join(cwd, 'assets/component-td-card.css'),
|
|
66
|
+
cwd,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect(find(warnings, 'CssSyntaxError')?.severity).toBe('error');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('accepts BEM-style class and custom-property names', async () => {
|
|
73
|
+
const warnings = await lint(
|
|
74
|
+
'.td-card__title--active {\n --tdCardGap: 1rem;\n}\n',
|
|
75
|
+
join(cwd, 'assets/component-td-card.css'),
|
|
76
|
+
cwd,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
expect(warnings).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('cannot be disabled by the theme', async () => {
|
|
83
|
+
// The whole point of the branch lint is that it is a gate. A theme that
|
|
84
|
+
// ships its own config, or ignores its own assets, must not silence it.
|
|
85
|
+
await writeFile(join(cwd, '.stylelintrc.json'), '{"rules":{}}\n');
|
|
86
|
+
await writeFile(join(cwd, '.stylelintignore'), 'assets/**\n*.liquid\n');
|
|
87
|
+
|
|
88
|
+
const warnings = await lint(
|
|
89
|
+
'.td-empty {}\n',
|
|
90
|
+
join(cwd, 'assets/component-td-card.css'),
|
|
91
|
+
cwd,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(find(warnings, 'block-no-empty')?.severity).toBe('error');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('linting the CSS inside a Liquid stylesheet block', () => {
|
|
99
|
+
let cwd: string;
|
|
100
|
+
|
|
101
|
+
beforeAll(async () => {
|
|
102
|
+
cwd = await mkdtemp(join(tmpdir(), 'stylelint-config-theory-'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
afterAll(async () => {
|
|
106
|
+
await rm(cwd, { recursive: true, force: true });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
/** Mask a `.liquid` source and lint it, as the runner does. */
|
|
110
|
+
async function lintLiquid(lines: string[]): Promise<stylelint.Warning[]> {
|
|
111
|
+
const masked = maskLiquidStylesheet(lines.join('\n'));
|
|
112
|
+
expect(masked).not.toBeNull();
|
|
113
|
+
return lint(masked!, join(cwd, 'sections/td-hero.liquid'), cwd);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
it('reports an offense at its real line and column in the liquid file', async () => {
|
|
117
|
+
const lines = [
|
|
118
|
+
'<div class="td-hero">{{ product.title }}</div>',
|
|
119
|
+
'{% stylesheet %}',
|
|
120
|
+
' .td-hero { color: red; }',
|
|
121
|
+
' .td-broken {}',
|
|
122
|
+
'{% endstylesheet %}',
|
|
123
|
+
'',
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const warnings = await lintLiquid(lines);
|
|
127
|
+
const offense = find(warnings, 'block-no-empty');
|
|
128
|
+
const offendingLine = ' .td-broken {}';
|
|
129
|
+
|
|
130
|
+
expect(offense?.severity).toBe('error');
|
|
131
|
+
// Derived, not hard-coded: this is the invariant the mask exists for. The
|
|
132
|
+
// position must be the one in the `.liquid` file, not in the masked source.
|
|
133
|
+
expect(offense?.line).toBe(lines.indexOf(offendingLine) + 1);
|
|
134
|
+
expect(offense?.column).toBe(offendingLine.indexOf('{}') + 1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('leaves a Liquid-free block clean under the full rule set', async () => {
|
|
138
|
+
const warnings = await lintLiquid([
|
|
139
|
+
'{% stylesheet %}',
|
|
140
|
+
' .td-hero {',
|
|
141
|
+
' margin: var(--td-hero-gap);',
|
|
142
|
+
" background: url('hero.png');",
|
|
143
|
+
' }',
|
|
144
|
+
'',
|
|
145
|
+
' @media (width >= 40em) {',
|
|
146
|
+
' .td-hero { color: red; }',
|
|
147
|
+
' }',
|
|
148
|
+
'{% endstylesheet %}',
|
|
149
|
+
'',
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
expect(warnings).toEqual([]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('grades a masked interpolation like any other value', async () => {
|
|
156
|
+
// The value-shape rules used to be disabled inside a block, because a
|
|
157
|
+
// masked `{{ … }}` stands in as `0`. Liquid here is now an error in its own
|
|
158
|
+
// right — `StaticStylesheetAndJavascriptTags` reports it — so the substitution
|
|
159
|
+
// no longer has to read as correct CSS, and the rules stay on. Whichever
|
|
160
|
+
// rules fire, the author has to remove the Liquid.
|
|
161
|
+
const warnings = await lintLiquid([
|
|
162
|
+
'{% stylesheet %}',
|
|
163
|
+
' .td-hero {',
|
|
164
|
+
' margin: {{ section.settings.gap }}px;',
|
|
165
|
+
' }',
|
|
166
|
+
'{% endstylesheet %}',
|
|
167
|
+
'',
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
expect(warnings.map(({ rule }) => rule)).toContain('declaration-property-value-no-unknown');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('still reports structural offenses inside the block', async () => {
|
|
174
|
+
const warnings = await lintLiquid([
|
|
175
|
+
'{% stylesheet %}',
|
|
176
|
+
' .td-hero {',
|
|
177
|
+
' colr: red;',
|
|
178
|
+
' margin: 0px;',
|
|
179
|
+
' }',
|
|
180
|
+
'{% endstylesheet %}',
|
|
181
|
+
'',
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
expect(find(warnings, 'property-no-unknown')?.severity).toBe('error');
|
|
185
|
+
expect(find(warnings, 'length-zero-no-unit')?.severity).toBe('warning');
|
|
186
|
+
});
|
|
187
|
+
});
|