@motion-proto/live-tokens 0.33.1 → 0.34.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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.34.0 — Token-as-API contract guardrail; opt-in autoMigrate
4
+
5
+ ### Added
6
+
7
+ - **Token names are now a versioned API contract, with a guardrail.** Each
8
+ `tokens.css` migration declares `kind: 'additive' | 'breaking'`. A new
9
+ `check:token-contract` (wired into `prepublishOnly`, plus
10
+ `tokensCssMigrations/contract.test.ts`) verifies behaviorally that an additive
11
+ migration never removes or renames a token (catches a breaking change shipped
12
+ as backward-compatible), and gates breaking migrations on a major bump from
13
+ 1.0.0 (pre-1.0 it warns). See TOKENS.md and RELEASING.md.
14
+ - **`themeFileApi({ autoMigrate: true })`.** Opt-in: the dev server applies
15
+ pending **additive** token migrations to your `tokens.css` at startup and
16
+ writes the file (shown in git), so it stays current with the package without a
17
+ manual step. Breaking migrations are never auto-applied. Off by default, which
18
+ preserves the invariant that the plugin never writes `tokensCssPath` unless you
19
+ enable it.
20
+
21
+ ### Docs
22
+
23
+ - **`TOKENS.md`** gained a plain-language section on how token changes are
24
+ versioned (additive vs breaking, what an upgrade can and cannot change).
25
+
3
26
  ## 0.33.1 — Ship the changelog in the package
4
27
 
5
28
  ### Fixed
package/README.md CHANGED
@@ -351,7 +351,7 @@ It enforces the file layout, `:global(:root)` block, token-suffix vocabulary, th
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/).
@@ -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);
@@ -33,12 +33,16 @@ __export(tokensCssMigrations_exports, {
33
33
  TOKENS_CSS_MIGRATIONS: () => TOKENS_CSS_MIGRATIONS,
34
34
  collectDefinedTokens: () => collectDefinedTokens,
35
35
  collectReferencedTokens: () => collectReferencedTokens,
36
+ enforceBreakingRequiresMajor: () => enforceBreakingRequiresMajor,
36
37
  ensureScale: () => ensureScale,
38
+ findContractViolations: () => findContractViolations,
37
39
  readLiveTokensConfig: () => readLiveTokensConfig,
38
40
  removeToken: () => removeToken,
39
41
  removeTokensMatching: () => removeTokensMatching,
40
42
  renameToken: () => renameToken,
43
+ runAdditiveTokensCssMigrations: () => runAdditiveTokensCssMigrations,
41
44
  runTokensCssMigrations: () => runTokensCssMigrations,
45
+ semverBumpType: () => semverBumpType,
42
46
  validateTokensCss: () => validateTokensCss
43
47
  });
44
48
  module.exports = __toCommonJS(tokensCssMigrations_exports);
