@mittwald/flow-codemods 1.1.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +106 -0
  2. package/dist/catalog/entries.d.ts +17 -0
  3. package/dist/catalog/entries.d.ts.map +1 -0
  4. package/dist/catalog/entries.js +9 -0
  5. package/dist/catalog/select.d.ts +28 -0
  6. package/dist/catalog/select.d.ts.map +1 -0
  7. package/dist/catalog/select.js +27 -0
  8. package/dist/catalog/types.d.ts +48 -0
  9. package/dist/catalog/types.d.ts.map +1 -0
  10. package/dist/catalog/types.js +0 -0
  11. package/dist/checks/context.d.ts +16 -0
  12. package/dist/checks/context.d.ts.map +1 -0
  13. package/dist/checks/context.js +72 -0
  14. package/dist/checks/load.d.ts +13 -0
  15. package/dist/checks/load.d.ts.map +1 -0
  16. package/dist/checks/load.js +53 -0
  17. package/dist/checks/types.d.ts +49 -0
  18. package/dist/checks/types.d.ts.map +1 -0
  19. package/dist/checks/types.js +0 -0
  20. package/dist/cli/args.d.ts +22 -0
  21. package/dist/cli/args.d.ts.map +1 -0
  22. package/dist/cli/args.js +56 -0
  23. package/dist/cli/choose.d.ts +31 -0
  24. package/dist/cli/choose.d.ts.map +1 -0
  25. package/dist/cli/choose.js +31 -0
  26. package/dist/cli/codemod.d.ts +24 -0
  27. package/dist/cli/codemod.d.ts.map +1 -0
  28. package/dist/cli/codemod.js +76 -0
  29. package/dist/cli/detect.d.ts +35 -0
  30. package/dist/cli/detect.d.ts.map +1 -0
  31. package/dist/cli/detect.js +62 -0
  32. package/dist/cli/list.d.ts +71 -0
  33. package/dist/cli/list.d.ts.map +1 -0
  34. package/dist/cli/list.js +293 -0
  35. package/dist/cli/upgrade.d.ts +34 -0
  36. package/dist/cli/upgrade.d.ts.map +1 -0
  37. package/dist/cli/upgrade.js +217 -0
  38. package/dist/cli/verify.d.ts +77 -0
  39. package/dist/cli/verify.d.ts.map +1 -0
  40. package/dist/cli/verify.js +148 -0
  41. package/dist/cli.d.ts +3 -0
  42. package/dist/cli.d.ts.map +1 -0
  43. package/dist/cli.js +117 -0
  44. package/dist/detect/align-to-combine.d.ts +8 -0
  45. package/dist/detect/align-to-combine.d.ts.map +1 -0
  46. package/dist/detect/align-to-combine.js +9 -0
  47. package/dist/detect/overlay-controller-add-on-close-return-type.d.ts +9 -0
  48. package/dist/detect/overlay-controller-add-on-close-return-type.d.ts.map +1 -0
  49. package/dist/detect/overlay-controller-add-on-close-return-type.js +10 -0
  50. package/dist/detect/tooltip-trigger-delay-type.d.ts +11 -0
  51. package/dist/detect/tooltip-trigger-delay-type.d.ts.map +1 -0
  52. package/dist/detect/tooltip-trigger-delay-type.js +12 -0
  53. package/dist/flowPackages.generated.d.ts +3 -0
  54. package/dist/flowPackages.generated.d.ts.map +1 -0
  55. package/dist/flowPackages.generated.js +18 -0
  56. package/dist/git.d.ts +13 -0
  57. package/dist/git.d.ts.map +1 -0
  58. package/dist/git.js +41 -0
  59. package/dist/install.d.ts +37 -0
  60. package/dist/install.d.ts.map +1 -0
  61. package/dist/install.js +71 -0
  62. package/dist/manifest.d.ts +45 -0
  63. package/dist/manifest.d.ts.map +1 -0
  64. package/dist/manifest.js +88 -0
  65. package/dist/migrations.generated.d.ts +4 -0
  66. package/dist/migrations.generated.d.ts.map +1 -0
  67. package/dist/migrations.generated.js +213 -0
  68. package/dist/resolve/range.d.ts +50 -0
  69. package/dist/resolve/range.d.ts.map +1 -0
  70. package/dist/resolve/range.js +173 -0
  71. package/dist/resolve/registry.d.ts +33 -0
  72. package/dist/resolve/registry.d.ts.map +1 -0
  73. package/dist/resolve/registry.js +69 -0
  74. package/dist/resolve/target.d.ts +63 -0
  75. package/dist/resolve/target.d.ts.map +1 -0
  76. package/dist/resolve/target.js +115 -0
  77. package/dist/run/jscodeshift.d.ts +42 -0
  78. package/dist/run/jscodeshift.d.ts.map +1 -0
  79. package/dist/run/jscodeshift.js +88 -0
  80. package/dist/tsconfig.tsbuildinfo +1 -0
  81. package/dist/verify/align-to-combine.d.ts +11 -0
  82. package/dist/verify/align-to-combine.d.ts.map +1 -0
  83. package/dist/verify/align-to-combine.js +22 -0
  84. package/dist/verify/overlay-controller-add-on-close-return-type.d.ts +14 -0
  85. package/dist/verify/overlay-controller-add-on-close-return-type.d.ts.map +1 -0
  86. package/dist/verify/overlay-controller-add-on-close-return-type.js +21 -0
  87. package/dist/verify/tooltip-trigger-delay-type.d.ts +13 -0
  88. package/dist/verify/tooltip-trigger-delay-type.d.ts.map +1 -0
  89. package/dist/verify/tooltip-trigger-delay-type.js +18 -0
  90. package/package.json +41 -0
  91. package/src/migrations/accent-box-color-to-background-color/transform.ts +218 -0
  92. package/src/migrations/action-prop-to-on-action/transform.ts +110 -0
  93. package/src/migrations/align-to-combine/transform.ts +212 -0
  94. package/src/migrations/button-color-accent-to-success/transform.ts +164 -0
  95. package/src/migrations/button-props-interfaces/transform.ts +229 -0
  96. package/src/migrations/color-primary-to-default/transform.ts +173 -0
  97. package/src/migrations/imports-to-package-root/transform.ts +107 -0
  98. package/src/migrations/muted-action-error-to-abort-action-error/transform.ts +260 -0
  99. package/src/migrations/password-tools-rule/transform.ts +219 -0
  100. package/src/migrations/password-tools-subpath-renamed/transform.ts +61 -0
  101. package/src/tools/to-remote-package.ts +37 -0
