@motion-proto/live-tokens 0.77.0 → 0.79.0
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/.claude/skills/live-tokens-check-compliance/SKILL.md +60 -29
- package/.claude/skills/live-tokens-create-component/SKILL.md +23 -28
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +61 -0
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -15
- package/.claude/skills/live-tokens-create-page/SKILL.md +9 -6
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
- package/.claude/skills/live-tokens-pick-component/SKILL.md +3 -3
- package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +1 -1
- package/.claude/skills/live-tokens-set-geometry/SKILL.md +1 -1
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +1 -1
- package/CHANGELOG.md +284 -0
- package/README.md +9 -15
- package/bin/check-component.mjs +152 -551
- package/bin/check-page.mjs +56 -420
- package/bin/cli.mjs +83 -15
- package/bin/contractRunner.mjs +380 -120
- package/bin/lib/catalogue.mjs +161 -34
- package/bin/lib/componentSource.mjs +152 -0
- package/bin/lib/cssValues.mjs +9 -0
- package/bin/lib/dataDir.mjs +126 -0
- package/bin/lib/findings.mjs +103 -12
- package/bin/lib/fixers.mjs +64 -0
- package/bin/lib/geometry.mjs +92 -0
- package/bin/lib/pageRoutes.mjs +309 -0
- package/bin/lib/pageSource.mjs +230 -0
- package/bin/lib/report.mjs +57 -59
- package/bin/lib/tokenVocabulary.mjs +104 -34
- package/bin/rules/componentStructure.mjs +344 -0
- package/bin/rules/componentUse.mjs +313 -0
- package/bin/rules/importsAndRoutes.mjs +136 -0
- package/bin/rules/testRuns.mjs +122 -0
- package/bin/rules/tokens.mjs +363 -0
- package/bin/setup-claude.mjs +1 -2
- package/dist-plugin/{chunk-PDNL4NC5.js → chunk-D4WRIKEZ.js} +7 -2
- package/dist-plugin/{chunk-SWXRVZKT.js → chunk-REBHE3ZM.js} +414 -1
- package/dist-plugin/index.cjs +432 -15
- package/dist-plugin/index.js +8 -9
- package/dist-plugin/migrateData/index.cjs +422 -4
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +414 -1
- package/dist-plugin/setColors/index.d.cts +1 -1
- package/dist-plugin/setColors/index.d.ts +1 -1
- package/dist-plugin/setColors/index.js +1 -1
- package/dist-plugin/setGeometry/index.cjs +423 -13
- package/dist-plugin/setGeometry/index.d.cts +3 -3
- package/dist-plugin/setGeometry/index.d.ts +3 -3
- package/dist-plugin/setGeometry/index.js +10 -13
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-BxRtuN5V.d.cts → themeTypes-B8_Idrp4.d.cts} +2 -2
- package/dist-plugin/{themeTypes-BxRtuN5V.d.ts → themeTypes-B8_Idrp4.d.ts} +2 -2
- package/package.json +6 -4
- package/src/editor/component-editor/CalloutEditor.svelte +2 -2
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +14 -15
- package/src/editor/component-editor/CornerBadgeEditor.svelte +14 -14
- package/src/editor/component-editor/DialogEditor.svelte +5 -5
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +2 -2
- package/src/editor/component-editor/RadioButtonEditor.svelte +21 -21
- package/src/editor/component-editor/SectionDividerEditor.svelte +7 -7
- package/src/editor/component-editor/SegmentedControlEditor.svelte +5 -5
- package/src/editor/component-editor/SideNavigationEditor.svelte +25 -25
- package/src/editor/component-editor/TabBarEditor.svelte +6 -6
- package/src/editor/component-editor/TableEditor.svelte +6 -6
- package/src/editor/component-editor/ToggleEditor.svelte +2 -2
- package/src/editor/component-editor/index.ts +3 -0
- package/src/editor/component-editor/registry.ts +57 -1
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +14 -14
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +3 -0
- package/src/editor/component-editor/scaffolding/types.ts +15 -0
- package/src/editor/core/components/adjustAliases.ts +4 -4
- package/src/editor/core/components/aliasKinds.ts +20 -19
- package/src/editor/core/sketch/sketchLayer.ts +3 -3
- package/src/editor/core/themes/migrateComponentConfig.ts +14 -6
- package/src/editor/core/themes/migrations/2026-09-13-badge-brand.ts +42 -0
- package/src/editor/core/themes/migrations/2026-09-13-collapsiblesection-open.ts +33 -0
- package/src/editor/core/themes/migrations/2026-09-13-cornerbadge-prefix.ts +70 -0
- package/src/editor/core/themes/migrations/2026-09-13-hairline.ts +92 -0
- package/src/editor/core/themes/migrations/2026-09-13-indicator.ts +54 -0
- package/src/editor/core/themes/migrations/2026-09-13-sectiondivider-surface.ts +36 -0
- package/src/editor/core/themes/migrations/2026-09-13-selected-state.ts +124 -0
- package/src/editor/core/themes/migrations/2026-09-13-toggle-label.ts +31 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/themeService.ts +3 -3
- package/src/editor/core/themes/themeTypes.ts +13 -15
- package/src/editor/docs/Docs.svelte +1 -1
- package/src/editor/docs/content/light-and-dark.md +3 -3
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/index.ts +1 -0
- package/src/editor/overlay/ColumnsOverlay.svelte +1 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +16 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +1 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte +4 -4
- package/src/editor/skill-atlas/TreeNodeCard.svelte +4 -4
- package/src/editor/skill-atlas/skillSources.generated.ts +11 -14
- package/src/editor/skill-atlas/skillTrees.ts +1 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +258 -93
- package/src/editor/skill-atlas/trees/create-component.ts +73 -62
- package/src/editor/skill-atlas/trees/create-page.ts +54 -33
- package/src/editor/skill-atlas/trees/pick-component.ts +3 -3
- package/src/editor/skill-atlas/trees/set-geometry.ts +1 -1
- package/src/editor/ui/UIPaletteSelector.svelte +11 -15
- package/src/editor/ui/variantScales.ts +8 -8
- package/src/live-tokens/data/themes/autumn.json +188 -188
- package/src/live-tokens/data/themes/halloween.json +188 -188
- package/src/live-tokens/data/themes/midnight-study.json +245 -245
- package/src/live-tokens/data/themes/ocean.json +188 -188
- package/src/live-tokens/data/themes/royal-velvet.json +188 -188
- package/src/live-tokens/data/themes/sketchy.json +188 -188
- package/src/live-tokens/data/themes/spring-meadow.json +188 -188
- package/src/live-tokens/data/themes/sunset.json +188 -188
- package/src/system/components/Badge.svelte +27 -23
- package/src/system/components/Button.svelte +13 -7
- package/src/system/components/Callout.svelte +16 -11
- package/src/system/components/Card.svelte +22 -15
- package/src/system/components/CodeSnippet.svelte +10 -6
- package/src/system/components/CollapsibleSection.svelte +68 -63
- package/src/system/components/CornerBadge.svelte +78 -72
- package/src/system/components/Dialog.svelte +21 -18
- package/src/system/components/IconButton.svelte +13 -9
- package/src/system/components/Image.svelte +14 -8
- package/src/system/components/ImageLightbox.svelte +10 -6
- package/src/system/components/InlineEditActions.svelte +17 -14
- package/src/system/components/Input.svelte +13 -7
- package/src/system/components/MenuSelect.svelte +13 -7
- package/src/system/components/Notification.svelte +17 -11
- package/src/system/components/Panel.svelte +13 -6
- package/src/system/components/ProgressBar.svelte +10 -5
- package/src/system/components/RadioButton.svelte +33 -30
- package/src/system/components/SectionDivider.svelte +28 -21
- package/src/system/components/SegmentedControl.svelte +27 -23
- package/src/system/components/SideNavigation.svelte +175 -171
- package/src/system/components/Slider.svelte +11 -7
- package/src/system/components/TabBar.svelte +53 -49
- package/src/system/components/Table.svelte +20 -15
- package/src/system/components/Toggle.svelte +14 -10
- package/src/system/components/Tooltip.svelte +10 -6
- package/src/system/styles/CONVENTIONS.md +3 -4
- package/src/testing-js/chunk-3UKGXCDL.js +48 -0
- package/src/testing-js/chunk-3UKGXCDL.js.map +1 -0
- package/src/testing-js/{chunk-ZMSX6CXR.js → chunk-NB3NZRBM.js} +13 -2
- package/src/testing-js/chunk-NB3NZRBM.js.map +1 -0
- package/src/testing-js/{chunk-FAFOAWYL.js → chunk-Q3YIAAG3.js} +17 -3
- package/src/testing-js/chunk-Q3YIAAG3.js.map +1 -0
- package/src/testing-js/{chunk-7TI7Z6Y6.js → chunk-U7OJE5DU.js} +19 -19
- package/src/testing-js/chunk-U7OJE5DU.js.map +1 -0
- package/src/testing-js/{chunk-L73N4NSO.js → chunk-WIZ6W7UT.js} +2 -2
- package/src/testing-js/{chunk-4JQX6WWL.js → chunk-ZMZQZ33J.js} +474 -166
- package/src/testing-js/chunk-ZMZQZ33J.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-behavior.contract.js +155 -0
- package/src/testing-js/component-behavior.contract.js.map +1 -0
- package/src/testing-js/component-editor.contract.js +7 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +15 -11
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +50 -4
- package/src/testing-js/index.js +32 -8
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +867 -0
- package/src/testing-js/page-compliance.contract.js.map +1 -0
- package/src/testing-js/registry.contract.js +5 -3
- package/src/testing-js/registry.contract.js.map +1 -1
- package/src/testing-js/{vitest-BE6uGF31.d.ts → vitest-BMLIbDs2.d.ts} +29 -3
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +3 -2
- package/template/README.md +16 -9
- package/template/package.json +2 -2
- package/template/src/pages/Home.svelte +13 -0
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +0 -196
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +0 -98
- package/src/editor/skill-atlas/trees/fix-findings.ts +0 -469
- package/src/testing-js/chunk-4JQX6WWL.js.map +0 -1
- package/src/testing-js/chunk-7TI7Z6Y6.js.map +0 -1
- package/src/testing-js/chunk-FAFOAWYL.js.map +0 -1
- package/src/testing-js/chunk-ZMSX6CXR.js.map +0 -1
- /package/src/testing-js/{chunk-L73N4NSO.js.map → chunk-WIZ6W7UT.js.map} +0 -0
package/bin/check-page.mjs
CHANGED
|
@@ -12,442 +12,78 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
14
14
|
import { join, relative, resolve, basename } from 'node:path';
|
|
15
|
-
import { isExcluded, lineOf } from './lib/findings.mjs';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
15
|
+
import { assembleRules, isExcluded, lineOf } from './lib/findings.mjs';
|
|
16
|
+
import { codeRegion, inlineStyleRegions, styleRegions } from './lib/pageSource.mjs';
|
|
17
|
+
import { loadVocabulary, walk } from './lib/tokenVocabulary.mjs';
|
|
18
18
|
import { resolveTokensCssPath } from './migrate.mjs';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export const COMPONENT_IMPORT =
|
|
42
|
-
/(?:@motion-proto\/live-tokens\/components|[./][^'"]*\/system\/components)\/([A-Za-z0-9]+)\.svelte$/;
|
|
43
|
-
|
|
44
|
-
const DEEP_IMPORT_PATTERNS = [
|
|
45
|
-
/^@motion-proto\/live-tokens\/src\//,
|
|
46
|
-
/node_modules\/@motion-proto\/live-tokens/,
|
|
47
|
-
];
|
|
48
|
-
|
|
49
|
-
const TEXT_AXES = ['font-size', 'font-family', 'font-weight', 'line-height', 'letter-spacing'];
|
|
50
|
-
|
|
51
|
-
// The single-axis scales in tokens.css. A text style bundle carries its axis
|
|
52
|
-
// as a suffix (--body-md-font-size, --code-font-family), so no bundle name
|
|
53
|
-
// matches, and neither does a custom property the page declares itself. A
|
|
54
|
-
// weight alone cannot move the scale or the fonts, so --font-weight-* is not one.
|
|
55
|
-
const SINGLE_AXIS_TOKEN =
|
|
56
|
-
/^--(?:font-size|line-height|letter-spacing)-|^--font-(?:sans|serif|mono|display|editorial)$/;
|
|
57
|
-
|
|
58
|
-
// The geometry the theme owns: spacing, stroke, radius, and shadow all have a
|
|
59
|
-
// token scale, and `set-geometry` moves them. Sizing (a hero's height, a
|
|
60
|
-
// column's minimum width, a max content width) is layout, has no scale, and
|
|
61
|
-
// stays literal.
|
|
62
|
-
const THEMED_GEOMETRY = /^(padding|margin|gap|row-gap|column-gap|border|outline|inset|top|right|bottom|left|box-shadow|text-shadow)(-|$)|-radius$/;
|
|
63
|
-
|
|
64
|
-
// A local two-up or three-up is a layout. From four columns on, a hardcoded
|
|
65
|
-
// count reads as a claim about the page grid, which `--columns-count` owns.
|
|
66
|
-
const PAGE_GRID_COLUMNS = 4;
|
|
67
|
-
|
|
68
|
-
/** Blank out comments, url() payloads, and string contents so none of them can match a rule. */
|
|
69
|
-
function neutralise(css) {
|
|
70
|
-
return blankStrings(
|
|
71
|
-
css
|
|
72
|
-
.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
|
|
73
|
-
.replace(/url\((?:[^()]|\([^()]*\))*\)/g, (m) => ' '.repeat(m.length)),
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** `<style>` blocks with their absolute offset in the file; whole file for .css. */
|
|
78
|
-
function styleRegions(text, file) {
|
|
79
|
-
if (file.endsWith('.css')) return [{ text, offset: 0 }];
|
|
80
|
-
const out = [];
|
|
81
|
-
for (const m of text.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) {
|
|
82
|
-
out.push({ text: m[1], offset: m.index + m[0].indexOf(m[1]) });
|
|
83
|
-
}
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Inline styles in markup, as declaration lists the value rules can read: a
|
|
89
|
-
* `style="..."` attribute verbatim, and a `style:prop="value"` directive
|
|
90
|
-
* rewritten as `prop: value;`. A `{...}` expression is dynamic and skipped.
|
|
91
|
-
*/
|
|
92
|
-
function inlineStyleRegions(code) {
|
|
93
|
-
const out = [];
|
|
94
|
-
for (const m of code.matchAll(/\sstyle=(["'])([^"']*)\1/g)) {
|
|
95
|
-
out.push({ text: `${m[2]};`, offset: m.index + m[0].indexOf(m[2]) });
|
|
96
|
-
}
|
|
97
|
-
for (const m of code.matchAll(/\sstyle:([a-z-]+)=(["'])([^"']*)\2/g)) {
|
|
98
|
-
out.push({ text: `${m[1]}: ${m[3]};`, offset: m.index + 1 });
|
|
99
|
-
}
|
|
100
|
-
return out;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Everything outside `<style>`: script and markup. */
|
|
104
|
-
function codeRegion(text, file) {
|
|
105
|
-
if (file.endsWith('.css')) return null;
|
|
106
|
-
return text.replace(/<style[^>]*>[\s\S]*?<\/style>/g, (m) => ' '.repeat(m.length));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Declarations in a stylesheet, with at-rule preludes excluded. A breakpoint in
|
|
111
|
-
* `@media (max-width: 768px)` is structural geometry, not a themeable value.
|
|
112
|
-
* A property name is read from its start, so `--heading-2xl` is one custom
|
|
113
|
-
* property and never the property `xl`.
|
|
114
|
-
*/
|
|
115
|
-
function declarations(css) {
|
|
116
|
-
const body = css.replace(/@[a-z-]+[^;{]*(?=\{)/gi, (m) => ' '.repeat(m.length));
|
|
117
|
-
const out = [];
|
|
118
|
-
for (const m of body.matchAll(/(?<![\w-])((?:--)?[a-z][\w-]*)\s*:\s*([^;{}]+)[;}]/gi)) {
|
|
119
|
-
out.push({ prop: m[1].toLowerCase(), value: m[2].trim(), index: m.index });
|
|
120
|
-
}
|
|
121
|
-
return out;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* The attributes of one component tag starting at `start` (the `<`), read with
|
|
126
|
-
* `{}` depth and quotes tracked so an expression holding `>` does not end the
|
|
127
|
-
* tag early. Returns null when the tag spreads an object, which makes its prop
|
|
128
|
-
* set unknowable.
|
|
129
|
-
*/
|
|
130
|
-
function tagAttributes(code, start) {
|
|
131
|
-
let i = code.indexOf(' ', start);
|
|
132
|
-
const tagEnd = (() => {
|
|
133
|
-
let depth = 0;
|
|
134
|
-
let quote = null;
|
|
135
|
-
for (let j = start; j < code.length; j++) {
|
|
136
|
-
const c = code[j];
|
|
137
|
-
if (quote) {
|
|
138
|
-
if (c === quote) quote = null;
|
|
139
|
-
} else if (c === '"' || c === "'") quote = c;
|
|
140
|
-
else if (c === '{') depth++;
|
|
141
|
-
else if (c === '}') depth--;
|
|
142
|
-
else if (c === '>' && depth === 0) return j;
|
|
143
|
-
}
|
|
144
|
-
return code.length;
|
|
145
|
-
})();
|
|
146
|
-
if (i === -1 || i > tagEnd) return { attrs: [], end: tagEnd };
|
|
147
|
-
const attrs = [];
|
|
148
|
-
while (i < tagEnd) {
|
|
149
|
-
const c = code[i];
|
|
150
|
-
if (/\s/.test(c) || c === '/') {
|
|
151
|
-
i++;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (c === '{') {
|
|
155
|
-
let depth = 0;
|
|
156
|
-
let j = i;
|
|
157
|
-
for (; j < tagEnd; j++) {
|
|
158
|
-
if (code[j] === '{') depth++;
|
|
159
|
-
else if (code[j] === '}' && --depth === 0) break;
|
|
160
|
-
}
|
|
161
|
-
const inner = code.slice(i + 1, j).trim();
|
|
162
|
-
if (inner.startsWith('...')) return null;
|
|
163
|
-
if (/^\w+$/.test(inner)) attrs.push({ name: inner, value: null, index: i });
|
|
164
|
-
i = j + 1;
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
const name = code.slice(i).match(/^[^\s=/>]+/)?.[0];
|
|
168
|
-
if (!name) break;
|
|
169
|
-
const at = i;
|
|
170
|
-
i += name.length;
|
|
171
|
-
let value = null;
|
|
172
|
-
if (code[i] === '=') {
|
|
173
|
-
i++;
|
|
174
|
-
const q = code[i];
|
|
175
|
-
if (q === '"' || q === "'") {
|
|
176
|
-
const close = code.indexOf(q, i + 1);
|
|
177
|
-
value = code.slice(i + 1, close === -1 ? tagEnd : close);
|
|
178
|
-
i = close === -1 ? tagEnd : close + 1;
|
|
179
|
-
} else if (q === '{') {
|
|
180
|
-
let depth = 0;
|
|
181
|
-
for (; i < tagEnd; i++) {
|
|
182
|
-
if (code[i] === '{') depth++;
|
|
183
|
-
else if (code[i] === '}' && --depth === 0) break;
|
|
184
|
-
}
|
|
185
|
-
i++;
|
|
186
|
-
} else {
|
|
187
|
-
const bare = code.slice(i).match(/^[^\s>]+/)?.[0] ?? '';
|
|
188
|
-
value = bare;
|
|
189
|
-
i += bare.length;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
attrs.push({ name, value, index: at });
|
|
193
|
-
}
|
|
194
|
-
return { attrs, end: tagEnd };
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Props a page passes that the component does not declare, values outside a
|
|
199
|
-
* prop's union, and a shipped component the page sizes itself.
|
|
200
|
-
*/
|
|
201
|
-
function checkComponentUsage(code, imports, add) {
|
|
202
|
-
for (const [local, entry] of imports) {
|
|
203
|
-
const props = entry.props;
|
|
204
|
-
if (!props) continue;
|
|
205
|
-
const re = new RegExp(`<${local}(?=[\\s/>])`, 'g');
|
|
206
|
-
for (const m of code.matchAll(re)) {
|
|
207
|
-
const tag = tagAttributes(code, m.index);
|
|
208
|
-
if (!tag) continue;
|
|
209
|
-
for (const { name, value, index } of tag.attrs) {
|
|
210
|
-
if (name.includes(':') || name.startsWith('@') || name === 'children') continue;
|
|
211
|
-
if (!props.props.has(name)) {
|
|
212
|
-
add('unknown-prop', index, `${entry.name} has no prop '${name}'; it accepts ${[...props.props].join(', ')}`);
|
|
213
|
-
continue;
|
|
214
|
-
}
|
|
215
|
-
const allowed = props.enums.get(name);
|
|
216
|
-
if (allowed && value !== null && !allowed.has(value)) {
|
|
217
|
-
add('unknown-prop-value', index, `${entry.name} ${name}="${value}" is not one of ${[...allowed].join(', ')}`);
|
|
218
|
-
}
|
|
219
|
-
if (name === 'size' && entry.origin === 'shipped') {
|
|
220
|
-
add(
|
|
221
|
-
'control-size',
|
|
222
|
-
index,
|
|
223
|
-
`${entry.name} ${value === null ? 'is sized here' : `size="${value}"`}. Drop it for the shipped default, or retune ${entry.name} for the whole project in /live-tokens/components.`,
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* One finding when a page holds more than one primary Button, at the second of
|
|
233
|
-
* them. Emphasis is what the variant carries, so a second primary leaves the
|
|
234
|
-
* page with no single most important action. A Button with no variant is
|
|
235
|
-
* primary, the component's default.
|
|
236
|
-
*/
|
|
237
|
-
function checkPrimaryActions(code, imports, add) {
|
|
238
|
-
const primaries = [];
|
|
239
|
-
for (const [local, entry] of imports) {
|
|
240
|
-
if (entry.id !== 'button') continue;
|
|
241
|
-
for (const m of code.matchAll(new RegExp(`<${local}(?=[\\s/>])`, 'g'))) {
|
|
242
|
-
const tag = tagAttributes(code, m.index);
|
|
243
|
-
const variant = tag?.attrs.find((a) => a.name === 'variant');
|
|
244
|
-
if (tag && (variant === undefined || variant.value === 'primary')) primaries.push(m.index);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
if (primaries.length < 2) return;
|
|
248
|
-
primaries.sort((a, b) => a - b);
|
|
249
|
-
add(
|
|
19
|
+
import * as componentUse from './rules/componentUse.mjs';
|
|
20
|
+
import * as importsAndRoutes from './rules/importsAndRoutes.mjs';
|
|
21
|
+
import * as testRuns from './rules/testRuns.mjs';
|
|
22
|
+
import * as tokenRules from './rules/tokens.mjs';
|
|
23
|
+
|
|
24
|
+
/** Every rule, with its default severity, its repair, and its guidance. Same
|
|
25
|
+
* three fields, same meanings, as `COMPONENT_RULES`. */
|
|
26
|
+
export const PAGE_RULES = assembleRules(
|
|
27
|
+
[
|
|
28
|
+
'unknown-component',
|
|
29
|
+
'unknown-prop',
|
|
30
|
+
'unknown-prop-value',
|
|
31
|
+
'deep-import',
|
|
32
|
+
'unknown-token',
|
|
33
|
+
'color-literal',
|
|
34
|
+
'reserved-route',
|
|
35
|
+
'site-css-in-main',
|
|
36
|
+
'raw-text-axis',
|
|
37
|
+
'dimension-literal',
|
|
38
|
+
'hardcoded-columns',
|
|
39
|
+
'missing-source',
|
|
40
|
+
'control-size',
|
|
250
41
|
'multiple-primary',
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
42
|
+
'danger-without-dialog',
|
|
43
|
+
'native-control',
|
|
44
|
+
'property-override',
|
|
45
|
+
'page-component-paint',
|
|
46
|
+
'page-text-style',
|
|
47
|
+
'page-contrast',
|
|
48
|
+
'page-grid',
|
|
49
|
+
'page-overflow',
|
|
50
|
+
'tests-not-installed',
|
|
51
|
+
'tests-setup',
|
|
52
|
+
'tests-incomplete',
|
|
53
|
+
],
|
|
54
|
+
componentUse.pageRules,
|
|
55
|
+
importsAndRoutes.pageRules,
|
|
56
|
+
tokenRules.pageRules,
|
|
57
|
+
testRuns.pageRules,
|
|
58
|
+
);
|
|
255
59
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
* danger action destroys saved work, and the page has nothing to confirm it.
|
|
259
|
-
*/
|
|
260
|
-
function checkDestructiveActions(code, imports, add) {
|
|
261
|
-
if ([...imports.values()].some((entry) => entry.id === 'dialog')) return;
|
|
262
|
-
for (const [local, entry] of imports) {
|
|
263
|
-
if (entry.id !== 'button' && entry.id !== 'iconbutton') continue;
|
|
264
|
-
for (const m of code.matchAll(new RegExp(`<${local}(?=[\\s/>])`, 'g'))) {
|
|
265
|
-
const tag = tagAttributes(code, m.index);
|
|
266
|
-
if (!tag?.attrs.some((a) => a.name === 'variant' && a.value === 'danger')) continue;
|
|
267
|
-
add(
|
|
268
|
-
'danger-without-dialog',
|
|
269
|
-
m.index,
|
|
270
|
-
`${entry.name} variant="danger" with no Dialog in this page. A destructive action confirms in a Dialog before it runs.`,
|
|
271
|
-
);
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
60
|
+
// Directories that hold the system, not pages built on it.
|
|
61
|
+
export const NOT_PAGES = ['src/system', 'src/editor', 'src/lib', 'src/live-tokens'];
|
|
276
62
|
|
|
277
|
-
|
|
278
|
-
function enclosingObject(text, index) {
|
|
279
|
-
let depth = 0;
|
|
280
|
-
let start = -1;
|
|
281
|
-
for (let i = index; i >= 0; i--) {
|
|
282
|
-
const c = text[i];
|
|
283
|
-
if (c === '}') depth++;
|
|
284
|
-
else if (c === '{') {
|
|
285
|
-
if (depth === 0) {
|
|
286
|
-
start = i;
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
depth--;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (start === -1) return null;
|
|
293
|
-
depth = 0;
|
|
294
|
-
for (let i = start; i < text.length; i++) {
|
|
295
|
-
const c = text[i];
|
|
296
|
-
if (c === '{') depth++;
|
|
297
|
-
else if (c === '}') {
|
|
298
|
-
depth--;
|
|
299
|
-
if (depth === 0) return text.slice(start, i + 1);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
63
|
+
export { COMPONENT_IMPORT } from './rules/componentUse.mjs';
|
|
304
64
|
|
|
305
65
|
function checkFile(file, text, vocab, root) {
|
|
306
66
|
const rel = relative(root, file);
|
|
307
67
|
const findings = [];
|
|
308
|
-
const add = (rule, index, message) =>
|
|
309
|
-
findings.push({ rule, file: rel, line: lineOf(text, index), message });
|
|
68
|
+
const add = (rule, index, message, extra = {}) =>
|
|
69
|
+
findings.push({ rule, file: rel, line: lineOf(text, index), message, ...extra });
|
|
310
70
|
|
|
311
71
|
const code = codeRegion(text, file);
|
|
72
|
+
const regions = styleRegions(text, file);
|
|
73
|
+
const inlineRegions = code === null ? [] : inlineStyleRegions(code);
|
|
312
74
|
if (code !== null) {
|
|
313
75
|
const imports = new Map();
|
|
314
76
|
for (const m of code.matchAll(/import\s+(?:([^'"]*?)\s+from\s+)?['"]([^'"]+)['"]/g)) {
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
add('deep-import', m.index, `deep import into package internals: ${spec}`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
const comp = spec.match(COMPONENT_IMPORT);
|
|
322
|
-
if (!comp) continue;
|
|
323
|
-
const entry = vocab.components.get(comp[1].toLowerCase());
|
|
324
|
-
if (!entry) {
|
|
325
|
-
add(
|
|
326
|
-
'unknown-component',
|
|
327
|
-
m.index,
|
|
328
|
-
`'${comp[1]}' is not in the component catalogue; author it with live-tokens-create-component or pick a shipped one`,
|
|
329
|
-
);
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
const local = m[1]?.trim().match(/^(\w+)$/)?.[1];
|
|
333
|
-
if (local) imports.set(local, entry);
|
|
334
|
-
}
|
|
335
|
-
checkComponentUsage(code, imports, add);
|
|
336
|
-
checkPrimaryActions(code, imports, add);
|
|
337
|
-
checkDestructiveActions(code, imports, add);
|
|
338
|
-
|
|
339
|
-
for (const m of code.matchAll(/['"](\/live-tokens[^'"]*)['"]\s*:/g)) {
|
|
340
|
-
add('reserved-route', m.index, `route '${m[1]}' is inside the reserved /live-tokens/* namespace`);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
for (const m of code.matchAll(/\blazy\s*:/g)) {
|
|
344
|
-
const entry = enclosingObject(code, m.index);
|
|
345
|
-
if (entry && !/\bsource\s*:/.test(entry)) {
|
|
346
|
-
add('missing-source', m.index, `route entry has no 'source', so Page Source cannot open it`);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
if (/^main\.(ts|js)$/.test(basename(file))) {
|
|
351
|
-
for (const m of code.matchAll(/import\s+['"]([^'"]*site\.css)['"]/g)) {
|
|
352
|
-
add(
|
|
353
|
-
'site-css-in-main',
|
|
354
|
-
m.index,
|
|
355
|
-
`site.css imported from main; import it from each page's <script> so it cannot leak into editor routes`,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
77
|
+
const statement = { index: m.index, clause: m[1], specifier: m[2] };
|
|
78
|
+
importsAndRoutes.checkPageImport(statement, add);
|
|
79
|
+
componentUse.resolveComponentImport(statement, vocab, imports, add);
|
|
358
80
|
}
|
|
81
|
+
componentUse.checkComponentTags(code, imports, add);
|
|
82
|
+
importsAndRoutes.checkRoutes({ file, code }, add);
|
|
359
83
|
}
|
|
360
84
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
// just as declared as one written in CSS.
|
|
364
|
-
const declaredHere = new Set();
|
|
365
|
-
const regions = styleRegions(text, file);
|
|
366
|
-
for (const region of regions) {
|
|
367
|
-
for (const m of neutralise(region.text).matchAll(/(?:^|[;{])\s*(--[a-z0-9-]+)\s*:/gim)) {
|
|
368
|
-
declaredHere.add(m[1]);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
for (const m of text.matchAll(/(?:style:|setProperty\(\s*['"`]|['"`])(--[a-z0-9-]+)/g)) {
|
|
372
|
-
declaredHere.add(m[1]);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
for (const region of [...regions, ...(code === null ? [] : inlineStyleRegions(code))]) {
|
|
376
|
-
const css = neutralise(region.text);
|
|
377
|
-
const at = (i) => region.offset + i;
|
|
378
|
-
|
|
379
|
-
for (const m of css.matchAll(/var\(\s*(--[a-z0-9-]+)/g)) {
|
|
380
|
-
const name = m[1];
|
|
381
|
-
if (declaredHere.has(name) || vocab.knows(name)) continue;
|
|
382
|
-
add(
|
|
383
|
-
'unknown-token',
|
|
384
|
-
at(m.index),
|
|
385
|
-
isContractToken(name)
|
|
386
|
-
? `${name} has the shape of a design token but no longer exists. Check tokens.css for a rename`
|
|
387
|
-
: `${name} is not a design token, a semantic property, or declared in this file`,
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
for (const decl of declarations(css)) {
|
|
392
|
-
const { prop, value, index } = decl;
|
|
393
|
-
if (prop.startsWith('--')) continue;
|
|
394
|
-
|
|
395
|
-
// A `var()` fallback only renders when the token is missing, so a literal
|
|
396
|
-
// inside one is not the page's value.
|
|
397
|
-
const painted = stripVarFallbacks(value);
|
|
398
|
-
if (!TEXT_AXES.includes(prop) && hasColorLiteral(painted)) {
|
|
399
|
-
add('color-literal', at(index), `${prop}: ${value}. Use a design token.`);
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (TEXT_AXES.includes(prop) || prop === 'font') {
|
|
404
|
-
const axis = [...painted.matchAll(/var\(\s*(--[a-z0-9-]+)/g)]
|
|
405
|
-
.map((m) => m[1])
|
|
406
|
-
.find((name) => SINGLE_AXIS_TOKEN.test(name));
|
|
407
|
-
if (axis) {
|
|
408
|
-
add(
|
|
409
|
-
'raw-text-axis',
|
|
410
|
-
at(index),
|
|
411
|
-
`${prop}: ${value}. ${axis} is one axis. Set every axis from one text style bundle (--heading-*, --body-*, --editorial-*, --code-*).`,
|
|
412
|
-
);
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Of the literals only absolute type values are a finding. `em`, `%`,
|
|
417
|
-
// and a unitless line-height are relative to the inherited type, so
|
|
418
|
-
// they ride whatever the theme sets rather than overriding it.
|
|
419
|
-
if (
|
|
420
|
-
!value.includes('var(') &&
|
|
421
|
-
!/^(inherit|initial|unset|normal)$/.test(value) &&
|
|
422
|
-
/\d(px|rem|pt)\b|^[a-z"']/i.test(value)
|
|
423
|
-
) {
|
|
424
|
-
add(
|
|
425
|
-
'raw-text-axis',
|
|
426
|
-
at(index),
|
|
427
|
-
`${prop}: ${value}. Set type from a text style bundle (--heading-*, --body-*, --editorial-*, --code-*).`,
|
|
428
|
-
);
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
if (THEMED_GEOMETRY.test(prop) && hasDimensionLiteral(painted)) {
|
|
434
|
-
add(
|
|
435
|
-
'dimension-literal',
|
|
436
|
-
at(index),
|
|
437
|
-
`${prop}: ${value}. Use a --space-*, --radius-*, --border-width-*, or --shadow-* token.`,
|
|
438
|
-
);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const columns = value.match(/\brepeat\(\s*(\d+)\s*,\s*1fr\s*\)/);
|
|
442
|
-
if (columns && Number(columns[1]) >= PAGE_GRID_COLUMNS) {
|
|
443
|
-
add(
|
|
444
|
-
'hardcoded-columns',
|
|
445
|
-
at(index),
|
|
446
|
-
`${prop}: ${value}. Use repeat(var(--columns-count), 1fr) so the page grid stays in step.`,
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
85
|
+
componentUse.checkPropertyOverrides({ text, regions, inlineRegions, vocab }, add);
|
|
86
|
+
tokenRules.checkPageValues({ text, regions, inlineRegions, vocab }, add);
|
|
451
87
|
|
|
452
88
|
return findings;
|
|
453
89
|
}
|
package/bin/cli.mjs
CHANGED
|
@@ -18,15 +18,18 @@ import { writeSync } from 'node:fs';
|
|
|
18
18
|
import { dirname, join, resolve } from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import process from 'node:process';
|
|
21
|
-
import { COMPONENT_RULES,
|
|
21
|
+
import { COMPONENT_RULES, checkComponent, discoverComponents, formatReport } from './check-component.mjs';
|
|
22
22
|
import { PAGE_RULES, checkPages, discoverPages } from './check-page.mjs';
|
|
23
|
+
import { resolvePageTestTargets } from './lib/pageRoutes.mjs';
|
|
23
24
|
import { describeComponents, describeTokens, formatComponents, formatTokens } from './lib/catalogue.mjs';
|
|
25
|
+
import { applyFixes } from './lib/fixers.mjs';
|
|
24
26
|
import { buildReport, formatReport as formatProjectReport } from './lib/report.mjs';
|
|
25
27
|
import { loadVocabulary } from './lib/tokenVocabulary.mjs';
|
|
26
28
|
import {
|
|
27
29
|
applyCoverageSeverity,
|
|
28
30
|
applySeverity,
|
|
29
31
|
countBySeverity,
|
|
32
|
+
dedupeAliasFindings,
|
|
30
33
|
formatFindings,
|
|
31
34
|
parseCheckFlags,
|
|
32
35
|
readChecksConfig,
|
|
@@ -74,10 +77,20 @@ Commands:
|
|
|
74
77
|
@playwright/test, vitest, and happy-dom; a
|
|
75
78
|
missing one is a tests-not-installed finding
|
|
76
79
|
naming the install command
|
|
77
|
-
check-page [paths...]
|
|
80
|
+
check-page [paths...] [--tests]
|
|
81
|
+
Validate pages against the live-tokens-create-page
|
|
78
82
|
contract: catalogue components only, and every CSS
|
|
79
83
|
value a design token. Checks every page under src/
|
|
80
|
-
when given no paths.
|
|
84
|
+
when given no paths. --tests also opens each
|
|
85
|
+
page's own route in the consumer's own app and
|
|
86
|
+
proves, per shipped rule id, that the cascade
|
|
87
|
+
painted every component from its semantic
|
|
88
|
+
properties, every run of text sits in one shipped
|
|
89
|
+
text style, every text/surface pair meets AA,
|
|
90
|
+
sections sit on the page grid, and nothing
|
|
91
|
+
overflows. Needs @playwright/test, vitest, and
|
|
92
|
+
happy-dom; a missing one is a tests-not-installed
|
|
93
|
+
finding naming the install command
|
|
81
94
|
|
|
82
95
|
check-component and check-page also accept:
|
|
83
96
|
--json Machine-readable findings, for a skill to iterate
|
|
@@ -87,6 +100,15 @@ check-component and check-page also accept:
|
|
|
87
100
|
(or set "checks": { "rules": {...} } in
|
|
88
101
|
live-tokens.config.json; "checks": { "exclude":
|
|
89
102
|
[...] } drops paths from discovery entirely)
|
|
103
|
+
--no-fix Report every finding and edit nothing, for a
|
|
104
|
+
build or CI. Without it, both checkers first
|
|
105
|
+
apply every finding whose repair is auto (a
|
|
106
|
+
deep import into a component, a raw dimension
|
|
107
|
+
with one nearest design token, a shipped
|
|
108
|
+
component's size prop, a semantic property
|
|
109
|
+
overridden in place), re-check, and list each
|
|
110
|
+
fix beside what remains. --tests runs after
|
|
111
|
+
the fixes land.
|
|
90
112
|
set-colors <base-colors.json> [--dry-run]
|
|
91
113
|
Build the theme's whole color identity from 10
|
|
92
114
|
OKLCH base colors (see the live-tokens-set-colors
|
|
@@ -183,14 +205,14 @@ if (command === 'create') {
|
|
|
183
205
|
}
|
|
184
206
|
}
|
|
185
207
|
|
|
186
|
-
function reportChecks(label, findings, checked, rules, opts, { coverage, hardFailure,
|
|
208
|
+
function reportChecks(label, findings, checked, rules, opts, { coverage, hardFailure, exclude, fix } = {}) {
|
|
187
209
|
const checksConfig = readChecksConfig(process.cwd());
|
|
188
|
-
const resolved = applySeverity(findings, rules, opts, checksConfig,
|
|
210
|
+
const resolved = applySeverity(findings, rules, opts, checksConfig, { exclude });
|
|
189
211
|
const resolvedCoverage = coverage ? applyCoverageSeverity(coverage, rules, opts, checksConfig) : coverage;
|
|
190
212
|
console.log(
|
|
191
213
|
opts.json
|
|
192
|
-
? toJson(resolved, { label, checked, coverage: resolvedCoverage })
|
|
193
|
-
: formatFindings(resolved, { label, checked }),
|
|
214
|
+
? toJson(resolved, { label, checked, coverage: resolvedCoverage, fix })
|
|
215
|
+
: [fix ? formatFixes(fix) : '', formatFindings(resolved, { label, checked })].filter(Boolean).join('\n'),
|
|
194
216
|
);
|
|
195
217
|
// Decision 2: a missing tool or a setup failure under --tests is an error
|
|
196
218
|
// even if a project silenced its rule id, since silencing it would read as
|
|
@@ -198,6 +220,29 @@ function reportChecks(label, findings, checked, rules, opts, { coverage, hardFai
|
|
|
198
220
|
process.exit(countBySeverity(resolved).errors === 0 && !hardFailure ? 0 : 1);
|
|
199
221
|
}
|
|
200
222
|
|
|
223
|
+
function shiftNote(f) {
|
|
224
|
+
if (f.rule !== 'dimension-literal') return '';
|
|
225
|
+
const shifts = f.details.literals.map((l) => l.candidates[0].shift).map((n) => `${n > 0 ? '+' : ''}${n}px`);
|
|
226
|
+
return ` shift ${shifts.join(', ')}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Resolves severities and exclusions first, so a silenced rule or an excluded
|
|
230
|
+
* file is never edited. */
|
|
231
|
+
function applyStaticFixes(findings, rules, opts, { exclude } = {}) {
|
|
232
|
+
const root = process.cwd();
|
|
233
|
+
return applyFixes(applySeverity(findings, rules, opts, readChecksConfig(root), { exclude }), root);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function formatFixes({ applied, skipped }) {
|
|
237
|
+
const lines = [];
|
|
238
|
+
for (const f of applied) {
|
|
239
|
+
lines.push(` fixed ${f.file}:${f.line} ${f.details.patch.from} → ${f.details.patch.to || '(removed)'} [${f.rule}]${shiftNote(f)}`);
|
|
240
|
+
}
|
|
241
|
+
for (const f of skipped) lines.push(` skipped ${f.file}:${f.line} ${f.details.patch.from} [${f.rule}]`);
|
|
242
|
+
if (lines.length === 0) return '';
|
|
243
|
+
return [`${applied.length} fix(es) applied, ${skipped.length} skipped:`, ...lines, ''].join('\n');
|
|
244
|
+
}
|
|
245
|
+
|
|
201
246
|
if (command === 'components') {
|
|
202
247
|
const opts = parseCheckFlags(rest);
|
|
203
248
|
const list = describeComponents(loadVocabulary());
|
|
@@ -246,21 +291,26 @@ if (command === 'check-component') {
|
|
|
246
291
|
console.log('✓ check-component: no component authored under src/system/components yet.');
|
|
247
292
|
process.exit(0);
|
|
248
293
|
}
|
|
249
|
-
|
|
294
|
+
let results = ids.map((id) => [id, checkComponent(id)]);
|
|
295
|
+
const label = ids.length === 1 ? `check-component ${ids[0]}${opts.tests ? ' --tests' : ''}` : `check-component${opts.tests ? ' --tests' : ''}`;
|
|
296
|
+
let fix;
|
|
297
|
+
if (!opts.noFix) {
|
|
298
|
+
fix = applyStaticFixes(results.flatMap(([, r]) => r.findings), COMPONENT_RULES, opts);
|
|
299
|
+
if (fix.applied.length > 0) results = ids.map((id) => [id, checkComponent(id)]);
|
|
300
|
+
}
|
|
250
301
|
if (!opts.tests && ids.length === 1 && !opts.json && !opts.strict && opts.off.length + opts.warn.length + opts.error.length === 0) {
|
|
251
302
|
const [id, result] = results[0];
|
|
252
|
-
console.log(formatReport(id, result));
|
|
303
|
+
console.log([fix ? formatFixes(fix) : '', formatReport(id, result)].filter(Boolean).join('\n'));
|
|
253
304
|
process.exit(result.errors.length === 0 ? 0 : 1);
|
|
254
305
|
}
|
|
255
|
-
const label = ids.length === 1 ? `check-component ${ids[0]}${opts.tests ? ' --tests' : ''}` : `check-component${opts.tests ? ' --tests' : ''}`;
|
|
256
306
|
if (!opts.tests) {
|
|
257
|
-
reportChecks(label, results.flatMap(([, r]) => r.findings), ids.length, COMPONENT_RULES, opts, {
|
|
307
|
+
reportChecks(label, results.flatMap(([, r]) => r.findings), ids.length, COMPONENT_RULES, opts, { fix });
|
|
258
308
|
}
|
|
259
309
|
const { hasHardFailure, runContractTests } = await import('./contractRunner.mjs');
|
|
260
310
|
const testOutcome = await runContractTests(opts.rest[0], { root: process.cwd() });
|
|
261
|
-
const findings = [...results.flatMap(([, r]) => r.findings), ...testOutcome.findings];
|
|
311
|
+
const findings = dedupeAliasFindings([...results.flatMap(([, r]) => r.findings), ...testOutcome.findings]);
|
|
262
312
|
reportChecks(label, findings, Math.max(ids.length, 1), COMPONENT_RULES, opts, {
|
|
263
|
-
|
|
313
|
+
fix,
|
|
264
314
|
coverage: testOutcome.coverage,
|
|
265
315
|
hardFailure: hasHardFailure(testOutcome.findings),
|
|
266
316
|
});
|
|
@@ -269,8 +319,26 @@ if (command === 'check-component') {
|
|
|
269
319
|
if (command === 'check-page') {
|
|
270
320
|
const opts = parseCheckFlags(rest);
|
|
271
321
|
const targets = opts.rest.length > 0 ? opts.rest : discoverPages(process.cwd());
|
|
272
|
-
|
|
273
|
-
|
|
322
|
+
let { findings, checked } = checkPages(targets, { root: process.cwd() });
|
|
323
|
+
let fix;
|
|
324
|
+
if (!opts.noFix) {
|
|
325
|
+
fix = applyStaticFixes(findings, PAGE_RULES, opts, { exclude: true });
|
|
326
|
+
if (fix.applied.length > 0) ({ findings, checked } = checkPages(targets, { root: process.cwd() }));
|
|
327
|
+
}
|
|
328
|
+
if (!opts.tests) {
|
|
329
|
+
reportChecks('check-page', findings, checked, PAGE_RULES, opts, { exclude: true, fix });
|
|
330
|
+
}
|
|
331
|
+
const { hasHardFailure, runPageTests } = await import('./contractRunner.mjs');
|
|
332
|
+
const pageTargets = resolvePageTestTargets(opts.rest, process.cwd());
|
|
333
|
+
const testOutcome = await runPageTests(pageTargets, { root: process.cwd() });
|
|
334
|
+
const label = 'check-page --tests';
|
|
335
|
+
const allFindings = [...findings, ...testOutcome.findings];
|
|
336
|
+
reportChecks(label, allFindings, Math.max(checked, pageTargets.length), PAGE_RULES, opts, {
|
|
337
|
+
exclude: true,
|
|
338
|
+
fix,
|
|
339
|
+
coverage: testOutcome.coverage,
|
|
340
|
+
hardFailure: hasHardFailure(testOutcome.findings),
|
|
341
|
+
});
|
|
274
342
|
}
|
|
275
343
|
|
|
276
344
|
if (command === 'set-colors') {
|