Migration
Keep themes current across versions
Theme Kit ships a built-in migration engine that walks registered steps to bring any theme definition up to date — from simple token renames to full structural rewrites.
1When to migrate
- Version bumps — every
ThemeDefinitioncarries ameta.version. When you ship a new schema, bump the version so the migration engine knows which steps to apply. - Breaking changes — renamed or removed tokens, restructured nesting, or changed semantics all need a migration step to keep existing themes working.
- API evolution — as Theme Kit grows, new token conventions emerge. A migration step lets you adopt them without manually editing every theme file.
1import { migrateTheme } from "@theme-kit/core";
2
3// A theme authored for an older version
4const legacyTheme = {
5 meta: { version: "0.1" },
6 tokens: {
7 colors: {
8 primary: "#6b21a8",
9 bg: "#ffffff",
10 },
11 },
12};
13
14// Migrate to the current version (or a specific target)
15const migrated = migrateTheme(legacyTheme);
16// migrated.meta.version === "0.2"2migrateTheme(theme, options)
Pass a theme and an optional targetVersion. The function finds every registered migration from the theme's current version toward the target, applies them in order, and updates meta.version on the result.
targetVersion— version to migrate toward (defaults to the latest)
- A new
ThemeDefinitionwith updated tokens and version - If already at the target, the original object is returned
3registerMigration({ from, to, remapColors?, migrate? })
Each call registers a single hop — from one version to the next. The engine walks hops in order when migrateTheme is called. Register steps at module load so they are available everywhere.
1import { registerMigration } from "@theme-kit/core";
2
3// Simple token rename: "bg" was renamed to "background" in v2
4registerMigration({
5 from: "0.1",
6 to: "0.2",
7 description: "Rename 'bg' token to 'background'",
8 remapColors: [
9 { from: "bg", to: "background" },
10 { from: "surface", to: "surface" },
11 ],
12});
13
14// With a custom migrate function for structural changes
15registerMigration({
16 from: "0.2",
17 to: "0.3",
18 description: "Restructure nested color tokens into a flat palette",
19 migrate: (theme) => ({
20 ...theme,
21 tokens: {
22 ...theme.tokens,
23 colors: {
24 background: theme.tokens?.colors?.background,
25 foreground: theme.tokens?.colors?.foreground,
26 primary: theme.tokens?.colors?.primary,
27 },
28 },
29 }),
30});4Multi-step migration
Register migrations for each consecutive version pair. When migrateTheme runs, it builds a chain from the theme's current version to the target and applies each step in sequence.
1import { registerMigration, migrateTheme } from "@theme-kit/core";
2
3// Register a chain of migrations
4registerMigration({
5 from: "0.1",
6 to: "0.2",
7 remapColors: [{ from: "bg", to: "background" }],
8});
9
10registerMigration({
11 from: "0.2",
12 to: "0.3",
13 remapColors: [{ from: "text", to: "foreground" }],
14});
15
16registerMigration({
17 from: "0.3",
18 to: "0.4",
19 migrate: (theme) => ({
20 ...theme,
21 tokens: {
22 ...theme.tokens,
23 colors: {
24 ...theme.tokens?.colors,
25 muted: "var(--theme-color-foreground, 50%)",
26 },
27 },
28 }),
29});
30
31// migrateTheme walks the chain automatically:
32// v0.1 → v0.2 → v0.3 → v0.4
33const theme = {
34 meta: { version: "0.1" },
35 tokens: { colors: { bg: "#fff", text: "#000" } },
36};
37
38const result = migrateTheme(theme);
39// result.meta.version === "0.4"
40// result.tokens.colors.background === "#fff"
41// result.tokens.colors.foreground === "#000"5remapColors vs migrate
- Token is renamed, not restructured
- One-to-one key mapping
- Declarative and safe
- No access to other tokens
- Structure changes (nesting, splitting, merging)
- You need to read other tokens
- You need conditional logic
- Full access to the ThemeDefinition
1// remapColors: declarative, safe, automatic
2// Use when a token is just renamed
3registerMigration({
4 from: "0.1",
5 to: "0.2",
6 remapColors: [
7 { from: "bg", to: "background" }, // simple rename
8 { from: "border", to: "borderColor" }, // simple rename
9 ],
10});
11
12// migrate: imperative, full control
13// Use when the structure changes or you need logic
14registerMigration({
15 from: "0.2",
16 to: "0.3",
17 migrate: (theme) => {
18 const old = theme.tokens?.colors ?? {};
19 return {
20 ...theme,
21 tokens: {
22 ...theme.tokens,
23 colors: {
24 // Derive a new token from existing values
25 surface: old.background ?? old.bg ?? "#fff",
26 onSurface: old.foreground ?? old.text ?? "#000",
27 primary: old.primary ?? old.accent ?? "#6b21a8",
28 },
29 },
30 };
31 },
32});remapColors and migrate. The migrate function runs first, then remapColors is applied to the result.6CI integration
Add a lightweight CI step that loads every theme in your repo, runs migrateTheme, and exits non-zero if any theme's version doesn't match the result. This prevents shipping themes that haven't been updated for breaking changes.
1name: Theme Migration Check
2on: [push, pull_request]
3
4jobs:
5 migrate-check:
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9
10 - uses: actions/setup-node@v4
11 with:
12 node-version: 20
13
14 - run: npm ci
15
16 - name: Check themes for outdated versions
17 run: |
18 node -e "
19 const { migrateTheme } = require('@theme-kit/core');
20 const themes = require('./themes.json');
21 let failed = false;
22 for (const theme of themes) {
23 const migrated = migrateTheme(theme);
24 if (migrated.meta.version !== theme.meta.version) {
25 console.log('Outdated theme:', theme.meta.name,
26 theme.meta.version, '->', migrated.meta.version);
27 failed = true;
28 }
29 }
30 if (failed) {
31 console.error('Run migrateTheme on all themes before pushing.');
32 process.exit(1);
33 }
34 console.log('All themes are up to date.');
35 "- The script compares each theme's
meta.versionagainst the migrated result — if they differ, the theme is outdated. - Works with both local theme files and programmatically generated themes.
- Pair with
clearMigrationsin tests to reset state between test cases.