@@ -0,0 +1,217 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { isAbsolute, relative } from "node:path";
3
+ import { gt } from "semver";
4
+ import { allEntries } from "../catalog/entries.js";
5
+ import { selectEntries } from "../catalog/select.js";
6
+ import { flowPackages } from "../flowPackages.generated.js";
7
+ import { hasUncommittedChanges } from "../git.js";
8
+ import { detectPackageManagerIn, runInstall, } from "../install.js";
9
+ import { applyTarget, isWritable } from "../manifest.js";
10
+ import { fetchVersions } from "../resolve/registry.js";
11
+ import { readInstalledVersion, resolveRange } from "../resolve/range.js";
12
+ import { runCodemod } from "../run/jscodeshift.js";
13
+ import { resolveSourcePath } from "./codemod.js";
14
+ import { renderList } from "./list.js";
15
+ export const defaultUpgradeDeps = (cwd) => ({
16
+ cwd,
17
+ fetchVersions,
18
+ install: runInstall,
19
+ runCodemod,
20
+ choose: async (entries) => entries,
21
+ isDirty: hasUncommittedChanges,
22
+ readInstalledVersion,
23
+ log: (message) => process.stdout.write(`${message}\n`),
24
+ });
25
+ /**
26
+ * Whether `path` lies outside `cwd`.
27
+ *
28
+ * `--path` can point anywhere, including outside the repository the dirty-tree
29
+ * guard checks. `path` is always resolved (see `resolveSourcePath`), so this is
30
+ * a plain prefix test via `relative` rather than a string comparison that a
31
+ * trailing slash or `..` segment could fool.
32
+ */
33
+ const isOutside = (path, cwd) => {
34
+ const rel = relative(cwd, path);
35
+ return rel !== "" && (rel.startsWith("..") || isAbsolute(rel));
36
+ };
37
+ /**
38
+ * The indentation the manifest already uses, read from its first indented line.
39
+ *
40
+ * Reusing it (instead of always emitting 2 spaces) keeps the rewrite to the
41
+ * dependency lines a consumer actually cares about — a tabs- or 4-space project
42
+ * otherwise gets its whole `package.json` reformatted inside the upgrade diff.
43
+ */
44
+ const detectIndent = (raw) => {
45
+ const match = /\n([ \t]+)\S/.exec(raw);
46
+ return match?.[1] ?? " ";
47
+ };
48
+ /**
49
+ * Bump every Flow dependency, install, then run the codemods the resolved range
50
+ * calls for — and end by naming what no codemod covers.
51
+ *
52
+ * The order is not cosmetic: the codemods run against the installed target, so
53
+ * `tsc` can be green when the command returns. `--dry` skips both the manifest
54
+ * write and the install, so the codemods it still runs act against whatever is
55
+ * currently installed rather than the target — their output is indicative, not
56
+ * exact.
57
+ *
58
+ * Selection has no lower bound at all (see `selectEntries`) — every entry with
59
+ * `since <= target` is in, codemod or manual — so a project already on `target`
60
+ * still gets a full pass: it skips only the write and the install, which
61
+ * genuinely have nothing to do.
62
+ */
63
+ export const runUpgrade = async (parsed, deps) => {
64
+ const { cwd, log } = deps;
65
+ const dry = parsed.dry === true;
66
+ // Resolved before the dirty-tree guard below: an explicit --path can point
67
+ // outside the repository the guard would otherwise check, and codemods
68
+ // rewrite files in place there just the same.
69
+ const path = resolveSourcePath(parsed.path, cwd);
70
+ const outsideRepo = isOutside(path, cwd);
71
+ if (!parsed.allowDirty &&
72
+ (deps.isDirty(cwd) || (outsideRepo && deps.isDirty(path)))) {
73
+ log("The working tree has uncommitted changes. Codemods rewrite files in place, so commit or stash first — or pass --allow-dirty.");
74
+ return 1;
75
+ }
76
+ const { revision } = parsed;
77
+ if (revision === undefined) {
78
+ // args.ts always defaults this for the "upgrade" command — this only
79
+ // guards a ParsedCommand built by hand without it, so it is not a second
80
+ // source of truth for the default.
81
+ log("No revision given.");
82
+ return 1;
83
+ }
84
+ const range = await resolveRange(revision, deps);
85
+ if (!range.ok) {
86
+ log(range.reason);
87
+ return 1;
88
+ }
89
+ const { manifestPath, manifestRaw, manifest, dependencies, current, target } = range;
90
+ // A stale dist-tag or an exact version at or below `current` resolves
91
+ // without complaint — `resolveRange` deliberately does not judge that
92
+ // (`list` treats the same fact as a legitimate answer). There is nothing to
93
+ // bump or install in that case, but that does not mean there is nothing to
94
+ // run: selection has no lower bound (see `selectEntries`). A consumer can
95
+ // already be sitting on `target` having never run this tool once, which is
96
+ // the exact gap this command exists to close, so the migration pass below
97
+ // still runs; only the bump and the install are skipped.
98
+ // One reporter for both branches: a peer range is reported, never rewritten
99
+ // (see `writableFields` in manifest.ts), and `--dry` must say the same thing
100
+ // the real run does.
101
+ const reportDependencies = () => {
102
+ for (const dependency of dependencies) {
103
+ log(isWritable(dependency)
104
+ ? ` ${dependency.name} → ${target}`
105
+ : ` ${dependency.name} ${dependency.range} — left as it is, ${dependency.field} states what your package supports, not what it installs`);
106
+ }
107
+ };
108
+ const bump = gt(target, current);
109
+ if (!bump) {
110
+ // Nothing to write or install — fall through to the codemod pass below.
111
+ log(`Already on ${current}; "${revision}" resolves to ${target}. No dependency bump needed — checking for codemods to catch up on, since nothing records whether this project already ran them.`);
112
+ }
113
+ else if (dry) {
114
+ log(`Upgrading Flow from ${current} to ${target} (--dry)`);
115
+ log(`--dry: would write the following to ${manifestPath}:`);
116
+ reportDependencies();
117
+ log("--dry: skipping the install.");
118
+ }
119
+ else {
120
+ log(`Upgrading Flow from ${current} to ${target}`);
121
+ const indent = detectIndent(manifestRaw);
122
+ writeFileSync(manifestPath, `${JSON.stringify(applyTarget(manifest, target, flowPackages), null, indent)}\n`, "utf8");
123
+ reportDependencies();
124
+ const manager = detectPackageManagerIn(cwd);
125
+ log(`Installing with ${manager}`);
126
+ try {
127
+ deps.install(manager, cwd);
128
+ }
129
+ catch (error) {
130
+ log(`The dependency bump was written but the install failed, so package.json is on\n${target} while node_modules still holds ${current}.\n\nEither re-run this command once the install works, or undo the bump with\n git checkout package.json\n\n${error instanceof Error ? error.message : error}`);
131
+ return 1;
132
+ }
133
+ }
134
+ const selected = selectEntries(allEntries, target);
135
+ const automatic = selected.filter((entry) => entry.action === "codemod");
136
+ const byHand = selected.filter((entry) => entry.action !== "codemod");
137
+ const chosen = await deps.choose(automatic);
138
+ // Only meaningful once a bump actually moves the installed version away
139
+ // from `target` — when there is no bump, the codemods below run against
140
+ // exactly `target` already, so there is no "not exact" caveat to raise.
141
+ if (dry && bump && chosen.length > 0) {
142
+ log(`\n--dry: running the codemods against the currently installed version, not ${target} — their output is indicative, not exact.`);
143
+ }
144
+ let hadFailure = false;
145
+ const incomplete = [];
146
+ let ranCount = 0;
147
+ let changedCount = 0;
148
+ for (const entry of chosen) {
149
+ ranCount += 1;
150
+ let result;
151
+ try {
152
+ result = await deps.runCodemod({
153
+ id: entry.id,
154
+ path,
155
+ dry: parsed.dry,
156
+ print: parsed.print,
157
+ });
158
+ }
159
+ catch (error) {
160
+ hadFailure = true;
161
+ incomplete.push(entry.id);
162
+ log(` ${entry.id}: failed to run — ${error instanceof Error ? error.message : error}`);
163
+ continue;
164
+ }
165
+ if (result.changed > 0) {
166
+ changedCount += 1;
167
+ }
168
+ // Same three-way distinction as the single-codemod command: "0 changed" on
169
+ // its own would read as success where nothing was looked at, or where the
170
+ // transform declined everything it saw.
171
+ if (result.errors > 0) {
172
+ hadFailure = true;
173
+ log(` ${entry.id}: ${result.errors} file(s) failed to transform`);
174
+ }
175
+ else if (result.processedNothing) {
176
+ hadFailure = true;
177
+ log(` ${entry.id}: no files under ${path} were processed`);
178
+ }
179
+ else if (result.changed === 0 && result.skipped > 0) {
180
+ hadFailure = true;
181
+ log(` ${entry.id}: declined all ${result.skipped} file(s) it looked at`);
182
+ }
183
+ else {
184
+ log(` ${entry.id}: ${result.changed} file(s) changed`);
185
+ }
186
+ }
187
+ // Selection has no lower bound (see `selectEntries`), so this loop can run
188
+ // every codemod in the catalogue on a project that never crossed a version
189
+ // at all — "N run, 0 changed" is the confirmation that there was nothing to
190
+ // catch up on, not a list of new work.
191
+ if (ranCount > 0) {
192
+ log(`\n${ranCount} codemod${ranCount === 1 ? "" : "s"} run, ${changedCount} changed something.`);
193
+ }
194
+ if (byHand.length > 0) {
195
+ log(`\n${byHand.length} migration(s) in this range have no codemod — apply them by hand:\n`);
196
+ // `frame: false` — this already printed the heading above and, further
197
+ // down, its own aggregate ("N codemods run, N changed something");
198
+ // renderList's own frame (the range/legend on top, the counts at the
199
+ // bottom) would just repeat both for the same list. `range` is still
200
+ // passed so each entry gets its catch-up mark — a manual entry that
201
+ // shipped at or before `current` may already be done, and the mark is
202
+ // the only thing left that says so now that hiding it is gone.
203
+ log(renderList({
204
+ entries: byHand,
205
+ range: { from: current, to: target },
206
+ json: false,
207
+ frame: false,
208
+ }));
209
+ }
210
+ else {
211
+ log("\nNo migration in this range required a change by hand.");
212
+ }
213
+ if (incomplete.length > 0) {
214
+ log(`\n${incomplete.length} codemod(s) did not complete: ${incomplete.join(", ")}`);
215
+ }
216
+ return hadFailure ? 1 : 0;
217
+ };
@@ -0,0 +1,77 @@
1
+ import { type CatalogEntry } from "../catalog/entries.js";
2
+ import { loadVerifier } from "../checks/load.js";
3
+ import type { VerifyResult } from "../checks/types.js";
4
+ export interface VerifyEntryResult {
5
+ entry: CatalogEntry;
6
+ result: VerifyResult;
7
+ }
8
+ export interface RunVerifyDeps {
9
+ /** Which entries to try. Defaults to the whole catalogue. */
10
+ entries?: CatalogEntry[];
11
+ /** Injectable for tests — defaults to the real module loader. */
12
+ load?: typeof loadVerifier;
13
+ }
14
+ /**
15
+ * Runs every available verifier over `path`.
16
+ *
17
+ * Only a fraction of the catalogue has a verifier module so far (see
18
+ * `checks/load.ts`); an id with none is skipped, not reported as a failure —
19
+ * the remaining verifiers are follow-up work, not something this run got
20
+ * wrong.
21
+ */
22
+ export declare const runVerify: (path: string, { entries, load }?: RunVerifyDeps) => Promise<VerifyEntryResult[]>;
23
+ /**
24
+ * Three counts, not one pass/fail verdict — conflating them is exactly the "23
25
+ * entries still need a person" problem this replaces: with every verifier in
26
+ * place, almost all of them would carry at least the typecheck reminder, so
27
+ * counting "has a hint" swamps the entries that actually need a person's
28
+ * judgement with the ones that only need the same routine `tsc --noEmit` run.
29
+ *
30
+ * - `failed` — `ok: false`: this run found something concretely wrong.
31
+ * - `needsReview` — `ok: true` but `hints` is non-empty: nothing this module
32
+ * could decide is wrong, but a person still has to judge something no
33
+ * compiler check can catch.
34
+ * - `verified` — `ok: true` and `hints` is empty: nothing left for a person to
35
+ * judge here (the typecheck below may still apply). Deliberately not
36
+ * "done"/"complete": see `checks/types.ts`'s doc comment on `VerifyResult`
37
+ * for why `ok: true` never means the migration is finished.
38
+ *
39
+ * `typecheckCount` is tallied separately again: it is not a judgement call like
40
+ * `needsReview`, and `tsc --noEmit` is one command a person runs once for the
41
+ * whole project, not once per entry — so it is reported as a single closing
42
+ * line, not folded into any of the three per-entry buckets.
43
+ */
44
+ export declare const summarize: (results: VerifyEntryResult[]) => {
45
+ failed: number;
46
+ needsReview: number;
47
+ verified: number;
48
+ typecheckCount: number;
49
+ };
50
+ /**
51
+ * Renders `runVerify`'s result as text — the presentation half kept separate
52
+ * from the async file-reading half, the same split `renderList` uses.
53
+ *
54
+ * Prints only what needs attention. An entry that is `ok: true` with no hints
55
+ * has nothing left for a person to judge (the closing typecheck line still
56
+ * covers it if it relies on one) — it swells the count in the header and
57
+ * nothing else, the same way `verified` is counted but never itself printed.
58
+ * That is the fix for the four problems the maintainer called out: a passing
59
+ * entry whose only hint was the typecheck reminder no longer earns a block at
60
+ * all, the reminder itself moves out of the loop to one line at the end,
61
+ * hints stop repeating their own label, and findings show a path relative to
62
+ * `path` instead of the absolute one the header already established.
63
+ */
64
+ export declare const renderVerify: (results: VerifyEntryResult[], path: string, { color, width }?: {
65
+ color?: boolean;
66
+ width?: number;
67
+ }) => string;
68
+ /**
69
+ * Whether a `verify` run should exit non-zero: exactly when at least one
70
+ * verifier decided `ok: false`. A hint alone — even every entry carrying one —
71
+ * is the normal "a person still has to look" state, not a failure, so it must
72
+ * never flip this. Kept as its own pure function (over `results`, not
73
+ * `process.exitCode` directly) so a test can drive it with injected fakes
74
+ * instead of spawning the CLI.
75
+ */
76
+ export declare const verifyExitCode: (results: VerifyEntryResult[]) => number;
77
+ //# sourceMappingURL=verify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/cli/verify.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,iEAAiE;IACjE,IAAI,CAAC,EAAE,OAAO,YAAY,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,SAAS,SACd,MAAM,sBACmC,aAAa,KAC3D,OAAO,CAAC,iBAAiB,EAAE,CAa7B,CAAC;AA2DF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,SAAS,YACX,iBAAiB,EAAE,KAC3B;IACD,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CAqBxB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,YACd,iBAAiB,EAAE,QACtB,MAAM,qBACmB;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KACjE,MA8BF,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc,YAAa,iBAAiB,EAAE,KAAG,MACZ,CAAC"}
@@ -0,0 +1,148 @@
1
+ import { relative } from "node:path";
2
+ import colors from "picocolors";
3
+ import { allEntries } from "../catalog/entries.js";
4
+ import { createCheckContext } from "../checks/context.js";
5
+ import { loadVerifier } from "../checks/load.js";
6
+ import { indent, painter, wrap } from "./list.js";
7
+ /**
8
+ * Runs every available verifier over `path`.
9
+ *
10
+ * Only a fraction of the catalogue has a verifier module so far (see
11
+ * `checks/load.ts`); an id with none is skipped, not reported as a failure —
12
+ * the remaining verifiers are follow-up work, not something this run got
13
+ * wrong.
14
+ */
15
+ export const runVerify = async (path, { entries = allEntries, load = loadVerifier } = {}) => {
16
+ const context = createCheckContext(path);
17
+ const results = [];
18
+ for (const entry of entries) {
19
+ const verifier = await load(entry.id);
20
+ if (verifier === undefined) {
21
+ continue;
22
+ }
23
+ results.push({ entry, result: await verifier.verify(context) });
24
+ }
25
+ return results;
26
+ };
27
+ /** One `path:line text` row, path shown relative to the search root. */
28
+ const findingLine = (finding, width, paint, root) => {
29
+ const location = paint.dim(`${relative(root, finding.file)}:${finding.line}`);
30
+ const body = wrap(finding.text, Math.max(width - indent.length, 20));
31
+ return body.map((line, index) => index === 0 ? `${indent}${location} ${line}` : `${indent} ${line}`);
32
+ };
33
+ /**
34
+ * One hint, wrapped and indented under its migration. No repeated label — a
35
+ * hint that wraps to several lines said "hint" once per line before this,
36
+ * which is the label, not the content, taking up the repetition.
37
+ */
38
+ const hintLine = (hint, width) => wrap(hint, Math.max(width - indent.length, 20)).map((line) => `${indent}${line}`);
39
+ /**
40
+ * Renders one migration's block: `!` for `ok: false`, `?` for `ok: true` with
41
+ * a hint — the only two states this function is ever called for, see
42
+ * `renderVerify`'s filter. Findings first, then hints; the `typecheckHint`
43
+ * never appears here — it is one global action, folded into the closing line
44
+ * instead of repeated under every entry that relies on it.
45
+ */
46
+ const renderEntry = ({ entry, result }, width, color, root) => {
47
+ const paint = painter(color);
48
+ const mark = result.ok
49
+ ? color
50
+ ? colors.yellow("?")
51
+ : "?"
52
+ : color
53
+ ? colors.red("!")
54
+ : "!";
55
+ const lines = [
56
+ `${mark} ${paint.bold(entry.id)}`,
57
+ ...result.findings.flatMap((finding) => findingLine(finding, width, paint, root)),
58
+ ...result.hints.flatMap((hint) => hintLine(hint, width)),
59
+ ];
60
+ return lines.join("\n");
61
+ };
62
+ /**
63
+ * Three counts, not one pass/fail verdict — conflating them is exactly the "23
64
+ * entries still need a person" problem this replaces: with every verifier in
65
+ * place, almost all of them would carry at least the typecheck reminder, so
66
+ * counting "has a hint" swamps the entries that actually need a person's
67
+ * judgement with the ones that only need the same routine `tsc --noEmit` run.
68
+ *
69
+ * - `failed` — `ok: false`: this run found something concretely wrong.
70
+ * - `needsReview` — `ok: true` but `hints` is non-empty: nothing this module
71
+ * could decide is wrong, but a person still has to judge something no
72
+ * compiler check can catch.
73
+ * - `verified` — `ok: true` and `hints` is empty: nothing left for a person to
74
+ * judge here (the typecheck below may still apply). Deliberately not
75
+ * "done"/"complete": see `checks/types.ts`'s doc comment on `VerifyResult`
76
+ * for why `ok: true` never means the migration is finished.
77
+ *
78
+ * `typecheckCount` is tallied separately again: it is not a judgement call like
79
+ * `needsReview`, and `tsc --noEmit` is one command a person runs once for the
80
+ * whole project, not once per entry — so it is reported as a single closing
81
+ * line, not folded into any of the three per-entry buckets.
82
+ */
83
+ export const summarize = (results) => {
84
+ let failed = 0;
85
+ let needsReview = 0;
86
+ let verified = 0;
87
+ let typecheckCount = 0;
88
+ for (const { result } of results) {
89
+ if (!result.ok) {
90
+ failed++;
91
+ }
92
+ else if (result.hints.length > 0) {
93
+ needsReview++;
94
+ }
95
+ else {
96
+ verified++;
97
+ }
98
+ if (result.typecheckHint !== undefined) {
99
+ typecheckCount++;
100
+ }
101
+ }
102
+ return { failed, needsReview, verified, typecheckCount };
103
+ };
104
+ /**
105
+ * Renders `runVerify`'s result as text — the presentation half kept separate
106
+ * from the async file-reading half, the same split `renderList` uses.
107
+ *
108
+ * Prints only what needs attention. An entry that is `ok: true` with no hints
109
+ * has nothing left for a person to judge (the closing typecheck line still
110
+ * covers it if it relies on one) — it swells the count in the header and
111
+ * nothing else, the same way `verified` is counted but never itself printed.
112
+ * That is the fix for the four problems the maintainer called out: a passing
113
+ * entry whose only hint was the typecheck reminder no longer earns a block at
114
+ * all, the reminder itself moves out of the loop to one line at the end,
115
+ * hints stop repeating their own label, and findings show a path relative to
116
+ * `path` instead of the absolute one the header already established.
117
+ */
118
+ export const renderVerify = (results, path, { color = false, width = 80 } = {}) => {
119
+ const paint = painter(color);
120
+ if (results.length === 0) {
121
+ return `No verifiers are available yet for anything under ${path}.\n`;
122
+ }
123
+ const { failed, needsReview, typecheckCount } = summarize(results);
124
+ const header = [
125
+ `${paint.bold(String(results.length))} checked`,
126
+ `${paint.bold(String(failed))} failed`,
127
+ `${paint.bold(String(needsReview))} needs review`,
128
+ ].join(" · ");
129
+ const needsAttention = results.filter(({ result }) => !result.ok || result.hints.length > 0);
130
+ const body = needsAttention.length === 0
131
+ ? "Nothing needs attention."
132
+ : needsAttention
133
+ .map((entryResult) => renderEntry(entryResult, width, color, path))
134
+ .join("\n\n");
135
+ const typecheckLine = typecheckCount === 0
136
+ ? ""
137
+ : `\n\nThen run \`tsc --noEmit\` once — ${typecheckCount} of these rely on it.`;
138
+ return `${header}\n\n${body}${typecheckLine}\n`;
139
+ };
140
+ /**
141
+ * Whether a `verify` run should exit non-zero: exactly when at least one
142
+ * verifier decided `ok: false`. A hint alone — even every entry carrying one —
143
+ * is the normal "a person still has to look" state, not a failure, so it must
144
+ * never flip this. Kept as its own pure function (over `results`, not
145
+ * `process.exitCode` directly) so a test can drive it with injected fakes
146
+ * instead of spawning the CLI.
147
+ */
148
+ export const verifyExitCode = (results) => results.some(({ result }) => !result.ok) ? 1 : 0;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from "node:fs";
3
+ import { parseArguments } from "./cli/args.js";
4
+ import { createChoose } from "./cli/choose.js";
5
+ import { runSingleCodemod } from "./cli/codemod.js";
6
+ import { defaultListDeps, runList } from "./cli/list.js";
7
+ import { defaultUpgradeDeps, runUpgrade } from "./cli/upgrade.js";
8
+ const usage = `flow-codemods — migrate a codebase across Flow versions
9
+
10
+ Usage:
11
+ flow-codemods upgrade [revision] Bump every Flow dependency, install, run codemods
12
+ flow-codemods <id> [path] Run a single codemod
13
+ flow-codemods list [revision] Show the migrations for the whole catalogue, or
14
+ for the range a revision would touch
15
+
16
+ Revision: patch | minor | major | a dist-tag (latest, next) | an exact version.
17
+ "upgrade" defaults to minor, which stays inside the current major. "list" has
18
+ no default — without one it lists the whole catalogue, offline.
19
+
20
+ Options:
21
+ -y, --yes Accept every default. Implied when stdin is not a TTY.
22
+ --allow-dirty Run even though the working tree has uncommitted changes
23
+ --dry Do not write files
24
+ --print Print the transformed output
25
+ --path Sources to transform
26
+ --json Machine-readable output for "list"
27
+ -h, --help Show this text
28
+ -V, --version Show the version
29
+ `;
30
+ const main = async () => {
31
+ const parsed = parseArguments(process.argv.slice(2));
32
+ // Deliberate single ordered report stream: every command-level failure below
33
+ // that a real invocation can hit — `list`'s and `upgrade`'s range resolution,
34
+ // `codemod`'s and `upgrade`'s injected `log` — writes to stdout, interleaved
35
+ // with the rest of that command's normal output, in the order it happened.
36
+ // (The `default` case is dead code — `Command` is exhaustively covered above
37
+ // it — so its stderr write never actually runs.) Only the top-level
38
+ // rejection handler below (an unexpected throw, not a command refusing)
39
+ // writes to stderr. So `2>/dev/null` on this CLI does not suppress a
40
+ // refusal reason today — do not "fix" a command's own failure message onto
41
+ // stderr without weighing that.
42
+ switch (parsed.command) {
43
+ case "help":
44
+ process.stdout.write(usage);
45
+ return 0;
46
+ case "version": {
47
+ // `readFileSync` rather than an import attribute: `with { type: "json" }`
48
+ // was the newest syntax in this package and the only thing forcing the
49
+ // Node floor above what the rest of the code needs. Consumers reach this
50
+ // CLI through `npx`, so that floor is theirs, not the repo's.
51
+ const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
52
+ process.stdout.write(`${manifest.version}\n`);
53
+ return 0;
54
+ }
55
+ case "list":
56
+ return await runList(parsed, {
57
+ ...defaultListDeps(process.cwd()),
58
+ // Colour only when a person is looking at a terminal. `NO_COLOR` is
59
+ // the cross-tool convention for turning it off; a pipe or a file gets
60
+ // plain text so the output stays greppable.
61
+ color: process.stdout.isTTY === true &&
62
+ process.env.NO_COLOR === undefined &&
63
+ !parsed.json,
64
+ // Clamped at both ends: a terminal can report an unusably small width
65
+ // (or none at all), and beyond ~100 columns long prose gets harder to
66
+ // read rather than easier.
67
+ width: Math.min(Math.max(process.stdout.columns ?? 80, 60), 100),
68
+ });
69
+ case "codemod":
70
+ return await runSingleCodemod(parsed, {
71
+ cwd: process.cwd(),
72
+ log: (message) => process.stdout.write(`${message}\n`),
73
+ });
74
+ case "upgrade": {
75
+ // Loaded here, not at the top: `list` and a single-codemod run never
76
+ // prompt, and paying for the prompt library's module graph on every
77
+ // invocation is the kind of cost a CLI is judged by.
78
+ const { checkbox } = await import("@inquirer/prompts");
79
+ // `-y` accepts every default, and no TTY implies it: CI and agent runs
80
+ // have nobody to answer the prompt. That is also why the dirty-tree guard
81
+ // exists — see git.ts.
82
+ const choose = createChoose({
83
+ yes: parsed.yes,
84
+ isTTY: process.stdin.isTTY === true,
85
+ isCI: process.env.CI !== undefined,
86
+ onCancel: (message) => process.stdout.write(`${message}\n`),
87
+ prompt: (entries) => checkbox({
88
+ message: "Which codemods should run?",
89
+ choices: entries.map((entry) => ({
90
+ name: `${entry.id} — ${entry.title}`,
91
+ value: entry.id,
92
+ checked: true,
93
+ })),
94
+ }),
95
+ });
96
+ return runUpgrade(parsed, {
97
+ ...defaultUpgradeDeps(process.cwd()),
98
+ choose,
99
+ });
100
+ }
101
+ default:
102
+ process.stderr.write(`"${parsed.command}" is not implemented yet\n`);
103
+ return 1;
104
+ }
105
+ };
106
+ // `process.exitCode` rather than `process.exit()`: exiting explicitly can
107
+ // terminate the process before a pending write to stdout has flushed, which on
108
+ // POSIX is the common case when stdout is a pipe. Every command exits through
109
+ // here, including `list --json` and the upgrade report, so the output that would
110
+ // be truncated is the largest output this CLI produces. Nothing keeps a handle
111
+ // open, so Node exits on its own once the event loop drains.
112
+ main().then((code) => {
113
+ process.exitCode = code;
114
+ }, (error) => {
115
+ process.stderr.write(`${error instanceof Error ? error.message : error}\n`);
116
+ process.exitCode = 1;
117
+ });
@@ -0,0 +1,8 @@
1
+ import type { Detector } from "../checks/types.js";
2
+ /**
3
+ * Translated from the catalogue's `detect: rg -t ts '\bAlign(Props)?\b'` — see
4
+ * the translation rule in the plan: `rg -t ts 'PATTERN'` becomes
5
+ * `search(/PATTERN/, tsExtensions)`.
6
+ */
7
+ export declare const detector: Detector;
8
+ //# sourceMappingURL=align-to-combine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"align-to-combine.d.ts","sourceRoot":"","sources":["../../src/detect/align-to-combine.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE,QAEtB,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { tsExtensions } from "../checks/context.js";
2
+ /**
3
+ * Translated from the catalogue's `detect: rg -t ts '\bAlign(Props)?\b'` — see
4
+ * the translation rule in the plan: `rg -t ts 'PATTERN'` becomes
5
+ * `search(/PATTERN/, tsExtensions)`.
6
+ */
7
+ export const detector = {
8
+ detect: (context) => context.search(/\bAlign(Props)?\b/, tsExtensions),
9
+ };
@@ -0,0 +1,9 @@
1
+ import type { Detector } from "../checks/types.js";
2
+ /**
3
+ * Translated from the catalogue's `detect: rg -t ts 'addOnClose|addOnOpen'`.
4
+ * Every call site needs a by-hand look at its handler — see
5
+ * `src/verify/overlay-controller-add-on-close-return-type.ts` for why no
6
+ * compiler check can narrow this list further.
7
+ */
8
+ export declare const detector: Detector;
9
+ //# sourceMappingURL=overlay-controller-add-on-close-return-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-controller-add-on-close-return-type.d.ts","sourceRoot":"","sources":["../../src/detect/overlay-controller-add-on-close-return-type.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QAEtB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { tsExtensions } from "../checks/context.js";
2
+ /**
3
+ * Translated from the catalogue's `detect: rg -t ts 'addOnClose|addOnOpen'`.
4
+ * Every call site needs a by-hand look at its handler — see
5
+ * `src/verify/overlay-controller-add-on-close-return-type.ts` for why no
6
+ * compiler check can narrow this list further.
7
+ */
8
+ export const detector = {
9
+ detect: (context) => context.search(/addOnClose|addOnOpen/, tsExtensions),
10
+ };
@@ -0,0 +1,11 @@
1
+ import type { Detector } from "../checks/types.js";
2
+ /**
3
+ * Translated from the catalogue's `detect: rg -t ts 'TooltipTrigger'`. This
4
+ * over-matches on purpose — every `TooltipTrigger` usage, not just the ones
5
+ * with a numeric `delay` — because a regex can't tell a numeric literal from a
6
+ * string one without a full parse; the reader looks at each hit. See
7
+ * `src/verify/tooltip-trigger-delay-type.ts` for why the real check is left to
8
+ * `tsc --noEmit` instead.
9
+ */
10
+ export declare const detector: Detector;
11
+ //# sourceMappingURL=tooltip-trigger-delay-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tooltip-trigger-delay-type.d.ts","sourceRoot":"","sources":["../../src/detect/tooltip-trigger-delay-type.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;;;GAOG;AACH,eAAO,MAAM,QAAQ,EAAE,QAEtB,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { tsExtensions } from "../checks/context.js";
2
+ /**
3
+ * Translated from the catalogue's `detect: rg -t ts 'TooltipTrigger'`. This
4
+ * over-matches on purpose — every `TooltipTrigger` usage, not just the ones
5
+ * with a numeric `delay` — because a regex can't tell a numeric literal from a
6
+ * string one without a full parse; the reader looks at each hit. See
7
+ * `src/verify/tooltip-trigger-delay-type.ts` for why the real check is left to
8
+ * `tsc --noEmit` instead.
9
+ */
10
+ export const detector = {
11
+ detect: (context) => context.search(/TooltipTrigger/, tsExtensions),
12
+ };
@@ -0,0 +1,3 @@
1
+ /** Every package published from the Flow monorepo, which all share one version. */
2
+ export declare const flowPackages: string[];
3
+ //# sourceMappingURL=flowPackages.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flowPackages.generated.d.ts","sourceRoot":"","sources":["../src/flowPackages.generated.ts"],"names":[],"mappings":"AAIA,mFAAmF;AACnF,eAAO,MAAM,YAAY,EAAE,MAAM,EAahC,CAAC"}