@motion-proto/live-tokens 0.78.0 → 0.80.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 +49 -38
- 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 +6 -4
- 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 +271 -0
- package/README.md +10 -16
- package/bin/check-component.mjs +154 -551
- package/bin/check-page.mjs +57 -541
- package/bin/cli.mjs +93 -16
- package/bin/contractRunner.mjs +37 -91
- package/bin/lib/buildChecks.mjs +32 -0
- 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/pageSource.mjs +230 -0
- package/bin/lib/report.mjs +57 -59
- package/bin/lib/tokenVocabulary.mjs +104 -34
- package/bin/migrate-build-script.mjs +66 -0
- package/bin/migrate.mjs +5 -0
- 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 +426 -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 +470 -19
- package/dist-plugin/index.d.cts +1 -0
- package/dist-plugin/index.d.ts +1 -0
- package/dist-plugin/index.js +87 -55
- 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 +2 -2
- 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/LiveEditorOverlay.svelte +15 -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 +37 -149
- package/src/editor/skill-atlas/trees/create-component.ts +62 -64
- package/src/editor/skill-atlas/trees/create-page.ts +38 -17
- 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 +81 -76
- 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-FAFOAWYL.js → chunk-Q3YIAAG3.js} +17 -3
- package/src/testing-js/chunk-Q3YIAAG3.js.map +1 -0
- package/src/testing-js/{chunk-GNIUPIU2.js → chunk-U7OJE5DU.js} +18 -18
- package/src/testing-js/chunk-U7OJE5DU.js.map +1 -0
- 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-behavior.contract.js +155 -0
- package/src/testing-js/component-behavior.contract.js.map +1 -0
- package/src/testing-js/component-editor.contract.js +6 -4
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +14 -10
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +44 -4
- package/src/testing-js/index.js +13 -7
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +48 -10
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/testing-js/registry.contract.js +5 -3
- package/src/testing-js/registry.contract.js.map +1 -1
- package/src/testing-js/{vitest-C-wNWcoA.d.ts → vitest-BMLIbDs2.d.ts} +8 -2
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +2 -1
- package/template/package.json +1 -2
- package/template/vite.config.ts +3 -2
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +0 -196
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +0 -105
- package/src/editor/skill-atlas/trees/fix-findings.ts +0 -504
- package/src/testing-js/chunk-4JQX6WWL.js.map +0 -1
- package/src/testing-js/chunk-FAFOAWYL.js.map +0 -1
- package/src/testing-js/chunk-GNIUPIU2.js.map +0 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { blankStrings } from './cssValues.mjs';
|
|
2
|
+
|
|
3
|
+
/** Blank out comments, url() payloads, and string contents so none of them can match a rule. */
|
|
4
|
+
export function neutralise(css) {
|
|
5
|
+
return blankStrings(
|
|
6
|
+
css
|
|
7
|
+
.replace(/\/\*[\s\S]*?\*\//g, (m) => ' '.repeat(m.length))
|
|
8
|
+
.replace(/url\((?:[^()]|\([^()]*\))*\)/g, (m) => ' '.repeat(m.length)),
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `<style>` blocks with their absolute offset in the file; whole file for .css.
|
|
13
|
+
* `site` is what a repair would edit, which the finding carries. */
|
|
14
|
+
export function styleRegions(text, file) {
|
|
15
|
+
if (file.endsWith('.css')) return [{ text, offset: 0, site: 'declaration' }];
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const m of text.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/g)) {
|
|
18
|
+
out.push({ text: m[1], offset: m.index + m[0].indexOf(m[1]), site: 'declaration' });
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Inline styles in markup, as declaration lists the value rules can read: a
|
|
25
|
+
* `style="..."` attribute verbatim, and a `style:prop="value"` directive
|
|
26
|
+
* rewritten as `prop: value;`. A `{...}` expression is dynamic and skipped.
|
|
27
|
+
* The directive keeps its source text as `raw`, since the rewritten
|
|
28
|
+
* declaration is not text a patch would find on disk.
|
|
29
|
+
*/
|
|
30
|
+
export function inlineStyleRegions(code) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const m of code.matchAll(/\sstyle=(["'])([^"']*)\1/g)) {
|
|
33
|
+
out.push({ text: `${m[2]};`, offset: m.index + m[0].indexOf(m[2]), site: 'attribute' });
|
|
34
|
+
}
|
|
35
|
+
for (const m of code.matchAll(/\sstyle:([a-z-]+)=(["'])([^"']*)\2/g)) {
|
|
36
|
+
out.push({ text: `${m[1]}: ${m[3]};`, offset: m.index + 1, site: 'directive', raw: m[0].slice(1) });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Everything outside `<style>`: script and markup. */
|
|
42
|
+
export function codeRegion(text, file) {
|
|
43
|
+
if (file.endsWith('.css')) return null;
|
|
44
|
+
return text.replace(/<style[^>]*>[\s\S]*?<\/style>/g, (m) => ' '.repeat(m.length));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Declarations in a stylesheet, with at-rule preludes excluded. A breakpoint in
|
|
49
|
+
* `@media (max-width: 768px)` is structural geometry, not a themeable value.
|
|
50
|
+
* A property name is read from its start, so `--heading-2xl` is one custom
|
|
51
|
+
* property and never the property `xl`.
|
|
52
|
+
*/
|
|
53
|
+
export function declarations(css) {
|
|
54
|
+
const body = css.replace(/@[a-z-]+[^;{]*(?=\{)/gi, (m) => ' '.repeat(m.length));
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const m of body.matchAll(/(?<![\w-])((?:--)?[a-z][\w-]*)\s*:\s*([^;{}]+)[;}]/gi)) {
|
|
57
|
+
out.push({ prop: m[1].toLowerCase(), value: m[2].trim(), index: m.index, text: m[0].slice(0, -1).trimEnd() });
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Each custom property the regions declare, passed to `at` with its offset in
|
|
63
|
+
* the file, its region's site, and the neutralised text from the match on,
|
|
64
|
+
* which a deletion patch bounds itself against. */
|
|
65
|
+
export function styleBlockDeclarations(regionList, at) {
|
|
66
|
+
for (const region of regionList) {
|
|
67
|
+
const clean = neutralise(region.text);
|
|
68
|
+
for (const m of clean.matchAll(/(?:^|[;{])\s*(--[a-z0-9-]+)\s*:/gim)) {
|
|
69
|
+
at(m[1], region.offset + m.index, region.site, clean.slice(m.index));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Every custom property a page declares, at the earliest offset it appears. A
|
|
76
|
+
* page may also mint one outside its <style> block, in a `style:--x={...}`
|
|
77
|
+
* directive or an el.style.setProperty call, and those are just as declared as
|
|
78
|
+
* one written in CSS. A bare quoted name counts too, so a name the page only
|
|
79
|
+
* reads through getPropertyValue("--x") is never an unknown token.
|
|
80
|
+
*/
|
|
81
|
+
export function pageDeclaredNames(text, regions) {
|
|
82
|
+
const sites = new Map();
|
|
83
|
+
const declareAt = (name, index) => {
|
|
84
|
+
if (!sites.has(name) || index < sites.get(name)) sites.set(name, index);
|
|
85
|
+
};
|
|
86
|
+
styleBlockDeclarations(regions, declareAt);
|
|
87
|
+
for (const m of text.matchAll(/(?:style:|setProperty\(\s*['"`]|['"`])(--[a-z0-9-]+)/g)) {
|
|
88
|
+
declareAt(m[1], m.index);
|
|
89
|
+
}
|
|
90
|
+
return sites;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A dimension-literal patch anchored at the declaration the finding names:
|
|
94
|
+
* its property through its value in a style block or a `style` attribute,
|
|
95
|
+
* or the whole `style:` directive. A value-only patch (`8px` → `var(...)`)
|
|
96
|
+
* applied at the first `8px` at or after the finding's line, which in
|
|
97
|
+
* `width: 8px; padding: 8px` was a property the rule never flags. No patch
|
|
98
|
+
* when the source under the declaration differs from the text the census
|
|
99
|
+
* read (a comment or a string the neutralised copy blanked), since a `from`
|
|
100
|
+
* built from the blanked copy would never match the file. */
|
|
101
|
+
export function declarationPatch(text, region, decl, rewritten) {
|
|
102
|
+
if (region.site === 'directive') {
|
|
103
|
+
const from = region.raw;
|
|
104
|
+
const quote = from.at(-1);
|
|
105
|
+
const open = from.indexOf(quote);
|
|
106
|
+
const inner = from.slice(open + 1, -1);
|
|
107
|
+
if (inner.trim() !== decl.value) return null;
|
|
108
|
+
return { from, to: from.slice(0, open + 1) + inner.replace(decl.value, rewritten) + quote };
|
|
109
|
+
}
|
|
110
|
+
const start = region.offset + decl.index;
|
|
111
|
+
const from = text.slice(start, start + decl.text.length);
|
|
112
|
+
if (from !== decl.text) return null;
|
|
113
|
+
return { from, to: from.slice(0, -decl.value.length) + rewritten };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The attributes of one component tag starting at `start` (the `<`), read with
|
|
118
|
+
* `{}` depth and quotes tracked so an expression holding `>` does not end the
|
|
119
|
+
* tag early. Returns null when the tag spreads an object, which makes its prop
|
|
120
|
+
* set unknowable.
|
|
121
|
+
*/
|
|
122
|
+
export function tagAttributes(code, start) {
|
|
123
|
+
const tagEnd = (() => {
|
|
124
|
+
let depth = 0;
|
|
125
|
+
let quote = null;
|
|
126
|
+
for (let j = start; j < code.length; j++) {
|
|
127
|
+
const c = code[j];
|
|
128
|
+
if (quote) {
|
|
129
|
+
if (c === quote) quote = null;
|
|
130
|
+
} else if (c === '"' || c === "'") quote = c;
|
|
131
|
+
else if (c === '{') depth++;
|
|
132
|
+
else if (c === '}') depth--;
|
|
133
|
+
else if (c === '>' && depth === 0) return j;
|
|
134
|
+
}
|
|
135
|
+
return code.length;
|
|
136
|
+
})();
|
|
137
|
+
// The tag name carries no whitespace of its own, so the first whitespace
|
|
138
|
+
// character at or after `start` — space, tab, or a wrapped newline — is
|
|
139
|
+
// always the boundary right after it, never only a literal space on the
|
|
140
|
+
// same line.
|
|
141
|
+
let i = -1;
|
|
142
|
+
for (let k = start; k < tagEnd; k++) {
|
|
143
|
+
if (/\s/.test(code[k])) {
|
|
144
|
+
i = k;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (i === -1) return { attrs: [], end: tagEnd };
|
|
149
|
+
const attrs = [];
|
|
150
|
+
while (i < tagEnd) {
|
|
151
|
+
const c = code[i];
|
|
152
|
+
if (/\s/.test(c) || c === '/') {
|
|
153
|
+
i++;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (c === '{') {
|
|
157
|
+
let depth = 0;
|
|
158
|
+
let j = i;
|
|
159
|
+
for (; j < tagEnd; j++) {
|
|
160
|
+
if (code[j] === '{') depth++;
|
|
161
|
+
else if (code[j] === '}' && --depth === 0) break;
|
|
162
|
+
}
|
|
163
|
+
const inner = code.slice(i + 1, j).trim();
|
|
164
|
+
if (inner.startsWith('...')) return null;
|
|
165
|
+
if (/^\w+$/.test(inner)) attrs.push({ name: inner, value: null, index: i, end: j + 1 });
|
|
166
|
+
i = j + 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const name = code.slice(i).match(/^[^\s=/>]+/)?.[0];
|
|
170
|
+
if (!name) break;
|
|
171
|
+
const at = i;
|
|
172
|
+
i += name.length;
|
|
173
|
+
// Horizontal whitespace around `=` reads as the same attribute — Svelte
|
|
174
|
+
// itself accepts `size = "small"` — but a newline on either side does
|
|
175
|
+
// not: that's a bare boolean attribute followed by unrelated markup.
|
|
176
|
+
let j = i;
|
|
177
|
+
while (j < tagEnd && /[^\S\n]/.test(code[j])) j++;
|
|
178
|
+
let value = null;
|
|
179
|
+
if (code[j] === '=') {
|
|
180
|
+
i = j + 1;
|
|
181
|
+
while (i < tagEnd && /[^\S\n]/.test(code[i])) i++;
|
|
182
|
+
const q = code[i];
|
|
183
|
+
if (q === '"' || q === "'") {
|
|
184
|
+
const close = code.indexOf(q, i + 1);
|
|
185
|
+
value = code.slice(i + 1, close === -1 ? tagEnd : close);
|
|
186
|
+
i = close === -1 ? tagEnd : close + 1;
|
|
187
|
+
} else if (q === '{') {
|
|
188
|
+
let depth = 0;
|
|
189
|
+
for (; i < tagEnd; i++) {
|
|
190
|
+
if (code[i] === '{') depth++;
|
|
191
|
+
else if (code[i] === '}' && --depth === 0) break;
|
|
192
|
+
}
|
|
193
|
+
i++;
|
|
194
|
+
} else {
|
|
195
|
+
const bare = code.slice(i).match(/^[^\s>]+/)?.[0] ?? '';
|
|
196
|
+
value = bare;
|
|
197
|
+
i += bare.length;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// `end` marks where this attribute's own text stops, so a fixer can delete
|
|
201
|
+
// exactly it without disturbing whatever follows in the tag.
|
|
202
|
+
attrs.push({ name, value, index: at, end: i });
|
|
203
|
+
}
|
|
204
|
+
return { attrs, end: tagEnd };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `{@html ...}` blanked, brace-balanced, so a tag textually present inside a
|
|
209
|
+
* raw HTML string is not mistaken for markup the page authored.
|
|
210
|
+
*/
|
|
211
|
+
export function blankHtmlExpressions(code) {
|
|
212
|
+
let out = code;
|
|
213
|
+
const re = /\{@html\b/g;
|
|
214
|
+
let m;
|
|
215
|
+
while ((m = re.exec(out)) !== null) {
|
|
216
|
+
const start = m.index;
|
|
217
|
+
let depth = 0;
|
|
218
|
+
let i = start;
|
|
219
|
+
for (; i < out.length; i++) {
|
|
220
|
+
if (out[i] === '{') depth++;
|
|
221
|
+
else if (out[i] === '}' && --depth === 0) {
|
|
222
|
+
i++;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
out = out.slice(0, start) + ' '.repeat(i - start) + out.slice(i);
|
|
227
|
+
re.lastIndex = start;
|
|
228
|
+
}
|
|
229
|
+
return out;
|
|
230
|
+
}
|
package/bin/lib/report.mjs
CHANGED
|
@@ -9,29 +9,6 @@ import { relative } from 'node:path';
|
|
|
9
9
|
import { COMPONENT_RULES, checkComponent, discoverComponents } from '../check-component.mjs';
|
|
10
10
|
import { COMPONENT_IMPORT, PAGE_RULES, checkPages, discoverPages } from '../check-page.mjs';
|
|
11
11
|
import { applySeverity, readChecksConfig } from './findings.mjs';
|
|
12
|
-
import { extractGlobalRootBlocks } from './tokenVocabulary.mjs';
|
|
13
|
-
|
|
14
|
-
const SIDES = ['-top', '-right', '-bottom', '-left'];
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Tokens a component declares that nothing in its file reads. A read is the
|
|
18
|
-
* name appearing outside the `:global(:root)` block: in a `var()`, in a `style:`
|
|
19
|
-
* directive, or as the string a padding mixin takes. SCSS interpolation
|
|
20
|
-
* (`--badge-#{$v}-surface`) reads every token the pattern covers. A per-side
|
|
21
|
-
* padding is read through its parent.
|
|
22
|
-
*/
|
|
23
|
-
export function unreadTokens(source, tokens) {
|
|
24
|
-
let body = source.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/<!--[\s\S]*?-->/g, ' ');
|
|
25
|
-
for (const block of extractGlobalRootBlocks(body)) body = body.replace(block, ' ');
|
|
26
|
-
const patterns = [...body.matchAll(/--[a-z0-9-]*(?:#\{[^}]*\}[a-z0-9-]*)+/g)].map(
|
|
27
|
-
(m) => new RegExp(`^${m[0].replace(/[.*+?^()|[\]\\]/g, '\\$&').replace(/#\{[^}]*\}/g, '[a-z0-9-]+')}$`),
|
|
28
|
-
);
|
|
29
|
-
const isRead = (name) => body.includes(name) || patterns.some((re) => re.test(name));
|
|
30
|
-
return [...tokens].filter((name) => {
|
|
31
|
-
const side = SIDES.find((s) => name.endsWith(s));
|
|
32
|
-
return !isRead(name) && !(side && isRead(name.slice(0, -side.length)));
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
12
|
|
|
36
13
|
function countByRule(findings) {
|
|
37
14
|
const out = {};
|
|
@@ -39,34 +16,50 @@ function countByRule(findings) {
|
|
|
39
16
|
return out;
|
|
40
17
|
}
|
|
41
18
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
19
|
+
const SEVERITY_ORDER = { error: 0, warn: 1 };
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Errors first, then the rule with the most findings, then file and line. The
|
|
23
|
+
* rule tiebreak keeps one rule's findings together when two rules tie on count,
|
|
24
|
+
* so the list reads as a fix list rather than a file listing.
|
|
25
|
+
*/
|
|
26
|
+
function sortFindings(findings) {
|
|
27
|
+
const counts = countByRule(findings);
|
|
28
|
+
return [...findings].sort(
|
|
29
|
+
(a, b) =>
|
|
30
|
+
SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] ||
|
|
31
|
+
counts[b.rule] - counts[a.rule] ||
|
|
32
|
+
a.rule.localeCompare(b.rule) ||
|
|
33
|
+
a.file.localeCompare(b.file) ||
|
|
34
|
+
a.line - b.line,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function summarise(findings, rules, config, exclude) {
|
|
39
|
+
const resolved = sortFindings(applySeverity(findings, rules, {}, config, { exclude }));
|
|
40
|
+
const strict = applySeverity(findings, rules, { strict: true }, config, { exclude });
|
|
45
41
|
return {
|
|
46
42
|
errors: resolved.filter((f) => f.severity === 'error').length,
|
|
47
43
|
warnings: resolved.filter((f) => f.severity === 'warn').length,
|
|
48
44
|
strictErrors: strict.filter((f) => f.severity === 'error').length,
|
|
49
45
|
byRule: countByRule(resolved),
|
|
50
|
-
items: resolved
|
|
46
|
+
items: resolved,
|
|
51
47
|
};
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
export function buildReport(vocab, { root = process.cwd() } = {}) {
|
|
55
51
|
const config = readChecksConfig(root);
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
unread: unreadTokens(source, c.tokens.keys()),
|
|
68
|
-
};
|
|
69
|
-
});
|
|
53
|
+
// What a component is, as facts. What is wrong with it is a finding:
|
|
54
|
+
// unread properties, a missing description, and an unregistered project
|
|
55
|
+
// component are rules now, each carrying its own severity and repair.
|
|
56
|
+
const components = [...vocab.components.values()].map((c) => ({
|
|
57
|
+
id: c.id,
|
|
58
|
+
origin: c.origin,
|
|
59
|
+
file: relative(root, c.file),
|
|
60
|
+
registered: vocab.builtIn.has(c.id) || vocab.registered.has(c.id),
|
|
61
|
+
tokens: c.tokens.size,
|
|
62
|
+
}));
|
|
70
63
|
|
|
71
64
|
const pageFiles = discoverPages(root);
|
|
72
65
|
const byPage = [];
|
|
@@ -92,6 +85,9 @@ export function buildReport(vocab, { root = process.cwd() } = {}) {
|
|
|
92
85
|
const authored = discoverComponents(root);
|
|
93
86
|
const componentFindings = authored.flatMap((id) => checkComponent(id, root, { vocabulary: vocab }).findings);
|
|
94
87
|
|
|
88
|
+
// Key order is the order a reader takes the report in, and `migrations` is
|
|
89
|
+
// filled by the caller (it needs the compiled engine), so it holds its place
|
|
90
|
+
// here rather than landing last.
|
|
95
91
|
return {
|
|
96
92
|
project: {
|
|
97
93
|
root,
|
|
@@ -100,24 +96,29 @@ export function buildReport(vocab, { root = process.cwd() } = {}) {
|
|
|
100
96
|
components: components.length,
|
|
101
97
|
pages: pageFiles.length,
|
|
102
98
|
},
|
|
99
|
+
migrations: null,
|
|
103
100
|
components,
|
|
101
|
+
findings: {
|
|
102
|
+
pages: summarise(pageFindings, PAGE_RULES, config, true),
|
|
103
|
+
components: { checked: authored, ...summarise(componentFindings, COMPONENT_RULES, config) },
|
|
104
|
+
},
|
|
104
105
|
usage: {
|
|
105
106
|
byPage,
|
|
106
107
|
byComponent,
|
|
107
108
|
unusedShipped: byComponent.filter((c) => c.origin === 'shipped' && c.pages.length === 0).map((c) => c.id),
|
|
108
|
-
customUnregistered: components.filter((c) => c.origin === 'custom' && !c.registered).map((c) => c.id),
|
|
109
109
|
customUnused: byComponent.filter((c) => c.origin === 'custom' && c.pages.length === 0).map((c) => c.id),
|
|
110
110
|
},
|
|
111
|
-
findings: {
|
|
112
|
-
pages: summarise(pageFindings, PAGE_RULES, config),
|
|
113
|
-
components: { checked: authored, ...summarise(componentFindings, COMPONENT_RULES, config) },
|
|
114
|
-
},
|
|
115
111
|
};
|
|
116
112
|
}
|
|
117
113
|
|
|
118
114
|
const list = (items, max = 20) =>
|
|
119
115
|
items.length <= max ? items.join(', ') : `${items.slice(0, max).join(', ')}, +${items.length - max} more`;
|
|
120
116
|
|
|
117
|
+
/** How a rule's findings are repaired. A rule that lowered its repair on some
|
|
118
|
+
* of them names both, so the line never overstates what code can do. */
|
|
119
|
+
const repairsFor = (items, rule) =>
|
|
120
|
+
[...new Set(items.filter((f) => f.rule === rule).map((f) => f.repair))].sort().join('/');
|
|
121
|
+
|
|
121
122
|
export function formatReport(r) {
|
|
122
123
|
const out = [];
|
|
123
124
|
out.push(`Project: ${r.project.pages} page file(s), ${r.project.components} component(s), ${r.project.themeTokens} design tokens from ${r.project.tokensCss ?? '(no tokens.css)'}`);
|
|
@@ -128,14 +129,19 @@ export function formatReport(r) {
|
|
|
128
129
|
|
|
129
130
|
out.push('');
|
|
130
131
|
out.push('Components');
|
|
131
|
-
|
|
132
|
-
out.push(` tokens declared and read by their own CSS: ${r.components.reduce((n, c) => n + c.tokens - c.unread.length, 0)} of ${r.components.reduce((n, c) => n + c.tokens, 0)}`);
|
|
133
|
-
for (const c of unread) out.push(` ${c.id}: ${c.unread.length} unread (${list(c.unread, 6)})`);
|
|
134
|
-
const undescribed = r.components.filter((c) => !c.described).map((c) => c.id);
|
|
135
|
-
if (undescribed.length) out.push(` no description comment: ${list(undescribed)}`);
|
|
132
|
+
out.push(` semantic properties declared: ${r.components.reduce((n, c) => n + c.tokens, 0)}`);
|
|
136
133
|
const custom = r.components.filter((c) => c.origin === 'custom');
|
|
137
134
|
out.push(` custom: ${custom.length}${custom.length ? ` (${list(custom.map((c) => c.id))})` : ''}`);
|
|
138
|
-
|
|
135
|
+
|
|
136
|
+
const section = (label, s) => {
|
|
137
|
+
out.push('');
|
|
138
|
+
out.push(`${label}: ${s.errors} error(s), ${s.warnings} warning(s); ${s.strictErrors} under --strict`);
|
|
139
|
+
for (const [rule, n] of Object.entries(s.byRule).sort((a, b) => b[1] - a[1])) {
|
|
140
|
+
out.push(` ${rule}: ${n} [${repairsFor(s.items, rule)}]`);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
section('check-page', r.findings.pages);
|
|
144
|
+
section(`check-component (${r.findings.components.checked.length} authored)`, r.findings.components);
|
|
139
145
|
|
|
140
146
|
out.push('');
|
|
141
147
|
out.push('Usage');
|
|
@@ -146,13 +152,5 @@ export function formatReport(r) {
|
|
|
146
152
|
out.push(` pages rendering no catalogue component: ${r.usage.byPage.filter((p) => p.components.length === 0).length}`);
|
|
147
153
|
out.push(` shipped components used nowhere: ${r.usage.unusedShipped.length}${r.usage.unusedShipped.length ? ` (${list(r.usage.unusedShipped)})` : ''}`);
|
|
148
154
|
if (r.usage.customUnused.length) out.push(` custom components used nowhere: ${list(r.usage.customUnused)}`);
|
|
149
|
-
|
|
150
|
-
const section = (label, s) => {
|
|
151
|
-
out.push('');
|
|
152
|
-
out.push(`${label}: ${s.errors} error(s), ${s.warnings} warning(s); ${s.strictErrors} under --strict`);
|
|
153
|
-
for (const [rule, n] of Object.entries(s.byRule).sort((a, b) => b[1] - a[1])) out.push(` ${rule}: ${n}`);
|
|
154
|
-
};
|
|
155
|
-
section('check-page', r.findings.pages);
|
|
156
|
-
section(`check-component (${r.findings.components.checked.length} authored)`, r.findings.components);
|
|
157
155
|
return out.join('\n');
|
|
158
156
|
}
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// Reads files only. Nothing here may import dist-plugin at module top: CI runs
|
|
17
17
|
// the suite before the plugin is built (see bin/engineLoadsLazily.test.ts).
|
|
18
18
|
|
|
19
|
-
import { existsSync, readFileSync, readdirSync,
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
20
20
|
import { dirname, join, resolve } from 'node:path';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
22
|
import { resolveTokensCssPath } from '../migrate.mjs';
|
|
@@ -25,9 +25,20 @@ const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
|
|
25
25
|
|
|
26
26
|
const SHIPPED_COMPONENTS_DIR = 'src/system/components';
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
// Shipped components keep their editor beside the other editors; a
|
|
29
|
+
// consumer-authored one sits next to its runtime. Probe both.
|
|
30
|
+
export const EDITOR_DIRS = ['src/system/components', 'src/editor/component-editor'];
|
|
31
|
+
|
|
32
|
+
function capitalize(id) {
|
|
33
|
+
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Token scales whose names are governed by the token contract (see TOKENS.md).
|
|
37
|
+
* `border-width` stands apart from `border` because the two measure different
|
|
38
|
+
* things: a stroke's width resolves on a length scale, its paint on a colour
|
|
39
|
+
* one, and a repair that offered the other's tokens would be nonsense. */
|
|
29
40
|
export const CONTRACT_SCALES = [
|
|
30
|
-
'surface', 'text', 'border', 'color', 'space', 'radius', 'font', 'line-height',
|
|
41
|
+
'surface', 'text', 'border', 'border-width', 'color', 'space', 'radius', 'font', 'line-height',
|
|
31
42
|
'letter-spacing', 'shadow', 'blur', 'icon-size', 'scrim', 'tint', 'columns',
|
|
32
43
|
'heading', 'body', 'editorial', 'eyebrow', 'code', 'easing', 'duration', 'zoom',
|
|
33
44
|
'gradient', 'stroke',
|
|
@@ -144,26 +155,6 @@ function walk(dir, exts, out = []) {
|
|
|
144
155
|
return out;
|
|
145
156
|
}
|
|
146
157
|
|
|
147
|
-
function componentFiles(roots) {
|
|
148
|
-
const seen = new Set();
|
|
149
|
-
const files = [];
|
|
150
|
-
for (const dir of roots) {
|
|
151
|
-
for (const file of walk(dir, ['.svelte'])) {
|
|
152
|
-
if (file.endsWith('Editor.svelte')) continue;
|
|
153
|
-
let key = file;
|
|
154
|
-
try {
|
|
155
|
-
key = realpathSync(file);
|
|
156
|
-
} catch {
|
|
157
|
-
// unreadable link; fall back to the path itself
|
|
158
|
-
}
|
|
159
|
-
if (seen.has(key)) continue;
|
|
160
|
-
seen.add(key);
|
|
161
|
-
files.push(file);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return files;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
158
|
/** The package's own component ids, read from the frozen registry that declares
|
|
168
159
|
them. A shipped component is registered by the package rather than by the
|
|
169
160
|
project, so it never appears in the project's own `registerComponent` scan. */
|
|
@@ -197,6 +188,73 @@ function registeredIds(root) {
|
|
|
197
188
|
return ids;
|
|
198
189
|
}
|
|
199
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Every component this project can resolve on its own: a runtime under
|
|
193
|
+
* `src/system/components` or a configured `componentDirs` entry, plus every
|
|
194
|
+
* id `registeredIds` finds with no runtime anywhere, so a dangling
|
|
195
|
+
* registration is a fact in the inventory rather than a silent gap.
|
|
196
|
+
*
|
|
197
|
+
* Deliberately never looks under `pkgRoot` for a shipped id a project hasn't
|
|
198
|
+
* vendored — that is `resolveComponentPaths`'s own fallback for a single
|
|
199
|
+
* named id, not a batch this inventory would run over (a consumer's batch is
|
|
200
|
+
* their own components only; shipped runtimes sit in node_modules, where no
|
|
201
|
+
* inventory looks).
|
|
202
|
+
*
|
|
203
|
+
* The single scan every caller reads: `report`, the `check-component` batch,
|
|
204
|
+
* `resolveComponentPaths`, and the `components` verb.
|
|
205
|
+
*/
|
|
206
|
+
export function componentInventory(root = process.cwd(), pkgRoot = PKG_ROOT) {
|
|
207
|
+
const built = builtInIds(root, pkgRoot);
|
|
208
|
+
const registered = registeredIds(root);
|
|
209
|
+
const dirs = [SHIPPED_COMPONENTS_DIR, ...(readProjectConfig(root).componentDirs ?? [])];
|
|
210
|
+
const entries = new Map();
|
|
211
|
+
|
|
212
|
+
const guessEditorPath = (Id) =>
|
|
213
|
+
EDITOR_DIRS.map((d) => join(root, d, `${Id}Editor.svelte`)).find(existsSync) ??
|
|
214
|
+
join(root, EDITOR_DIRS[0], `${Id}Editor.svelte`);
|
|
215
|
+
|
|
216
|
+
for (const dirRel of dirs) {
|
|
217
|
+
const dir = join(root, dirRel);
|
|
218
|
+
if (!existsSync(dir)) continue;
|
|
219
|
+
for (const file of walk(dir, ['.svelte'])) {
|
|
220
|
+
const fileName = file.slice(file.lastIndexOf('/') + 1);
|
|
221
|
+
if (fileName.endsWith('Editor.svelte')) continue;
|
|
222
|
+
const Id = fileName.replace('.svelte', '');
|
|
223
|
+
const id = Id.toLowerCase();
|
|
224
|
+
if (entries.has(id)) continue;
|
|
225
|
+
const editorPath = guessEditorPath(Id);
|
|
226
|
+
entries.set(id, {
|
|
227
|
+
id,
|
|
228
|
+
Id,
|
|
229
|
+
origin: built.has(id) ? 'shipped' : 'custom',
|
|
230
|
+
runtimePath: file,
|
|
231
|
+
editorPath,
|
|
232
|
+
registered: built.has(id) || registered.has(id),
|
|
233
|
+
runtimeExists: true,
|
|
234
|
+
editorExists: existsSync(editorPath),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const id of registered) {
|
|
240
|
+
if (entries.has(id)) continue;
|
|
241
|
+
const Id = capitalize(id);
|
|
242
|
+
const editorPath = guessEditorPath(Id);
|
|
243
|
+
entries.set(id, {
|
|
244
|
+
id,
|
|
245
|
+
Id,
|
|
246
|
+
origin: built.has(id) ? 'shipped' : 'custom',
|
|
247
|
+
runtimePath: join(root, SHIPPED_COMPONENTS_DIR, `${Id}.svelte`),
|
|
248
|
+
editorPath,
|
|
249
|
+
registered: true,
|
|
250
|
+
runtimeExists: false,
|
|
251
|
+
editorExists: existsSync(editorPath),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return entries;
|
|
256
|
+
}
|
|
257
|
+
|
|
200
258
|
/**
|
|
201
259
|
* Build the vocabulary for `root` (a consumer project, or this repo).
|
|
202
260
|
*
|
|
@@ -217,12 +275,26 @@ export function loadVocabulary({ root = process.cwd(), pkgRoot = PKG_ROOT } = {}
|
|
|
217
275
|
|
|
218
276
|
const componentTokens = new Set();
|
|
219
277
|
const components = new Map();
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
278
|
+
const registered = registeredIds(root);
|
|
279
|
+
const builtIn = builtInIds(root, pkgRoot);
|
|
280
|
+
|
|
281
|
+
// The package's shipped directory first, so a shipped component's tokens
|
|
282
|
+
// are in the vocabulary even when a project hasn't vendored it — the one
|
|
283
|
+
// gap the inventory itself deliberately leaves, since it never looks under
|
|
284
|
+
// `pkgRoot`. Then the project's own inventory (its `src/system/components`
|
|
285
|
+
// plus any `componentDirs`), which overrides by id: a project's own copy of
|
|
286
|
+
// a shipped component wins over the package's. Reusing the inventory here,
|
|
287
|
+
// instead of a second directory scan, is what keeps `report.components` and
|
|
288
|
+
// `report.findings.components.checked` naming the same ids.
|
|
289
|
+
const filesById = new Map();
|
|
290
|
+
for (const entry of componentInventory(pkgRoot, pkgRoot).values()) {
|
|
291
|
+
if (entry.runtimeExists) filesById.set(entry.id, entry.runtimePath);
|
|
292
|
+
}
|
|
293
|
+
for (const entry of componentInventory(root, pkgRoot).values()) {
|
|
294
|
+
if (entry.runtimeExists) filesById.set(entry.id, entry.runtimePath);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
for (const [id, file] of filesById) {
|
|
226
298
|
const Id = file.slice(file.lastIndexOf('/') + 1).replace('.svelte', '');
|
|
227
299
|
const src = readFileSync(file, 'utf8');
|
|
228
300
|
const tokens = new Map();
|
|
@@ -231,17 +303,15 @@ export function loadVocabulary({ root = process.cwd(), pkgRoot = PKG_ROOT } = {}
|
|
|
231
303
|
for (const n of declaredCustomProperties(clean)) componentTokens.add(n);
|
|
232
304
|
for (const m of clean.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)) if (!tokens.has(m[1])) tokens.set(m[1], m[2].trim());
|
|
233
305
|
}
|
|
234
|
-
components.set(
|
|
235
|
-
id
|
|
306
|
+
components.set(id, {
|
|
307
|
+
id,
|
|
236
308
|
name: Id,
|
|
237
309
|
file,
|
|
238
|
-
origin:
|
|
310
|
+
origin: builtIn.has(id) ? 'shipped' : 'custom',
|
|
239
311
|
props: componentProps(src),
|
|
240
312
|
tokens,
|
|
241
313
|
});
|
|
242
314
|
}
|
|
243
|
-
const registered = registeredIds(root);
|
|
244
|
-
const builtIn = builtInIds(root, pkgRoot);
|
|
245
315
|
|
|
246
316
|
return {
|
|
247
317
|
themeTokens,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `live-tokens migrate` build-script pass.
|
|
2
|
+
//
|
|
3
|
+
// Through 0.79 the project template, and the check-compliance skill for an
|
|
4
|
+
// existing project, ran both checkers as `npm run check:design` ahead of
|
|
5
|
+
// `vite build`. themeFileApi now runs them during `vite build` itself, so the
|
|
6
|
+
// script only repeats the work. This pass removes it when package.json holds
|
|
7
|
+
// exactly what the template and the skill wrote and the build still gets the
|
|
8
|
+
// checks from the plugin. Anything else is reported and left alone.
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
|
|
13
|
+
const SCRIPT = 'check:design';
|
|
14
|
+
const SHIPPED_SCRIPT = /^(npx )?live-tokens check-page --no-fix && (npx )?live-tokens check-component --no-fix$/;
|
|
15
|
+
const BUILD_PREFIX = /^npm run check:design\s*&&\s*/;
|
|
16
|
+
const VITE_CONFIGS = ['vite.config.ts', 'vite.config.js', 'vite.config.mjs', 'vite.config.mts'];
|
|
17
|
+
|
|
18
|
+
function viteConfigText(root) {
|
|
19
|
+
const file = VITE_CONFIGS.map((name) => join(root, name)).find((p) => existsSync(p));
|
|
20
|
+
return file ? readFileSync(file, 'utf8') : '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function runMigrateBuildScript({ root = process.cwd(), apply = true } = {}) {
|
|
24
|
+
const file = join(root, 'package.json');
|
|
25
|
+
if (!existsSync(file)) return { status: 'unchanged' };
|
|
26
|
+
const text = readFileSync(file, 'utf8');
|
|
27
|
+
let pkg;
|
|
28
|
+
try {
|
|
29
|
+
pkg = JSON.parse(text);
|
|
30
|
+
} catch {
|
|
31
|
+
return { status: 'unchanged' };
|
|
32
|
+
}
|
|
33
|
+
const scripts = pkg.scripts ?? {};
|
|
34
|
+
if (!(SCRIPT in scripts)) return { status: 'unchanged' };
|
|
35
|
+
|
|
36
|
+
const vite = viteConfigText(root);
|
|
37
|
+
if (!vite.includes('themeFileApi')) {
|
|
38
|
+
return { status: 'advisory', reason: `keeps "${SCRIPT}": the Vite config does not use themeFileApi, so the build has no other design checks.` };
|
|
39
|
+
}
|
|
40
|
+
if (/checks\s*:\s*false/.test(vite)) {
|
|
41
|
+
return { status: 'advisory', reason: `keeps "${SCRIPT}": themeFileApi is set to checks: false.` };
|
|
42
|
+
}
|
|
43
|
+
if (!SHIPPED_SCRIPT.test(String(scripts[SCRIPT]).trim())) {
|
|
44
|
+
return { status: 'advisory', reason: `keeps "${SCRIPT}": it differs from the script live-tokens added. vite build now runs the design checks, so remove it by hand if it only repeats them.` };
|
|
45
|
+
}
|
|
46
|
+
const build = String(scripts.build ?? '');
|
|
47
|
+
const users = Object.keys(scripts).filter((name) => name !== SCRIPT && name !== 'build' && String(scripts[name]).includes(SCRIPT));
|
|
48
|
+
if (build.includes(SCRIPT) && !BUILD_PREFIX.test(build)) users.push('build');
|
|
49
|
+
if (users.length > 0) {
|
|
50
|
+
return { status: 'advisory', reason: `keeps "${SCRIPT}": ${users.map((u) => `"${u}"`).join(', ')} still run${users.length === 1 ? 's' : ''} it.` };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!apply) return { status: 'would-change' };
|
|
54
|
+
if (BUILD_PREFIX.test(build)) scripts.build = build.replace(BUILD_PREFIX, '');
|
|
55
|
+
delete scripts[SCRIPT];
|
|
56
|
+
const indent = /^([ \t]+)"/m.exec(text)?.[1] ?? ' ';
|
|
57
|
+
writeFileSync(file, JSON.stringify(pkg, null, indent) + (text.endsWith('\n') ? '\n' : ''));
|
|
58
|
+
return { status: 'changed' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function formatBuildScriptResult(result) {
|
|
62
|
+
if (result.status === 'changed') return `✓ package.json: removed "${SCRIPT}" from the build. vite build runs the design checks through themeFileApi.`;
|
|
63
|
+
if (result.status === 'would-change') return `package.json: would remove "${SCRIPT}" from the build. vite build runs the design checks through themeFileApi.`;
|
|
64
|
+
if (result.status === 'advisory') return `! package.json ${result.reason}`;
|
|
65
|
+
return '';
|
|
66
|
+
}
|
package/bin/migrate.mjs
CHANGED
|
@@ -37,6 +37,11 @@ async function loadEngine() {
|
|
|
37
37
|
return import(ENGINE);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Null only before build:plugin, which is this repo's own `npm test` in CI; a published package always ships the engine. */
|
|
41
|
+
export async function loadBuiltEngine() {
|
|
42
|
+
return existsSync(ENGINE) ? import(ENGINE) : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
/** --tokens <path> > live-tokens.config.json tokensCssPath > default scan. */
|
|
41
46
|
export function resolveTokensCssPath(explicit, configPath, root = process.cwd()) {
|
|
42
47
|
if (explicit) return resolve(root, explicit);
|