@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/check-component.mjs
CHANGED
|
@@ -14,378 +14,119 @@
|
|
|
14
14
|
// token, so the component repaints when the theme changes. A value with no
|
|
15
15
|
// token behind it must be a declared intrinsic (a structural keyword the
|
|
16
16
|
// editor exports in `intrinsics`), never a literal.
|
|
17
|
+
// - every property the runtime declares is read by the runtime's own CSS
|
|
18
|
+
// - the runtime exports a `catalogue` the CLI and the registry both read
|
|
17
19
|
//
|
|
18
20
|
// Returns { errors, warnings, findings }. `errors`/`warnings` are the message
|
|
19
|
-
// strings; `findings` carries the same items with a stable `rule` id
|
|
20
|
-
// number
|
|
21
|
-
|
|
22
|
-
import { existsSync,
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { lineOf } from './lib/findings.mjs';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
'deep-import': 'error',
|
|
41
|
-
'missing-registration': 'error',
|
|
42
|
-
'unknown-token-ref': 'error',
|
|
43
|
-
'default-not-token': 'error',
|
|
44
|
-
'phantom-link': 'warn',
|
|
45
|
-
'dimension-literal': 'warn',
|
|
46
|
-
// `--tests` (bin/contractRunner.mjs). Fixed by design decision 8 so
|
|
47
|
-
// `fix-findings` can map them; every one is an error, including the setup
|
|
48
|
-
// rules, which `--tests` treats as never-silenceable (see cli.mjs).
|
|
49
|
-
'contract-registry': 'error',
|
|
50
|
-
'contract-render': 'error',
|
|
51
|
-
'contract-alias': 'error',
|
|
52
|
-
'contract-persist': 'error',
|
|
53
|
-
'contract-theme': 'error',
|
|
54
|
-
'contract-states': 'error',
|
|
55
|
-
'contract-interaction': 'error',
|
|
56
|
-
'contract-listed': 'error',
|
|
57
|
-
'contract-sketch': 'error',
|
|
58
|
-
'contract-missing': 'error',
|
|
59
|
-
'tests-not-installed': 'error',
|
|
60
|
-
'tests-setup': 'error',
|
|
61
|
-
'tests-incomplete': 'error',
|
|
62
|
-
};
|
|
21
|
+
// strings; `findings` carries the same items with a stable `rule` id, a line
|
|
22
|
+
// number, and, where a repair needs more than the message, `details`.
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
25
|
+
import { join, relative } from 'node:path';
|
|
26
|
+
import { declaredTokens, intrinsicMatchers } from './lib/componentSource.mjs';
|
|
27
|
+
import { assembleRules, lineOf } from './lib/findings.mjs';
|
|
28
|
+
import {
|
|
29
|
+
EDITOR_DIRS,
|
|
30
|
+
PKG_ROOT,
|
|
31
|
+
builtInIds,
|
|
32
|
+
componentInventory,
|
|
33
|
+
extractGlobalRootBlocks,
|
|
34
|
+
loadVocabulary,
|
|
35
|
+
} from './lib/tokenVocabulary.mjs';
|
|
36
|
+
import * as componentStructure from './rules/componentStructure.mjs';
|
|
37
|
+
import * as importsAndRoutes from './rules/importsAndRoutes.mjs';
|
|
38
|
+
import * as testRuns from './rules/testRuns.mjs';
|
|
39
|
+
import * as tokenRules from './rules/tokens.mjs';
|
|
40
|
+
|
|
41
|
+
export { unreadTokens } from './lib/componentSource.mjs';
|
|
63
42
|
|
|
64
43
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
44
|
+
* Every rule, with its default severity, its repair, and the guidance every
|
|
45
|
+
* finding of it carries.
|
|
46
|
+
*
|
|
47
|
+
* `repair` is the ceiling: `auto` when code can rewrite the site, `choice` when
|
|
48
|
+
* code can name the candidates but not pick one, `authored` when a person
|
|
49
|
+
* writes the repair. A finding may lower its rule's `repair`; none raises it.
|
|
70
50
|
*/
|
|
71
|
-
export const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
for (const base of [root, PKG_ROOT]) {
|
|
116
|
-
const path = join(base, ALIAS_KINDS);
|
|
117
|
-
if (!existsSync(path)) continue;
|
|
118
|
-
const src = readFileSync(path, 'utf8');
|
|
119
|
-
const block = src.match(/KIND_RULES[^=]*=\s*\[([\s\S]*?)\n\];/);
|
|
120
|
-
if (!block) continue;
|
|
121
|
-
const out = new Set();
|
|
122
|
-
for (const m of block[1].matchAll(/suffix:\s*\[([\s\S]*?)\]/g)) {
|
|
123
|
-
for (const n of m[1].matchAll(/'-([a-z0-9-]+)'/g)) out.add(n[1]);
|
|
124
|
-
}
|
|
125
|
-
if (out.size > 0) return [...out];
|
|
126
|
-
}
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** True when `id` is one of the package's own components. */
|
|
131
|
-
function isBuiltIn(id) {
|
|
132
|
-
return builtInIds(process.cwd(), PKG_ROOT).has(id);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Per-side padding names (`--card-body-padding-top`) are written by the padding
|
|
136
|
-
// selector, never declared by hand, and belong with their parent.
|
|
137
|
-
const SIDE_SUFFIXES = ['-top', '-right', '-bottom', '-left'];
|
|
138
|
-
|
|
139
|
-
// State tokens that must come *before* the property, never after.
|
|
140
|
-
const STATE_TOKENS = ['hover', 'disabled', 'selected', 'focus', 'active', 'focused'];
|
|
141
|
-
|
|
142
|
-
// Disabled is terminal: a disabled component cannot be hovered, focused, or
|
|
143
|
-
// selected, so a token naming both describes a state that never paints.
|
|
144
|
-
const TERMINAL_CONFLICTS = ['hover', 'focus', 'focused', 'selected', 'on', 'active', 'checked'];
|
|
145
|
-
|
|
146
|
-
// Deep imports into the package internals are not a supported API.
|
|
147
|
-
const DEEP_IMPORT_PATTERNS = [
|
|
148
|
-
/^@motion-proto\/live-tokens\/src\//,
|
|
149
|
-
/node_modules\/@motion-proto\/live-tokens/,
|
|
150
|
-
];
|
|
51
|
+
export const COMPONENT_RULES = assembleRules(
|
|
52
|
+
[
|
|
53
|
+
'tokens-migration',
|
|
54
|
+
'tokens-breaking-migration',
|
|
55
|
+
'invalid-id',
|
|
56
|
+
'missing-file',
|
|
57
|
+
'missing-root-block',
|
|
58
|
+
'no-tokens',
|
|
59
|
+
'missing-description',
|
|
60
|
+
'unread-token',
|
|
61
|
+
'state-after-property',
|
|
62
|
+
'disabled-is-terminal',
|
|
63
|
+
'unknown-suffix',
|
|
64
|
+
'phantom-editor-token',
|
|
65
|
+
'color-literal',
|
|
66
|
+
'missing-component-const',
|
|
67
|
+
'missing-all-tokens',
|
|
68
|
+
'deep-import',
|
|
69
|
+
'missing-registration',
|
|
70
|
+
'unknown-token-ref',
|
|
71
|
+
'default-not-token',
|
|
72
|
+
'phantom-link',
|
|
73
|
+
'dimension-literal',
|
|
74
|
+
'config-token',
|
|
75
|
+
'contract-registry',
|
|
76
|
+
'contract-behavior',
|
|
77
|
+
'contract-render',
|
|
78
|
+
'contract-alias',
|
|
79
|
+
'contract-persist',
|
|
80
|
+
'contract-theme',
|
|
81
|
+
'contract-states',
|
|
82
|
+
'contract-interaction',
|
|
83
|
+
'contract-listed',
|
|
84
|
+
'contract-sketch',
|
|
85
|
+
'contract-missing',
|
|
86
|
+
'tests-not-installed',
|
|
87
|
+
'tests-setup',
|
|
88
|
+
'tests-incomplete',
|
|
89
|
+
],
|
|
90
|
+
componentStructure.componentRules,
|
|
91
|
+
tokenRules.componentRules,
|
|
92
|
+
importsAndRoutes.componentRules,
|
|
93
|
+
testRuns.componentRules,
|
|
94
|
+
);
|
|
151
95
|
|
|
152
96
|
function capitalize(id) {
|
|
153
97
|
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
154
98
|
}
|
|
155
99
|
|
|
100
|
+
const pick = (entry) => ({ Id: entry.Id, runtimePath: entry.runtimePath, editorPath: entry.editorPath });
|
|
101
|
+
|
|
156
102
|
/**
|
|
157
|
-
* Where `id`'s runtime and editor files live, resolved
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
103
|
+
* Where `id`'s runtime and editor files live, resolved from `root`'s own
|
|
104
|
+
* inventory first, the real on-disk filename rather than a capitalised guess,
|
|
105
|
+
* since `cornerbadge` ships as `CornerBadge.svelte`. Exported so a caller
|
|
106
|
+
* outside the lint (the `--tests` runner, attributing a contract finding to a
|
|
107
|
+
* consumer artifact) doesn't re-derive it differently.
|
|
162
108
|
*/
|
|
163
109
|
export function resolveComponentPaths(id, root = process.cwd()) {
|
|
110
|
+
const local = componentInventory(root).get(id);
|
|
111
|
+
if (local?.runtimeExists) return pick(local);
|
|
112
|
+
|
|
164
113
|
// A consumer names a shipped id and owns no copy of its files. Falling back
|
|
165
114
|
// to the package keeps the lint reading the same source the contract run
|
|
166
115
|
// resolves, instead of reporting the component's own files missing. In this
|
|
167
|
-
// repo the consumer paths always exist, so
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const Id =
|
|
172
|
-
(existsSync(dir) ? readdirSync(dir) : [])
|
|
173
|
-
.find((f) => f.toLowerCase() === `${id}.svelte`)
|
|
174
|
-
?.replace('.svelte', '') ?? capitalize(id);
|
|
175
|
-
const runtimePath = join(dir, `${Id}.svelte`);
|
|
176
|
-
const editorPath =
|
|
177
|
-
EDITOR_DIRS.map((d) => join(base, d, `${Id}Editor.svelte`)).find(existsSync) ??
|
|
178
|
-
join(base, EDITOR_DIRS[0], `${Id}Editor.svelte`);
|
|
179
|
-
if (base === roots.at(-1) || (existsSync(runtimePath) && existsSync(editorPath))) {
|
|
180
|
-
return { Id, runtimePath, editorPath };
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function extractImports(source) {
|
|
186
|
-
const out = [];
|
|
187
|
-
const re = /import\s+(?:[^'"]*\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
188
|
-
let m;
|
|
189
|
-
while ((m = re.exec(source)) !== null) {
|
|
190
|
-
out.push(m[1]);
|
|
191
|
-
}
|
|
192
|
-
return out;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* `--<id>-*` tokens declared in the given blocks.
|
|
197
|
-
*
|
|
198
|
-
* The prefix may also be the hyphenated word form of the id: CornerBadge is
|
|
199
|
-
* registered as `cornerbadge` but names its tokens `--corner-badge-*`, and the
|
|
200
|
-
* whole system (config, theme, editor) follows that. Segments never end on a
|
|
201
|
-
* hyphen, so a trailing `-` cannot be mistaken for a token name.
|
|
202
|
-
*/
|
|
203
|
-
function extractTokensForId(blocks, id, kebab) {
|
|
204
|
-
const tokens = new Set();
|
|
205
|
-
// Comments name tokens too (`the --card-hover-* tokens`); they are prose.
|
|
206
|
-
blocks = blocks.map((b) => b.replace(/\/\*[\s\S]*?\*\//g, ' '));
|
|
207
|
-
const prefixes = kebab && kebab !== id ? [id, kebab] : [id];
|
|
208
|
-
for (const prefix of prefixes) {
|
|
209
|
-
const re = new RegExp(`--${prefix}(?:-[a-z0-9]+)+`, 'g');
|
|
210
|
-
for (const block of blocks) {
|
|
211
|
-
for (const t of block.match(re) ?? []) tokens.add(t);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return [...tokens];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/** `CornerBadge` -> `corner-badge`; the other accepted token prefix. */
|
|
218
|
-
function kebabOf(name) {
|
|
219
|
-
return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function tokenSuffix(token, known) {
|
|
223
|
-
const bare = SIDE_SUFFIXES.find((x) => token.endsWith(x)) ? token.slice(0, token.lastIndexOf('-')) : token;
|
|
224
|
-
for (const suffix of known) {
|
|
225
|
-
if (bare.endsWith(`-${suffix}`)) return suffix;
|
|
226
|
-
}
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function detectStateAfterProperty(token, known) {
|
|
231
|
-
// e.g. --comp-part-surface-hover (wrong) vs --comp-part-hover-surface (right)
|
|
232
|
-
for (const state of STATE_TOKENS) {
|
|
233
|
-
if (token.endsWith(`-${state}`)) {
|
|
234
|
-
const head = token.slice(0, -(state.length + 1));
|
|
235
|
-
if (tokenSuffix(head, known)) return state;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// True if `source` calls `fnName(` at least once with a single argument (no
|
|
242
|
-
// top-level comma before the matching close paren). Brackets/braces are balanced
|
|
243
|
-
// so commas inside nested objects/arrays/calls don't count.
|
|
244
|
-
function hasBareCall(source, fnName) {
|
|
245
|
-
const needle = fnName + '(';
|
|
246
|
-
let idx = 0;
|
|
247
|
-
while ((idx = source.indexOf(needle, idx)) !== -1) {
|
|
248
|
-
let i = idx + needle.length;
|
|
249
|
-
let depth = 1;
|
|
250
|
-
let topComma = false;
|
|
251
|
-
for (; i < source.length && depth > 0; i++) {
|
|
252
|
-
const c = source[i];
|
|
253
|
-
if (c === '(' || c === '[' || c === '{') depth++;
|
|
254
|
-
else if (c === ')' || c === ']' || c === '}') depth--;
|
|
255
|
-
else if (c === ',' && depth === 1) topComma = true;
|
|
256
|
-
}
|
|
257
|
-
if (!topComma) return true;
|
|
258
|
-
idx = i;
|
|
259
|
-
}
|
|
260
|
-
return false;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function findFilesRecursive(dir, exts) {
|
|
264
|
-
if (!existsSync(dir)) return [];
|
|
265
|
-
const out = [];
|
|
266
|
-
for (const ent of readdirSync(dir, { withFileTypes: true })) {
|
|
267
|
-
if (ent.name === 'node_modules' || ent.name.startsWith('.')) continue;
|
|
268
|
-
const full = join(dir, ent.name);
|
|
269
|
-
if (ent.isDirectory()) {
|
|
270
|
-
out.push(...findFilesRecursive(full, exts));
|
|
271
|
-
} else if (exts.includes(extname(ent.name))) {
|
|
272
|
-
out.push(full);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
return out;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Every token the editor names in a row: `variable: '--x'`, the type-group
|
|
280
|
-
* `colorVariable` / `familyVariable` / ... keys, and the arrow form intrinsics
|
|
281
|
-
* use. A `${...}` hole stands for a variant segment. Per-side padding names are
|
|
282
|
-
* written by the padding selector rather than declared, so they resolve to
|
|
283
|
-
* their parent.
|
|
284
|
-
*/
|
|
285
|
-
function editorTokenRefs(editor) {
|
|
286
|
-
const literals = new Set();
|
|
287
|
-
const patterns = [];
|
|
288
|
-
for (const m of editor.matchAll(/\b(?:variable|[a-zA-Z]+Variable)\s*:\s*(?:\([^)]*\)\s*=>\s*)?[`'"](--(?:\$\{[^}]*\}|[^`'"])+)[`'"]/g)) {
|
|
289
|
-
const name = stripSide(m[1]);
|
|
290
|
-
if (name.includes('${')) {
|
|
291
|
-
patterns.push([name, new RegExp(`^${name.replace(/[.*+?^()|[\]\\]/g, '\\$&').replace(/\$\{[^}]*\}/g, '[a-z0-9-]+')}$`)]);
|
|
292
|
-
} else {
|
|
293
|
-
literals.add(name);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return { literals, patterns };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function stripSide(token) {
|
|
300
|
-
const side = SIDE_SUFFIXES.find((x) => token.endsWith(x));
|
|
301
|
-
return side ? token.slice(0, -side.length) : token;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Token patterns the editor declares in `intrinsics` — the only tokens allowed a
|
|
306
|
-
* bare keyword instead of a theme token.
|
|
307
|
-
*
|
|
308
|
-
* Matched on each spec's `variable`, not its `key`: the two need not agree
|
|
309
|
-
* (Image's `zoom` key declares `--image-zoom-enabled`), and `variable` is what
|
|
310
|
-
* actually names the token. A `${...}` hole stands for a variant segment.
|
|
311
|
-
*/
|
|
312
|
-
function intrinsicMatchers(editor) {
|
|
313
|
-
const block = editor.match(/export\s+const\s+intrinsics[^=]*=\s*\[([\s\S]*?)\];/);
|
|
314
|
-
if (!block) return [];
|
|
315
|
-
const out = [];
|
|
316
|
-
for (const m of block[1].matchAll(/\bvariable\s*:[^`'"]*[`'"]([^`'"]+)[`'"]/g)) {
|
|
317
|
-
const pattern = m[1]
|
|
318
|
-
.replace(/[.*+?^${}()|[\]\\]/g, (c) => (c === '$' ? '$' : `\\${c}`))
|
|
319
|
-
.replace(/\$\\\{[^}]*\\\}/g, '[a-z0-9-]+')
|
|
320
|
-
.replace(/\$\{[^}]*\}/g, '[a-z0-9-]+');
|
|
321
|
-
out.push(new RegExp(`^${pattern}$`));
|
|
322
|
-
}
|
|
323
|
-
return out;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* The semantic half of the contract: a component token is a *property name*, and
|
|
328
|
-
* its default is the theme token that property reads. So every default must
|
|
329
|
-
* resolve to a real token — otherwise the component stops repainting when the
|
|
330
|
-
* theme changes, which is the whole point of declaring it.
|
|
331
|
-
*/
|
|
332
|
-
function checkDefaultsAreSemantic({ blocks, runtime, editor, root, runtimePath, record, vocabulary }) {
|
|
333
|
-
const vocab = vocabulary ?? loadVocabulary({ root });
|
|
334
|
-
const matchers = intrinsicMatchers(editor);
|
|
335
|
-
const rel = relative(root, runtimePath);
|
|
336
|
-
|
|
337
|
-
const own = new Set();
|
|
338
|
-
for (const block of blocks) {
|
|
339
|
-
for (const m of block.matchAll(/(?:^|[;{])\s*(--[a-z0-9-]+)\s*:/gm)) own.add(m[1]);
|
|
116
|
+
// repo the consumer paths always exist, so this fallback never fires here.
|
|
117
|
+
if (root !== PKG_ROOT && builtInIds(root, PKG_ROOT).has(id)) {
|
|
118
|
+
const shipped = componentInventory(PKG_ROOT).get(id);
|
|
119
|
+
if (shipped?.runtimeExists) return pick(shipped);
|
|
340
120
|
}
|
|
341
121
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
const [decl, name, raw] = m;
|
|
345
|
-
const value = raw.trim();
|
|
346
|
-
const at = runtime.indexOf(decl);
|
|
122
|
+
// A registered id with no runtime anywhere: report where it would live.
|
|
123
|
+
if (local) return pick(local);
|
|
347
124
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
'unknown-token-ref',
|
|
354
|
-
STATE_TOKENS.includes(ref.replace(/^--/, '').split('-')[0])
|
|
355
|
-
? `${rel}: ${name} reads ${ref}, but a state is a segment of a property name. Read the token the state should paint`
|
|
356
|
-
: isContractToken(ref)
|
|
357
|
-
? `${rel}: ${name} reads ${ref}, which has the shape of a design token but no longer exists. Check tokens.css for a rename`
|
|
358
|
-
: `${rel}: ${name} reads ${ref}, which is not a design token or a semantic property`,
|
|
359
|
-
at,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (hasColorLiteral(painted)) {
|
|
364
|
-
record(
|
|
365
|
-
'color-literal',
|
|
366
|
-
`${rel}: ${name}: ${value} is a colour literal; defaults must reference design tokens (e.g. var(--surface-primary))`,
|
|
367
|
-
at,
|
|
368
|
-
);
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (refs.length === 0 && !matchers.some((re) => re.test(name))) {
|
|
373
|
-
record(
|
|
374
|
-
'default-not-token',
|
|
375
|
-
`${rel}: ${name}: ${value} has no design token behind it. Back it with a token, or declare it in the editor's \`intrinsics\` when it is a structural keyword`,
|
|
376
|
-
at,
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (hasDimensionLiteral(painted)) {
|
|
381
|
-
record(
|
|
382
|
-
'dimension-literal',
|
|
383
|
-
`${rel}: ${name}: ${value} pins a raw dimension; use a --space-*, --radius-*, or --border-width-* token`,
|
|
384
|
-
at,
|
|
385
|
-
);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
125
|
+
const Id = capitalize(id);
|
|
126
|
+
const editorPath =
|
|
127
|
+
EDITOR_DIRS.map((d) => join(root, d, `${Id}Editor.svelte`)).find(existsSync) ??
|
|
128
|
+
join(root, EDITOR_DIRS[0], `${Id}Editor.svelte`);
|
|
129
|
+
return { Id, runtimePath: join(root, 'src/system/components', `${Id}.svelte`), editorPath };
|
|
389
130
|
}
|
|
390
131
|
|
|
391
132
|
export function checkComponent(id, root = process.cwd(), { vocabulary } = {}) {
|
|
@@ -395,227 +136,82 @@ export function checkComponent(id, root = process.cwd(), { vocabulary } = {}) {
|
|
|
395
136
|
let file = '';
|
|
396
137
|
let source = '';
|
|
397
138
|
|
|
398
|
-
const record = (rule, message, index = -1) => {
|
|
139
|
+
const record = (rule, message, index = -1, extra = {}) => {
|
|
399
140
|
findings.push({
|
|
400
141
|
rule,
|
|
401
142
|
file,
|
|
402
143
|
line: index >= 0 && source ? lineOf(source, index) : 1,
|
|
403
144
|
message,
|
|
145
|
+
...extra,
|
|
404
146
|
});
|
|
405
|
-
(COMPONENT_RULES[rule] === 'warn' ? warnings : errors).push(message);
|
|
147
|
+
(COMPONENT_RULES[rule]?.severity === 'warn' ? warnings : errors).push(message);
|
|
148
|
+
};
|
|
149
|
+
// For a finding about a file other than the runtime (config-token against
|
|
150
|
+
// the saved alias config, deep-import against the editor or the registration
|
|
151
|
+
// file): the line is already resolved against that file's own text, never
|
|
152
|
+
// `source` (the runtime).
|
|
153
|
+
const recordAt = (rule, message, atFile, line = 1, extra = {}) => {
|
|
154
|
+
findings.push({ rule, file: atFile, line, message, ...extra });
|
|
155
|
+
(COMPONENT_RULES[rule]?.severity === 'warn' ? warnings : errors).push(message);
|
|
406
156
|
};
|
|
407
157
|
const done = () => ({ errors, warnings, findings });
|
|
408
158
|
|
|
409
|
-
if (
|
|
410
|
-
record('invalid-id', `id "${id}" is invalid; must be lowercase letters/digits, no dashes`);
|
|
411
|
-
return done();
|
|
412
|
-
}
|
|
159
|
+
if (!componentStructure.checkId(id, record)) return done();
|
|
413
160
|
|
|
414
161
|
const { Id, runtimePath, editorPath } = resolveComponentPaths(id, root);
|
|
415
|
-
|
|
416
162
|
file = relative(root, runtimePath);
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
if (errors.length) return done();
|
|
163
|
+
const { editorMissing, runtimeMissing } = componentStructure.checkFiles({ root, runtimePath, editorPath }, record);
|
|
164
|
+
if (runtimeMissing) return done();
|
|
165
|
+
// A missing editor still gets its own finding, but the rules that need only
|
|
166
|
+
// the runtime (token shape, semantics, config, registration) still run —
|
|
167
|
+
// an editor-less component is exactly the case those rules exist to catch.
|
|
424
168
|
|
|
425
169
|
const runtime = readFileSync(runtimePath, 'utf8');
|
|
426
|
-
const editor = readFileSync(editorPath, 'utf8');
|
|
170
|
+
const editor = editorMissing ? '' : readFileSync(editorPath, 'utf8');
|
|
427
171
|
source = runtime;
|
|
428
|
-
|
|
429
|
-
// Runtime: :global(:root) block present.
|
|
430
172
|
const blocks = extractGlobalRootBlocks(runtime);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
// Runtime: state-after-property anti-pattern. Report this first; if it fires
|
|
447
|
-
// for a token, skip the unknown-suffix error for the same token (the state-
|
|
448
|
-
// suffix wouldn't be in the suffix list anyway, so it's the same root cause).
|
|
449
|
-
const stateAfterTokens = new Set();
|
|
450
|
-
for (const token of tokens) {
|
|
451
|
-
if (intrinsic.some((re) => re.test(token))) continue;
|
|
452
|
-
const trailingState = detectStateAfterProperty(token, known);
|
|
453
|
-
if (trailingState) {
|
|
454
|
-
stateAfterTokens.add(token);
|
|
455
|
-
record(
|
|
456
|
-
'state-after-property',
|
|
457
|
-
`${relative(root, runtimePath)}: ${token} has '${trailingState}' after the property; ` +
|
|
458
|
-
`state must come before property (e.g. -${trailingState}-surface)`,
|
|
459
|
-
runtime.indexOf(token),
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
for (const token of tokens) {
|
|
465
|
-
const segments = token.slice(2).split('-');
|
|
466
|
-
if (!segments.includes('disabled')) continue;
|
|
467
|
-
const conflict = TERMINAL_CONFLICTS.find((s) => segments.includes(s));
|
|
468
|
-
if (conflict) {
|
|
469
|
-
record(
|
|
470
|
-
'disabled-is-terminal',
|
|
471
|
-
`${relative(root, runtimePath)}: ${token} combines 'disabled' with '${conflict}'; disabled is terminal, so that state never paints. Drop the token.`,
|
|
472
|
-
runtime.indexOf(token),
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// Runtime: every token ends in a known suffix.
|
|
478
|
-
for (const token of tokens) {
|
|
479
|
-
if (stateAfterTokens.has(token) || intrinsic.some((re) => re.test(token))) continue;
|
|
480
|
-
if (!tokenSuffix(token, known)) {
|
|
481
|
-
record(
|
|
482
|
-
'unknown-suffix',
|
|
483
|
-
`${relative(root, runtimePath)}: ${token} doesn't end in a known suffix`,
|
|
484
|
-
runtime.indexOf(token),
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
checkDefaultsAreSemantic({ blocks, runtime, editor, root, runtimePath, record, vocabulary });
|
|
490
|
-
|
|
491
|
-
// Editor: declares `const component = '<id>'` (module block).
|
|
492
|
-
const componentDecl = new RegExp(`\\bconst\\s+component\\s*=\\s*['"]${id}['"]`);
|
|
493
|
-
if (!componentDecl.test(editor)) {
|
|
494
|
-
record(
|
|
495
|
-
'missing-component-const',
|
|
496
|
-
`${relative(root, editorPath)}: missing 'const component = "${id}"' in <script module>`,
|
|
497
|
-
);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
// Editor: exports allTokens.
|
|
501
|
-
if (!/\bexport\s+const\s+allTokens\b/.test(editor)) {
|
|
502
|
-
record('missing-all-tokens', `${relative(root, editorPath)}: missing 'export const allTokens'`);
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Editor: every token a row names is one the runtime declares. A row that
|
|
506
|
-
// names nothing renders a control that edits nothing.
|
|
507
|
-
const declared = new Set();
|
|
508
|
-
for (const block of blocks) {
|
|
509
|
-
for (const n of declaredCustomProperties(block.replace(/\/\*[\s\S]*?\*\//g, ' '))) declared.add(n);
|
|
510
|
-
}
|
|
511
|
-
const refs = editorTokenRefs(editor);
|
|
512
|
-
for (const name of refs.literals) {
|
|
513
|
-
if (!declared.has(name)) {
|
|
514
|
-
record(
|
|
515
|
-
'phantom-editor-token',
|
|
516
|
-
`${relative(root, editorPath)}: names ${name}, which ${relative(root, runtimePath)} never declares in :global(:root)`,
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
for (const [name, re] of refs.patterns) {
|
|
521
|
-
if (![...declared].some((d) => re.test(d))) {
|
|
522
|
-
record(
|
|
523
|
-
'phantom-editor-token',
|
|
524
|
-
`${relative(root, editorPath)}: names ${name}, which matches nothing ${relative(root, runtimePath)} declares in :global(:root)`,
|
|
525
|
-
);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Editor: phantom-link guard. The font type-group helpers fall back to bare
|
|
530
|
-
// `font-family`/`font-size`/… keys when called with a single argument (no
|
|
531
|
-
// derivation). Across more than one slot that silently links every slot's fonts
|
|
532
|
-
// into one tree. Passing `{ component, variants }` (a second arg) opts into
|
|
533
|
-
// distinct, structural keys and suppresses the check, so this only fires on the
|
|
534
|
-
// silent inference path. (The color helper no longer infers — a bare call there
|
|
535
|
-
// emits solo, un-grouped colors, which can't phantom-link.)
|
|
536
|
-
const colorPatterns = new Set();
|
|
537
|
-
for (const m of editor.matchAll(/colorVariable\s*:\s*[`'"]([^`'"]+)[`'"]/g)) {
|
|
538
|
-
colorPatterns.add(m[1].replace(/\$\{[^}]*\}/g, '*'));
|
|
539
|
-
}
|
|
540
|
-
const slots = colorPatterns.size;
|
|
541
|
-
const fontBare =
|
|
542
|
-
hasBareCall(editor, 'buildTypeGroupFontTokens') || hasBareCall(editor, 'buildTypeGroupTokens');
|
|
543
|
-
if (slots > 1 && fontBare) {
|
|
544
|
-
record(
|
|
545
|
-
'phantom-link',
|
|
546
|
-
`${relative(root, editorPath)}: a type-group font helper is called across ${slots} slots without a derivation; ` +
|
|
547
|
-
`its bare font-family/font-size/… keys would phantom-link every slot's fonts. Pass { component, variants } to buildTypeGroupTokens/buildTypeGroupFontTokens.`,
|
|
548
|
-
);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
// Imports across runtime + editor: reject deep imports into the package.
|
|
552
|
-
for (const [path, source] of [[runtimePath, runtime], [editorPath, editor]]) {
|
|
553
|
-
for (const imp of extractImports(source)) {
|
|
554
|
-
for (const pattern of DEEP_IMPORT_PATTERNS) {
|
|
555
|
-
if (pattern.test(imp)) {
|
|
556
|
-
record('deep-import', `${relative(root, path)}: deep import not supported: ${imp}`);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Registration: either a direct registerComponent({ id }) call or the id
|
|
563
|
-
// passed through bootLiveTokens({ components: [{ id }] }) — the standard
|
|
564
|
-
// scaffold boot. Accept both, somewhere under src/.
|
|
565
|
-
const srcFiles = findFilesRecursive(join(root, 'src'), ['.ts', '.js', '.svelte', '.mjs']);
|
|
566
|
-
const idLiteral = `id\\s*:\\s*['"]${id}['"]`;
|
|
567
|
-
const directPattern = new RegExp(`registerComponent\\s*\\(\\s*\\{[^}]*${idLiteral}`, 's');
|
|
568
|
-
const bootPattern = new RegExp(`bootLiveTokens\\s*\\([\\s\\S]*?components\\s*:\\s*\\[[\\s\\S]*?${idLiteral}`, 's');
|
|
569
|
-
let registrationFile = null;
|
|
570
|
-
for (const file of srcFiles) {
|
|
571
|
-
try {
|
|
572
|
-
const src = readFileSync(file, 'utf8');
|
|
573
|
-
if (directPattern.test(src) || bootPattern.test(src)) {
|
|
574
|
-
registrationFile = file;
|
|
575
|
-
break;
|
|
576
|
-
}
|
|
577
|
-
} catch {
|
|
578
|
-
// ignore unreadable files
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// A first-party component is registered by membership in the package's own
|
|
582
|
-
// `builtInRegistry`, not by a `registerComponent` call, so look there too
|
|
583
|
-
// before calling it unregistered.
|
|
584
|
-
if (!registrationFile && isBuiltIn(id)) return done();
|
|
173
|
+
const context = {
|
|
174
|
+
id,
|
|
175
|
+
Id,
|
|
176
|
+
root,
|
|
177
|
+
runtimePath,
|
|
178
|
+
editorPath,
|
|
179
|
+
runtime,
|
|
180
|
+
editor,
|
|
181
|
+
blocks,
|
|
182
|
+
vocab: vocabulary ?? loadVocabulary({ root }),
|
|
183
|
+
// A token the editor declares as an intrinsic carries a structural keyword,
|
|
184
|
+
// not a themeable value, so a property-suffix rule is the wrong test for it.
|
|
185
|
+
intrinsic: intrinsicMatchers(editor),
|
|
186
|
+
declared: declaredTokens(blocks),
|
|
187
|
+
};
|
|
585
188
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
}
|
|
189
|
+
componentStructure.checkRuntime(context, record);
|
|
190
|
+
tokenRules.checkDefaultsAreSemantic(context, record);
|
|
191
|
+
tokenRules.checkConfigTokens(context, recordAt);
|
|
192
|
+
tokenRules.checkUnreadTokens(context, record);
|
|
193
|
+
// The editor rules need real editor content; a missing editor already has
|
|
194
|
+
// its own `missing-file` finding, and running them against an empty string
|
|
195
|
+
// would only restate it under different rules.
|
|
196
|
+
if (!editorMissing) componentStructure.checkEditor(context, record);
|
|
197
|
+
importsAndRoutes.checkComponentImports(root, [[runtimePath, runtime], [editorPath, editor]], recordAt);
|
|
198
|
+
|
|
199
|
+
const registrationFile = componentStructure.checkRegistration(context, record);
|
|
200
|
+
if (registrationFile) {
|
|
201
|
+
importsAndRoutes.checkComponentImports(root, [[registrationFile, readFileSync(registrationFile, 'utf8')]], recordAt);
|
|
601
202
|
}
|
|
602
|
-
|
|
603
203
|
return done();
|
|
604
204
|
}
|
|
605
205
|
|
|
606
206
|
/**
|
|
607
207
|
* Every component authored in `root`: a runtime file under
|
|
608
|
-
* src/system/components
|
|
609
|
-
*
|
|
208
|
+
* `src/system/components` or a configured `componentDirs` entry, editor or
|
|
209
|
+
* not — a runtime with no editor is still in the batch, so `checkComponent`
|
|
210
|
+
* can report its own `missing-file`. What `check-component` runs over when no
|
|
211
|
+
* id is named.
|
|
610
212
|
*/
|
|
611
213
|
export function discoverComponents(root = process.cwd()) {
|
|
612
|
-
|
|
613
|
-
if (!existsSync(dir)) return [];
|
|
614
|
-
return readdirSync(dir)
|
|
615
|
-
.filter((f) => f.endsWith('.svelte') && !f.endsWith('Editor.svelte'))
|
|
616
|
-
.map((f) => f.replace('.svelte', ''))
|
|
617
|
-
.filter((Id) => EDITOR_DIRS.some((d) => existsSync(join(root, d, `${Id}Editor.svelte`))))
|
|
618
|
-
.map((Id) => Id.toLowerCase());
|
|
214
|
+
return [...componentInventory(root).values()].filter((e) => e.runtimeExists).map((e) => e.id);
|
|
619
215
|
}
|
|
620
216
|
|
|
621
217
|
export function formatReport(id, result) {
|
|
@@ -655,7 +251,14 @@ export function checkComponentDefaults(runtimePath, { root = process.cwd(), voca
|
|
|
655
251
|
const editorPath = EDITOR_DIRS.map((d) => join(root, d, `${name}Editor.svelte`)).find(existsSync);
|
|
656
252
|
const editor = editorPath ? readFileSync(editorPath, 'utf8') : '';
|
|
657
253
|
|
|
658
|
-
const
|
|
659
|
-
|
|
254
|
+
const context = {
|
|
255
|
+
blocks: extractGlobalRootBlocks(runtime),
|
|
256
|
+
runtime,
|
|
257
|
+
root,
|
|
258
|
+
runtimePath,
|
|
259
|
+
vocab: vocabulary ?? loadVocabulary({ root }),
|
|
260
|
+
intrinsic: intrinsicMatchers(editor),
|
|
261
|
+
};
|
|
262
|
+
tokenRules.checkDefaultsAreSemantic(context, record);
|
|
660
263
|
return findings;
|
|
661
264
|
}
|