Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/dst-1543-cli-migrate-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@marigold/docs': patch
---

docs(DST-1543): document `marigold migrate` on the CLI page

Adds a `marigold migrate` section to `/getting-started/cli`, alongside the other commands: what the codemods cover (theme slot restructures and baseline swaps, application-code renames, report-only design-token checks), the optional version and path positionals, the `--dry-run` and `--only` flags, and the safety model (warnings instead of guesses, idempotent runs, the typecheck as the completeness check).
1 change: 1 addition & 0 deletions .changeset/iconography-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Old names from the legacy `@marigold/icons` set and their Lucide replacement. Na
| Notification | `MessageSquareWarning` |
| Parking | `CircleParking` |
| PDF | `PDF` _(custom)_ |
| Print | `Printer` |
Comment thread
sarahgm marked this conversation as resolved.
| Reports | `FileText` |
| Required | `Asterisk` |
| ResaleLogbook | `BookOpenText` |
Expand Down
5 changes: 5 additions & 0 deletions .changeset/migrate-codemods.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@marigold/cli': minor
---

feat(DST-1543): add `marigold migrate <version>` codemods for breaking Marigold releases. The v18 migration restructures theme files to the new slot shapes (never overriding consumer classes), swaps exact-baseline layout classes with a token diff report, scaffolds missing theme components, applies safe application-code renames (icon imports per the official mapping, `Tabs.TabPanel`/`SelectList.Item`, `Inset` spacing props, `TextField` min/max), and reports everything that needs a human decision with pinned source links. The report also covers design-token breakage that no typecheck can see: renamed/removed tokens still referenced, new tokens components require but the consumer CSS does not define, and repurposed tokens that kept their name but changed meaning (with a remap recipe at the definition site). Interactive runs pre-analyze the target and offer the fired changes as a multiselect (Enter applies everything; `--only <names>` selects non-interactively). Run `npx marigold migrate v18 --dry-run` first.
37 changes: 36 additions & 1 deletion docs/content/getting-started/cli/index.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Marigold CLI
description: Component docs, discovery, and project setup, straight from the terminal.
badge: new
badge: updated
---

The **`@marigold/cli`** package brings the Marigold documentation into your terminal: fetch component docs, discover components and pages, scaffold Marigold into an existing project, and more. It's useful on its own and is built to ground AI coding agents on the canonical component API.
Expand Down Expand Up @@ -241,6 +241,41 @@ It checks, against the current working directory:
health from `errors.length === 0` and act on each `suggestion`.
</Callout>

### marigold migrate

Apply codemods for a breaking Marigold release, to your theme files and your application code. Run it after upgrading `@marigold/components`, from your project root.

```bash
marigold migrate v18 # migrate the current directory
marigold migrate v18 ./src --dry-run # report what would change, write nothing
marigold migrate # detect the installed version and confirm
marigold migrate v18 --only rename-imports # apply a subset, non-interactively
```

| Flag | Description | Default |
| ---------------- | ----------------------------------------------------------------------- | ------- |
| `--dry-run` | Report what would change without writing files | |
| `--only <names>` | Apply only these changes (comma-separated), skipping the selection step | |

Both positionals are optional. Without a version, the installed `@marigold/components` is detected and the applicable migration is proposed for confirmation. Without a path, the current directory is used.

It anchors on imports rather than on file names or directory layout, so it works whatever your project structure is:

1. **Theme files** (anchored on `ThemeComponent<'X'>` from `@marigold/system`): restructures single-style components into the slot shape the new version requires, moving your classes verbatim, swaps a baseline style only when it still matches the old baseline byte for byte (proof you never customized it), stubs new slots as `cva({})`, and scaffolds theme files for components the new version requires.
2. **Application code** (anchored on `@marigold/components` and `@marigold/icons`): renamed exports such as the icon migration, renamed compound components, renamed and removed props.
3. **Design tokens**: report-only, since token values are yours. Covers renamed or removed tokens you still reference, new tokens that component internals hardcode but your CSS does not define, and tokens that kept their name but changed meaning.

Anything that cannot be decided from the source, a spread that hides slots, a customized style, a DOM change your own CSS may target, becomes a warning pointing at the release notes, never a guess.

<Callout title="Dry run first">
`--dry-run` prints the full report and writes nothing. Interactive runs
pre-analyze the project and list the changes that actually fire, so you can
deselect any of them before applying; report-only checks always run. Applying
twice is a no-op, so a partial run can be completed later. After an applied
run, your typechecker is the completeness check: the slot `Record`s in
`@marigold/system` are exhaustive.
</Callout>

