@motion-proto/live-tokens 0.16.1 → 0.17.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/bin/cli.mjs +25 -3
- package/bin/migrate.mjs +113 -0
- package/dist-plugin/chunk-UBS57IYV.js +249 -0
- package/dist-plugin/index.cjs +200 -3
- package/dist-plugin/index.d.cts +1 -0
- package/dist-plugin/index.d.ts +1 -0
- package/dist-plugin/index.js +165 -182
- package/dist-plugin/tokensCssMigrations/index.cjs +279 -0
- package/dist-plugin/tokensCssMigrations/index.d.cts +144 -0
- package/dist-plugin/tokensCssMigrations/index.d.ts +144 -0
- package/dist-plugin/tokensCssMigrations/index.js +24 -0
- package/package.json +1 -1
- package/src/system/components/CollapsibleSection.svelte +47 -4
package/bin/cli.mjs
CHANGED
|
@@ -9,13 +9,18 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import process from 'node:process';
|
|
11
11
|
import { checkComponent, formatReport } from './check-component.mjs';
|
|
12
|
+
import { runMigrate, formatMigrateResult } from './migrate.mjs';
|
|
12
13
|
|
|
13
14
|
const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
|
|
14
15
|
|
|
15
16
|
Commands:
|
|
16
|
-
setup-claude [--force]
|
|
17
|
-
check-component <id>
|
|
18
|
-
|
|
17
|
+
setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
|
|
18
|
+
check-component <id> Validate <id>'s runtime, editor, and registration
|
|
19
|
+
against the live-tokens-create-component contract
|
|
20
|
+
migrate [--check] [--tokens <path>]
|
|
21
|
+
Reconcile your tokens.css with the installed
|
|
22
|
+
package's Layer-1 token vocabulary. --check reports
|
|
23
|
+
without writing (exit 1 if changes are needed).
|
|
19
24
|
`;
|
|
20
25
|
|
|
21
26
|
function fail(message, code = 1) {
|
|
@@ -38,6 +43,23 @@ if (command === 'check-component') {
|
|
|
38
43
|
process.exit(result.errors.length === 0 ? 0 : 1);
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
if (command === 'migrate') {
|
|
47
|
+
const check = rest.includes('--check');
|
|
48
|
+
const tokensIdx = rest.indexOf('--tokens');
|
|
49
|
+
const tokensArg = tokensIdx !== -1 ? rest[tokensIdx + 1] : undefined;
|
|
50
|
+
if (tokensIdx !== -1 && !tokensArg) fail(`--tokens requires a path`);
|
|
51
|
+
try {
|
|
52
|
+
const result = await runMigrate({ tokensArg, check });
|
|
53
|
+
console.log(formatMigrateResult(result, { check }));
|
|
54
|
+
// Exit 1 when --check finds pending changes (CI-friendly) or on no-path.
|
|
55
|
+
if (result.status === 'no-path') process.exit(1);
|
|
56
|
+
if (check && result.status === 'would-change') process.exit(1);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
fail(`migrate failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
41
63
|
if (command !== 'setup-claude') {
|
|
42
64
|
fail(`Unknown command: ${command}\n\n${USAGE}`);
|
|
43
65
|
}
|
package/bin/migrate.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `live-tokens migrate` worker.
|
|
2
|
+
//
|
|
3
|
+
// Applies the registered tokens-css migrations to a consumer's developer-authored
|
|
4
|
+
// tokens.css, reconciling Layer-1 drift after a package upgrade. The migration
|
|
5
|
+
// engine itself lives in the built plugin (dist-plugin/tokensCssMigrations) so
|
|
6
|
+
// the CLI and the dev-plugin guardrail share one source of truth; this module
|
|
7
|
+
// only handles path resolution, file IO, and reporting.
|
|
8
|
+
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
+
const ENGINE = resolve(pkgRoot, 'dist-plugin/tokensCssMigrations/index.js');
|
|
15
|
+
|
|
16
|
+
// Locations to probe when neither --tokens nor config.tokensCssPath is given.
|
|
17
|
+
const DEFAULT_CANDIDATES = [
|
|
18
|
+
'src/system/styles/tokens.css',
|
|
19
|
+
'src/styles/tokens.css',
|
|
20
|
+
'src/tokens.css',
|
|
21
|
+
'tokens.css',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
async function loadEngine() {
|
|
25
|
+
if (!existsSync(ENGINE)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`migration engine not found at ${relative(process.cwd(), ENGINE)}. ` +
|
|
28
|
+
`Build the plugin first (npm run build:plugin).`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return import(ENGINE);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** --tokens <path> > live-tokens.config.json tokensCssPath > default scan. */
|
|
35
|
+
export function resolveTokensCssPath(explicit, configPath, root = process.cwd()) {
|
|
36
|
+
if (explicit) return resolve(root, explicit);
|
|
37
|
+
if (configPath) return resolve(root, configPath);
|
|
38
|
+
for (const c of DEFAULT_CANDIDATES) {
|
|
39
|
+
const full = resolve(root, c);
|
|
40
|
+
if (existsSync(full)) return full;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Run migrations. Returns a result object; never writes when `check` is true.
|
|
47
|
+
* `{ status: 'no-path' | 'unchanged' | 'changed' | 'would-change', ... }`
|
|
48
|
+
*/
|
|
49
|
+
export async function runMigrate({ tokensArg, check = false, root = process.cwd() } = {}) {
|
|
50
|
+
const { runTokensCssMigrations, readLiveTokensConfig } = await loadEngine();
|
|
51
|
+
|
|
52
|
+
const config = readLiveTokensConfig();
|
|
53
|
+
const tokensPath = resolveTokensCssPath(tokensArg, config.tokensCssPath, root);
|
|
54
|
+
if (!tokensPath) {
|
|
55
|
+
return {
|
|
56
|
+
status: 'no-path',
|
|
57
|
+
message:
|
|
58
|
+
`Could not locate tokens.css. Pass --tokens <path>, set "tokensCssPath" in ` +
|
|
59
|
+
`live-tokens.config.json, or place it at one of: ${DEFAULT_CANDIDATES.join(', ')}.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (!existsSync(tokensPath)) {
|
|
63
|
+
return { status: 'no-path', message: `tokens.css not found at ${relative(root, tokensPath)}.` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const before = readFileSync(tokensPath, 'utf8');
|
|
67
|
+
const { css, applied, changed } = runTokensCssMigrations(before);
|
|
68
|
+
const addedLines = diffAddedLines(before, css);
|
|
69
|
+
|
|
70
|
+
if (!changed) {
|
|
71
|
+
return { status: 'unchanged', tokensPath };
|
|
72
|
+
}
|
|
73
|
+
if (check) {
|
|
74
|
+
return { status: 'would-change', tokensPath, applied, addedLines };
|
|
75
|
+
}
|
|
76
|
+
writeFileSync(tokensPath, css);
|
|
77
|
+
return { status: 'changed', tokensPath, applied, addedLines };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function diffAddedLines(before, after) {
|
|
81
|
+
const beforeSet = new Set(before.split('\n'));
|
|
82
|
+
return after.split('\n').filter((l) => l.trim() && !beforeSet.has(l));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function formatMigrateResult(result, { check }) {
|
|
86
|
+
const root = process.cwd();
|
|
87
|
+
switch (result.status) {
|
|
88
|
+
case 'no-path':
|
|
89
|
+
return result.message;
|
|
90
|
+
case 'unchanged':
|
|
91
|
+
return `✓ ${relative(root, result.tokensPath)} is already up to date.`;
|
|
92
|
+
case 'would-change':
|
|
93
|
+
case 'changed': {
|
|
94
|
+
const verb = result.status === 'changed' ? 'Applied' : 'Would apply';
|
|
95
|
+
const lines = [
|
|
96
|
+
`${verb} ${result.applied.length} migration(s) to ${relative(root, result.tokensPath)}:`,
|
|
97
|
+
...result.applied.map((id) => ` • ${id}`),
|
|
98
|
+
];
|
|
99
|
+
if (result.addedLines.length) {
|
|
100
|
+
lines.push(`Added ${result.addedLines.length} line(s):`);
|
|
101
|
+
for (const l of result.addedLines) lines.push(` + ${l.trim()}`);
|
|
102
|
+
}
|
|
103
|
+
if (check) {
|
|
104
|
+
lines.push(`\nRun without --check to write these changes, then review the diff in git.`);
|
|
105
|
+
} else {
|
|
106
|
+
lines.push(`\nReview the diff in git before committing.`);
|
|
107
|
+
}
|
|
108
|
+
return lines.join('\n');
|
|
109
|
+
}
|
|
110
|
+
default:
|
|
111
|
+
return `Unknown result: ${JSON.stringify(result)}`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// src/editor/core/themes/parsers/globalRootBlock.ts
|
|
2
|
+
function extractGlobalRootBody(source) {
|
|
3
|
+
const re = /:global\(:root\)\s*\{([^}]*)\}/g;
|
|
4
|
+
const bodies = [];
|
|
5
|
+
let m;
|
|
6
|
+
while ((m = re.exec(source)) !== null) {
|
|
7
|
+
bodies.push(m[1]);
|
|
8
|
+
}
|
|
9
|
+
return bodies.join("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// vite-plugin/tokensCssMigrations/cssTokenOps.ts
|
|
13
|
+
var DECL_RE = /(^|[\s;{])(--[a-z0-9-]+)\s*:/gi;
|
|
14
|
+
var REF_RE = /var\(\s*(--[a-z0-9-]+)/gi;
|
|
15
|
+
function collectDefinedTokens(css) {
|
|
16
|
+
const out = /* @__PURE__ */ new Set();
|
|
17
|
+
for (const m of css.matchAll(DECL_RE)) out.add(m[2]);
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
function collectReferencedTokens(css) {
|
|
21
|
+
const out = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const m of css.matchAll(REF_RE)) out.add(m[1]);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
function ensureScale(css, opts) {
|
|
26
|
+
const defined = collectDefinedTokens(css);
|
|
27
|
+
const missing = opts.entries.filter((e) => !defined.has(e.name));
|
|
28
|
+
if (missing.length === 0) return css;
|
|
29
|
+
const lines = css.split("\n");
|
|
30
|
+
const { indent, insertAfter } = findInsertionPoint(lines, opts.anchorPrefixes ?? []);
|
|
31
|
+
const block = [];
|
|
32
|
+
if (opts.sectionComment) block.push(`${indent}/* ${opts.sectionComment} */`);
|
|
33
|
+
for (const e of missing) block.push(`${indent}${e.name}: ${e.value};`);
|
|
34
|
+
lines.splice(insertAfter + 1, 0, ...block);
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
function renameToken(css, oldName, newName) {
|
|
38
|
+
const defined = collectDefinedTokens(css);
|
|
39
|
+
if (!defined.has(oldName) || defined.has(newName)) return css;
|
|
40
|
+
const re = new RegExp(escapeRe(oldName) + "(?![a-z0-9-])", "gi");
|
|
41
|
+
return css.replace(re, newName);
|
|
42
|
+
}
|
|
43
|
+
function removeToken(css, name) {
|
|
44
|
+
const lines = css.split("\n");
|
|
45
|
+
const declRe = new RegExp("(^|[\\s;{])" + escapeRe(name) + "\\s*:");
|
|
46
|
+
const kept = lines.filter((line) => !declRe.test(line));
|
|
47
|
+
if (kept.length === lines.length) return css;
|
|
48
|
+
return kept.join("\n");
|
|
49
|
+
}
|
|
50
|
+
function removeTokensMatching(css, predicate) {
|
|
51
|
+
const lines = css.split("\n");
|
|
52
|
+
const declRe = /^\s*(--[a-z0-9-]+)\s*:/;
|
|
53
|
+
const kept = lines.filter((line) => {
|
|
54
|
+
const m = line.match(declRe);
|
|
55
|
+
return !(m && predicate(m[1]));
|
|
56
|
+
});
|
|
57
|
+
if (kept.length === lines.length) return css;
|
|
58
|
+
return kept.join("\n");
|
|
59
|
+
}
|
|
60
|
+
function findInsertionPoint(lines, anchorPrefixes) {
|
|
61
|
+
for (const prefix of anchorPrefixes) {
|
|
62
|
+
let last = -1;
|
|
63
|
+
let indent = " ";
|
|
64
|
+
for (let i = 0; i < lines.length; i++) {
|
|
65
|
+
const m = lines[i].match(/^(\s*)(--[a-z0-9-]+)\s*:/);
|
|
66
|
+
if (m && m[2].startsWith(prefix)) {
|
|
67
|
+
last = i;
|
|
68
|
+
indent = m[1] || " ";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (last !== -1) return { indent, insertAfter: last };
|
|
72
|
+
}
|
|
73
|
+
let depth = 0;
|
|
74
|
+
let inRoot = false;
|
|
75
|
+
let lastDeclIndent = " ";
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const line = lines[i];
|
|
78
|
+
if (!inRoot && /(^|\s):root[^{]*\{/.test(line)) {
|
|
79
|
+
inRoot = true;
|
|
80
|
+
depth = 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!inRoot) continue;
|
|
84
|
+
const declIndent = line.match(/^(\s*)--[a-z0-9-]+\s*:/);
|
|
85
|
+
if (declIndent) lastDeclIndent = declIndent[1] || " ";
|
|
86
|
+
depth += (line.match(/\{/g)?.length ?? 0) - (line.match(/\}/g)?.length ?? 0);
|
|
87
|
+
if (depth === 0) {
|
|
88
|
+
return { indent: lastDeclIndent, insertAfter: i - 1 };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { indent: " ", insertAfter: lines.length - 1 };
|
|
92
|
+
}
|
|
93
|
+
function escapeRe(s) {
|
|
94
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-05-29-typography-scale-additions.ts
|
|
98
|
+
var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
|
|
99
|
+
id: "2026-05-29-typography-scale-additions",
|
|
100
|
+
description: "Add --line-height-{xs..xl}, --letter-spacing-* and --ease-out-quart scales",
|
|
101
|
+
apply(css) {
|
|
102
|
+
let out = css;
|
|
103
|
+
out = ensureScale(out, {
|
|
104
|
+
sectionComment: "Line height (t-shirt scale)",
|
|
105
|
+
anchorPrefixes: ["--line-height-", "--font-size-", "--font-weight-", "--font-"],
|
|
106
|
+
entries: [
|
|
107
|
+
{ name: "--line-height-xs", value: "1" },
|
|
108
|
+
{ name: "--line-height-sm", value: "1.25" },
|
|
109
|
+
{ name: "--line-height-md", value: "1.5" },
|
|
110
|
+
{ name: "--line-height-lg", value: "1.75" },
|
|
111
|
+
{ name: "--line-height-xl", value: "2" }
|
|
112
|
+
]
|
|
113
|
+
});
|
|
114
|
+
out = ensureScale(out, {
|
|
115
|
+
sectionComment: "Letter spacing",
|
|
116
|
+
anchorPrefixes: ["--letter-spacing-", "--line-height-", "--font-size-", "--font-"],
|
|
117
|
+
entries: [
|
|
118
|
+
{ name: "--letter-spacing-tighter", value: "-0.04em" },
|
|
119
|
+
{ name: "--letter-spacing-tight", value: "-0.02em" },
|
|
120
|
+
{ name: "--letter-spacing-normal", value: "0" },
|
|
121
|
+
{ name: "--letter-spacing-wide", value: "0.04em" },
|
|
122
|
+
{ name: "--letter-spacing-wider", value: "0.08em" }
|
|
123
|
+
]
|
|
124
|
+
});
|
|
125
|
+
out = ensureScale(out, {
|
|
126
|
+
sectionComment: "Easing",
|
|
127
|
+
anchorPrefixes: ["--ease-", "--transition-", "--duration-"],
|
|
128
|
+
entries: [{ name: "--ease-out-quart", value: "cubic-bezier(0.25, 1, 0.5, 1)" }]
|
|
129
|
+
});
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-05-29-sectiondivider-legacy-axis-cleanup.ts
|
|
135
|
+
var KEEP_SEGMENTS = /* @__PURE__ */ new Set(["lg", "md", "sm"]);
|
|
136
|
+
var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
|
|
137
|
+
id: "2026-05-29-sectiondivider-legacy-axis-cleanup",
|
|
138
|
+
description: "Remove legacy --sectiondivider-* tokens not on the lg/md/sm axis",
|
|
139
|
+
apply(css) {
|
|
140
|
+
return removeTokensMatching(css, (name) => {
|
|
141
|
+
if (!name.startsWith("--sectiondivider-")) return false;
|
|
142
|
+
const segment = name.slice("--sectiondivider-".length).split("-")[0];
|
|
143
|
+
return !KEEP_SEGMENTS.has(segment);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// vite-plugin/files/dataPaths.ts
|
|
149
|
+
import fs from "fs";
|
|
150
|
+
import path from "path";
|
|
151
|
+
var DEFAULT_DATA_DIR = "src/live-tokens/data";
|
|
152
|
+
var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
153
|
+
"dataDir",
|
|
154
|
+
"themesDir",
|
|
155
|
+
"componentConfigsDir",
|
|
156
|
+
"manifestsDir",
|
|
157
|
+
"tokensCssPath"
|
|
158
|
+
]);
|
|
159
|
+
var cached = null;
|
|
160
|
+
function readLiveTokensConfig() {
|
|
161
|
+
if (cached) return cached;
|
|
162
|
+
try {
|
|
163
|
+
const configPath = path.resolve("live-tokens.config.json");
|
|
164
|
+
if (!fs.existsSync(configPath)) return cached = {};
|
|
165
|
+
const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
166
|
+
if (!parsed || typeof parsed !== "object") return cached = {};
|
|
167
|
+
const unknown = Object.keys(parsed).filter(
|
|
168
|
+
(k) => k !== "$schema" && !KNOWN_CONFIG_KEYS.has(k)
|
|
169
|
+
);
|
|
170
|
+
if (unknown.length > 0) {
|
|
171
|
+
console.warn(
|
|
172
|
+
`[live-tokens] Unknown key(s) in live-tokens.config.json: ${unknown.join(", ")}. Known keys: ${Array.from(KNOWN_CONFIG_KEYS).join(", ")}.`
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
cached = parsed;
|
|
176
|
+
return cached;
|
|
177
|
+
} catch {
|
|
178
|
+
return cached = {};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function resolveDataDirs(opts = {}) {
|
|
182
|
+
const fileConfig = readLiveTokensConfig();
|
|
183
|
+
const dataDirRaw = opts.dataDir ?? fileConfig.dataDir ?? DEFAULT_DATA_DIR;
|
|
184
|
+
const dataDir = path.resolve(dataDirRaw);
|
|
185
|
+
const sub = (name) => path.resolve(dataDir, name);
|
|
186
|
+
return {
|
|
187
|
+
dataDir,
|
|
188
|
+
themesDir: opts.themesDir ? path.resolve(opts.themesDir) : fileConfig.themesDir ? path.resolve(fileConfig.themesDir) : sub("themes"),
|
|
189
|
+
componentConfigsDir: opts.componentConfigsDir ? path.resolve(opts.componentConfigsDir) : fileConfig.componentConfigsDir ? path.resolve(fileConfig.componentConfigsDir) : sub("component-configs"),
|
|
190
|
+
manifestsDir: opts.manifestsDir ? path.resolve(opts.manifestsDir) : fileConfig.manifestsDir ? path.resolve(fileConfig.manifestsDir) : sub("manifests")
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// vite-plugin/tokensCssMigrations/index.ts
|
|
195
|
+
var TOKENS_CSS_MIGRATIONS = [
|
|
196
|
+
tokensCssMigration_2026_05_29_typographyScaleAdditions,
|
|
197
|
+
tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup
|
|
198
|
+
];
|
|
199
|
+
function runTokensCssMigrations(css) {
|
|
200
|
+
let out = css;
|
|
201
|
+
const applied = [];
|
|
202
|
+
for (const m of TOKENS_CSS_MIGRATIONS) {
|
|
203
|
+
const next = m.apply(out);
|
|
204
|
+
if (next !== out) {
|
|
205
|
+
applied.push(m.id);
|
|
206
|
+
out = next;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { css: out, applied, changed: out !== css };
|
|
210
|
+
}
|
|
211
|
+
function validateTokensCss(input) {
|
|
212
|
+
const defined = /* @__PURE__ */ new Set([
|
|
213
|
+
...collectDefinedTokens(input.tokensCss),
|
|
214
|
+
...collectDefinedTokens(input.generatedCss ?? "")
|
|
215
|
+
]);
|
|
216
|
+
const referencedBy = /* @__PURE__ */ new Map();
|
|
217
|
+
for (const { name, source } of input.componentSources) {
|
|
218
|
+
const body = extractGlobalRootBody(source);
|
|
219
|
+
if (!body) continue;
|
|
220
|
+
for (const t of collectDefinedTokens(body)) defined.add(t);
|
|
221
|
+
for (const ref of collectReferencedTokens(body)) {
|
|
222
|
+
let set = referencedBy.get(ref);
|
|
223
|
+
if (!set) referencedBy.set(ref, set = /* @__PURE__ */ new Set());
|
|
224
|
+
set.add(name);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
const missing = [];
|
|
228
|
+
for (const [token, names] of referencedBy) {
|
|
229
|
+
if (!defined.has(token)) {
|
|
230
|
+
missing.push({ token, referencedBy: [...names].sort() });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return missing.sort((a, b) => a.token.localeCompare(b.token));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export {
|
|
237
|
+
extractGlobalRootBody,
|
|
238
|
+
readLiveTokensConfig,
|
|
239
|
+
resolveDataDirs,
|
|
240
|
+
collectDefinedTokens,
|
|
241
|
+
collectReferencedTokens,
|
|
242
|
+
ensureScale,
|
|
243
|
+
renameToken,
|
|
244
|
+
removeToken,
|
|
245
|
+
removeTokensMatching,
|
|
246
|
+
TOKENS_CSS_MIGRATIONS,
|
|
247
|
+
runTokensCssMigrations,
|
|
248
|
+
validateTokensCss
|
|
249
|
+
};
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -30,7 +30,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// vite-plugin/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
|
|
33
|
+
TOKENS_CSS_MIGRATIONS: () => TOKENS_CSS_MIGRATIONS,
|
|
34
|
+
runTokensCssMigrations: () => runTokensCssMigrations,
|
|
35
|
+
themeFileApi: () => themeFileApi,
|
|
36
|
+
validateTokensCss: () => validateTokensCss
|
|
34
37
|
});
|
|
35
38
|
module.exports = __toCommonJS(index_exports);
|
|
36
39
|
|
|
@@ -564,7 +567,8 @@ var KNOWN_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
|
564
567
|
"dataDir",
|
|
565
568
|
"themesDir",
|
|
566
569
|
"componentConfigsDir",
|
|
567
|
-
"manifestsDir"
|
|
570
|
+
"manifestsDir",
|
|
571
|
+
"tokensCssPath"
|
|
568
572
|
]);
|
|
569
573
|
var cached = null;
|
|
570
574
|
function readLiveTokensConfig() {
|
|
@@ -601,6 +605,168 @@ function resolveDataDirs(opts = {}) {
|
|
|
601
605
|
};
|
|
602
606
|
}
|
|
603
607
|
|
|
608
|
+
// vite-plugin/tokensCssMigrations/cssTokenOps.ts
|
|
609
|
+
var DECL_RE = /(^|[\s;{])(--[a-z0-9-]+)\s*:/gi;
|
|
610
|
+
var REF_RE = /var\(\s*(--[a-z0-9-]+)/gi;
|
|
611
|
+
function collectDefinedTokens(css) {
|
|
612
|
+
const out = /* @__PURE__ */ new Set();
|
|
613
|
+
for (const m of css.matchAll(DECL_RE)) out.add(m[2]);
|
|
614
|
+
return out;
|
|
615
|
+
}
|
|
616
|
+
function collectReferencedTokens(css) {
|
|
617
|
+
const out = /* @__PURE__ */ new Set();
|
|
618
|
+
for (const m of css.matchAll(REF_RE)) out.add(m[1]);
|
|
619
|
+
return out;
|
|
620
|
+
}
|
|
621
|
+
function ensureScale(css, opts) {
|
|
622
|
+
const defined = collectDefinedTokens(css);
|
|
623
|
+
const missing = opts.entries.filter((e) => !defined.has(e.name));
|
|
624
|
+
if (missing.length === 0) return css;
|
|
625
|
+
const lines = css.split("\n");
|
|
626
|
+
const { indent, insertAfter } = findInsertionPoint(lines, opts.anchorPrefixes ?? []);
|
|
627
|
+
const block = [];
|
|
628
|
+
if (opts.sectionComment) block.push(`${indent}/* ${opts.sectionComment} */`);
|
|
629
|
+
for (const e of missing) block.push(`${indent}${e.name}: ${e.value};`);
|
|
630
|
+
lines.splice(insertAfter + 1, 0, ...block);
|
|
631
|
+
return lines.join("\n");
|
|
632
|
+
}
|
|
633
|
+
function removeTokensMatching(css, predicate) {
|
|
634
|
+
const lines = css.split("\n");
|
|
635
|
+
const declRe = /^\s*(--[a-z0-9-]+)\s*:/;
|
|
636
|
+
const kept = lines.filter((line) => {
|
|
637
|
+
const m = line.match(declRe);
|
|
638
|
+
return !(m && predicate(m[1]));
|
|
639
|
+
});
|
|
640
|
+
if (kept.length === lines.length) return css;
|
|
641
|
+
return kept.join("\n");
|
|
642
|
+
}
|
|
643
|
+
function findInsertionPoint(lines, anchorPrefixes) {
|
|
644
|
+
for (const prefix of anchorPrefixes) {
|
|
645
|
+
let last = -1;
|
|
646
|
+
let indent = " ";
|
|
647
|
+
for (let i = 0; i < lines.length; i++) {
|
|
648
|
+
const m = lines[i].match(/^(\s*)(--[a-z0-9-]+)\s*:/);
|
|
649
|
+
if (m && m[2].startsWith(prefix)) {
|
|
650
|
+
last = i;
|
|
651
|
+
indent = m[1] || " ";
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (last !== -1) return { indent, insertAfter: last };
|
|
655
|
+
}
|
|
656
|
+
let depth = 0;
|
|
657
|
+
let inRoot = false;
|
|
658
|
+
let lastDeclIndent = " ";
|
|
659
|
+
for (let i = 0; i < lines.length; i++) {
|
|
660
|
+
const line = lines[i];
|
|
661
|
+
if (!inRoot && /(^|\s):root[^{]*\{/.test(line)) {
|
|
662
|
+
inRoot = true;
|
|
663
|
+
depth = 1;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (!inRoot) continue;
|
|
667
|
+
const declIndent = line.match(/^(\s*)--[a-z0-9-]+\s*:/);
|
|
668
|
+
if (declIndent) lastDeclIndent = declIndent[1] || " ";
|
|
669
|
+
depth += (line.match(/\{/g)?.length ?? 0) - (line.match(/\}/g)?.length ?? 0);
|
|
670
|
+
if (depth === 0) {
|
|
671
|
+
return { indent: lastDeclIndent, insertAfter: i - 1 };
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { indent: " ", insertAfter: lines.length - 1 };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-05-29-typography-scale-additions.ts
|
|
678
|
+
var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
|
|
679
|
+
id: "2026-05-29-typography-scale-additions",
|
|
680
|
+
description: "Add --line-height-{xs..xl}, --letter-spacing-* and --ease-out-quart scales",
|
|
681
|
+
apply(css) {
|
|
682
|
+
let out = css;
|
|
683
|
+
out = ensureScale(out, {
|
|
684
|
+
sectionComment: "Line height (t-shirt scale)",
|
|
685
|
+
anchorPrefixes: ["--line-height-", "--font-size-", "--font-weight-", "--font-"],
|
|
686
|
+
entries: [
|
|
687
|
+
{ name: "--line-height-xs", value: "1" },
|
|
688
|
+
{ name: "--line-height-sm", value: "1.25" },
|
|
689
|
+
{ name: "--line-height-md", value: "1.5" },
|
|
690
|
+
{ name: "--line-height-lg", value: "1.75" },
|
|
691
|
+
{ name: "--line-height-xl", value: "2" }
|
|
692
|
+
]
|
|
693
|
+
});
|
|
694
|
+
out = ensureScale(out, {
|
|
695
|
+
sectionComment: "Letter spacing",
|
|
696
|
+
anchorPrefixes: ["--letter-spacing-", "--line-height-", "--font-size-", "--font-"],
|
|
697
|
+
entries: [
|
|
698
|
+
{ name: "--letter-spacing-tighter", value: "-0.04em" },
|
|
699
|
+
{ name: "--letter-spacing-tight", value: "-0.02em" },
|
|
700
|
+
{ name: "--letter-spacing-normal", value: "0" },
|
|
701
|
+
{ name: "--letter-spacing-wide", value: "0.04em" },
|
|
702
|
+
{ name: "--letter-spacing-wider", value: "0.08em" }
|
|
703
|
+
]
|
|
704
|
+
});
|
|
705
|
+
out = ensureScale(out, {
|
|
706
|
+
sectionComment: "Easing",
|
|
707
|
+
anchorPrefixes: ["--ease-", "--transition-", "--duration-"],
|
|
708
|
+
entries: [{ name: "--ease-out-quart", value: "cubic-bezier(0.25, 1, 0.5, 1)" }]
|
|
709
|
+
});
|
|
710
|
+
return out;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// vite-plugin/tokensCssMigrations/migrations/2026-05-29-sectiondivider-legacy-axis-cleanup.ts
|
|
715
|
+
var KEEP_SEGMENTS = /* @__PURE__ */ new Set(["lg", "md", "sm"]);
|
|
716
|
+
var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
|
|
717
|
+
id: "2026-05-29-sectiondivider-legacy-axis-cleanup",
|
|
718
|
+
description: "Remove legacy --sectiondivider-* tokens not on the lg/md/sm axis",
|
|
719
|
+
apply(css) {
|
|
720
|
+
return removeTokensMatching(css, (name) => {
|
|
721
|
+
if (!name.startsWith("--sectiondivider-")) return false;
|
|
722
|
+
const segment = name.slice("--sectiondivider-".length).split("-")[0];
|
|
723
|
+
return !KEEP_SEGMENTS.has(segment);
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
// vite-plugin/tokensCssMigrations/index.ts
|
|
729
|
+
var TOKENS_CSS_MIGRATIONS = [
|
|
730
|
+
tokensCssMigration_2026_05_29_typographyScaleAdditions,
|
|
731
|
+
tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup
|
|
732
|
+
];
|
|
733
|
+
function runTokensCssMigrations(css) {
|
|
734
|
+
let out = css;
|
|
735
|
+
const applied = [];
|
|
736
|
+
for (const m of TOKENS_CSS_MIGRATIONS) {
|
|
737
|
+
const next = m.apply(out);
|
|
738
|
+
if (next !== out) {
|
|
739
|
+
applied.push(m.id);
|
|
740
|
+
out = next;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return { css: out, applied, changed: out !== css };
|
|
744
|
+
}
|
|
745
|
+
function validateTokensCss(input) {
|
|
746
|
+
const defined = /* @__PURE__ */ new Set([
|
|
747
|
+
...collectDefinedTokens(input.tokensCss),
|
|
748
|
+
...collectDefinedTokens(input.generatedCss ?? "")
|
|
749
|
+
]);
|
|
750
|
+
const referencedBy = /* @__PURE__ */ new Map();
|
|
751
|
+
for (const { name, source } of input.componentSources) {
|
|
752
|
+
const body = extractGlobalRootBody(source);
|
|
753
|
+
if (!body) continue;
|
|
754
|
+
for (const t of collectDefinedTokens(body)) defined.add(t);
|
|
755
|
+
for (const ref of collectReferencedTokens(body)) {
|
|
756
|
+
let set = referencedBy.get(ref);
|
|
757
|
+
if (!set) referencedBy.set(ref, set = /* @__PURE__ */ new Set());
|
|
758
|
+
set.add(name);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const missing = [];
|
|
762
|
+
for (const [token, names] of referencedBy) {
|
|
763
|
+
if (!defined.has(token)) {
|
|
764
|
+
missing.push({ token, referencedBy: [...names].sort() });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return missing.sort((a, b) => a.token.localeCompare(b.token));
|
|
768
|
+
}
|
|
769
|
+
|
|
604
770
|
// vite-plugin/themeFileApi.ts
|
|
605
771
|
var import_node_url = require("url");
|
|
606
772
|
var import_meta = {};
|
|
@@ -871,6 +1037,33 @@ function themeFileApi(opts) {
|
|
|
871
1037
|
function listComponentNames() {
|
|
872
1038
|
return listComponentSourcePaths().map(componentNameFromFile);
|
|
873
1039
|
}
|
|
1040
|
+
function warnOnTokenDrift(log) {
|
|
1041
|
+
let tokensCss = "";
|
|
1042
|
+
try {
|
|
1043
|
+
tokensCss = import_fs3.default.readFileSync(CSS_PATH, "utf-8");
|
|
1044
|
+
} catch {
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
let generatedCss = "";
|
|
1048
|
+
try {
|
|
1049
|
+
generatedCss = import_fs3.default.readFileSync(GENERATED_CSS_PATH, "utf-8");
|
|
1050
|
+
} catch {
|
|
1051
|
+
}
|
|
1052
|
+
const componentSources = listComponentSourcePaths().map((p) => ({
|
|
1053
|
+
name: componentNameFromFile(p),
|
|
1054
|
+
source: import_fs3.default.readFileSync(p, "utf-8")
|
|
1055
|
+
}));
|
|
1056
|
+
const missing = validateTokensCss({ tokensCss, generatedCss, componentSources });
|
|
1057
|
+
if (missing.length === 0) return;
|
|
1058
|
+
const lines = missing.map(
|
|
1059
|
+
(m) => ` ${m.token} (referenced by ${m.referencedBy.join(", ")})`
|
|
1060
|
+
);
|
|
1061
|
+
log(
|
|
1062
|
+
`[live-tokens] ${missing.length} token(s) referenced by components are not defined in ${import_path3.default.relative(process.cwd(), CSS_PATH)}:
|
|
1063
|
+
${lines.join("\n")}
|
|
1064
|
+
These render as blank/empty editor slots. Run \`npx live-tokens migrate\` to reconcile.`
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
874
1067
|
function generateDefaultConfig(comp, sourcePath) {
|
|
875
1068
|
if (!import_fs3.default.existsSync(sourcePath)) return;
|
|
876
1069
|
const r = componentResource(comp);
|
|
@@ -1696,6 +1889,7 @@ function themeFileApi(opts) {
|
|
|
1696
1889
|
ensureComponentConfigsDir();
|
|
1697
1890
|
ensureManifestsDir();
|
|
1698
1891
|
regenerateTokensCss();
|
|
1892
|
+
warnOnTokenDrift((msg) => server.config.logger.warn(msg));
|
|
1699
1893
|
server.middlewares.use(async (req, res, next) => {
|
|
1700
1894
|
const handled = await dispatch(req, res, routes);
|
|
1701
1895
|
if (!handled) next();
|
|
@@ -1713,5 +1907,8 @@ function themeFileApi(opts) {
|
|
|
1713
1907
|
}
|
|
1714
1908
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1715
1909
|
0 && (module.exports = {
|
|
1716
|
-
|
|
1910
|
+
TOKENS_CSS_MIGRATIONS,
|
|
1911
|
+
runTokensCssMigrations,
|
|
1912
|
+
themeFileApi,
|
|
1913
|
+
validateTokensCss
|
|
1717
1914
|
});
|
package/dist-plugin/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
|
+
export { ComponentSource, MissingToken, RunResult, TOKENS_CSS_MIGRATIONS, TokensCssMigration, ValidateInput, runTokensCssMigrations, validateTokensCss } from './tokensCssMigrations/index.cjs';
|
|
2
3
|
|
|
3
4
|
interface ThemeFileApiOptions {
|
|
4
5
|
tokensCssPath: string;
|
package/dist-plugin/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
|
+
export { ComponentSource, MissingToken, RunResult, TOKENS_CSS_MIGRATIONS, TokensCssMigration, ValidateInput, runTokensCssMigrations, validateTokensCss } from './tokensCssMigrations/index.js';
|
|
2
3
|
|
|
3
4
|
interface ThemeFileApiOptions {
|
|
4
5
|
tokensCssPath: string;
|