@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
package/bin/cli.mjs
CHANGED
|
@@ -12,27 +12,31 @@
|
|
|
12
12
|
// set-geometry <ops> Apply radius/padding/gap/border-width ops to the open buffer.
|
|
13
13
|
// set-type <pairing> Bind Google Fonts families to the theme's font stacks.
|
|
14
14
|
// save-theme <name> Compose the live state into themes/<slug>.json and open it.
|
|
15
|
-
// migrate [...] Reconcile tokens.css, the data tree, and
|
|
15
|
+
// migrate [...] Reconcile tokens.css, the data tree, route references, and the build script.
|
|
16
16
|
|
|
17
17
|
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
23
|
import { resolvePageTestTargets } from './lib/pageRoutes.mjs';
|
|
24
24
|
import { describeComponents, describeTokens, formatComponents, formatTokens } from './lib/catalogue.mjs';
|
|
25
|
+
import { applyFixes } from './lib/fixers.mjs';
|
|
25
26
|
import { buildReport, formatReport as formatProjectReport } from './lib/report.mjs';
|
|
26
27
|
import { loadVocabulary } from './lib/tokenVocabulary.mjs';
|
|
27
28
|
import {
|
|
28
29
|
applyCoverageSeverity,
|
|
29
30
|
applySeverity,
|
|
30
31
|
countBySeverity,
|
|
32
|
+
dedupeAliasFindings,
|
|
31
33
|
formatFindings,
|
|
32
34
|
parseCheckFlags,
|
|
33
35
|
readChecksConfig,
|
|
36
|
+
resolveRuleSeverity,
|
|
34
37
|
toJson,
|
|
35
38
|
} from './lib/findings.mjs';
|
|
39
|
+
import { checkTokensCssMigrations } from './rules/tokens.mjs';
|
|
36
40
|
import {
|
|
37
41
|
runMigrate,
|
|
38
42
|
formatMigrateResult,
|
|
@@ -40,6 +44,7 @@ import {
|
|
|
40
44
|
formatMigrateDataResult,
|
|
41
45
|
} from './migrate.mjs';
|
|
42
46
|
import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
|
|
47
|
+
import { runMigrateBuildScript, formatBuildScriptResult } from './migrate-build-script.mjs';
|
|
43
48
|
import { runCreate, formatCreateResult } from './create.mjs';
|
|
44
49
|
import { runSetupClaude, formatSetupResult } from './setup-claude.mjs';
|
|
45
50
|
import { runSetColors, formatSetColorsResult } from './set-colors.mjs';
|
|
@@ -98,6 +103,16 @@ check-component and check-page also accept:
|
|
|
98
103
|
(or set "checks": { "rules": {...} } in
|
|
99
104
|
live-tokens.config.json; "checks": { "exclude":
|
|
100
105
|
[...] } drops paths from discovery entirely)
|
|
106
|
+
--no-fix Report every finding and edit nothing, for a
|
|
107
|
+
build or CI. Without it, both checkers first
|
|
108
|
+
apply every additive tokens.css migration and
|
|
109
|
+
every finding whose repair is auto (a
|
|
110
|
+
deep import into a component, a raw dimension
|
|
111
|
+
with one nearest design token, a shipped
|
|
112
|
+
component's size prop, a semantic property
|
|
113
|
+
overridden in place), re-check, and list each
|
|
114
|
+
fix beside what remains. --tests runs after
|
|
115
|
+
the fixes land.
|
|
101
116
|
set-colors <base-colors.json> [--dry-run]
|
|
102
117
|
Build the theme's whole color identity from 10
|
|
103
118
|
OKLCH base colors (see the live-tokens-set-colors
|
|
@@ -139,7 +154,8 @@ check-component and check-page also accept:
|
|
|
139
154
|
without writing.
|
|
140
155
|
migrate [--check] [--write] [--tokens <path>]
|
|
141
156
|
Reconcile the project with the installed package:
|
|
142
|
-
applies
|
|
157
|
+
applies every tokens.css migration, breaking
|
|
158
|
+
ones included, moves a
|
|
143
159
|
pre-0.48 data tree onto the current directory
|
|
144
160
|
names, heals what the retired pointer files named,
|
|
145
161
|
and reports source references to the
|
|
@@ -148,6 +164,9 @@ check-component and check-page also accept:
|
|
|
148
164
|
unambiguous route references (never /docs). --check
|
|
149
165
|
prints both plans without writing (exit 1 when
|
|
150
166
|
either is pending; route findings are advisory).
|
|
167
|
+
Also removes the check:design script live-tokens
|
|
168
|
+
added to the build, since vite build now runs the
|
|
169
|
+
design checks through themeFileApi.
|
|
151
170
|
`;
|
|
152
171
|
|
|
153
172
|
// A large body written through console.log is cut at the pipe buffer when the
|
|
@@ -194,14 +213,14 @@ if (command === 'create') {
|
|
|
194
213
|
}
|
|
195
214
|
}
|
|
196
215
|
|
|
197
|
-
function reportChecks(label, findings, checked, rules, opts, { coverage, hardFailure,
|
|
216
|
+
function reportChecks(label, findings, checked, rules, opts, { coverage, hardFailure, exclude, fix } = {}) {
|
|
198
217
|
const checksConfig = readChecksConfig(process.cwd());
|
|
199
|
-
const resolved = applySeverity(findings, rules, opts, checksConfig,
|
|
218
|
+
const resolved = applySeverity(findings, rules, opts, checksConfig, { exclude });
|
|
200
219
|
const resolvedCoverage = coverage ? applyCoverageSeverity(coverage, rules, opts, checksConfig) : coverage;
|
|
201
220
|
console.log(
|
|
202
221
|
opts.json
|
|
203
|
-
? toJson(resolved, { label, checked, coverage: resolvedCoverage })
|
|
204
|
-
: formatFindings(resolved, { label, checked }),
|
|
222
|
+
? toJson(resolved, { label, checked, coverage: resolvedCoverage, fix })
|
|
223
|
+
: [fix ? formatFixes(fix) : '', formatFindings(resolved, { label, checked })].filter(Boolean).join('\n'),
|
|
205
224
|
);
|
|
206
225
|
// Decision 2: a missing tool or a setup failure under --tests is an error
|
|
207
226
|
// even if a project silenced its rule id, since silencing it would read as
|
|
@@ -209,6 +228,41 @@ function reportChecks(label, findings, checked, rules, opts, { coverage, hardFai
|
|
|
209
228
|
process.exit(countBySeverity(resolved).errors === 0 && !hardFailure ? 0 : 1);
|
|
210
229
|
}
|
|
211
230
|
|
|
231
|
+
function shiftNote(f) {
|
|
232
|
+
if (f.rule !== 'dimension-literal') return '';
|
|
233
|
+
const shifts = f.details.literals.map((l) => l.candidates[0].shift).map((n) => `${n > 0 ? '+' : ''}${n}px`);
|
|
234
|
+
return ` shift ${shifts.join(', ')}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function migrateTokensCss(rules, opts) {
|
|
238
|
+
const root = process.cwd();
|
|
239
|
+
const config = readChecksConfig(root);
|
|
240
|
+
const apply = !opts.noFix && resolveRuleSeverity('tokens-migration', rules, opts, config) !== 'off';
|
|
241
|
+
const { applied, findings } = await checkTokensCssMigrations({ root, apply });
|
|
242
|
+
return { applied: applySeverity(applied, rules, opts, config), findings };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Resolves severities and exclusions first, so a silenced rule or an excluded
|
|
246
|
+
* file is never edited. */
|
|
247
|
+
function applyStaticFixes(findings, rules, opts, { exclude } = {}) {
|
|
248
|
+
const root = process.cwd();
|
|
249
|
+
return applyFixes(applySeverity(findings, rules, opts, readChecksConfig(root), { exclude }), root);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function formatFixes({ applied, skipped }) {
|
|
253
|
+
const lines = [];
|
|
254
|
+
for (const f of applied) {
|
|
255
|
+
lines.push(
|
|
256
|
+
f.details.patch
|
|
257
|
+
? ` fixed ${f.file}:${f.line} ${f.details.patch.from} → ${f.details.patch.to || '(removed)'} [${f.rule}]${shiftNote(f)}`
|
|
258
|
+
: ` fixed ${f.file} ${f.details.migrations.map((m) => m.id).join(', ')} [${f.rule}]`,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
for (const f of skipped) lines.push(` skipped ${f.file}:${f.line} ${f.details.patch.from} [${f.rule}]`);
|
|
262
|
+
if (lines.length === 0) return '';
|
|
263
|
+
return [`${applied.length} fix(es) applied, ${skipped.length} skipped:`, ...lines, ''].join('\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
212
266
|
if (command === 'components') {
|
|
213
267
|
const opts = parseCheckFlags(rest);
|
|
214
268
|
const list = describeComponents(loadVocabulary());
|
|
@@ -257,21 +311,28 @@ if (command === 'check-component') {
|
|
|
257
311
|
console.log('✓ check-component: no component authored under src/system/components yet.');
|
|
258
312
|
process.exit(0);
|
|
259
313
|
}
|
|
260
|
-
const
|
|
261
|
-
|
|
314
|
+
const migration = await migrateTokensCss(COMPONENT_RULES, opts);
|
|
315
|
+
let results = ids.map((id) => [id, checkComponent(id)]);
|
|
316
|
+
const label = ids.length === 1 ? `check-component ${ids[0]}${opts.tests ? ' --tests' : ''}` : `check-component${opts.tests ? ' --tests' : ''}`;
|
|
317
|
+
let fix;
|
|
318
|
+
if (!opts.noFix) {
|
|
319
|
+
fix = applyStaticFixes(results.flatMap(([, r]) => r.findings), COMPONENT_RULES, opts);
|
|
320
|
+
if (fix.applied.length > 0) results = ids.map((id) => [id, checkComponent(id)]);
|
|
321
|
+
fix.applied.unshift(...migration.applied);
|
|
322
|
+
}
|
|
323
|
+
if (!opts.tests && migration.findings.length === 0 && ids.length === 1 && !opts.json && !opts.strict && opts.off.length + opts.warn.length + opts.error.length === 0) {
|
|
262
324
|
const [id, result] = results[0];
|
|
263
|
-
console.log(formatReport(id, result));
|
|
325
|
+
console.log([fix ? formatFixes(fix) : '', formatReport(id, result)].filter(Boolean).join('\n'));
|
|
264
326
|
process.exit(result.errors.length === 0 ? 0 : 1);
|
|
265
327
|
}
|
|
266
|
-
const label = ids.length === 1 ? `check-component ${ids[0]}${opts.tests ? ' --tests' : ''}` : `check-component${opts.tests ? ' --tests' : ''}`;
|
|
267
328
|
if (!opts.tests) {
|
|
268
|
-
reportChecks(label, results.flatMap(([, r]) => r.findings), ids.length, COMPONENT_RULES, opts, {
|
|
329
|
+
reportChecks(label, [...migration.findings, ...results.flatMap(([, r]) => r.findings)], ids.length, COMPONENT_RULES, opts, { fix });
|
|
269
330
|
}
|
|
270
331
|
const { hasHardFailure, runContractTests } = await import('./contractRunner.mjs');
|
|
271
332
|
const testOutcome = await runContractTests(opts.rest[0], { root: process.cwd() });
|
|
272
|
-
const findings = [...results.flatMap(([, r]) => r.findings), ...testOutcome.findings];
|
|
333
|
+
const findings = dedupeAliasFindings([...migration.findings, ...results.flatMap(([, r]) => r.findings), ...testOutcome.findings]);
|
|
273
334
|
reportChecks(label, findings, Math.max(ids.length, 1), COMPONENT_RULES, opts, {
|
|
274
|
-
|
|
335
|
+
fix,
|
|
275
336
|
coverage: testOutcome.coverage,
|
|
276
337
|
hardFailure: hasHardFailure(testOutcome.findings),
|
|
277
338
|
});
|
|
@@ -280,9 +341,17 @@ if (command === 'check-component') {
|
|
|
280
341
|
if (command === 'check-page') {
|
|
281
342
|
const opts = parseCheckFlags(rest);
|
|
282
343
|
const targets = opts.rest.length > 0 ? opts.rest : discoverPages(process.cwd());
|
|
283
|
-
const
|
|
344
|
+
const migration = await migrateTokensCss(PAGE_RULES, opts);
|
|
345
|
+
let { findings, checked } = checkPages(targets, { root: process.cwd() });
|
|
346
|
+
let fix;
|
|
347
|
+
if (!opts.noFix) {
|
|
348
|
+
fix = applyStaticFixes(findings, PAGE_RULES, opts, { exclude: true });
|
|
349
|
+
if (fix.applied.length > 0) ({ findings, checked } = checkPages(targets, { root: process.cwd() }));
|
|
350
|
+
fix.applied.unshift(...migration.applied);
|
|
351
|
+
}
|
|
352
|
+
findings = [...migration.findings, ...findings];
|
|
284
353
|
if (!opts.tests) {
|
|
285
|
-
reportChecks('check-page', findings, checked, PAGE_RULES, opts);
|
|
354
|
+
reportChecks('check-page', findings, checked, PAGE_RULES, opts, { exclude: true, fix });
|
|
286
355
|
}
|
|
287
356
|
const { hasHardFailure, runPageTests } = await import('./contractRunner.mjs');
|
|
288
357
|
const pageTargets = resolvePageTestTargets(opts.rest, process.cwd());
|
|
@@ -290,6 +359,8 @@ if (command === 'check-page') {
|
|
|
290
359
|
const label = 'check-page --tests';
|
|
291
360
|
const allFindings = [...findings, ...testOutcome.findings];
|
|
292
361
|
reportChecks(label, allFindings, Math.max(checked, pageTargets.length), PAGE_RULES, opts, {
|
|
362
|
+
exclude: true,
|
|
363
|
+
fix,
|
|
293
364
|
coverage: testOutcome.coverage,
|
|
294
365
|
hardFailure: hasHardFailure(testOutcome.findings),
|
|
295
366
|
});
|
|
@@ -412,6 +483,12 @@ if (command === 'migrate') {
|
|
|
412
483
|
const routeOut = formatRouteResult(routes, { check });
|
|
413
484
|
if (routeOut) console.log('\n' + routeOut);
|
|
414
485
|
|
|
486
|
+
// Build-script pass: removes the check:design step that themeFileApi now
|
|
487
|
+
// runs during vite build. Advisory under --check, like the data heal.
|
|
488
|
+
const buildScript = runMigrateBuildScript({ root: process.cwd(), apply: !check });
|
|
489
|
+
const buildScriptOut = formatBuildScriptResult(buildScript);
|
|
490
|
+
if (buildScriptOut) console.log('\n' + buildScriptOut);
|
|
491
|
+
|
|
415
492
|
// Route findings are advisory; token migrations and the data heal gate the
|
|
416
493
|
// exit code.
|
|
417
494
|
if (result.status === 'no-path') process.exit(1);
|
package/bin/contractRunner.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { tmpdir } from 'node:os';
|
|
|
28
28
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
29
29
|
import { fileURLToPath } from 'node:url';
|
|
30
30
|
import { discoverComponents, resolveComponentPaths } from './check-component.mjs';
|
|
31
|
+
import { resolveSourceDataDir, scrapeSettingsField, settingsFilePath } from './lib/dataDir.mjs';
|
|
31
32
|
import { lineOf } from './lib/findings.mjs';
|
|
32
33
|
import { settingsPageViewports } from './lib/pageRoutes.mjs';
|
|
33
34
|
|
|
@@ -74,6 +75,7 @@ const HARD_FAILURE_RULES = new Set(['tests-not-installed', 'tests-setup', 'tests
|
|
|
74
75
|
* coverage against. */
|
|
75
76
|
const ALL_CONTRACT_RULES = [
|
|
76
77
|
'contract-registry',
|
|
78
|
+
'contract-behavior',
|
|
77
79
|
'contract-listed',
|
|
78
80
|
'contract-alias',
|
|
79
81
|
'contract-states',
|
|
@@ -183,100 +185,10 @@ function resolveTestingEntry(name) {
|
|
|
183
185
|
);
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
function settingsFilePath(root) {
|
|
187
|
-
for (const name of ['live-tokens.testing.ts', 'live-tokens.testing.mts', 'live-tokens.testing.js', 'live-tokens.testing.mjs']) {
|
|
188
|
-
const path = join(root, name);
|
|
189
|
-
if (existsSync(path)) return path;
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* A generated config's own extensionless relative imports (Vite/Playwright's
|
|
196
|
-
* loaders resolve those, matching how this repo's own `vite.config.ts` and
|
|
197
|
-
* `live-tokens.testing.ts` are written) only get that treatment for a
|
|
198
|
-
* *static* import. A dynamic `import()` of the same path, called after a Vite
|
|
199
|
-
* config finishes loading, runs through plain Node resolution instead and
|
|
200
|
-
* fails on the same files: verified against `vitest.contract.config.ts` with
|
|
201
|
-
* both a `file://` URL and a plain absolute path as the dynamic specifier,
|
|
202
|
-
* identical `ERR_MODULE_NOT_FOUND` both times. So neither `settings.viteConfig`
|
|
203
|
-
* nor `settings.dataDir` (the resolved values `resolveTestingConfig` computes)
|
|
204
|
-
* has a reader here on purpose — reading either would mean importing the
|
|
205
|
-
* settings file dynamically first, which reintroduces exactly this failure.
|
|
206
|
-
* Regexing the settings file's *source text* for a field, imprecise as that
|
|
207
|
-
* is, is what stays inside the static-import constraint: a wrong guess still
|
|
208
|
-
* fails loudly (a bad `viteConfig` throws on its own static import in the
|
|
209
|
-
* generated file; a bad `dataDir` throws "no data directory at ..." below),
|
|
210
|
-
* never as a silent pass. A settings-level `dataDir` this cannot see at all
|
|
211
|
-
* is worse than one resolved this imprecisely, since a project that names its
|
|
212
|
-
* data directory only in `live-tokens.testing.ts` would otherwise have its
|
|
213
|
-
* contracts checked against whatever happens to sit at the default path.
|
|
214
|
-
*/
|
|
215
|
-
function stripComments(text) {
|
|
216
|
-
// Block comments first: a `//` inside one (`/* // note */`) must not seed a
|
|
217
|
-
// second, overlapping strip.
|
|
218
|
-
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** Measured across eleven settings-file shapes: a commented-out `dataDir:`
|
|
222
|
-
* above the real one won the regex, because the regex only sees text
|
|
223
|
-
* position, never comment syntax, and a comment naming the field with
|
|
224
|
-
* nothing else present invented a setting out of prose. Comments are
|
|
225
|
-
* stripped before the field ever gets a chance to match. A field present in
|
|
226
|
-
* live code as a template literal, a computed value, or an import throws
|
|
227
|
-
* instead of falling through to a default silently: the two guesses this
|
|
228
|
-
* feeds, `viteConfig` and `dataDir`, can each resolve to a real path that
|
|
229
|
-
* simply names the wrong tree, which then reads as a clean run. */
|
|
230
|
-
function scrapeSettingsField(settingsPath, fieldName) {
|
|
231
|
-
if (!settingsPath) return null;
|
|
232
|
-
let text;
|
|
233
|
-
try {
|
|
234
|
-
text = readFileSync(settingsPath, 'utf8');
|
|
235
|
-
} catch {
|
|
236
|
-
return null;
|
|
237
|
-
}
|
|
238
|
-
const live = stripComments(text);
|
|
239
|
-
const literal = new RegExp(`\\b${fieldName}\\s*:\\s*['"]([^'"]+)['"]`).exec(live);
|
|
240
|
-
if (literal) return literal[1];
|
|
241
|
-
if (new RegExp(`\\b${fieldName}\\s*:`).test(live)) {
|
|
242
|
-
throw new Error(
|
|
243
|
-
`"${fieldName}" in ${settingsPath} is set to something other than a plain string literal, ` +
|
|
244
|
-
`so --tests cannot read it statically. Use a literal string, or remove the key to fall back to the default.`,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
188
|
function guessViteConfigPath(root, settingsPath) {
|
|
251
189
|
return resolve(root, scrapeSettingsField(settingsPath, 'viteConfig') ?? 'vite.config.ts');
|
|
252
190
|
}
|
|
253
191
|
|
|
254
|
-
/**
|
|
255
|
-
* `dataDir` as the plugin resolves it: a `dataDir` field scraped from the
|
|
256
|
-
* settings file (see above), else `live-tokens.config.json`'s own key, else
|
|
257
|
-
* the default. Mirrors `resolveTestingConfig`'s own `configuredDataDir`
|
|
258
|
-
* fallback (`src/testing/config.ts`) rather than importing it: that module
|
|
259
|
-
* compiles into `src/testing-js`, which does not exist until `build:testing`
|
|
260
|
-
* runs, and importing the *source* `.ts` module to reach it hits the same
|
|
261
|
-
* extensionless-import failure documented above — measured with
|
|
262
|
-
* `src/testing-js` moved aside, `resolveTestingEntry`'s `.ts` fallback threw
|
|
263
|
-
* exactly that trying to load `vitest.ts`. Duplicating this small a resolver
|
|
264
|
-
* has precedent in this file already, at `SESSION_FILES`.
|
|
265
|
-
*/
|
|
266
|
-
export function resolveSourceDataDir(root, settingsPath) {
|
|
267
|
-
const scraped = scrapeSettingsField(settingsPath, 'dataDir');
|
|
268
|
-
if (scraped) return resolve(root, scraped);
|
|
269
|
-
try {
|
|
270
|
-
const parsed = JSON.parse(readFileSync(join(root, 'live-tokens.config.json'), 'utf8'));
|
|
271
|
-
if (parsed && typeof parsed === 'object' && typeof parsed.dataDir === 'string') {
|
|
272
|
-
return resolve(root, parsed.dataDir);
|
|
273
|
-
}
|
|
274
|
-
} catch {
|
|
275
|
-
// Missing or unparseable reads as absent, matching the plugin's own resolver.
|
|
276
|
-
}
|
|
277
|
-
return resolve(root, 'src/live-tokens/data');
|
|
278
|
-
}
|
|
279
|
-
|
|
280
192
|
// ─── data isolation ─────────────────────────────────────────────────────────
|
|
281
193
|
|
|
282
194
|
function copyIsolatedDataDir(sourceDataDir) {
|
|
@@ -423,6 +335,7 @@ const settings = resolveTestingConfig(${settingsExpr}, ${JSON.stringify(root)});
|
|
|
423
335
|
|
|
424
336
|
export default createVitestConfig(viteConfigModule.default ?? viteConfigModule, {
|
|
425
337
|
registrySetup: settings.registrySetup,
|
|
338
|
+
contractsModule: settings.contractsModule,
|
|
426
339
|
});
|
|
427
340
|
`,
|
|
428
341
|
);
|
|
@@ -945,6 +858,12 @@ export function mapRegistryViolation(root, sourceDataDir, componentId, text) {
|
|
|
945
858
|
return { file: relative(root, target), line: findTokenLine(target, token) };
|
|
946
859
|
}
|
|
947
860
|
|
|
861
|
+
/** `component-behavior.contract.ts` shares the same `describe(id) > it(case)`
|
|
862
|
+
* shape `registry.contract.ts` uses (see `structuralRule`'s Playwright
|
|
863
|
+
* analogue), so the file name — not the describe titles — is what tells the
|
|
864
|
+
* two suites' assertions apart. */
|
|
865
|
+
const BEHAVIOR_FILE_PREFIX = 'component-behavior.contract';
|
|
866
|
+
|
|
948
867
|
/** Mirrors the Playwright side's zero-collection check: a file that failed
|
|
949
868
|
* before it ran a single assertion (`assertionResults` empty, `status`
|
|
950
869
|
* 'failed') carries its own `message` — measured against a bad
|
|
@@ -979,16 +898,43 @@ export function mapVitestResults(report, { root, sourceDataDir, stderr } = {}) {
|
|
|
979
898
|
const coverage = {};
|
|
980
899
|
for (const file of files) {
|
|
981
900
|
if (collectionFailures.includes(file)) continue;
|
|
901
|
+
const isBehaviorFile = basename(file.name ?? '').startsWith(BEHAVIOR_FILE_PREFIX);
|
|
902
|
+
const passRule = isBehaviorFile ? 'contract-behavior' : 'contract-registry';
|
|
982
903
|
for (const assertion of file.assertionResults ?? []) {
|
|
983
904
|
const componentId = assertion.ancestorTitles.length >= 2 ? assertion.ancestorTitles[1] : null;
|
|
984
905
|
if (assertion.status !== 'failed') {
|
|
985
906
|
if (assertion.status === 'passed' && componentId) {
|
|
986
907
|
coverage[componentId] ??= {};
|
|
987
|
-
coverage[componentId][
|
|
908
|
+
coverage[componentId][passRule] = { status: 'passed' };
|
|
988
909
|
}
|
|
989
910
|
continue;
|
|
990
911
|
}
|
|
991
912
|
const text = (assertion.failureMessages ?? []).join('\n');
|
|
913
|
+
// `component-behavior.contract.ts` throws `ContractViolation` directly
|
|
914
|
+
// rather than a registry-style violation array, and Vitest's own stack
|
|
915
|
+
// frames follow the message on their own line with no blank line
|
|
916
|
+
// between (measured; unlike Playwright's ANSI code frame), so only the
|
|
917
|
+
// first line is the message `messageBlock`'s blank-line split expects.
|
|
918
|
+
if (isBehaviorFile) {
|
|
919
|
+
const violation = VIOLATION_RE.exec(text.split('\n')[0].trim());
|
|
920
|
+
const finalRule = violation?.[1] ?? 'contract-behavior';
|
|
921
|
+
const id = violation?.[2] ?? componentId;
|
|
922
|
+
const message = violation?.[3] ?? text.split('\n')[0].trim();
|
|
923
|
+
const token = extractToken(message);
|
|
924
|
+
const { file: f, line } = artifactForContractRule(root, sourceDataDir, finalRule, id, token);
|
|
925
|
+
findings.push({
|
|
926
|
+
rule: finalRule,
|
|
927
|
+
file: f,
|
|
928
|
+
line,
|
|
929
|
+
message: id ? `${id}: ${message}` : message,
|
|
930
|
+
context: { suite: 'vitest', title: assertion.fullName },
|
|
931
|
+
});
|
|
932
|
+
if (id) {
|
|
933
|
+
coverage[id] ??= {};
|
|
934
|
+
coverage[id][finalRule] = { status: 'failed' };
|
|
935
|
+
}
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
992
938
|
const violations = componentId ? extractViolationArray(text) : [];
|
|
993
939
|
if (violations.length > 0) {
|
|
994
940
|
for (const violation of violations) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// The design checks `vite build` runs through themeFileApi: both static
|
|
2
|
+
// checkers with no repairs, under the project's own severities. The same bar
|
|
3
|
+
// `check-page --no-fix` and `check-component --no-fix` hold.
|
|
4
|
+
|
|
5
|
+
import { COMPONENT_RULES, checkComponent, discoverComponents } from '../check-component.mjs';
|
|
6
|
+
import { PAGE_RULES, checkPages, discoverPages } from '../check-page.mjs';
|
|
7
|
+
import { checkTokensCssMigrations } from '../rules/tokens.mjs';
|
|
8
|
+
import { applySeverity, countBySeverity, formatFindings, readChecksConfig } from './findings.mjs';
|
|
9
|
+
import { loadVocabulary } from './tokenVocabulary.mjs';
|
|
10
|
+
|
|
11
|
+
export async function runBuildChecks({ root = process.cwd(), engine } = {}) {
|
|
12
|
+
const config = readChecksConfig(root);
|
|
13
|
+
const vocabulary = loadVocabulary({ root });
|
|
14
|
+
const migration = await checkTokensCssMigrations({ root, apply: false, engine });
|
|
15
|
+
const pages = checkPages(discoverPages(root), { root, vocabulary });
|
|
16
|
+
const ids = discoverComponents(root);
|
|
17
|
+
const components = ids.map((id) => checkComponent(id, root, { vocabulary }));
|
|
18
|
+
|
|
19
|
+
const pageFindings = applySeverity([...migration.findings, ...pages.findings], PAGE_RULES, {}, config, { exclude: true });
|
|
20
|
+
const componentFindings = applySeverity(components.flatMap((r) => r.findings), COMPONENT_RULES, {}, config);
|
|
21
|
+
const page = countBySeverity(pageFindings);
|
|
22
|
+
const component = countBySeverity(componentFindings);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
errors: page.errors + component.errors,
|
|
26
|
+
warnings: page.warnings + component.warnings,
|
|
27
|
+
report: [
|
|
28
|
+
formatFindings(pageFindings, { label: 'check-page', checked: pages.checked }),
|
|
29
|
+
formatFindings(componentFindings, { label: 'check-component', checked: ids.length }),
|
|
30
|
+
].join('\n'),
|
|
31
|
+
};
|
|
32
|
+
}
|
package/bin/lib/catalogue.mjs
CHANGED
|
@@ -4,25 +4,121 @@
|
|
|
4
4
|
// use, so a skill or a script sees exactly what the checkers will hold it to.
|
|
5
5
|
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
|
-
import { relative } from 'node:path';
|
|
8
|
-
import { CONTRACT_SCALES } from './tokenVocabulary.mjs';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
import { join, relative } from 'node:path';
|
|
8
|
+
import { CONTRACT_SCALES, PKG_ROOT } from './tokenVocabulary.mjs';
|
|
9
|
+
|
|
10
|
+
const PKG = '@motion-proto/live-tokens';
|
|
11
|
+
|
|
12
|
+
// The one deep import with a public equivalent: a component by file.
|
|
13
|
+
const DEEP_COMPONENT = new RegExp(`^${PKG}/src/system/components/([A-Za-z0-9]+\\.svelte)$`);
|
|
14
|
+
|
|
15
|
+
let subpaths = null;
|
|
16
|
+
|
|
17
|
+
/** Everything the package's `exports` map makes importable, as specifiers. */
|
|
18
|
+
export function publicSubpaths() {
|
|
19
|
+
if (subpaths) return subpaths;
|
|
20
|
+
try {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
22
|
+
subpaths = Object.keys(pkg.exports ?? {}).map((key) => (key === '.' ? PKG : `${PKG}/${key.slice(2)}`));
|
|
23
|
+
} catch {
|
|
24
|
+
subpaths = [];
|
|
23
25
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
return subpaths;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* What a `deep-import` finding's repair needs. A component file has one public
|
|
31
|
+
* specifier, so the rewrite is mechanical; anything else reaches for internals
|
|
32
|
+
* that may have no public equivalent at all, which leaves the choice open.
|
|
33
|
+
*/
|
|
34
|
+
export function deepImportRepair(specifier) {
|
|
35
|
+
const m = DEEP_COMPONENT.exec(specifier);
|
|
36
|
+
if (!m) return { details: { specifier, exports: publicSubpaths() }, repair: 'choice' };
|
|
37
|
+
const publicSpecifier = `${PKG}/components/${m[1]}`;
|
|
38
|
+
return { details: { specifier, public: publicSpecifier, patch: { from: specifier, to: publicSpecifier } } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const STRING_LITERAL = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`/;
|
|
42
|
+
|
|
43
|
+
function literalValue(raw) {
|
|
44
|
+
const quote = raw[0];
|
|
45
|
+
const inner = raw.slice(1, -1);
|
|
46
|
+
if (quote === '`' && inner.includes('${')) return undefined;
|
|
47
|
+
return inner.replace(/\s+/g, ' ').trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Brace matching skips over quoted literals so a `}` inside a description
|
|
51
|
+
// (or a description containing a stray brace) never closes the object early.
|
|
52
|
+
function findBalanced(text, openIndex) {
|
|
53
|
+
let depth = 0;
|
|
54
|
+
for (let i = openIndex; i < text.length; i++) {
|
|
55
|
+
const ch = text[i];
|
|
56
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
57
|
+
const quote = ch;
|
|
58
|
+
i++;
|
|
59
|
+
while (i < text.length && text[i] !== quote) i += text[i] === '\\' ? 2 : 1;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (ch === '{') depth++;
|
|
63
|
+
else if (ch === '}') {
|
|
64
|
+
depth--;
|
|
65
|
+
if (depth === 0) return { content: text.slice(openIndex + 1, i), end: i + 1 };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseFieldObject(body) {
|
|
72
|
+
const fields = {};
|
|
73
|
+
const re = new RegExp(`([A-Za-z_$][A-Za-z0-9_$]*)\\s*:\\s*(${STRING_LITERAL.source})`, 'g');
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = re.exec(body))) {
|
|
76
|
+
if (m[1] in fields) continue;
|
|
77
|
+
const value = literalValue(m[2]);
|
|
78
|
+
if (value !== undefined) fields[m[1]] = value;
|
|
79
|
+
}
|
|
80
|
+
return fields;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Bounded, non-evaluating parse of the runtime file's `catalogue` export:
|
|
85
|
+
* `key: <string literal>` pairs inside the `<script module>` block's
|
|
86
|
+
* `export const catalogue = { ... }`, the same way `builtInIds` and
|
|
87
|
+
* `componentProps` read the rest of the vocabulary without importing the
|
|
88
|
+
* module. A field given as an identifier, a template with `${}`, or a
|
|
89
|
+
* concatenation is not a string literal, so it is silently absent rather
|
|
90
|
+
* than evaluated. Returns `null` when the file has no such export.
|
|
91
|
+
*/
|
|
92
|
+
export function catalogueOf(source) {
|
|
93
|
+
const moduleBlock = source.match(/<script\s+module[^>]*>([\s\S]*?)<\/script>/);
|
|
94
|
+
if (!moduleBlock) return null;
|
|
95
|
+
const exportMatch = /export\s+const\s+catalogue\s*=\s*\{/.exec(moduleBlock[1]);
|
|
96
|
+
if (!exportMatch) return null;
|
|
97
|
+
const openIndex = exportMatch.index + exportMatch[0].length - 1;
|
|
98
|
+
const balanced = findBalanced(moduleBlock[1], openIndex);
|
|
99
|
+
if (!balanced) return null;
|
|
100
|
+
|
|
101
|
+
let body = balanced.content;
|
|
102
|
+
let props;
|
|
103
|
+
const propsMatch = /\bprops\s*:\s*\{/.exec(body);
|
|
104
|
+
if (propsMatch) {
|
|
105
|
+
const propsOpen = propsMatch.index + propsMatch[0].length - 1;
|
|
106
|
+
const propsBalanced = findBalanced(body, propsOpen);
|
|
107
|
+
if (propsBalanced) {
|
|
108
|
+
const parsedProps = parseFieldObject(propsBalanced.content);
|
|
109
|
+
if (Object.keys(parsedProps).length) props = parsedProps;
|
|
110
|
+
body = body.slice(0, propsMatch.index) + body.slice(propsBalanced.end);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fields = parseFieldObject(body);
|
|
115
|
+
const catalogue = {
|
|
116
|
+
description: fields.description,
|
|
117
|
+
useFor: fields.useFor,
|
|
118
|
+
notFor: fields.notFor,
|
|
119
|
+
};
|
|
120
|
+
if (props) catalogue.props = props;
|
|
121
|
+
return catalogue;
|
|
26
122
|
}
|
|
27
123
|
|
|
28
124
|
function scaleOf(name) {
|
|
@@ -33,6 +129,41 @@ function scaleOf(name) {
|
|
|
33
129
|
return hit ?? stem.split('-')[0];
|
|
34
130
|
}
|
|
35
131
|
|
|
132
|
+
// Cached per vocabulary: a checker asks for a scale once per finding, and the
|
|
133
|
+
// values come from re-reading tokens.css.
|
|
134
|
+
const scalesCache = new WeakMap();
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Every design token grouped by its scale, each with the value tokens.css
|
|
138
|
+
* declares for it. The candidates a `color-literal` or `dimension-literal`
|
|
139
|
+
* repair picks from, and what `describeTokens` prints.
|
|
140
|
+
*/
|
|
141
|
+
export function tokenScales(vocab) {
|
|
142
|
+
const cached = scalesCache.get(vocab);
|
|
143
|
+
if (cached) return cached;
|
|
144
|
+
|
|
145
|
+
const values = new Map();
|
|
146
|
+
if (vocab.tokensCssPath) {
|
|
147
|
+
const css = readFileSync(vocab.tokensCssPath, 'utf8').replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
148
|
+
for (const m of css.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)) {
|
|
149
|
+
if (!values.has(m[1])) values.set(m[1], m[2].trim());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const byScale = new Map();
|
|
153
|
+
for (const name of vocab.themeTokens) {
|
|
154
|
+
const scale = scaleOf(name);
|
|
155
|
+
if (!byScale.has(scale)) byScale.set(scale, []);
|
|
156
|
+
byScale.get(scale).push({ name, value: values.get(name) ?? '' });
|
|
157
|
+
}
|
|
158
|
+
scalesCache.set(vocab, byScale);
|
|
159
|
+
return byScale;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** One scale's tokens, empty when the project has no such scale. */
|
|
163
|
+
export function scaleTokens(vocab, scale) {
|
|
164
|
+
return (scale && tokenScales(vocab).get(scale)) || [];
|
|
165
|
+
}
|
|
166
|
+
|
|
36
167
|
export function describeComponents(vocab, { root = process.cwd() } = {}) {
|
|
37
168
|
const out = [];
|
|
38
169
|
for (const entry of vocab.components.values()) {
|
|
@@ -50,7 +181,7 @@ export function describeComponents(vocab, { root = process.cwd() } = {}) {
|
|
|
50
181
|
origin: entry.origin,
|
|
51
182
|
file: relative(root, entry.file),
|
|
52
183
|
registered: vocab.builtIn.has(entry.id) || vocab.registered.has(entry.id),
|
|
53
|
-
|
|
184
|
+
catalogue: catalogueOf(source),
|
|
54
185
|
variants: entry.props?.enums.get('variant') ? [...entry.props.enums.get('variant')] : [],
|
|
55
186
|
props,
|
|
56
187
|
tokens: [...entry.tokens].map(([name, value]) => ({ name, default: value })),
|
|
@@ -60,22 +191,9 @@ export function describeComponents(vocab, { root = process.cwd() } = {}) {
|
|
|
60
191
|
}
|
|
61
192
|
|
|
62
193
|
export function describeTokens(vocab, { root = process.cwd() } = {}) {
|
|
63
|
-
const values = new Map();
|
|
64
|
-
if (vocab.tokensCssPath) {
|
|
65
|
-
const css = readFileSync(vocab.tokensCssPath, 'utf8').replace(/\/\*[\s\S]*?\*\//g, ' ');
|
|
66
|
-
for (const m of css.matchAll(/(--[a-z0-9-]+)\s*:\s*([^;]+);/g)) {
|
|
67
|
-
if (!values.has(m[1])) values.set(m[1], m[2].trim());
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const byScale = new Map();
|
|
71
|
-
for (const name of vocab.themeTokens) {
|
|
72
|
-
const scale = scaleOf(name);
|
|
73
|
-
if (!byScale.has(scale)) byScale.set(scale, []);
|
|
74
|
-
byScale.get(scale).push({ name, value: values.get(name) ?? '' });
|
|
75
|
-
}
|
|
76
194
|
return {
|
|
77
195
|
tokensCss: vocab.tokensCssPath ? relative(root, vocab.tokensCssPath) : null,
|
|
78
|
-
scales: [...
|
|
196
|
+
scales: [...tokenScales(vocab)].map(([scale, tokens]) => ({ scale, tokens })),
|
|
79
197
|
components: [...vocab.components.values()].map((c) => ({
|
|
80
198
|
id: c.id,
|
|
81
199
|
tokens: [...c.tokens].map(([name, value]) => ({ name, default: value })),
|
|
@@ -83,7 +201,16 @@ export function describeTokens(vocab, { root = process.cwd() } = {}) {
|
|
|
83
201
|
};
|
|
84
202
|
}
|
|
85
203
|
|
|
86
|
-
|
|
204
|
+
function describeLines(c) {
|
|
205
|
+
if (!c.catalogue) return [];
|
|
206
|
+
const { description, useFor, notFor, props } = c.catalogue;
|
|
207
|
+
const lines = [];
|
|
208
|
+
if (description) lines.push(description);
|
|
209
|
+
if (useFor) lines.push(`Use for: ${useFor}`);
|
|
210
|
+
if (notFor) lines.push(`Not for: ${notFor}`);
|
|
211
|
+
for (const [prop, text] of Object.entries(props ?? {})) lines.push(`${prop}: ${text}`);
|
|
212
|
+
return lines;
|
|
213
|
+
}
|
|
87
214
|
|
|
88
215
|
export function formatComponents(list, { id } = {}) {
|
|
89
216
|
const lines = [];
|