### marigold completion

Print a tab-completion script for `bash`, `zsh`, or `fish`. Source it once per shell, or write it to your shell's completion directory for persistence.
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/bin/marigold.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,20 @@ describe('main() — doctor command', () => {
});
});
});

describe('main() — migrate command', () => {
// The version positional is optional, so a mistyped version is otherwise
// indistinguishable from a path. The hint has to be checked before the
// positional-count validation, which would reject this as "too many paths".
test('names the migration a version-ish positional probably meant', async () => {
const code = await main(['migrate', '18.1', './src']);

expect(code).toBe(1);
expect(stderrSpy.mock.calls.flat().join('')).toContain('Did you mean v18?');
expect(emitMock.mock.calls[0][0]).toMatchObject({
command: 'migrate',
exitCode: 1,
args: expect.objectContaining({ version: 'auto' }),
});
});
});
72 changes: 72 additions & 0 deletions packages/cli/src/bin/marigold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ ${pc.bold('Commands:')}
examples <action> Browse application patterns (list | get <slug>)
init Set up Marigold in a project
doctor Diagnose a project's Marigold setup
migrate [version] Apply codemods for a breaking Marigold release
telemetry <action> Manage telemetry (status|enable|disable)
completion <shell> Print shell completion script (bash|zsh|fish)

Expand Down Expand Up @@ -102,6 +103,16 @@ ${pc.bold('Doctor options:')}
--format <name> text | json (default: text)
--offline Skip the network; use only the local cache

${pc.bold('Migrate options:')}
[version] Migration to run (e.g. v18). When omitted, the
installed @marigold/components version is detected
and the proposed migration confirmed interactively
[path] Directory to migrate (default: current directory)
--dry-run Report what would change without writing files
--only <names> Apply only these changes (comma-separated codemod
names from the pre-analysis); skips the interactive
selection. Warnings always run.

${pc.bold('Environment:')}
MARIGOLD_DOCS_URL Override docs site base URL
MARIGOLD_CACHE_TTL_MS Override cache TTL in milliseconds
Expand Down Expand Up @@ -202,6 +213,16 @@ const parseDoctorCommand = (argv: string[]) =>
},
});

const parseMigrateCommand = (argv: string[]) =>
parseArgs({
args: argv,
allowPositionals: true,
options: {
'dry-run': { type: 'boolean', default: false },
only: { type: 'string' },
},
});

const isExamplesSub = (v: string): v is ExamplesSubcommand =>
(EXAMPLES_SUBCOMMANDS as readonly string[]).includes(v);

Expand Down Expand Up @@ -419,6 +440,57 @@ export const main = async (

writeOutput(result.output);
if (result.hasErrors) exitCode = 1;
} else if (command === 'migrate') {
Comment thread
sebald marked this conversation as resolved.
const { positionals, values } = parseMigrateCommand(rest);
// the version positional is optional: `migrate ./src` treats the first
// positional as a path, `migrate v18 ./src` as version + path. Both
// `18` and `v18` name a migration.
const [first, second] = positionals;
const explicitVersion =
first !== undefined && /^v?\d+$/.test(first)
? first.startsWith('v')
? first
: `v${first}`
: undefined;
const targetPath = (explicitVersion ? second : first) ?? process.cwd();

telemetryArgs = {
version: explicitVersion ?? 'auto',
...(values['dry-run'] ? { dryRun: 'true' } : {}),
};

// A version-ish first positional that is not an exact major is a typo,
// not a directory. Checked before the count below, which would
// otherwise swallow `migrate 18.1 ./src` as "too many paths".
if (
!explicitVersion &&
first !== undefined &&
/^v?\d+\.[\d.]*$/.test(first)
) {
fail(
`Unknown migration '${first}' — migrations are named by major version. ` +
`Did you mean v${Number.parseInt(first.replace(/^v/, ''), 10)}?`
);
}
if (positionals.length > (explicitVersion ? 2 : 1)) {
fail(
'Usage: marigold migrate [version] [path] [--dry-run] [--only <names>]'
);
}

// Lazy-load: migrate pulls in @babel/parser and magic-string, which we
// keep off the docs/list hot path.
const { runMigrateCommand } = await import('../commands/migrate.js');
exitCode = await runMigrateCommand({
version: explicitVersion,
targetPath,
dryRun: values['dry-run'],
only: values.only
?.split(',')
.map(s => s.trim())
.filter(Boolean),
write: writeOutput,
});
} else if (command === 'telemetry') {
const [sub] = rest;
telemetryArgs = sub ? { sub } : {};
Expand Down
Loading
Loading