feat(codemods): add an upgrade CLI backed by a migration catalogue - #2978
Conversation
🚀 Preview DeploymentPreview environments are ready:
Images:
|
|
Blocked on #2980. The version-contract check fails here because it reads adding The one-line fix was on this branch and I have reverted it out: it is CI infrastructure, a reviewer of this CLI should not have to review it, and the squash-merge would land it under a codemods commit message. #2980 carries the diff and the two tests. |
|
The guard fix is now #2982, targeting |
35bd781 to
71d11dc
Compare
Implementation plan for publishing @mittwald/flow-codemods as a CLI with an `upgrade` command, backed by a machine-readable migration catalogue that generates MIGRATION.md. 14 tasks, 103 TDD steps. Key decisions recorded in the plan header: the raw-GitHub-URL delivery path is retired, transform files are renamed to dashed catalogue ids, the gate is exact-version (`current < since <= target`) with revision keywords bounding the target, and every catalogue entry carries detect/apply/verify so a migration without a codemod is still executable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes after review of the first draft. PR #2942 is no longer a prerequisite. Task 0 pulls its useful parts onto this branch — the four added transforms, the rewritten flowAlphaAlignToCombine (+155/-38 for alias and namespace resolution), the test harness, remoteScope, and the migration prose it added to MIGRATION.md — and deliberately leaves the composite bundler behind. It supersedes #2942, which should be closed rather than merged: merging it to main afterwards would conflict with every rename. Task 15 adds the deprecated APIs that have no MIGRATION.md entry today. The first draft silently delivered only half the agreed "breaking plus deprecations" scope: all 22 ported entries come from the existing guide and only two are deprecations, both already documented. Without Task 15, `upgrade` on the 1.x line still finds almost nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The snippet pinned 1.0.1 while the repo is on 1.0.2, and Lerna owns the field. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pulls reviewed, CI-green work from PR #2942 onto this branch: the runTransform test harness, the remoteScope authority, nine transforms (four new, three improved), and package configs. Drops the broken flowAlphaAll composite and everything that exists only to serve it (the bundler scripts, flow1.ts, and the standalone/bundledComposites/ documented test suites) — a later task replaces the composite mechanism. remoteScope.test.ts, idempotency.test.ts and transforms.test.ts had their flow1-specific fixtures, targets and comments removed accordingly so the hand-maintained lists match what is actually on disk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Makes @mittwald/flow-codemods publishable (drops private/peerDependencies, moves jscodeshift to dependencies for npx, adds bin) and adds the flow-codemods CLI with argument parsing via node:util parseArgs. Only --help and --version are wired up; every later command errors with "not implemented yet". Two deviations from the task brief, both needed to make `tsc` actually build and run this package in this repo: - tsconfig.build.json declares `"types": ["node"]`. Excluding src/tests/**/*.ts and src/transforms/**/*.ts shrinks its program to 2 files (cli.ts, cli/args.ts). The native `tsc` binary this repo resolves bare `tsc` to (TypeScript 7, aliased `@typescript/native` per pnpm-workspace.yaml) does not run its usual automatic @types/* discovery for a program this small under `composite`, so `process` and `node:util` resolved to nothing. tsconfig.json's own tsc --noEmit gate is unaffected because its program is much larger. - tsconfig.json's `include` gained "package.json", and cli.ts imports "./cli/args.js" (not "./cli/args", as the brief's block had it). Without the extension, tsc's `module: "preserve"` emits the import as-is and Node's ESM loader can't resolve it (ERR_MODULE_NOT_FOUND) — ".js" is required so tsc maps it back to the sibling .ts file. Without the package.json include, `tsc --noEmit` failed with TS6307 because the composite project's file list didn't cover the JSON file cli.ts imports for --version. @inquirer/prompts, @types/semver and yaml resolved within the brief's declared ranges (^7.9.0 -> 7.10.1, ^7.7.3 -> 7.8.0, ^2.8.1 -> 2.9.0); no minimumReleaseAge rejection. 59 tests pass (47 pre-existing + 10 args + 2 bin). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Relative imports in emitted modules need a .js extension: the shared config sets module=preserve, so an extensionless import type-checks and is emitted unchanged, which Node ESM rejects. Every other package here is consumed by a bundler, so this is the first one to hit it. Also keeps catalog/read.ts out of dist — it imports yaml, a devDependency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
process.exit() can end the process before a pending stdout write has flushed, which on POSIX is the common case when stdout is a pipe. Every command exits through this path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Introduces the migration catalogue: MigrationEntry/Kind/Action types, a build-time reader that parses YAML-frontmatter Markdown files under src/migrations, and a generator that emits them as a typed src/migrations.generated.ts the CLI imports at run time via catalog/entries.ts. Two proof entries author the schema (one codemod, one manual); the other 19 follow in Task 3. src/catalog/read.ts imports yaml (a devDependency) and is excluded from tsconfig.build.json so dist never ships an unresolvable import; it stays covered by the tsc --noEmit gate. Renames flowAlphaAlignToCombine.ts to align-to-combine.ts so the catalogue's action:codemod <-> transform-file-exists invariant holds, and updates every place that named it, including remoteScope.test.ts's target map (missed by the task brief but broken by the same rename). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task briefs, per-task reports, review diffs and the progress ledger are session-local bookkeeping. They were neither gitignored nor prettier-ignored, so one got committed and two turned format:check red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ect runnable Three review findings: - The generator sorted only on `since`, so the two entries sharing a release ordered by directory listing — not guaranteed across platforms, and enough to fail CI's generated-code check on another machine. - `align-to-combine`'s detect command passed `-t tsx`, which ripgrep rejects; it errored instead of matching, the one failure mode the field must not have. - `fail` was an arrow, which does not narrow control flow, so its call sites carried a redundant `throw`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author the remaining 20 catalogue entries (segmented-control-deprecated through renamed-css-export's two pre-existing siblings now sit alongside them) and add a guide generator that renders packages/components/MIGRATION.md from the catalogue, wired into `pnpm nx build codemods`. MIGRATION.md is now generated, not hand-maintained. Fixed the two Task 2 "proof of schema" entries (align-to-combine, renamed-css-export) to carry their body verbatim from the previous guide, since the generated file must not silently drop migration content. Rewrote remoteScope.test.ts to check every catalogue entry's `remotePackage` against the remote package's real export surface, replacing the old transform-name-keyed target list. This lands with a known-red catalog.test.ts: 8 `action: codemod` entries still point at a `flowAlpha*`/`flow020`-named transform file, because renaming transforms is Task 4's job. See the task-3 report for the full failure breakdown, including one additional ordering issue in guide.test.ts unrelated to the transform renames (`since: 0.2.0` sorts above `0.2.0-alpha.*` under strict semver, even though those alphas shipped later in this project's real history) — left unresolved pending a maintainer decision, not silently patched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…igration The entry claimed `0.2.0`, taken from the guide's "From version 0.1.0 to version 0.2.0" heading. Neither version was ever published — the headings name release lines, and the first stable release is 1.0.0. Semver ranks 0.2.0 above every 0.2.0-alpha.*, so the oldest migration sorted as the newest and the version gate would have offered a consumer on 0.2.0 none of the alpha migrations. The export flattening shipped in 0.2.0-alpha.28: alpha.27 still published one entry per component, alpha.28 published the flat set. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review found several fields that were written but never tested against realistic input. Four detect commands under-matched: three anchored a render-prop to the opening tag on one line, so Prettier-wrapped JSX matched nothing, and the package-root import entry restricted itself to TypeScript while its own examples are JavaScript. A detect that matches nothing is the field's worst failure — an agent concludes the migration does not apply. Two apply fields were factually wrong. The OverlayController handler type was widened, not narrowed, so no consumer change is needed; what matters is that a handler returning false now vetoes the close, which the entry did not mention. The TooltipTrigger entry invented a numeric mapping that mapped 500ms onto the 1500ms preset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… path Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… manual migrations The lower bound (current < since) on selectEntries encoded a guess: that a consumer already applied every codemod behind their version. Nothing records that — bumping a dependency and running this tool are two separate acts, and a project can sit on a version for months without ever running upgrade. For a codemod that guess buys nothing: idempotency.test.ts proves re-running an applied codemod is a no-op. So the gate now drops the lower bound for action: "codemod" (since <= target only), matching how detectCurrentVersion already leans on the same idempotence when it falls back to the lowest version a range allows. The lower bound stays for manual/none migrations, which have no no-op — showing those again would just be noise a person has to re-read and re-judge. upgrade's early "already on X, nothing to do" return no longer skips the codemod pass: under the old gate that was equivalent to "nothing to run" too, but a codemod's lower bound is gone, so a project already on target can still have codemods nothing ever ran. It still skips the manifest write and the install (those genuinely have nothing to do), and its closing summary now reports how many codemods ran and how many changed something, so "N run, 0 changed" reads as confirmation rather than a to-do list. list marks a catch-up codemod (since <= current) apart from a new one, and names how many manual migrations the window hides without listing them (list with no revision still shows the whole catalogue). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nd duplicate header
Four presentation-only fixes, no behaviour change:
- A zero-width range ("from 1.0.9 to 1.0.9") now reads as "nothing newer
than 1.0.9", with a note that the codemods below are catch-up when any
are.
- The catch-up legend is two short sentences (what the mark means, why
re-running is safe) instead of one sentence doing three jobs; the trailing
"(N of M shown here)" is dropped since the counts line above it already
gives the codemod count.
- The hidden-count line no longer reads as contradicting "N by hand" above
it: it now says deprecations aren't windowed by version and stay listed,
unlike the manual migrations it hides.
- `renderList` gets a `header` option (default true); `upgrade`'s by-hand
section passes `header: false` so it no longer prints its own heading
followed by renderList's duplicate one for the same entries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fold the hidden manual migration count into the counts line as a third bullet-separated item (using "manual" to distinguish from "by hand"). Reduce the catch-up legend to one clause naming the version and the reassurance; drop the internal explanation. When nothing is hidden or catch-up, omit those items entirely. Presentation only—no behaviour change, --json stays byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
src/migrations, src/transforms and the pooled fixtures in src/tests/transforms.test.ts and src/tests/idempotency.test.ts held one migration's material together only by a shared id spread across three directories. Move it under one directory per id instead: src/migrations/<id>/entry.md frontmatter + guide prose src/migrations/<id>/transform.ts the codemod (9 of 22 ids) src/migrations/<id>/transform.test.ts its fixtures + idempotency case to-remote-package has no catalogue entry (a port, not a migration), so it moved to src/tools/to-remote-package.ts with its test alongside it. The idempotency suite used to assert coverage (every transform has a fixture) and run the proof (twice, compare output) in one file. Splitting fixtures per transform meant splitting that too: the proof now lives in each transform's own transform.test.ts, and src/tests/transformCoverage.test.ts (renamed from idempotency.test.ts) is a directory scan that fails when a transform.ts has no transform.test.ts beside it — independent of what any test file claims to do, so a forgotten test file can't hide. catalog.test.ts and remoteScope.test.ts re-expressed their bidirectional entry/transform checks over the new paths; both got simpler since entry and transform are now siblings. Updated alongside: readCatalog, runCodemod's transform resolution, runTransform's test helper, package.json's files (ships only transform.ts files, not entry.md or tests — verified with npm pack --dry-run), tsconfig.build.json's exclude list, vitest.config.ts's include globs, and AGENTS.md. No behaviour change: migrations.generated.ts and MIGRATION.md rebuild byte-identical. Test count is unchanged at 237 (across 28 files, up from 20, as the two pooled fixture files split one-per-transform). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erministic The counts sat above the entries, where a 22-entry list scrolls them out of sight before the reader reaches the end. Split renderList's header into a top context (range, catch-up legend) and a bottom summary (counts, hidden count) that now follows the last entry. `header` is renamed to `frame` since it gates both halves; upgrade passes `frame: false` to suppress both, since it already prints its own heading and its own aggregate. Also fixes a determinism bug found while checking the above: renderList claimed colour was driven only by its `color` parameter, but `actions` and `painter` read colour functions off picocolors' module-level default export, which auto-detects a TTY and silently disables itself off one -- the decision was made twice, once by `color` and once by picocolors, and they could disagree. `src/tests/list.test.ts` > "colour only when asked for" passed under the nx target (something sets FORCE_COLOR) and failed under a bare `vitest run`. Fixed by building the palette per call from `colors.createColors(color)`, which forces colour on/off instead of sniffing it -- `color` is now the only input. `env -u FORCE_COLOR corepack pnpm vitest run` now passes (240/240). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…where
selectEntries gated a manual/no-code-change entry on current < since,
on the assumption a consumer already applied everything behind their
version. That was a guess about work already done, not a fact —
nothing records which migrations a project has performed. Dropping
the same guess for codemods (idempotent, verified by
transformCoverage.test.ts) but keeping it for manual entries was
inconsistent, and it hid exactly the migrations a consumer who never
ran this tool needed to see: for them, the manual steps are as
undone as the codemods.
The gate is now one rule for every entry: since <= target. current no
longer takes part in selection at all — it still flows into list and
upgrade for the catch-up mark, extended from codemods to every entry.
The mark replaces the hiding: a reader sees what's new and what's
catch-up ("may already be done", never "already done") instead of a
hidden count. The hidden-count helper and its summary line are gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unwrapped it ran to 150 characters and broke hard in any terminal narrower than that — the one line in the output that ignored the width. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The co-location commit invalidated the path `src/migrations/*.md`, which four places still named — two of them generators, so the wrong path was reaching MIGRATION.md and migrations.generated.ts, where an agent looks to find the source. CONTRIBUTE.md still told contributors to ship "a MIGRATION.md entry". That file is generated; the entry goes into the catalogue. The consumer front doors (root README, the components README/USAGE, the remote package's USAGE) named the guide but not the command that applies the codemods in it, and the Align docs page said "use the codemod from MIGRATION.md" where the id now gives an exact invocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page got you installed and stopped there — it named neither the upgrade command nor the versioning guarantees, so the one guide that explains what a version bump does to your code was reachable only through the navigation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9e1473e to
081cb1f
Compare
…subpath
Two findings from a real consumer upgrade.
A `color` value hidden in a ternary was left behind. The three transforms that
rewrite a literal prop value all skipped anything dynamic, which their
docblocks called out but which a real codebase hits immediately —
`color={hasApiToken ? "secondary" : "accent"}` needed hand work twice in one
upgrade. They now rewrite every position whose value can reach the prop: the
expression itself, both ternary branches, the operands of `??`/`||`, and the
right operand of `&&` (its left operand only surfaces when falsy, and a
non-empty string never is). A lookup key stays, and so does a value the
transform cannot see into.
AccentBox needs a stricter rule than the other two, because it decides per
attribute rather than per literal: it renames only when every value position is
a literal and they all fall on the same side of the background/content split.
A mix has no single right answer and keeps `color`.
The renamed password-tools subpath was missing from the catalogue entirely.
`0.2.0-alpha.913` renamed the export to `/password-tools` and
`0.2.0-alpha.1000` reverted it, so code written inside that window fails to
compile with TS2307 — no deprecation, no fallback. Its transform is guarded by
a test asserting the path it writes is one the package really exports and the
path it rewrites away from is not.
The helpers are duplicated per transform on purpose: the CLI loads
src/migrations/<id>/transform.ts straight out of the published package, which
ships nothing else, so a shared module would pass every local test and crash
for consumers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pgrade-cli-f87427
…pgrade-cli-f87427
The `unit` job failed on my new path guard with an ENOENT on `packages/remote-react-components/src/auto-generated/index.ts` — a file that is committed and present at checkout. The cause is a race, not a missing file. `generateRemoteComponents.ts` calls `jetpack.remove()` on the whole `auto-generated` directory and writes `index.ts` back only after generating every component file, so the file is gone for as long as that loop runs. nx schedules this package's `test:unit` concurrently with `components:build`, and `tests/remoteScope.ts` read that file at module scope — putting the window in front of every test file that imports the module. The pre-existing importer got lucky; mine doubled the exposure and hit it. Two changes. The guard now reads `packages/components/package.json` directly: its subject is that package's `exports` map, the manifest is committed, and no generator touches it. And `remoteExports` is read on demand instead of at import time, so only the one test that needs the remote surface is exposed. Verified by removing the generated file and re-running: the guard passes, and the failure is confined to `remoteScope.test.ts` as test failures rather than an import-time crash that takes a whole file down. The generator's non-atomic regeneration is untouched — it is a repo-wide race that belongs in its own change, not in a codemods PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four claims in ADR 0006 described the draft, not the result. The entry fields: detect and verify were built and then cut, so "every entry carries detect, apply and verify" was false, and so was the consequence that rested on it. Only apply remains, plus a codemod where the change is mechanically decidable. The cut is now recorded as a non-goal with its two reasons, so nobody re-proposes it without solving the part that failed. The command surface: list takes a revision, not --from/--to. And upgrade runs every codemod up to the target, not "the crossed range" — selectEntries filters on `lte(entry.since, target)` alone, deliberately, because nothing records what a project has already done. AGENTS.md carried the same detect/apply/verify claim and is corrected with it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Dropping the gate's lower bound left the wording behind in nine places. "Runs the codemods the crossed version range calls for" describes a range with two ends; selectEntries has one — `since <= target`, deliberately, because nothing records which migrations a project already performed. A reader following the old wording would not expect the older entries the command actually offers, and would read them as a bug. The same sentence had spread through the root README, the ADR's consequences, the codemods README twice, the generated guide's intro, and both German docs pages. The guide and the versioning page now also say why older entries appear, since that is where a consumer meets them. Two more claims were wrong rather than vague. The source-path default is `./src` when that directory exists and only otherwise the project root — worth stating, because an unrelated `src/` beside the real sources silently wins. And the exit codes omitted two refusals the CLI has: an unknown id, and an id whose migration has no codemod. Finally the "adding a migration" checklist: it said to add `targets` in remoteScope.test.ts only when the migration applies to the remote package. Every catalogue id has to be listed there. Following the old step is what broke the unit job two commits ago, so the step now says so, including the `[]` + notNameScoped case for a layout entry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two claims in the repo's guides described a state that no longer exists. CONTRIBUTE.md told a contributor to create four files per component doc page — index.mdx plus overview.mdx, guidelines.mdx and develop.mdx. #2730 deleted 256 of those files and consolidated everything onto index.mdx; none of the three exist anywhere in the repo any more. Following the instruction produced three files the docs app does not render. apps/docs/README.md and apps/docs/AGENTS.md already described the consolidated layout, so CONTRIBUTE.md was the only one left behind — which is the worst place for it, being what a new contributor reads first. The cross-version claim was inverted by #3006. Both this package's CONTRIBUTE.md and the root AGENTS.md footgun row said the harnesses run on a schedule and "not automatically on pull requests". The iframe harness now runs on every PR as the `cross-version` job in test.yml, gated on nx affected and installing the published versions from the registry. Only the in-process harness and the full per-version matrix are schedule- or label-only. The label advice survives, but its premise had to change: it is no longer "nothing runs on the PR". Also: `pnpm test` runs test:links too, which its comment did not mention. Verified mechanically alongside this: every backticked path in all 32 AGENTS/README/CONTRIBUTE/USAGE files resolves, every documented `pnpm nx` target exists against nx's own project graph, and every identifier they name appears in the source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pgrade-cli-f87427
…pgrade-cli-f87427
|
Reviewed by checking out the branch: build clean, The architecture is right — catalogue as single source, generated 1. Selection has no lower bound, so import "@mittwald/flow-react-components/all-layered.css"; // → import "@mittwald/flow-react-components"; (stylesheet silently dropped)
import { Rule } from ".../mittwald-password-tools-js"; // → from "@mittwald/flow-react-components";
import { Button } from ".../flr-universal"; // → from "@mittwald/flow-react-components";
import { something } from ".../internal"; // → from "@mittwald/flow-react-components";Default imports also become named specifiers. On a clean 1.0.0 project whose only Flow line is a correct Idempotency doesn't cover this. It proves "running the same transform twice changes nothing", not "an era-specific transform is harmless against code from a later era". Either era-scoped entries need an upper bound ( 2. The run order lets one migration cancel another.
The root doesn't export 3. First publish of a new package name under trusted publishing.
Should still go in
Smaller
The error messages are genuinely good — |
Review found that dropping the gate's lower bound turned an era-specific transform into a corrupter of correct code, and I reproduced all of it. `imports-to-package-root` (since alpha.28) collapsed 94 subpath exports onto the package root, and it did that with a catch-all `else`. With no lower bound it reaches every 1.x project, where five of today's nine subpaths were collateral: `all-layered.css` became a JS import with the stylesheet silently gone, and `Rule` from `mittwald-password-tools-js` moved onto a root that does not export it. A default import became a named specifier along the way. It also cancelled another migration. Sorted by `since` it runs first, so it flattened `…/password-tools` before `password-tools-subpath-renamed` — the entry whose whole job is that path — could see it. Both then reported success while the file no longer compiled. Both have one cause, so one fix: an explicit leave-alone set. It holds every subpath the package still exports, plus `password-tools`, which another entry owns. Era code still migrates — verified both directions. Two independent tests pin it, one comparing the set against the real `exports` map so a new subpath cannot quietly become collateral again; both fail if a single entry is removed. Idempotency could never have caught this, and a comment in manifest.ts leaned on it as if it could. It proves a second pass over a transform's own output changes nothing — not that an era-specific transform is harmless against later code. Also from the review: - The dirty-tree guard treated every git exit 128 as "not a repository", so `detected dubious ownership` — the minimal CI container the guard exists for — read as a clean tree. It now matches the message, with `LC_ALL=C` pinning it. - `peerDependencies` were rewritten. A peer range states what a package supports, not what it installs; narrowing `^1.0.0` to `^1.0.14` is a breaking change to someone else's package made by a command they ran on their own. They are now reported and left, through one reporter `--dry` shares. - `engines.node` was `>=24.0.0` on a CLI consumers reach through `npx`. The only thing needing 24 was an import attribute for reading the version; that is a `readFileSync` now, and the floor is `>=22.0.0`. - `components-src` included the `MIGRATION.md` that `codemods:build` writes, with no edge between the projects, so its hash depended on task order. Excluded. - Ctrl+C at the codemod prompt rejected out of `runUpgrade` after the bump and install, showing an inquirer trace instead of what had happened. It now reports that and runs nothing. `@inquirer/prompts` also loads lazily, so `list` no longer pays for it. - `list --json` was a bare array that could not say which range it described — on the 1.x line `list` and `list minor` select the same entries. It now carries `range` and per-entry `catchUp`. This replaces a test that pinned the bare array; that test recorded the old shape, it did not defend it. - `kind`'s docblock still described the selection it once drove. It is descriptive now, and says so. Not fixed here, because it is not in the repo: `@mittwald/flow-codemods` does not exist on npm, and npm binds one Trusted Publisher per package. The first publish needs an npm-side bootstrap or `lerna publish from-package` aborts mid-release with the packages ahead of it already out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reproduced all three blockers before touching anything. 1 and 2 are fixed in 1 + 2 have one cause, so one fix. Both come from Two independent tests pin it, one comparing the set against the real You're right that idempotency could never have caught this — and worse, a comment in 3 stands and is yours. Confirmed: Fixed from the rest: the git One correction. CI runs Node 24, not 22 — On the out-of-scope three: all deliberate, and two exist because of this PR's timeline rather than despite it. The cross-version row and the CONTRIBUTE docs section were both wrong when I found them — #3006 landed the PR-gated iframe harness under this branch's feet and left the "not on pull requests" claim behind, and #2730's tab consolidation left CONTRIBUTE telling contributors to create three files that no longer exist. Happy to split them into their own PR if you'd rather review them separately; Comment density: agreed, and Not addressed, by choice: that 🤖 Addressed by Claude Code |
…pgrade-cli-f87427
… read The `unit` job failed on the cancel case I added with the review fixes. It passed here and failed on the runner for one reason: `createChoose` read `process.env.CI` inside itself, underneath the parameters the caller passes. On a runner `CI` is set, so the prompt never ran, so it never rejected, so the cancel path the test asserts was unreachable. That is the second time a hidden environment read under an explicit parameter has produced a green local suite and a red CI one — picocolors self-disabling beneath the `color` argument was the first. So `isCI` is an input now, stated by `cli.ts` where reading the environment is the job, and TypeScript makes every call site name it. Two things fell out. The behaviour that read gave — a TTY on an unattended runner must not prompt — was untested, because no test could reach it; it has a case now. And the one existing test that wanted the prompt shown had to save, delete and restore `process.env.CI` around itself, with a comment explaining the workaround. That scaffolding is gone, and no test in this package touches `process.env` any more. Verified both ways: 251 tests with `CI=true` and with `CI` unset. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pgrade-cli-f87427
Publishes
@mittwald/flow-codemodsas a CLI and turns the migration guide into generated output of a machine-readable catalogue.What this adds
upgradebumps every Flow dependency to a resolved target, installs, runs the codemods the crossed version range calls for, and ends by listing the migrations no codemod covers.revisionispatch|minor|major| a dist-tag | an exact version, defaultminor. The keyword bounds the target; the entry set then falls out of an exact-version gate against it (see below). Keyword resolution excludes prereleases, soupgrade minornever drifts onto thenextline — only an explicit dist-tag or version reaches a-next.N.It refuses to run on a dirty working tree unless
--allow-dirty, because-yis implied when stdin is not a TTY (CI, agent runs) and an unattended run would otherwise blend its own uncommitted work into the codemod diff.--drywrites nothing and installs nothing.listchanges nothing, and has two forms. Bare, it prints the whole catalogue — offline, with no manifest and no network, so it answers in an empty directory. Given a revision, it reads the current version frompackage.jsonand shows exactly the rangeupgrade <revision>would act on, which makes it a dry run of the upgrade rather than a separate thing to learn.--jsonfor an agent.It takes a revision rather than
--from/--tobecause--fromasks for something the project already knows.The catalogue
packages/codemods/src/migrations/<id>/entry.mdis now the single source for all 23 consumer migrations.packages/components/MIGRATION.md— which ships in theflow-react-componentstarball — andsrc/migrations.generated.tsare generated from it, so the guide and the data cannot drift apart.Each entry carries the instruction as prose (
apply) plus, where the change is mechanically decidable, a codemod. That is 10 of the 23. The other 13 are executable by hand or by an agent from the instruction alone, without reading 18 KB of guide.Two orthogonal fields decide when an entry applies and what it costs:
kindmigration·deprecationactioncodemod·manual·nonenonecovers a behaviour change needing no edit, which an agent has to be told or it keeps lookingThe
deprecationhalf matters more than it looks: ADR 0005 §4 leaves type-level changes outside the semver guarantee, so they may ship in a patch. Without it,liston the 1.x line would return nothing.The gate is one rule
An entry is selected when
since <= target. That is all of it —currentplays no part in selection.It used to carry a lower bound (
current < since) for entries needing a person, on the reasoning that you had already applied what lay behind you. Nothing records which migrations a project has performed, so that was a guess — and a bad one here, because this tool did not exist until now. On the 1.x line every catalogue entry predates1.0.0, so a lower bound madelistandupgradereport almost nothing to precisely the people who had never migrated.currentstill decides how an entry is presented: each is marked new in this range or catch-up (shipped at or before your version, so you may already have done it). Re-running a codemod over its own output is a verified no-op —transformCoverage.test.tsrequires atransform.test.tsbeside every transform, and each of the 10 carries a "running it twice changes nothing" case. That is not the same as being harmless against code from a later era, which is a per-transform property:imports-to-package-rootneeded an explicit leave-alone set to earn it (see review thread). A manual step marked catch-up needs the reader's own judgement, and the legend says so rather than implying it is done.upgradereports how many codemods actually changed something. "10 codemods run, 0 changed anything" is itself the confirmation that nothing was outstanding.Groundwork for an AI-assisted update
This is a step towards a migration an agent can carry out, not only a human. That intent explains several choices that otherwise look incidental:
applyis an instruction, not a description. 13 of the 23 entries have no codemod, and today they are 18 KB of prose a reader has to search. As a field they are actionable per entry, so an agent can work through them without reading the guide.list [revision] --jsonis read-only. It answers "what does this range require of me" against the consumer's own manifest, with no writes, no install and no network beyond the registry lookup. That gives an agent a way to plan before it changes anything.upgradeends by naming what no codemod covered. A codemod is not the whole migration, and a command that stayed silent about the remainder would read as "done".-yis implied when stdin is not a TTY, so an agent run needs no flag — and the dirty-tree refusal exists precisely because of that: unattended, it would otherwise blend its own uncommitted work into the codemod diff with no way to separate them afterwards.Nothing here is agent-specific, and none of it is a finished feature. It is the data and the seams such a thing needs.
One transform is not a migration
to-remote-package(formerlyflowRemote) ports an app onto@mittwald/flow-remote-react-components. No version range calls for it, so it has no catalogue entry and appears in neither the guide norlist— it is run deliberately, by id, and documented in the package README.catalog.test.tsnames it as the one commented exception to its otherwise exact transform-set invariant.What this removes
The raw-GitHub-URL delivery path, and with it the self-contained-transform rule, the composite bundler and
src/composites. Transform files are renamed to their dashed catalogue ids.URLs printed in already-published
MIGRATION.mdcopies now 404. Accepted deliberately: doc URLs are not covered by the semver contract (ADR 0005), and those consumers are better served bynpx @mittwald/flow-codemods@latest upgrade, which works from any old version because npx always fetches the current CLI.Not a breaking change under ADR 0005 — the package was
private: true, so no published package API changed.Supersedes #2942
Its transform suite, test harness,
remoteScope.tsand the migration prose it added are adopted here; they were reviewed and green. Its composite bundler is not, because the CLI runs codemods individually and reports per codemod. Close #2942 rather than merging it — merging it after this lands would conflict with every rename.Worth knowing while reviewing
upgraderesolves the target from the intersection of what every declared Flow dependency has published, not from one of them. Lerna publishes packages one at a time, so during a release the versions diverge for the length of the run — around 25 minutes, measured. The window is narrow and self-healing, but it is exactly when someone runsupgrade. Documented for users in Document the publish window: Flow versions diverge for minutes during a release #2974.Runnerin process rather than its CLI. Scraping the CLI's text summary is unsafe:--printwrites the transformed source before the summary, so a source comment like// 42 okbeat the regex reading the count.flow-react-componentsdeclaresflow-icons-proas a peer at an exact version. Bumping one alone is a peer conflict. The package list is generated from the workspace, not hand-picked.Out of scope
packages/ext-bridge/MIGRATION.mdkeeps its own hand-written guide.detect/verifychecks were built and taken back out — not solid enough to ship here, and better delivered on their own.Verification
pnpm buildclean,git diff --exit-codeclean,pnpm lintclean, 250 tests.upgradeverified end to end against the real npm registry, including the dirty-tree refusal,--dry, and that codemods no longer reach into a freshly installednode_modules.