Skip to content

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

Use migrateTheme when you bump a version, hit a breaking change, or adopt a new token schema.
  • Version bumps — every ThemeDefinition carries a meta.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.
core — checking version and migrating
ts
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)

migrateTheme(theme, options) reads meta.version, walks the migration chain, and returns an updated theme.

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.

Options
  • targetVersion — version to migrate toward (defaults to the latest)
Returns
  • A new ThemeDefinition with updated tokens and version
  • If already at the target, the original object is returned
Guard against runaway chains |The engine caps the chain at 20 steps to prevent infinite loops from circular migration registrations.

3registerMigration({ from, to, remapColors?, migrate? })

registerMigration adds a step between two versions. Each step can carry a color remap table, a custom migrate function, or both.

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.

core — registerMigration
ts
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

When a theme is several versions behind, the engine walks every intermediate step automatically.

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.

core — chaining migrations
ts
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

Use remapColors for simple renames. Use migrate when the transformation requires logic or structural changes.
When to use each
remapColors
  • Token is renamed, not restructured
  • One-to-one key mapping
  • Declarative and safe
  • No access to other tokens
migrate
  • Structure changes (nesting, splitting, merging)
  • You need to read other tokens
  • You need conditional logic
  • Full access to the ThemeDefinition
core — remapColors vs migrate
ts
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});
Both can be combined |A single migration step can carry both remapColors and migrate. The migrate function runs first, then remapColors is applied to the result.

6CI integration

Catch outdated themes in CI before they ship. Run migrateTheme against every theme and fail the build if any are behind.

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.

.github/workflows/migrate-check.yml
yaml
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.version against the migrated result — if they differ, the theme is outdated.
  • Works with both local theme files and programmatically generated themes.
  • Pair with clearMigrations in tests to reset state between test cases.
Migration — Theme Kit