upgrade lists and runs codemods: automated migrations that move your app
across a breaking change in @customerio/pluma-components. Codemods are
grouped by the @customerio/pluma-components version each change lands in, so
you can see which ones the versions you are crossing need.
Run upgrade when you bump @customerio/pluma-components across a version
that has codemods, before you fix anything by hand. Each codemod covers one
breaking change; what it migrates, and what it leaves for you, is in
each codemod's own README.
A codemod doesn't replace reading the release notes: changes without a codemod
still need migrating by hand. See When upgrading for the
steps.
After --codemod <id>, pass the framework (react or ember), then one or more
paths, then any flags. Flags and paths are forwarded to the codemod, so its
own options (--dry, and — for React — standard jscodeshift flags) work as
documented in each codemod's README.
The flags in the table below are the CLI's own and mean the same for every
codemod. None of them ever reaches jscodeshift, which reads a flag it doesn't
know as taking a value and would drop the path after it. The codemods built on
the shared dispatcher — every 5.0.0 one among them — go further: a bare flag
that is neither the CLI's nor jscodeshift's own, or a path that doesn't exist,
stops the run with exit status 2 before anything is written, and a file
jscodeshift fails to transform makes the run exit non-zero. An option meant for
the transform goes through in the --name=value form, which can't swallow the
argument after it. --silent is refused while the run ledger is on, because it
hides the summary the ledger is checked against.
| Flag | Description |
|---|---|
--list | List available codemods, grouped by target @customerio/pluma-components version. |
--codemod <id> | Run the codemod with this id (see --list). |
--json | With --list, print machine-readable JSON. |
--dry, -d | Preview changes without writing them. |
--force | Bypass a codemod's re-run safety check. Only codemods that have one take it; it never changes which files are transformed. |
--report <p> | Where to write the findings report (default .pluma/codemod-report.json). Also --report=<p>. |
--ledger <p> | Where the run ledger lives (default .pluma/codemod-ledger.json). Also --ledger=<p>. |
--no-ledger | Neither read nor write the run ledger. |
--baseline | Record every site a run would change as already migrated, and change no files. Only for codemods that keep a run ledger. |
-h, --help | Show help. |
Always preview with --dry first, then run it for real:
The run prints the codemod's own progress and a "needs manual review" summary.
A codemod that records findings ends by printing how many rows the
findings report holds, split by class, and where it
went. A codemod that keeps a run ledger writes it to
.pluma/codemod-ledger.json unless --dry or --no-ledger is set.
--list --json prints an array with one object per codemod:
| Key | Value |
|---|---|
id | The id --codemod takes. |
version | The @customerio/pluma-components version whose breaking change the codemod migrates. |
title | A one-line summary of what it changes. |
frameworks | The frameworks it can transform: react, ember. |
rerunGuard | true when the codemod has a re-run safety check that --force bypasses. Absent otherwise. |
ledger | true when the codemod keeps a run ledger. Absent otherwise. |
A codemod run has no JSON output; its results are the changed files, the findings report and the run ledger.
The id is misspelled, or the codemod isn't in the version of pluma-cli you
ran. The command exits with status 2 and lists every id it knows. Run with
@latest to get the newest codemods, and copy the id from
pluma-cli upgrade --list.
The framework comes straight after the codemod id, before the paths:
pluma-cli upgrade --codemod button-variant-rename react ./src. The command
exits with status 2. A run with no path at all prints jscodeshift's usage
instead of migrating anything, and a codemod built on the shared dispatcher
refuses a path that doesn't exist with No such file or directory: <path>.
jscodeshift reads a flag it doesn't know as taking a value and would drop the
path after it, so a codemod built on the shared dispatcher stops the run with
exit status 2 before anything is written. Check the flag's spelling against
the flags table and the codemod's README. An option meant for the
transform goes through in the --name=value form, which can't swallow the
argument after it.
A codemod that keeps a run ledger checks it against the
summary jscodeshift prints, and --silent (-s) hides that summary. The run
stops with exit status 2. Drop --silent, or pass --no-ledger if you
really mean to run without the ledger.
@customerio/pluma-components.pluma-cli upgrade --list and note the codemods whose version falls in
the range you're crossing.--dry first, review the diff and the "needs manual review"
summary, then run for real and commit — with the .pluma/ directory the
run wrote.A codemod prints two kinds of note as it runs: warnings (a token it could not rewrite) and review notes (a rewrite worth a second look). Both scroll past. A run that touches fifty files prints hundreds of lines, and the migrations that shook this out showed exactly what happens next: the notes were read once and the sites they pointed at shipped unfixed.
Some codemods therefore also write a findings report — one JSON file per
run, at .pluma/codemod-report.json (see The .pluma/ directory) unless --report <path> says otherwise.
Both halves of a React run (the stylesheet pass and the JS pass) write into the
same file, and so does a pre-flight check that found something the run went
ahead over. At the end of the run the CLI prints how many rows it holds, split
by class, and where it went.
Every row is one site. The two fields to sort on:
| Field | Meaning |
|---|---|
class | Which problem this is. Codemod-specific; see the codemod's own README. |
action | rewritten — the codemod changed the source and you should check the result. |
fix-by-hand — the source is untouched, or — where leaving the old name would | |
| have broken the file — renamed with the colour decision still open. The right answer | |
| needed a look at the element, and a plausible wrong colour is worse than a work item. | |
| This row is the task. | |
renamed-only — the name was migrated so the file stays valid, but the site still | |
| needs a hand fix. Used where leaving the old name would break silently rather than | |
| loudly — a stylesheet has no typecheck, and an undefined custom property simply | |
| vanishes. These also print a warning during the run. |
A fix-by-hand row does not always mean the codemod declined a rewrite. Some
classes describe a site the codemod was right to leave alone and a reader still
has to judge — theme-tokens-rename's token-revival is one, where the line is
already valid 5.0 and the question is what it used to paint. The codemod's own
README says which of its classes are which.
Filter the hand-work list with jq:
rewritten rows are excluded because nothing is owed on them, not because
they're finished — the codemod made a call there and the result still wants a
look. Read those too, by class; the codemod's own README says which ones are
worth the time.
Most codemods are safe to run twice. A few are not: theme-tokens-rename
renames color.navigation-background to color.surface-base and
color.surface-base to color.surface-content, so a second pass over code the
first one migrated shifts it one rung further. And a branch migrated to a new
major rarely stays still — it keeps merging in code written against the old
one, often into files it already migrated. After that merge, a file reads a
surface-base the codemod wrote (5.0, the page plane) next to a
surface-base main just added (4.x, the white panel), and nothing in the text
says which is which.
So the 5.0 codemods (theme-tokens-rename, text-icon-color-rename,
color-variant-rename) keep a run ledger: .pluma/codemod-ledger.json, next
to the findings report unless --ledger <path> says otherwise. Commit it
with the migrated files. A later run over the same files leaves every site it
records exactly as it is, and migrates the rest — the new code — even inside a
file it migrated before.
Not every rewrite: only the sites a second run would change. After writing a
file, the codemod runs over its own output once more, and whatever that pass
would still change is a site the next run must leave alone. So the ledger needs
no knowledge of any codemod's tables, and a codemod that never rewrites its
own output records nothing. text-icon-color-rename and color-variant-rename
are like that; theme-tokens-rename is not, and records every cascade site it
writes. Each run recomputes the rows for the files it visits from those files
as it leaves them, so the ledger always describes the tree as it stands. A row
for a site the run never reached — with the bare pass off, say — is kept while
the site is still in the file. A run stopped with Ctrl-C records the files it
wrote before it stopped.
A site is keyed by its tokens and what surrounds them, never by a line number:
text — the tokens a re-run would change, joined: surface-base,
--pluma-palette-blue-600.rerun — what a re-run would write there. For reading only; it is not part
of the key.line — a hash of the line the site sits on.before / after — a 4-character hash of each of the 16 tokens either side,
nearest first, ending in ^ / $ where the file does.The code is read as tokens — words and punctuation, whitespace ignored — with
, and ; skipped and " read as '. So indentation, re-wrapping, quote
style, trailing commas and semicolons change nothing but, at most, the line
hash; lines added above or below a site, and a block a merge moved, carry the
site with them. Matching measures how far out each side still agrees, both as
an unbroken run from the site and as a longest common subsequence (so a line
inserted right next to a site costs only that line), and weighs a word the file
rarely uses — a style's name, a selector — above punctuation, which is the same
everywhere.
The trade-off is duplication: two sites with the same text on identical lines
in identical surroundings can't be told apart by anything in the file. When a
file holds more such copies than the ledger recorded, one of them is new and
nothing says which, so none of them is rewritten. Each lands in the
findings report as a ledger-ambiguous row, fix-by-hand, and so does a site
whose surroundings changed too much for the ledger to be sure. Every uncertain
case leaves the source as it is and says so, on every run until you migrate the
new copies by hand and then, before any other run, record the file with
--baseline <path>.
| Field | Meaning |
|---|---|
version | The format. A CLI refuses a ledger newer than it understands rather than ignore its rows. |
context | How many tokens either side each row records. |
codemods | Rows per codemod, then per file — the path from the ledger's root, with forward slashes. |
hashes | Per codemod and file, a hash of the content its rows were recorded against. |
A ledger in a .pluma/ directory records paths from the directory that holds
.pluma/ — the repository root, for the default ledger — so they read the same
whichever directory a run starts in. A ledger anywhere else records paths from
its own directory.
A file with an empty list holds nothing a re-run would change, but reads the new
names: it is listed so theme-tokens-rename's re-run guard knows it is the
codemod's own output (see below). The file is deterministic — codemods, files
and rows sorted, one row per line, no timestamps — so a re-run diffs as the rows
that actually changed. Rows for files that no longer exist are dropped. If a
merge conflicts in it, keep both sides' rows; the order doesn't matter.
--dry reads the ledger (so the preview is what a real run would write) and
never writes it.--no-ledger neither reads nor writes it: the codemod behaves exactly as it
did before the ledger existed.--baseline changes no file. It records every site a run would change as
already migrated, and lists them. That is how a tree migrated before the
ledger existed — or code written against the new API by hand, which reads
exactly like code still to migrate — is brought under it. Run it on the
branch before merging main in, then merge and run as normal, and run it
again before each later merge whenever new-API code has been written by hand
since. Check the list: a recorded site is never migrated.--force still only bypasses a codemod's re-run guard. The ledger applies
under it; --no-ledger is the flag that turns the ledger off.theme-tokens-rename's re-run guard, which refuses a tree that already reads
5.0-only names, skips every file the ledger lists — so re-running over a tree
it migrated is no longer refused. A listed file that changed since its rows
were recorded is named in a warning instead; it never refuses the run, and its
sites without a row are migrated. Files the ledger has no record of are scanned
as before, except that where the ledger lists files under the same paths, 5.0
names in a single unlisted file refuse the run — record it with --baseline <path> if it is already 5.0. See its
README.
--baseline recorded. Code
migrated by hand, or written against the new API since, is invisible to it
until it is baselined.theme-tokens-rename carries the rows of a migrated
file that was moved or renamed unchanged. Moved and edited, or pulled out into
a new file, the run is refused: record the new file with --baseline <path>,
or, if it is not the same code, drop the old rows with --forget <old path>.src/codemods/<target-version>/<id>/ containing a bin.mjs dispatcher
(bin.mjs <react|ember> <path…>), the react.cjs and/or ember.mjs
transforms, a README.md, and a tests/ suite. Use an existing codemod as
the template.src/registry.ts.@customerio/pluma-cli
release.