@@ -151,6 +155,7 @@ function escapeRe(s) {
151
155
  // vite-plugin/tokensCssMigrations/migrations/2026-05-29-typography-scale-additions.ts
152
156
  var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
153
157
  id: "2026-05-29-typography-scale-additions",
158
+ kind: "additive",
154
159
  description: "Add --line-height-{xs..xl}, --letter-spacing-* and --ease-out-quart scales",
155
160
  apply(css) {
156
161
  let out = css;
@@ -189,6 +194,7 @@ var tokensCssMigration_2026_05_29_typographyScaleAdditions = {
189
194
  var KEEP_SEGMENTS = /* @__PURE__ */ new Set(["lg", "md", "sm"]);
190
195
  var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
191
196
  id: "2026-05-29-sectiondivider-legacy-axis-cleanup",
197
+ kind: "breaking",
192
198
  description: "Remove legacy --sectiondivider-* tokens not on the lg/md/sm axis",
193
199
  apply(css) {
194
200
  return removeTokensMatching(css, (name) => {
@@ -202,6 +208,7 @@ var tokensCssMigration_2026_05_29_sectiondividerLegacyAxisCleanup = {
202
208
  // vite-plugin/tokensCssMigrations/migrations/2026-06-03-transform-scale-additions.ts
203
209
  var tokensCssMigration_2026_06_03_transformScaleAdditions = {
204
210
  id: "2026-06-03-transform-scale-additions",
211
+ kind: "additive",
205
212
  description: "Add the --scale-{sm..2xl} transform-multiplier scale",
206
213
  apply(css) {
207
214
  return ensureScale(css, {
@@ -221,6 +228,7 @@ var tokensCssMigration_2026_06_03_transformScaleAdditions = {
221
228
  // vite-plugin/tokensCssMigrations/migrations/2026-06-04-remove-dead-size-icon-scale.ts
222
229
  var tokensCssMigration_2026_06_04_removeDeadSizeIconScale = {
223
230
  id: "2026-06-04-remove-dead-size-icon-scale",
231
+ kind: "breaking",
224
232
  description: "Remove the unused --size-icon-* scale (live scale is --icon-size-*)",
225
233
  apply(css) {
226
234
  return removeTokensMatching(css, (name) => name.startsWith("--size-icon-"));
@@ -230,6 +238,7 @@ var tokensCssMigration_2026_06_04_removeDeadSizeIconScale = {
230
238
  // vite-plugin/tokensCssMigrations/migrations/2026-06-04-easing-color-and-typescale-additions.ts
231
239
  var tokensCssMigration_2026_06_04_easingColorAndTypescaleAdditions = {
232
240
  id: "2026-06-04-easing-color-and-typescale-additions",
241
+ kind: "additive",
233
242
  description: "Add the full --ease-* scale, --color-white/black, and --font-size-7xl",
234
243
  apply(css) {
235
244
  let out = css;
@@ -345,9 +354,16 @@ var TOKENS_CSS_MIGRATIONS = [
345
354
  tokensCssMigration_2026_06_04_easingColorAndTypescaleAdditions
346
355
  ];
347
356
  function runTokensCssMigrations(css) {
357
+ return foldMigrations(css, () => true);
358
+ }
359
+ function runAdditiveTokensCssMigrations(css) {
360
+ return foldMigrations(css, (m) => m.kind === "additive");
361
+ }
362
+ function foldMigrations(css, include) {
348
363
  let out = css;
349
364
  const applied = [];
350
365
  for (const m of TOKENS_CSS_MIGRATIONS) {
366
+ if (!include(m)) continue;
351
367
  const next = m.apply(out);
352
368
  if (next !== out) {
353
369
  applied.push(m.id);
@@ -356,6 +372,45 @@ function runTokensCssMigrations(css) {
356
372
  }
357
373
  return { css: out, applied, changed: out !== css };
358
374
  }
375
+ function findContractViolations(canonicalCss) {
376
+ const violations = [];
377
+ for (const m of TOKENS_CSS_MIGRATIONS) {
378
+ if (m.kind !== "additive") continue;
379
+ const before = collectDefinedTokens(canonicalCss);
380
+ const after = collectDefinedTokens(m.apply(canonicalCss));
381
+ const removed = [...before].filter((t) => !after.has(t)).sort();
382
+ if (removed.length) violations.push({ id: m.id, removed });
383
+ }
384
+ return violations;
385
+ }
386
+ function semverBumpType(prev, next) {
387
+ const p = parseSemver(prev);
388
+ const n = parseSemver(next);
389
+ if (n.major > p.major) return "major";
390
+ if (n.major === p.major && n.minor > p.minor) return "minor";
391
+ if (n.major === p.major && n.minor === p.minor && n.patch > p.patch) return "patch";
392
+ return "none";
393
+ }
394
+ function parseSemver(v) {
395
+ const [core] = v.replace(/^v/, "").split(/[-+]/);
396
+ const [major = 0, minor = 0, patch = 0] = core.split(".").map((n) => Number(n) || 0);
397
+ return { major, minor, patch };
398
+ }
399
+ function enforceBreakingRequiresMajor(args) {
400
+ const breakingIds = args.newMigrations.filter((m) => m.kind === "breaking").map((m) => m.id);
401
+ const bump = semverBumpType(args.prevVersion, args.nextVersion);
402
+ if (breakingIds.length === 0 || bump === "major") {
403
+ return { level: "ok", breakingIds, bump, message: "" };
404
+ }
405
+ const pre1 = parseSemver(args.nextVersion).major < 1;
406
+ const ids = breakingIds.join(", ");
407
+ return {
408
+ level: pre1 ? "warn" : "error",
409
+ breakingIds,
410
+ bump,
411
+ 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.`)
412
+ };
413
+ }
359
414
  function validateTokensCss(input) {
360
415
  const defined = /* @__PURE__ */ new Set([
361
416
  ...collectDefinedTokens(input.tokensCss),
@@ -385,11 +440,15 @@ function validateTokensCss(input) {
385
440
  TOKENS_CSS_MIGRATIONS,
386
441
  collectDefinedTokens,
387
442
  collectReferencedTokens,
443
+ enforceBreakingRequiresMajor,
388
444
  ensureScale,
445
+ findContractViolations,
389
446
  readLiveTokensConfig,
390
447
  removeToken,
391
448
  removeTokensMatching,
392
449
  renameToken,
450
+ runAdditiveTokensCssMigrations,
393
451
  runTokensCssMigrations,
452
+ semverBumpType,
394
453
  validateTokensCss
395
454
  });
@@ -16,6 +16,16 @@
16
16
  interface TokensCssMigration {
17
17
  /** Unique id; convention: `YYYY-MM-DD-<short-name>`. */
18
18
  id: string;
19
+ /**
20
+ * Whether this migration is backward-compatible.
21
+ *
22
+ * Token names are public API (see TOKENS.md). `'additive'` only ever inserts
23
+ * new names, so it is safe to auto-apply to a consumer's vendored `tokens.css`
24
+ * (the dev plugin's `autoMigrate`). `'breaking'` renames or removes a name a
25
+ * consumer may reference, so it ships only with a major version (post-1.0) and
26
+ * is never auto-applied — it rides an explicit `live-tokens migrate`.
27
+ */
28
+ kind: 'additive' | 'breaking';
19
29
  /** One-line summary shown by the CLI when the migration applies. */
20
30
  description: string;
21
31
  /** Pure, idempotent transform on the `tokens.css` source. */
@@ -113,6 +123,50 @@ interface RunResult {
113
123
  }
114
124
  /** Fold every registered migration over `css`. Pure and idempotent. */
115
125
  declare function runTokensCssMigrations(css: string): RunResult;
126
+ /**
127
+ * Fold only the `additive` migrations. Additive changes only insert new token
128
+ * names, so applying them to a consumer's vendored `tokens.css` is always
129
+ * backward-compatible — this is what the dev plugin's `autoMigrate` runs. The
130
+ * `breaking` migrations (rename/remove) are deliberately skipped; they ride an
131
+ * explicit `live-tokens migrate` during a major upgrade.
132
+ */
133
+ declare function runAdditiveTokensCssMigrations(css: string): RunResult;
134
+ interface ContractViolation {
135
+ id: string;
136
+ /** Token names the migration removed or renamed away despite declaring `additive`. */
137
+ removed: string[];
138
+ }
139
+ /**
140
+ * Guardrail for the token-as-API contract: an `additive` migration must never
141
+ * remove or rename a token. We verify behaviorally rather than trusting the
142
+ * label — apply each additive migration to the package's own canonical
143
+ * `tokens.css` (which defines the full current vocabulary) and flag any token
144
+ * that disappears. A rename surfaces as the removal of its old name, so it is
145
+ * caught too. This catches the dangerous direction: a breaking change shipped as
146
+ * a backward-compatible one. (Over-labeling a no-op as `breaking` is harmless
147
+ * and not checked.)
148
+ */
149
+ declare function findContractViolations(canonicalCss: string): ContractViolation[];
150
+ type SemverBump = 'major' | 'minor' | 'patch' | 'none';
151
+ /** Classify the bump from `prev` to `next` (leading `v` and pre-release tags ignored). */
152
+ declare function semverBumpType(prev: string, next: string): SemverBump;
153
+ interface BreakingGateResult {
154
+ level: 'ok' | 'warn' | 'error';
155
+ breakingIds: string[];
156
+ bump: SemverBump;
157
+ message: string;
158
+ }
159
+ /**
160
+ * The version side of the token contract: a `breaking` migration introduced in a
161
+ * release requires a major version bump. Pre-1.0 (next major is 0) this is only
162
+ * a warning, since semver permits breaking changes in 0.x minors; from 1.0.0 on
163
+ * it is an error. Pure so the release check and its tests share one rule.
164
+ */
165
+ declare function enforceBreakingRequiresMajor(args: {
166
+ newMigrations: TokensCssMigration[];
167
+ prevVersion: string;
168
+ nextVersion: string;
169
+ }): BreakingGateResult;
116
170
  interface ComponentSource {
117
171
  name: string;
118
172
  source: string;
@@ -141,4 +195,4 @@ interface ValidateInput {
141
195
  */
142
196
  declare function validateTokensCss(input: ValidateInput): MissingToken[];
143
197
 
144
- export { type ComponentSource, type MissingToken, type RunResult, TOKENS_CSS_MIGRATIONS, type TokensCssMigration, type ValidateInput, collectDefinedTokens, collectReferencedTokens, ensureScale, readLiveTokensConfig, removeToken, removeTokensMatching, renameToken, runTokensCssMigrations, validateTokensCss };
198
+ export { type BreakingGateResult, type ComponentSource, type ContractViolation, type MissingToken, type RunResult, type SemverBump, TOKENS_CSS_MIGRATIONS, type TokensCssMigration, type ValidateInput, collectDefinedTokens, collectReferencedTokens, enforceBreakingRequiresMajor, ensureScale, findContractViolations, readLiveTokensConfig, removeToken, removeTokensMatching, renameToken, runAdditiveTokensCssMigrations, runTokensCssMigrations, semverBumpType, validateTokensCss };
@@ -16,6 +16,16 @@
16
16
  interface TokensCssMigration {
17
17
  /** Unique id; convention: `YYYY-MM-DD-<short-name>`. */
18
18
  id: string;
19
+ /**
20
+ * Whether this migration is backward-compatible.
21
+ *
22
+ * Token names are public API (see TOKENS.md). `'additive'` only ever inserts
23
+ * new names, so it is safe to auto-apply to a consumer's vendored `tokens.css`
24
+ * (the dev plugin's `autoMigrate`). `'breaking'` renames or removes a name a
25
+ * consumer may reference, so it ships only with a major version (post-1.0) and
26
+ * is never auto-applied — it rides an explicit `live-tokens migrate`.
27
+ */
28
+ kind: 'additive' | 'breaking';
19
29
  /** One-line summary shown by the CLI when the migration applies. */
20
30
  description: string;
21
31
  /** Pure, idempotent transform on the `tokens.css` source. */
@@ -113,6 +123,50 @@ interface RunResult {
113
123
  }
114
124
  /** Fold every registered migration over `css`. Pure and idempotent. */
115
125
  declare function runTokensCssMigrations(css: string): RunResult;
126
+ /**
127
+ * Fold only the `additive` migrations. Additive changes only insert new token
128
+ * names, so applying them to a consumer's vendored `tokens.css` is always
129
+ * backward-compatible — this is what the dev plugin's `autoMigrate` runs. The
130
+ * `breaking` migrations (rename/remove) are deliberately skipped; they ride an
131
+ * explicit `live-tokens migrate` during a major upgrade.
132
+ */
133
+ declare function runAdditiveTokensCssMigrations(css: string): RunResult;
134
+ interface ContractViolation {
135
+ id: string;
136
+ /** Token names the migration removed or renamed away despite declaring `additive`. */
137
+ removed: string[];
138
+ }
139
+ /**
140
+ * Guardrail for the token-as-API contract: an `additive` migration must never
141
+ * remove or rename a token. We verify behaviorally rather than trusting the
142
+ * label — apply each additive migration to the package's own canonical
143
+ * `tokens.css` (which defines the full current vocabulary) and flag any token
144
+ * that disappears. A rename surfaces as the removal of its old name, so it is
145
+ * caught too. This catches the dangerous direction: a breaking change shipped as
146
+ * a backward-compatible one. (Over-labeling a no-op as `breaking` is harmless
147
+ * and not checked.)
148
+ */
149
+ declare function findContractViolations(canonicalCss: string): ContractViolation[];
150
+ type SemverBump = 'major' | 'minor' | 'patch' | 'none';
151
+ /** Classify the bump from `prev` to `next` (leading `v` and pre-release tags ignored). */
152
+ declare function semverBumpType(prev: string, next: string): SemverBump;
153
+ interface BreakingGateResult {
154
+ level: 'ok' | 'warn' | 'error';
155
+ breakingIds: string[];
156
+ bump: SemverBump;
157
+ message: string;
158
+ }
159
+ /**
160
+ * The version side of the token contract: a `breaking` migration introduced in a
161
+ * release requires a major version bump. Pre-1.0 (next major is 0) this is only
162
+ * a warning, since semver permits breaking changes in 0.x minors; from 1.0.0 on
163
+ * it is an error. Pure so the release check and its tests share one rule.
164
+ */
165
+ declare function enforceBreakingRequiresMajor(args: {
166
+ newMigrations: TokensCssMigration[];
167
+ prevVersion: string;
168
+ nextVersion: string;
169
+ }): BreakingGateResult;
116
170
  interface ComponentSource {
117
171
  name: string;
118
172
  source: string;
@@ -141,4 +195,4 @@ interface ValidateInput {
141
195
  */
142
196
  declare function validateTokensCss(input: ValidateInput): MissingToken[];
143
197
 
144
- export { type ComponentSource, type MissingToken, type RunResult, TOKENS_CSS_MIGRATIONS, type TokensCssMigration, type ValidateInput, collectDefinedTokens, collectReferencedTokens, ensureScale, readLiveTokensConfig, removeToken, removeTokensMatching, renameToken, runTokensCssMigrations, validateTokensCss };
198
+ export { type BreakingGateResult, type ComponentSource, type ContractViolation, type MissingToken, type RunResult, type SemverBump, TOKENS_CSS_MIGRATIONS, type TokensCssMigration, type ValidateInput, collectDefinedTokens, collectReferencedTokens, enforceBreakingRequiresMajor, ensureScale, findContractViolations, readLiveTokensConfig, removeToken, removeTokensMatching, renameToken, runAdditiveTokensCssMigrations, runTokensCssMigrations, semverBumpType, validateTokensCss };
@@ -2,23 +2,31 @@ import {
2
2
  TOKENS_CSS_MIGRATIONS,
3
3
  collectDefinedTokens,
4
4
  collectReferencedTokens,
5
+ enforceBreakingRequiresMajor,
5
6
  ensureScale,
7
+ findContractViolations,
6
8
  readLiveTokensConfig,
7
9
  removeToken,
8
10
  removeTokensMatching,
9
11
  renameToken,
12
+ runAdditiveTokensCssMigrations,
10
13
  runTokensCssMigrations,
14
+ semverBumpType,
11
15
  validateTokensCss
12
- } from "../chunk-MJO4T3CM.js";
16
+ } from "../chunk-D77VD4Z6.js";
13
17
  export {
14
18
  TOKENS_CSS_MIGRATIONS,
15
19
  collectDefinedTokens,
16
20
  collectReferencedTokens,
21
+ enforceBreakingRequiresMajor,
17
22
  ensureScale,
23
+ findContractViolations,
18
24
  readLiveTokensConfig,
19
25
  removeToken,
20
26
  removeTokensMatching,
21
27
  renameToken,
28
+ runAdditiveTokensCssMigrations,
22
29
  runTokensCssMigrations,
30
+ semverBumpType,
23
31
  validateTokensCss
24
32
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.33.1",
3
+ "version": "0.34.0",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -101,12 +101,13 @@
101
101
  "check:component-defaults": "node scripts/sync-component-defaults.mjs --check",
102
102
  "check:production-is-default": "node scripts/check-production-is-default.mjs",
103
103
  "check:docs-content": "node scripts/sync-docs.mjs --check",
104
+ "check:token-contract": "node scripts/check-token-contract.mjs",
104
105
  "sync:component-defaults": "node scripts/sync-component-defaults.mjs --write",
105
106
  "sync:docs": "node scripts/sync-docs.mjs --write",
106
107
  "collapse:manifest": "node scripts/collapse-manifest-to-default.mjs",
107
108
  "check:smoke-install": "bash scripts/smoke-install.sh",
108
109
  "check:smoke-create": "bash scripts/smoke-create.sh",
109
- "prepublishOnly": "npm run check:no-style-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:smoke-install && npm run check:smoke-create"
110
+ "prepublishOnly": "npm run check:no-style-imports && npm run check:slot-prose && npm run check:overlay-portal && npm run check:editor-font-isolation && npm run check:component-defaults && npm run check:production-is-default && npm run check:docs-content && npm run build:lib && npm run check:token-contract && npm run check:smoke-install && npm run check:smoke-create"
110
111
  },
111
112
  "peerDependencies": {
112
113
  "@sveltejs/vite-plugin-svelte": "^7.0",