@motion-proto/live-tokens 0.33.1 → 0.35.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.
@@ -8,7 +8,7 @@ description: Apply the @motion-proto/live-tokens project conventions when buildi
8
8
  Two rules above all else:
9
9
 
10
10
  1. **Use a shipped component if one fits.** Import from `@motion-proto/live-tokens/components/<Name>.svelte`. See [[live-tokens-pick-component]] for the catalogue and the confusing-pair decisions. Author custom markup only when nothing fits, and then consider [[live-tokens-create-component]] so the new piece is editable too.
11
- 2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`. No hex literals. No pixel literals. A change in `/editor` should repaint your page.
11
+ 2. **Use theme tokens for every value.** Every color, spacing, radius, font-size, and font-family in page CSS is a `var(--token-*)`. No hex literals. No pixel literals. A change in `/live-tokens/editor` should repaint your page.
12
12
 
13
13
  ## Layout
14
14
 
@@ -28,10 +28,10 @@ To place children at specific page-column positions, span the parent grid (`grid
28
28
 
29
29
  - Hex or pixel literals in page CSS.
30
30
  - Hardcoded column counts (`repeat(10, 1fr)`). Use `repeat(var(--columns-count), 1fr)`.
31
- - Utility classes overriding shipped components. Extend via the `/components` editor instead.
31
+ - Utility classes overriding shipped components. Extend via the `/live-tokens/components` editor instead.
32
32
  - Deep imports from `node_modules/@motion-proto/live-tokens/src/...`. Use public entry points only.
33
33
  - Mounting `Editor` or `ComponentEditorPage` outside their dedicated routes.
34
34
 
35
35
  ## Verify
36
36
 
37
- In dev: change a colour in `/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the route's `source`). `ColumnsOverlay` (Cmd+G) shows content sitting inside `--columns-max-width`.
37
+ In dev: change a colour in `/live-tokens/editor` and confirm your page repaints (proves token usage). The overlay's "Page Source" button on the new route opens the page in VS Code (proves the route's `source`). `ColumnsOverlay` (Cmd+G) shows content sitting inside `--columns-max-width`.
@@ -5,7 +5,7 @@ description: Author a brand-new editable component for a @motion-proto/live-toke
5
5
 
6
6
  # Authoring a component for a live-tokens project
7
7
 
8
- This skill teaches you how to add a new editable component to a project that consumes `@motion-proto/live-tokens`. The end state: a runtime Svelte file, an editor Svelte file, one `registerComponent()` call, and a `/components` page entry under the **CUSTOM** group with full token editing, linked-block sharing, and persistence.
8
+ This skill teaches you how to add a new editable component to a project that consumes `@motion-proto/live-tokens`. The end state: a runtime Svelte file, an editor Svelte file, one `registerComponent()` call, and a `/live-tokens/components` page entry under the **CUSTOM** group with full token editing, linked-block sharing, and persistence.
9
9
 
10
10
  ## Worked examples ship inside the package
11
11
 
@@ -44,7 +44,7 @@ For pattern reference, read any shipped component's source directly from the con
44
44
  ```
45
45
  The schema side-effect happens inside `registerComponent` (which `bootLiveTokens` calls for you), so you don't call `registerComponentSchema` separately. **Do not place a standalone `registerComponent(...)` *before* `bootLiveTokens`** — that registers before the editor's init hooks run, which is the wrong window and can leave editor changes disconnected from the live page. Only call `registerComponent` directly if your app mounts manually (no `bootLiveTokens`), in which case call it before `mount(App, ...)`.
46
46
  4. **Tell the picker** — open `.claude/skills/live-tokens-pick-component/SKILL.md` and add your new component to the **Catalogue** line under the family it belongs to (Action / Input / Selection / Containers / Messaging / Display). If it's confusable with an existing component (a second selection control, a competing container), add a row to that family's decision table explaining the use-case it owns. Without this step, the component exists but [[live-tokens-pick-component]] can't recommend it when a user asks "which component should I use?" — the same rule applies whether the component is first-party (update the picker shipped in this package) or consumer-authored (update the local copy at `.claude/skills/live-tokens-pick-component/SKILL.md` that `setup-claude` placed in your project).
47
- 5. **Verify** — open `/components` and run the verification checklist at the bottom of this file.
47
+ 5. **Verify** — open `/live-tokens/components` and run the verification checklist at the bottom of this file.
48
48
 
49
49
  ## Token discipline
50
50
 
@@ -559,7 +559,7 @@ A new first-party component is auto-covered the moment it lands in `builtInRegis
559
559
 
560
560
  **If your component declares `intrinsics`, the intrinsics contract test covers it too.** `src/editor/component-editor/intrinsicsContract.test.ts` iterates every entry with an `intrinsics` array and asserts, per (intrinsic, variant), that the runtime `:global(:root)` declares a default, the default is one of the spec's `values`, and the editor's `default` equals the runtime default. This is what would have caught a getter defaulting to `center` while `:global(:root)` says `start`. Same auto-coverage rule: declare `intrinsics` on the registry entry and the test picks it up.
561
561
 
562
- Finally navigate to `/components` and confirm the runtime behaviours no static check can see:
562
+ Finally navigate to `/live-tokens/components` and confirm the runtime behaviours no static check can see:
563
563
 
564
564
  - [ ] The new component appears in the nav rail under the **CUSTOM** group (system entries above, custom below the labeled divider).
565
565
  - [ ] Token rows render. Color pickers, radius selectors, font selectors all work.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.0 — Dev routes moved to a reserved `/live-tokens/*` namespace
4
+
5
+ ### Changed (breaking)
6
+
7
+ - **The package's dev-only routes moved under a reserved `/live-tokens/*`
8
+ namespace:** `/editor` → `/live-tokens/editor`, `/components` →
9
+ `/live-tokens/components`, `/docs` → `/live-tokens/docs`. These routes are
10
+ `import.meta.env.DEV`-only and never appear in production, so the longer paths
11
+ cost nothing where users actually see URLs. Reserving a namespace means a
12
+ consumer's own `/docs` or `/components` page no longer collides with a package
13
+ route, and any owned route added in a future release stays inside the namespace
14
+ (no surprise collisions on a version bump). Relocate or disable any of them via
15
+ the `editorRoutes` prop exactly as before.
16
+
17
+ ### Fixed
18
+
19
+ - **A consumer page at `/docs` or `/components` no longer crashes the app.** The
20
+ package auto-injected nav entries at those paths; a consumer page at the same
21
+ path produced a duplicate key in the overlay's keyed nav list and threw an
22
+ uncaught error, and silently shadowed the consumer's page at dispatch. With the
23
+ reserved namespace the collision cannot occur, so `editorRoutes.docs = false`
24
+ is no longer needed to dodge it.
25
+ - **`editorRoutes.components` relocation now also moves the overlay's
26
+ components-view pairing**, which previously compared a hardcoded `/components`.
27
+
28
+ ### Migrating
29
+
30
+ - **`npx live-tokens migrate` now flags hardcoded route references.** If your
31
+ source navigates to the old paths (e.g. `navigate('/editor')` from the old
32
+ scaffold, or `<a href="/components">`), `migrate` reports each one with its
33
+ file, line, and suggested `/live-tokens/*` replacement. Add `--write` to
34
+ rewrite the unambiguous ones automatically. `/docs` is never auto-rewritten
35
+ (you likely own that route), and any path you declare in `pages` or relocate
36
+ via `editorRoutes` is left for manual review. The editor stays reachable via
37
+ the dev overlay regardless, so this only affects hardcoded shortcut links.
38
+
39
+ ## 0.34.0 — Token-as-API contract guardrail; opt-in autoMigrate
40
+
41
+ ### Added
42
+
43
+ - **Token names are now a versioned API contract, with a guardrail.** Each
44
+ `tokens.css` migration declares `kind: 'additive' | 'breaking'`. A new
45
+ `check:token-contract` (wired into `prepublishOnly`, plus
46
+ `tokensCssMigrations/contract.test.ts`) verifies behaviorally that an additive
47
+ migration never removes or renames a token (catches a breaking change shipped
48
+ as backward-compatible), and gates breaking migrations on a major bump from
49
+ 1.0.0 (pre-1.0 it warns). See TOKENS.md and RELEASING.md.
50
+ - **`themeFileApi({ autoMigrate: true })`.** Opt-in: the dev server applies
51
+ pending **additive** token migrations to your `tokens.css` at startup and
52
+ writes the file (shown in git), so it stays current with the package without a
53
+ manual step. Breaking migrations are never auto-applied. Off by default, which
54
+ preserves the invariant that the plugin never writes `tokensCssPath` unless you
55
+ enable it.
56
+
57
+ ### Docs
58
+
59
+ - **`TOKENS.md`** gained a plain-language section on how token changes are
60
+ versioned (additive vs breaking, what an upgrade can and cannot change).
61
+
3
62
  ## 0.33.1 — Ship the changelog in the package
4
63
 
5
64
  ### Fixed
package/README.md CHANGED
@@ -8,8 +8,8 @@ A foundational design system for quickly styling and building Svelte + Vite micr
8
8
 
9
9
  - **Real-time token editing.** Pick a color, drag a hue slider, retype a font size — the page repaints on every input event via CSS-variable writes. No reload, no save-and-refresh, no build step. Works across colors, typography, spacing, radii, shadows, motion, palettes, and gradients.
10
10
  - **Real-time component editing.** Each of ~24 shipped Svelte components (Button, Input, Card, Dialog, Badge, Callout, Table, Tooltip, Toggle, TabBar, SegmentedControl, RadioButton, MenuSelect, ProgressBar, CornerBadge, SectionDivider, CollapsibleSection, Notification, Image, ImageLightbox, CodeSnippet, SideNavigation, and more) declares its own design-token aliases in a `:global(:root)` block. Rewire any alias from a per-component picker and see that component update everywhere it's used — live, on your real pages, not in a Storybook sandbox.
11
- - **Theme editor** (`/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a static `tokens.css` for the build.
12
- - **Per-component editor** (`/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
11
+ - **Theme editor** (`/live-tokens/editor` route, dev-only) — the home of real-time token editing. Save themes to disk as JSON, promote one to "production" to bake it into a static `tokens.css` for the build.
12
+ - **Per-component editor** (`/live-tokens/components` route, dev-only) — the home of real-time component-alias editing. Pick token aliases per component without writing CSS.
13
13
  - **Live editor overlay** — pins to the top-right of every dev page. Opens the editor in a side panel or floating window so you edit *on the page you're styling*, not in a separate tab. Includes a "Page Source" button that opens the current page's `.svelte` file in VS Code.
14
14
  - **Manifests** — a manifest captures a whole site configuration as one portable artifact: the theme in one slot, every component in its own slot, each holding either the shipped default or a custom file. Export it as a bundle and import it into another project to restore the full styling in one step.
15
15
  - **Vite plugin** — hosts the `/api/live-tokens/{themes,component-configs,manifests}/*` routes that persist your edits to disk as you make them. The single namespace keeps live-tokens' routes from colliding with anything your app serves under `/api`.
@@ -122,7 +122,7 @@ bootLiveTokens(App, '#app', {
122
122
  ```
123
123
 
124
124
  `<LiveTokensRouter>` owns the dev overlay (`<LiveEditorOverlay>` +
125
- `<ColumnsOverlay>`), the editor routes (`/editor`, `/components`, `/docs`), the
125
+ `<ColumnsOverlay>`), the editor routes (`/live-tokens/editor`, `/live-tokens/components`, `/live-tokens/docs`), the
126
126
  in-app link-click interception, and the nav-rail/page-source plumbing the
127
127
  overlay needs. Each entry in `pages` is one of your routes; entries with a
128
128
  `label` appear in the overlay's nav rail. Pass pages as `lazy: () => import('./Page.svelte')`
@@ -307,7 +307,7 @@ bootLiveTokens(App, '#app', {
307
307
 
308
308
  (`bootLiveTokens` calls `registerComponent` internally for each entry, gated on `import.meta.env.DEV` so the registration tree-shakes out of production builds. Call `registerComponent` directly if you need finer control over timing.)
309
309
 
310
- The component appears in the `/components` page under a **CUSTOM** group in the nav rail. Token rows, linked-block sharing, per-component config persistence, and reset-to-default work identically to the built-in set. All imports must come from `@motion-proto/live-tokens` or `@motion-proto/live-tokens/component-editor`; never deep-import from `src/`.
310
+ The component appears in the `/live-tokens/components` page under a **CUSTOM** group in the nav rail. Token rows, linked-block sharing, per-component config persistence, and reset-to-default work identically to the built-in set. All imports must come from `@motion-proto/live-tokens` or `@motion-proto/live-tokens/component-editor`; never deep-import from `src/`.
311
311
 
312
312
  ## Claude Code skills
313
313
 
@@ -345,13 +345,13 @@ It enforces the file layout, `:global(:root)` block, token-suffix vocabulary, th
345
345
 
346
346
  ## How the editor ships changes to prod
347
347
 
348
- 1. Edit in `/editor` or `/components`. Saves write to `<dataDir>/themes/{name}.json` and `<dataDir>/component-configs/{comp}/{name}.json`.
348
+ 1. Edit in `/live-tokens/editor` or `/live-tokens/components`. Saves write to `<dataDir>/themes/{name}.json` and `<dataDir>/component-configs/{comp}/{name}.json`.
349
349
  2. Promote a theme to "production." Its variables are written into `tokens.generated.css` next to your authored `tokens.css`.
350
350
  3. `npm run build` bundles both as plain CSS. No editor code, no JSON lookups, no dev surfaces ship to prod.
351
351
 
352
352
  ## File ownership — what the plugin writes
353
353
 
354
- Knowing which files the plugin touches matters when upgrading the package or working in a repo you don't want overwritten.
354
+ Knowing which files the plugin touches matters when upgrading the package or working in a repo you don't want overwritten. For a plain-language version of how your saved look stays safe across upgrades while `tokens.css` holds the building blocks, see [TOKENS.md](./TOKENS.md).
355
355
 
356
356
  **On `npm install` or `npm update`: nothing outside `node_modules/`.** No install hooks. Upgrading versions never touches your `src/live-tokens/data/`, or any file in `src/` outside it.
357
357
 
@@ -378,6 +378,8 @@ It never writes to your project root, your `src/` outside the data folder, or an
378
378
 
379
379
  The developer-authored `tokens.css` itself is **never written** by the plugin — it holds defaults you're free to hand-edit. The editor's overrides land in the sidecar `tokens.generated.css`, which the package imports immediately after `tokens.css`.
380
380
 
381
+ The one exception is the opt-in `themeFileApi({ autoMigrate: true })` option. When enabled, the dev server applies pending **additive** token migrations (new token names only) to your `tokens.css` at startup and writes the file, so it stays current with the package as you upgrade. The change shows up in git for review. Breaking migrations (rename/remove) are never auto-applied; run `npx live-tokens migrate` for those during a deliberate upgrade. Off by default, so the "never written" rule holds unless you turn it on. See [TOKENS.md](./TOKENS.md).
382
+
381
383
  ## License
382
384
 
383
385
  MIT. Originally extracted from [RuneGoblin](https://www.runegoblin.com/).
package/bin/cli.mjs CHANGED
@@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
11
11
  import process from 'node:process';
12
12
  import { checkComponent, formatReport } from './check-component.mjs';
13
13
  import { runMigrate, formatMigrateResult } from './migrate.mjs';
14
+ import { runMigrateRoutes, formatRouteResult } from './migrate-routes.mjs';
14
15
  import { runCreate, formatCreateResult } from './create.mjs';
15
16
 
16
17
  const USAGE = `Usage: npx @motion-proto/live-tokens <command> [options]
@@ -21,10 +22,15 @@ Commands:
21
22
  setup-claude [--force] Install bundled Claude Code skills into ./.claude/skills/
22
23
  check-component <id> Validate <id>'s runtime, editor, and registration
23
24
  against the live-tokens-create-component contract
24
- migrate [--check] [--tokens <path>]
25
- Reconcile your tokens.css with the installed
26
- package's Layer-1 token vocabulary. --check reports
27
- without writing (exit 1 if changes are needed).
25
+ migrate [--check] [--write] [--tokens <path>]
26
+ Reconcile your project with the installed package:
27
+ applies additive tokens.css migrations (unless
28
+ --check), and reports source references to the
29
+ editor/components/docs routes that moved to
30
+ /live-tokens/* in 0.35.0. --write also rewrites the
31
+ unambiguous route references (never /docs). --check
32
+ reports without writing (exit 1 if token migrations
33
+ are pending; route findings are advisory).
28
34
  `;
29
35
 
30
36
  function fail(message, code = 1) {
@@ -67,13 +73,21 @@ if (command === 'check-component') {
67
73
 
68
74
  if (command === 'migrate') {
69
75
  const check = rest.includes('--check');
76
+ const write = rest.includes('--write');
70
77
  const tokensIdx = rest.indexOf('--tokens');
71
78
  const tokensArg = tokensIdx !== -1 ? rest[tokensIdx + 1] : undefined;
72
79
  if (tokensIdx !== -1 && !tokensArg) fail(`--tokens requires a path`);
73
80
  try {
74
81
  const result = await runMigrate({ tokensArg, check });
75
82
  console.log(formatMigrateResult(result, { check }));
76
- // Exit 1 when --check finds pending changes (CI-friendly) or on no-path.
83
+
84
+ // Route-reference pass: advisory by default, rewrites the unambiguous hits
85
+ // only with --write (and never under --check).
86
+ const routes = runMigrateRoutes({ root: process.cwd(), apply: write && !check });
87
+ const routeOut = formatRouteResult(routes, { check });
88
+ if (routeOut) console.log('\n' + routeOut);
89
+
90
+ // Route findings are advisory; only token migrations gate the exit code.
77
91
  if (result.status === 'no-path') process.exit(1);
78
92
  if (check && result.status === 'would-change') process.exit(1);
79
93
  process.exit(0);
@@ -0,0 +1,179 @@
1
+ // `live-tokens migrate` route-reference pass (0.35.0 namespace move).
2
+ //
3
+ // The package's dev-only routes moved to a reserved `/live-tokens/*` namespace,
4
+ // so consumer source that hardcoded `/editor`, `/components`, or `/docs` (e.g.
5
+ // `navigate('/editor')` from the old scaffold) now 404s. This scans the
6
+ // consumer's source and reports those references; with `apply`, it rewrites the
7
+ // unambiguous ones.
8
+ //
9
+ // Safety: `/docs` is never auto-rewritten (consumers commonly own a docs page),
10
+ // and any path the project declares as its own (a `pages` key) or manages via
11
+ // `editorRoutes` is left as an advisory. A reference is rewritten only when the
12
+ // package definitely owns that path.
13
+
14
+ import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
15
+ import { extname, join, relative } from 'node:path';
16
+
17
+ const MOVED = [
18
+ { old: '/editor', next: '/live-tokens/editor' },
19
+ { old: '/components', next: '/live-tokens/components' },
20
+ { old: '/docs', next: '/live-tokens/docs' },
21
+ ];
22
+
23
+ // Never auto-rewritten: a consumer's own docs page is the common case, and we
24
+ // can't tell their `/docs` from the package's without guessing.
25
+ const NEVER_AUTOWRITE = new Set(['/docs']);
26
+
27
+ const SCAN_EXTS = new Set(['.svelte', '.ts', '.js', '.mjs', '.tsx', '.jsx']);
28
+ const SKIP_DIRS = new Set(['node_modules', 'dist', 'dist-plugin', '.git', '.svelte-kit', 'build']);
29
+
30
+ function walk(dir, out = []) {
31
+ let entries;
32
+ try {
33
+ entries = readdirSync(dir, { withFileTypes: true });
34
+ } catch {
35
+ return out;
36
+ }
37
+ for (const e of entries) {
38
+ if (e.isDirectory()) {
39
+ if (!SKIP_DIRS.has(e.name)) walk(join(dir, e.name), out);
40
+ } else if (SCAN_EXTS.has(extname(e.name))) {
41
+ out.push(join(dir, e.name));
42
+ }
43
+ }
44
+ return out;
45
+ }
46
+
47
+ // `navigate('/p')` / `navigate("/p")` / `navigate(`/p`)` and the href forms
48
+ // `href="/p"` / `href='/p'` / `href={'/p'}`. The closing backreference quote
49
+ // pins the path to the exact string, so `/live-tokens/editor` never matches
50
+ // `/editor` (the char before `/editor` there is `s`, not a quote).
51
+ function refRegex(oldPath) {
52
+ return new RegExp(`(navigate\\(\\s*|href\\s*=\\s*\\{?\\s*)(['"\`])${oldPath}\\2`, 'g');
53
+ }
54
+
55
+ function kindOf(prefix) {
56
+ return prefix.trimStart().startsWith('href') ? 'href' : 'navigate';
57
+ }
58
+
59
+ /**
60
+ * A path is the consumer's, not the package's, if they declare a page at it
61
+ * (`'/x':` in a pages map) or name it in an `editorRoutes` override. Conservative
62
+ * by design: when unsure we mark it owned, which only ever downgrades a rewrite
63
+ * to an advisory.
64
+ */
65
+ function detectConsumerOwned(files) {
66
+ const owned = new Set();
67
+ for (const file of files) {
68
+ const c = readFileSync(file, 'utf8');
69
+ const hasEditorRoutes = /editorRoutes/.test(c);
70
+ for (const { old } of MOVED) {
71
+ const key = old.slice(1);
72
+ if (new RegExp(`['"]\\/${key}['"]\\s*:`).test(c)) owned.add(old);
73
+ if (hasEditorRoutes && new RegExp(`\\b${key}\\s*:`).test(c)) owned.add(old);
74
+ }
75
+ }
76
+ return owned;
77
+ }
78
+
79
+ function scanContent(content) {
80
+ const lines = content.split('\n');
81
+ const found = [];
82
+ for (const { old, next } of MOVED) {
83
+ const re = refRegex(old);
84
+ lines.forEach((text, i) => {
85
+ re.lastIndex = 0;
86
+ let m;
87
+ while ((m = re.exec(text))) {
88
+ found.push({ line: i + 1, kind: kindOf(m[1]), old, next });
89
+ }
90
+ });
91
+ }
92
+ return found;
93
+ }
94
+
95
+ function applyRewrites(content, paths) {
96
+ let out = content;
97
+ for (const { old, next } of paths) {
98
+ out = out.replace(refRegex(old), (_full, prefix, quote) => `${prefix}${quote}${next}${quote}`);
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * Scan (and with `apply`, rewrite) source under `root`. Returns
105
+ * `{ scannedFiles, rewritten, pendingWrite, advisory, owned }` — `rewritten` is
106
+ * populated only when `apply` is true; otherwise auto-writable hits land in
107
+ * `pendingWrite`. `advisory` always holds the hits we won't touch automatically.
108
+ */
109
+ export function runMigrateRoutes({ root = process.cwd(), apply = false } = {}) {
110
+ const srcDir = join(root, 'src');
111
+ const base = existsSync(srcDir) ? srcDir : root;
112
+ const files = walk(base);
113
+ const owned = detectConsumerOwned(files);
114
+
115
+ const rewritten = [];
116
+ const pendingWrite = [];
117
+ const advisory = [];
118
+
119
+ for (const file of files) {
120
+ const content = readFileSync(file, 'utf8');
121
+ const rel = relative(root, file);
122
+ const hits = scanContent(content).map((h) => ({
123
+ ...h,
124
+ file: rel,
125
+ autoWrite: !NEVER_AUTOWRITE.has(h.old) && !owned.has(h.old),
126
+ reason: NEVER_AUTOWRITE.has(h.old) ? 'docs-never' : owned.has(h.old) ? 'consumer-owned' : null,
127
+ }));
128
+ if (hits.length === 0) continue;
129
+
130
+ const auto = hits.filter((h) => h.autoWrite);
131
+ advisory.push(...hits.filter((h) => !h.autoWrite));
132
+
133
+ if (apply && auto.length) {
134
+ const paths = MOVED.filter((m) => auto.some((h) => h.old === m.old));
135
+ const next = applyRewrites(content, paths);
136
+ if (next !== content) {
137
+ writeFileSync(file, next);
138
+ rewritten.push(...auto);
139
+ }
140
+ } else {
141
+ pendingWrite.push(...auto);
142
+ }
143
+ }
144
+
145
+ return { scannedFiles: files.length, rewritten, pendingWrite, advisory, owned: [...owned] };
146
+ }
147
+
148
+ export function formatRouteResult(result, { check = false } = {}) {
149
+ const { rewritten, pendingWrite, advisory } = result;
150
+ if (!rewritten.length && !pendingWrite.length && !advisory.length) return '';
151
+
152
+ const ref = (h) => ` ${h.file}:${h.line} ${h.kind} '${h.old}' → '${h.next}'`;
153
+ const lines = ['Route references — /editor, /components, /docs moved to /live-tokens/* in 0.35.0:'];
154
+
155
+ if (rewritten.length) {
156
+ lines.push(` ✓ Rewrote ${rewritten.length} reference(s):`);
157
+ rewritten.forEach((h) => lines.push(ref(h)));
158
+ }
159
+ if (pendingWrite.length) {
160
+ lines.push(
161
+ check
162
+ ? ` Would rewrite ${pendingWrite.length} reference(s) with --write:`
163
+ : ` ${pendingWrite.length} reference(s) can be rewritten — re-run with --write to apply:`,
164
+ );
165
+ pendingWrite.forEach((h) => lines.push(ref(h)));
166
+ }
167
+ if (advisory.length) {
168
+ lines.push(` ⚠ ${advisory.length} reference(s) need manual review:`);
169
+ advisory.forEach((h) => {
170
+ const why =
171
+ h.reason === 'docs-never'
172
+ ? `/docs is never auto-rewritten — update to '${h.next}' only if it points at the package guide`
173
+ : `you declare or relocate '${h.old}' yourself — leave it if it's your route`;
174
+ lines.push(` ${h.file}:${h.line} ${h.kind} '${h.old}' (${why})`);
175
+ });
176
+ }
177
+ if (rewritten.length) lines.push('\n Review the diff in git before committing.');
178
+ return lines.join('\n');
179
+ }
@@ -106,6 +106,7 @@ function escapeRe(s) {
106
106
  // vite-plugin/tokensCssMigrations/migrations/2026-05-29-typography-scale-additions.ts
107
107
  var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
108
108
  id: "2026-05-29-typography-scale-additions",
109
+ kind: "additive",
109
110
  description: "Add --line-height-{xs..xl}, --letter-spacing-* and --ease-out-quart scales",
110
111
  apply(css) {
111
112
  let out = css;
@@ -144,6 +145,7 @@ var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
144
145
  var KEEP_SEGMENTS = /* @__PURE__ */ new Set(["lg", "md", "sm"]);
145
146
  var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
146
147
  id: "2026-05-29-sectiondivider-legacy-axis-cleanup",
148
+ kind: "breaking",
147
149
  description: "Remove legacy --sectiondivider-* tokens not on the lg/md/sm axis",
148
150
  apply(css) {
149
151
  return removeTokensMatching(css, (name) => {
@@ -157,6 +159,7 @@ var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
157
159
  // vite-plugin/tokensCssMigrations/migrations/2026-06-03-transform-scale-additions.ts
158
160
  var tokensCssMigration_2026_06_03_transformScaleAdditions = {
159
161
  id: "2026-06-03-transform-scale-additions",
162
+ kind: "additive",
160
163
  description: "Add the --scale-{sm..2xl} transform-multiplier scale",
161
164
  apply(css) {
162
165
  return ensureScale(css, {
@@ -176,6 +179,7 @@ var tokensCssMigration_2026_06_03_transformScaleAdditions = {
176
179
  // vite-plugin/tokensCssMigrations/migrations/2026-06-04-remove-dead-size-icon-scale.ts
177
180
  var tokensCssMigration_2026_06_04_removeDeadSizeIconScale = {
178
181
  id: "2026-06-04-remove-dead-size-icon-scale",
182
+ kind: "breaking",
179
183
  description: "Remove the unused --size-icon-* scale (live scale is --icon-size-*)",
180
184
  apply(css) {
181
185
  return removeTokensMatching(css, (name) => name.startsWith("--size-icon-"));
@@ -185,6 +189,7 @@ var tokensCssMigration_2026_06_04_removeDeadSizeIconScale = {
185
189
  // vite-plugin/tokensCssMigrations/migrations/2026-06-04-easing-color-and-typescale-additions.ts
186
190
  var tokensCssMigration_2026_06_04_easingColorAndTypescaleAdditions = {
187
191
  id: "2026-06-04-easing-color-and-typescale-additions",
192
+ kind: "additive",
188
193
  description: "Add the full --ease-* scale, --color-white/black, and --font-size-7xl",
189
194
  apply(css) {
190
195
  let out = css;
@@ -313,9 +318,16 @@ var TOKENS_CSS_MIGRATIONS = [
313
318
  tokensCssMigration_2026_06_04_easingColorAndTypescaleAdditions
314
319
  ];
315
320
  function runTokensCssMigrations(css) {
321
+ return foldMigrations(css, () => true);
322
+ }
323
+ function runAdditiveTokensCssMigrations(css) {
324
+ return foldMigrations(css, (m) => m.kind === "additive");
325
+ }
326
+ function foldMigrations(css, include) {
316
327
  let out = css;
317
328
  const applied = [];
318
329
  for (const m of TOKENS_CSS_MIGRATIONS) {
330
+ if (!include(m)) continue;
319
331
  const next = m.apply(out);
320
332
  if (next !== out) {
321
333
  applied.push(m.id);
@@ -324,6 +336,45 @@ function runTokensCssMigrations(css) {
324
336
  }
325
337
  return { css: out, applied, changed: out !== css };
326
338
  }
339
+ function findContractViolations(canonicalCss) {
340
+ const violations = [];
341
+ for (const m of TOKENS_CSS_MIGRATIONS) {
342
+ if (m.kind !== "additive") continue;
343
+ const before = collectDefinedTokens(canonicalCss);
344
+ const after = collectDefinedTokens(m.apply(canonicalCss));
345
+ const removed = [...before].filter((t) => !after.has(t)).sort();
346
+ if (removed.length) violations.push({ id: m.id, removed });
347
+ }
348
+ return violations;
349
+ }
350
+ function semverBumpType(prev, next) {
351
+ const p = parseSemver(prev);
352
+ const n = parseSemver(next);
353
+ if (n.major > p.major) return "major";
354
+ if (n.major === p.major && n.minor > p.minor) return "minor";
355
+ if (n.major === p.major && n.minor === p.minor && n.patch > p.patch) return "patch";
356
+ return "none";
357
+ }
358
+ function parseSemver(v) {
359
+ const [core] = v.replace(/^v/, "").split(/[-+]/);
360
+ const [major = 0, minor = 0, patch = 0] = core.split(".").map((n) => Number(n) || 0);
361
+ return { major, minor, patch };
362
+ }
363
+ function enforceBreakingRequiresMajor(args) {
364
+ const breakingIds = args.newMigrations.filter((m) => m.kind === "breaking").map((m) => m.id);
365
+ const bump = semverBumpType(args.prevVersion, args.nextVersion);
366
+ if (breakingIds.length === 0 || bump === "major") {
367
+ return { level: "ok", breakingIds, bump, message: "" };
368
+ }
369
+ const pre1 = parseSemver(args.nextVersion).major < 1;
370
+ const ids = breakingIds.join(", ");
371
+ return {
372
+ level: pre1 ? "warn" : "error",
373
+ breakingIds,
374
+ bump,
375
+ message: `Breaking token migration(s) [${ids}] are shipping in a ${bump} bump (${args.prevVersion} -> ${args.nextVersion}). Token names are public API; ` + (pre1 ? `pre-1.0 this is allowed, but the CHANGELOG must flag it under "Changed (breaking)".` : `from 1.0.0 a breaking token change requires a major bump.`)
376
+ };
377
+ }
327
378
  function validateTokensCss(input) {
328
379
  const defined = /* @__PURE__ */ new Set([
329
380
  ...collectDefinedTokens(input.tokensCss),
@@ -361,5 +412,9 @@ export {
361
412
  removeTokensMatching,
362
413
  TOKENS_CSS_MIGRATIONS,
363
414
  runTokensCssMigrations,
415
+ runAdditiveTokensCssMigrations,
416
+ findContractViolations,
417
+ semverBumpType,
418
+ enforceBreakingRequiresMajor,
364
419
  validateTokensCss
365
420
  };
@@ -730,6 +730,7 @@ function findTopLevelRoot(lines) {
730
730
  // vite-plugin/tokensCssMigrations/migrations/2026-05-29-typography-scale-additions.ts
731
731
  var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
732
732
  id: "2026-05-29-typography-scale-additions",
733
+ kind: "additive",
733
734
  description: "Add --line-height-{xs..xl}, --letter-spacing-* and --ease-out-quart scales",
734
735
  apply(css) {
735
736
  let out = css;
@@ -768,6 +769,7 @@ var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
768
769
  var KEEP_SEGMENTS = /* @__PURE__ */ new Set(["lg", "md", "sm"]);
769
770
  var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
770
771
  id: "2026-05-29-sectiondivider-legacy-axis-cleanup",
772
+ kind: "breaking",
771
773
  description: "Remove legacy --sectiondivider-* tokens not on the lg/md/sm axis",
772
774
  apply(css) {
773
775
  return removeTokensMatching(css, (name) => {
@@ -781,6 +783,7 @@ var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
781
783
  // vite-plugin/tokensCssMigrations/migrations/2026-06-03-transform-scale-additions.ts
782
784
  var tokensCssMigration_2026_06_03_transformScaleAdditions = {
783
785
  id: "2026-06-03-transform-scale-additions",
786
+ kind: "additive",
784
787
  description: "Add the --scale-{sm..2xl} transform-multiplier scale",
785
788
  apply(css) {
786
789
  return ensureScale(css, {
@@ -800,6 +803,7 @@ var tokensCssMigration_2026_06_03_transformScaleAdditions = {
800
803
  // vite-plugin/tokensCssMigrations/migrations/2026-06-04-remove-dead-size-icon-scale.ts
801
804
  var tokensCssMigration_2026_06_04_removeDeadSizeIconScale = {
802
805
  id: "2026-06-04-remove-dead-size-icon-scale",
806
+ kind: "breaking",
803
807
  description: "Remove the unused --size-icon-* scale (live scale is --icon-size-*)",
804
808
  apply(css) {
805
809
  return removeTokensMatching(css, (name) => name.startsWith("--size-icon-"));
@@ -809,6 +813,7 @@ var tokensCssMigration_2026_06_04_removeDeadSizeIconScale = {
809
813
  // vite-plugin/tokensCssMigrations/migrations/2026-06-04-easing-color-and-typescale-additions.ts
810
814
  var tokensCssMigration_2026_06_04_easingColorAndTypescaleAdditions = {
811
815
  id: "2026-06-04-easing-color-and-typescale-additions",
816
+ kind: "additive",
812
817
  description: "Add the full --ease-* scale, --color-white/black, and --font-size-7xl",
813
818
  apply(css) {
814
819
  let out = css;
@@ -891,9 +896,16 @@ var TOKENS_CSS_MIGRATIONS = [
891
896
  tokensCssMigration_2026_06_04_easingColorAndTypescaleAdditions
892
897
  ];
893
898
  function runTokensCssMigrations(css) {
899
+ return foldMigrations(css, () => true);
900
+ }
901
+ function runAdditiveTokensCssMigrations(css) {
902
+ return foldMigrations(css, (m) => m.kind === "additive");
903
+ }
904
+ function foldMigrations(css, include) {
894
905
  let out = css;
895
906
  const applied = [];
896
907
  for (const m of TOKENS_CSS_MIGRATIONS) {
908
+ if (!include(m)) continue;
897
909
  const next = m.apply(out);
898
910
  if (next !== out) {
899
911
  applied.push(m.id);
@@ -1233,6 +1245,20 @@ ${lines.join("\n")}
1233
1245
  These render as blank/empty editor slots. Run \`npx live-tokens migrate\` to reconcile.`
1234
1246
  );
1235
1247
  }
1248
+ function autoMigrateAdditive(log) {
1249
+ let before = "";
1250
+ try {
1251
+ before = import_fs3.default.readFileSync(CSS_PATH, "utf-8");
1252
+ } catch {
1253
+ return;
1254
+ }
1255
+ const { css, applied, changed } = runAdditiveTokensCssMigrations(before);
1256
+ if (!changed) return;
1257
+ import_fs3.default.writeFileSync(CSS_PATH, css);
1258
+ log(
1259
+ `[live-tokens] autoMigrate applied ${applied.length} additive migration(s) to ${import_path3.default.relative(process.cwd(), CSS_PATH)}: ${applied.join(", ")}. Review the diff in git.`
1260
+ );
1261
+ }
1236
1262
  function generateDefaultConfig(comp, sourcePath) {
1237
1263
  if (!import_fs3.default.existsSync(sourcePath)) return;
1238
1264
  const r = componentResource(comp);
@@ -2030,6 +2056,7 @@ ${lines.join("\n")}
2030
2056
  ensureComponentConfigsDir();
2031
2057
  ensureManifestsDir();
2032
2058
  regenerateTokensCss();
2059
+ if (opts.autoMigrate) autoMigrateAdditive((msg) => server.config.logger.info(msg));
2033
2060
  warnOnTokenDrift((msg) => server.config.logger.warn(msg));
2034
2061
  server.middlewares.use(async (req, res, next) => {
2035
2062
  const handled = await dispatch(req, res, routes);
@@ -11,6 +11,7 @@ interface ThemeFileApiOptions {
11
11
  componentConfigsDir?: string;
12
12
  manifestsDir?: string;
13
13
  componentsSrcDir?: string;
14
+ autoMigrate?: boolean;
14
15
  }
15
16
  declare function themeFileApi(opts: ThemeFileApiOptions): Plugin;
16
17
 
@@ -11,6 +11,7 @@ interface ThemeFileApiOptions {
11
11
  componentConfigsDir?: string;
12
12
  manifestsDir?: string;
13
13
  componentsSrcDir?: string;
14
+ autoMigrate?: boolean;
14
15
  }
15
16
  declare function themeFileApi(opts: ThemeFileApiOptions): Plugin;
16
17
 
@@ -2,9 +2,10 @@ import {
2
2
  TOKENS_CSS_MIGRATIONS,
3
3
  extractGlobalRootBody,
4
4
  resolveDataDirs,
5
+ runAdditiveTokensCssMigrations,
5
6
  runTokensCssMigrations,
6
7
  validateTokensCss
7
- } from "./chunk-MJO4T3CM.js";
8
+ } from "./chunk-D77VD4Z6.js";
8
9
 
9
10
  // vite-plugin/themeFileApi.ts
10
11
  import fs2 from "fs";
@@ -866,6 +867,20 @@ ${lines.join("\n")}
866
867
  These render as blank/empty editor slots. Run \`npx live-tokens migrate\` to reconcile.`
867
868
  );
868
869
  }
870
+ function autoMigrateAdditive(log) {
871
+ let before = "";
872
+ try {
873
+ before = fs2.readFileSync(CSS_PATH, "utf-8");
874
+ } catch {
875
+ return;
876
+ }
877
+ const { css, applied, changed } = runAdditiveTokensCssMigrations(before);
878
+ if (!changed) return;
879
+ fs2.writeFileSync(CSS_PATH, css);
880
+ log(
881
+ `[live-tokens] autoMigrate applied ${applied.length} additive migration(s) to ${path2.relative(process.cwd(), CSS_PATH)}: ${applied.join(", ")}. Review the diff in git.`
882
+ );
883
+ }
869
884
  function generateDefaultConfig(comp, sourcePath) {
870
885
  if (!fs2.existsSync(sourcePath)) return;
871
886
  const r = componentResource(comp);
@@ -1663,6 +1678,7 @@ ${lines.join("\n")}
1663
1678
  ensureComponentConfigsDir();
1664
1679
  ensureManifestsDir();
1665
1680
  regenerateTokensCss();
1681
+ if (opts.autoMigrate) autoMigrateAdditive((msg) => server.config.logger.info(msg));
1666
1682
  warnOnTokenDrift((msg) => server.config.logger.warn(msg));
1667
1683
  server.middlewares.use(async (req, res, next) => {
1668
1684
  const handled = await dispatch(req, res, routes);