@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,24 @@
1
+ import type { ParsedCommand } from "./args.js";
2
+ import { runCodemod } from "../run/jscodeshift.js";
3
+ /**
4
+ * Which sources to transform, resolved against `cwd`.
5
+ *
6
+ * `src` is the default because that is where a Flow consumer's components live,
7
+ * and the working directory is the fallback. The caller prints the result
8
+ * either way — a codemod that silently ran over the wrong tree is worse than
9
+ * one that refused.
10
+ *
11
+ * Always returns a path rooted at `cwd` (an absolute `--path` is left alone).
12
+ * jscodeshift resolves a relative path against `process.cwd()`, not against
13
+ * whatever `cwd` a caller injected — a bare `"src"` would only be correct when
14
+ * the two coincide, which is true in production but not in a test that injects
15
+ * a different `cwd`.
16
+ */
17
+ export declare const resolveSourcePath: (explicit: string | undefined, cwd: string, exists?: (path: string) => boolean) => string;
18
+ export interface CodemodCommandDeps {
19
+ cwd: string;
20
+ log: (message: string) => void;
21
+ run?: typeof runCodemod;
22
+ }
23
+ export declare const runSingleCodemod: (parsed: ParsedCommand, { cwd, log, run }: CodemodCommandDeps) => Promise<number>;
24
+ //# sourceMappingURL=codemod.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codemod.d.ts","sourceRoot":"","sources":["../../src/cli/codemod.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAmB,MAAM,uBAAuB,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,aAClB,MAAM,GAAG,SAAS,OACvB,MAAM,WACH,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,KAChC,MAKF,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,GAAG,CAAC,EAAE,OAAO,UAAU,CAAC;CACzB;AAED,eAAO,MAAM,gBAAgB,WACnB,aAAa,qBACW,kBAAkB,KACjD,OAAO,CAAC,MAAM,CA6DhB,CAAC"}
@@ -0,0 +1,76 @@
1
+ import { existsSync } from "node:fs";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { allEntries, unknownCodemodMessage } from "../catalog/entries.js";
4
+ import { runCodemod, transformExists } from "../run/jscodeshift.js";
5
+ /**
6
+ * Which sources to transform, resolved against `cwd`.
7
+ *
8
+ * `src` is the default because that is where a Flow consumer's components live,
9
+ * and the working directory is the fallback. The caller prints the result
10
+ * either way — a codemod that silently ran over the wrong tree is worse than
11
+ * one that refused.
12
+ *
13
+ * Always returns a path rooted at `cwd` (an absolute `--path` is left alone).
14
+ * jscodeshift resolves a relative path against `process.cwd()`, not against
15
+ * whatever `cwd` a caller injected — a bare `"src"` would only be correct when
16
+ * the two coincide, which is true in production but not in a test that injects
17
+ * a different `cwd`.
18
+ */
19
+ export const resolveSourcePath = (explicit, cwd, exists = existsSync) => {
20
+ if (explicit !== undefined) {
21
+ return isAbsolute(explicit) ? explicit : join(cwd, explicit);
22
+ }
23
+ return exists(join(cwd, "src")) ? join(cwd, "src") : cwd;
24
+ };
25
+ export const runSingleCodemod = async (parsed, { cwd, log, run = runCodemod }) => {
26
+ const id = parsed.id ?? "";
27
+ const entry = allEntries.find((candidate) => candidate.id === id);
28
+ // No catalogue entry is not the same as unknown: `to-remote-package` is a
29
+ // transform deliberately kept out of the catalogue (it is a port, not a
30
+ // migration — see `notAMigration` in `src/tests/remoteScope.test.ts`), and it
31
+ // still has to be reachable by id. Only fall back to the transform file when
32
+ // the catalogue does not know the id at all; a catalogued id whose action is
33
+ // "manual" or "none" still has no transform to run, regardless of what is on
34
+ // disk.
35
+ if (entry === undefined) {
36
+ if (!transformExists(id)) {
37
+ log(unknownCodemodMessage(id));
38
+ return 1;
39
+ }
40
+ }
41
+ else if (entry.action !== "codemod") {
42
+ log(`"${id}" has no codemod — it is a ${entry.action === "none" ? "behaviour change" : "manual change"}.\n\napply: ${entry.apply}`);
43
+ return 1;
44
+ }
45
+ const path = resolveSourcePath(parsed.path, cwd);
46
+ log(`Running ${id} over ${path}`);
47
+ const result = await run({ id, path, dry: parsed.dry, print: parsed.print });
48
+ if (result.errors > 0) {
49
+ log(`${id}: ${result.errors} file(s) failed to transform.`);
50
+ return 1;
51
+ }
52
+ // Not the same as "0 files changed": jscodeshift reports an empty path and a
53
+ // dead worker identically, so say what happened rather than implying success.
54
+ if (result.processedNothing) {
55
+ log(`${id}: no files under ${path} were processed. Is the path right?`);
56
+ return 1;
57
+ }
58
+ // The same trap as `processedNothing`, one field over: a transform that
59
+ // declines a file by returning nothing counts as `skipped`, not `unmodified`.
60
+ // If every file was skipped and none changed, "0 file(s) changed" would read
61
+ // as a clean no-op run when in fact the transform bailed on everything.
62
+ if (result.changed === 0 && result.skipped > 0) {
63
+ log(`${id}: the transform declined all ${result.skipped} file(s) it looked at, and changed none.`);
64
+ return 1;
65
+ }
66
+ const skipped = result.skipped > 0 ? `, ${result.skipped} skipped` : "";
67
+ const summary = `${id}: ${result.changed} file(s) changed, ${result.unmodified} unchanged${skipped}.`;
68
+ // Only a catalogued id has a migration guide entry to point at — a transform
69
+ // like `to-remote-package` with no catalogue entry has no anchor in
70
+ // `MIGRATION.md` to link, so pointing there would be a dead link.
71
+ const pointer = entry === undefined
72
+ ? ""
73
+ : `\nSee https://github.com/mittwald/flow/blob/main/packages/components/MIGRATION.md#${id} for what's left.`;
74
+ log(`${summary}${pointer}`);
75
+ return 0;
76
+ };
@@ -0,0 +1,35 @@
1
+ import { type CatalogEntry } from "../catalog/entries.js";
2
+ import { loadDetector } from "../checks/load.js";
3
+ import type { Finding } from "../checks/types.js";
4
+ export interface DetectEntryResult {
5
+ entry: CatalogEntry;
6
+ findings: Finding[];
7
+ }
8
+ export interface RunDetectDeps {
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 loadDetector;
13
+ }
14
+ /**
15
+ * Runs every available detector over `path` and returns only the entries that
16
+ * found something.
17
+ *
18
+ * Only a fraction of the catalogue has a detector module so far (see
19
+ * `checks/load.ts`); an id with none is skipped rather than treated as a
20
+ * failure — the remaining detectors are mechanical follow-up work, not a bug in
21
+ * this command.
22
+ */
23
+ export declare const runDetect: (path: string, { entries, load }?: RunDetectDeps) => Promise<DetectEntryResult[]>;
24
+ /**
25
+ * Renders `runDetect`'s result as text — the presentation half kept separate
26
+ * from the async file-reading half, the same split `renderList` uses, so
27
+ * formatting is testable without touching a file. Reuses `renderList`'s
28
+ * vocabulary (`painter`, `wrap`, `indent`) rather than a second
29
+ * implementation.
30
+ */
31
+ export declare const renderDetect: (results: DetectEntryResult[], path: string, { color, width }?: {
32
+ color?: boolean;
33
+ width?: number;
34
+ }) => string;
35
+ //# sourceMappingURL=detect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect.d.ts","sourceRoot":"","sources":["../../src/cli/detect.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,OAAO,CAAC,EAAE,YAAY,EAAE,CAAC;IACzB,iEAAiE;IACjE,IAAI,CAAC,EAAE,OAAO,YAAY,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,SACd,MAAM,sBACmC,aAAa,KAC3D,OAAO,CAAC,iBAAiB,EAAE,CAgB7B,CAAC;AAeF;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,YACd,iBAAiB,EAAE,QACtB,MAAM,qBACmB;IAAE,KAAK,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KACjE,MAuBF,CAAC"}
@@ -0,0 +1,62 @@
1
+ import colors from "picocolors";
2
+ import { allEntries } from "../catalog/entries.js";
3
+ import { createCheckContext } from "../checks/context.js";
4
+ import { loadDetector } from "../checks/load.js";
5
+ import { indent, painter, wrap } from "./list.js";
6
+ /**
7
+ * Runs every available detector over `path` and returns only the entries that
8
+ * found something.
9
+ *
10
+ * Only a fraction of the catalogue has a detector module so far (see
11
+ * `checks/load.ts`); an id with none is skipped rather than treated as a
12
+ * failure — the remaining detectors are mechanical follow-up work, not a bug in
13
+ * this command.
14
+ */
15
+ export const runDetect = async (path, { entries = allEntries, load = loadDetector } = {}) => {
16
+ const context = createCheckContext(path);
17
+ const results = [];
18
+ for (const entry of entries) {
19
+ const detector = await load(entry.id);
20
+ if (detector === undefined) {
21
+ continue;
22
+ }
23
+ const findings = await detector.detect(context);
24
+ if (findings.length > 0) {
25
+ results.push({ entry, findings });
26
+ }
27
+ }
28
+ return results;
29
+ };
30
+ /** One `file:line text` row, wrapped and indented like `renderList`'s rows. */
31
+ const findingLine = (finding, width, paint) => {
32
+ const location = paint.dim(`${finding.file}:${finding.line}`);
33
+ const body = wrap(finding.text, Math.max(width - indent.length, 20));
34
+ return body.map((line, index) => index === 0 ? `${indent}${location} ${line}` : `${indent} ${line}`);
35
+ };
36
+ /**
37
+ * Renders `runDetect`'s result as text — the presentation half kept separate
38
+ * from the async file-reading half, the same split `renderList` uses, so
39
+ * formatting is testable without touching a file. Reuses `renderList`'s
40
+ * vocabulary (`painter`, `wrap`, `indent`) rather than a second
41
+ * implementation.
42
+ */
43
+ export const renderDetect = (results, path, { color = false, width = 80 } = {}) => {
44
+ const paint = painter(color);
45
+ if (results.length === 0) {
46
+ return `No migrations under ${path} were found by the available detectors.\n`;
47
+ }
48
+ const noun = results.length === 1 ? "migration" : "migrations";
49
+ const verb = results.length === 1 ? "touches" : "touch";
50
+ const header = `${paint.bold(`${results.length} ${noun}`)} ${verb} ${path}\n`;
51
+ const body = results
52
+ .map(({ entry, findings }) => {
53
+ const mark = color ? colors.green("●") : "*";
54
+ const lines = [
55
+ `${mark} ${paint.bold(entry.id)}`,
56
+ ...findings.flatMap((finding) => findingLine(finding, width, paint)),
57
+ ];
58
+ return lines.join("\n");
59
+ })
60
+ .join("\n\n");
61
+ return `${header}\n${body}\n`;
62
+ };
@@ -0,0 +1,71 @@
1
+ import { type CatalogEntry } from "../catalog/entries.js";
2
+ import { type RangeDeps } from "../resolve/range.js";
3
+ import type { ParsedCommand } from "./args.js";
4
+ export interface RenderListInput {
5
+ entries: CatalogEntry[];
6
+ /**
7
+ * The version range to show migrations for. Both bounds always arrive
8
+ * together — there is no partial range — so omitting it entirely is what
9
+ * lists the whole catalogue.
10
+ */
11
+ range?: {
12
+ from: string;
13
+ to: string;
14
+ };
15
+ json: boolean;
16
+ /** Emit ANSI colour. Off by default so a test sees plain text. */
17
+ color?: boolean;
18
+ /** Terminal width to wrap prose to. */
19
+ width?: number;
20
+ /**
21
+ * Render the frame around the entries: the context on top (the range, the
22
+ * catch-up legend) and the summary at the bottom (the counts). On by default.
23
+ * `upgrade` turns it off for its by-hand section: it already printed its own
24
+ * heading and its own aggregate ("N codemods run, N changed something"), and
25
+ * this renderer's frame would just repeat both — see `runUpgrade`. Named for
26
+ * the whole frame, not just the top half, since it now gates both. The
27
+ * per-entry catch-up mark itself is unaffected — it lives in the body, not
28
+ * the frame.
29
+ */
30
+ frame?: boolean;
31
+ }
32
+ export declare const stripAnsi: (text: string) => string;
33
+ /**
34
+ * The migrations for a version range, as text or JSON.
35
+ *
36
+ * Read-only by design: this is what an agent can call to plan before it changes
37
+ * anything. `--json` carries `apply` through unchanged, because that is the
38
+ * field it acts on — and it returns before any styling, so no escape sequence
39
+ * can ever reach a parser.
40
+ *
41
+ * `color` and `width` are arguments rather than read from the environment here,
42
+ * so the rendering is deterministic in a test — `painter` builds its palette
43
+ * from `color` alone, never from the terminal. `cli.ts` supplies them.
44
+ */
45
+ export declare const renderList: ({ entries, range, json, color, width, frame, }: RenderListInput) => string;
46
+ export interface ListDeps extends RangeDeps {
47
+ /**
48
+ * Raw output writer — matches `process.stdout.write`'s own contract: no
49
+ * newline is appended automatically.
50
+ */
51
+ write: (text: string) => void;
52
+ color?: boolean;
53
+ width?: number;
54
+ }
55
+ export declare const defaultListDeps: (cwd: string) => ListDeps;
56
+ /**
57
+ * `list [revision]` — the catalogue browser (no argument) or a dry run of
58
+ * `upgrade <revision>` (with one).
59
+ *
60
+ * The two forms are distinguished by whether a revision was given, not by a
61
+ * default: unlike `upgrade`, `list` has none, because a bare `list` is a
62
+ * deliberately different thing — the whole catalogue, offline, no manifest
63
+ * read. Given a revision, this shares `resolveRange` with `upgrade`, so the
64
+ * range shown is exactly what `upgrade <revision>` would act on. Unlike
65
+ * `upgrade`, an unresolved-but-not-an-upgrade target (e.g. an exact version at
66
+ * or below current) is not a refusal here — `resolveRange` reports it as `ok:
67
+ * true`, and `list` shows it like any other range (which is typically empty,
68
+ * and prints "Nothing to migrate in that range.").
69
+ */
70
+ export declare const runList: (parsed: ParsedCommand, deps: ListDeps) => Promise<number>;
71
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/cli/list.ts"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEtE,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB;;;;OAIG;IACH,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,IAAI,EAAE,OAAO,CAAC;IACd,kEAAkE;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uCAAuC;IACvC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAkCD,eAAO,MAAM,SAAS,SAAU,MAAM,KAAG,MAAgC,CAAC;AAoO1E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,mDAOpB,eAAe,KAAG,MA0DpB,CAAC;AAEF,MAAM,WAAW,QAAS,SAAQ,SAAS;IACzC;;;OAGG;IACH,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,eAAO,MAAM,eAAe,QAAS,MAAM,KAAG,QAG5C,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,OAAO,WACV,aAAa,QACf,QAAQ,KACb,OAAO,CAAC,MAAM,CA+BhB,CAAC"}
@@ -0,0 +1,293 @@
1
+ import colors from "picocolors";
2
+ import { lte } from "semver";
3
+ import { allEntries } from "../catalog/entries.js";
4
+ import { selectEntries, sortBySince } from "../catalog/select.js";
5
+ import { defaultRangeDeps, resolveRange, } from "../resolve/range.js";
6
+ /**
7
+ * What the reader has to do, and the colour that says it at a glance.
8
+ *
9
+ * `plural` only differs for `codemod`, which is a countable thing; "by hand"
10
+ * and "no code change" describe how, not how many. `tone` names a key into the
11
+ * `Painter` a call site already built with `painter(color)` — not a bound
12
+ * colour function — so which palette applies is decided once, by the `color`
13
+ * argument, not baked into this module-level table. See `painter` for why.
14
+ */
15
+ const actions = {
16
+ codemod: { label: "codemod", plural: "codemods", tone: "green" },
17
+ manual: { label: "by hand", plural: "by hand", tone: "yellow" },
18
+ none: {
19
+ label: "no code change",
20
+ plural: "no code change",
21
+ tone: "blue",
22
+ },
23
+ };
24
+ /**
25
+ * ANSI escapes have no width; measuring must ignore them.
26
+ *
27
+ * Built from a char code rather than written as a literal, so no control
28
+ * character sits in the source — which is also what `no-control-regex` wants.
29
+ */
30
+ const ansi = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
31
+ export const stripAnsi = (text) => text.replace(ansi, "");
32
+ const visibleWidth = (text) => stripAnsi(text).length;
33
+ /**
34
+ * Wraps to `width`, measuring visible width so already-coloured text still
35
+ * breaks in the right place.
36
+ */
37
+ const wrap = (text, width) => {
38
+ const lines = [];
39
+ let line = "";
40
+ for (const word of text.split(/\s+/).filter((part) => part !== "")) {
41
+ if (line === "") {
42
+ line = word;
43
+ }
44
+ else if (visibleWidth(line) + 1 + visibleWidth(word) <= width) {
45
+ line = `${line} ${word}`;
46
+ }
47
+ else {
48
+ lines.push(line);
49
+ line = word;
50
+ }
51
+ }
52
+ if (line !== "") {
53
+ lines.push(line);
54
+ }
55
+ return lines.length > 0 ? lines : [""];
56
+ };
57
+ /**
58
+ * Renders `code` spans as colour instead of literal backticks.
59
+ *
60
+ * The catalogue bodies are Markdown, so its prose fields carry backticks. In a
61
+ * terminal those are noise, but the emphasis they mark is exactly what a reader
62
+ * scans for — a symbol name or a command.
63
+ */
64
+ const inlineCode = (text, paint) => text.replace(/`([^`]+)`/g, (_, code) => paint.code(code));
65
+ /**
66
+ * Builds a palette from `color`, and only from `color`.
67
+ *
68
+ * `colors` (the module-level `picocolors` default export) auto-detects a TTY
69
+ * and disables itself under one — reading `colors.green` etc. directly would
70
+ * make the _environment_, not this argument, the real decider, silently
71
+ * disagreeing with `cli.ts`'s own TTY/`NO_COLOR`/`--json` check. `createColors`
72
+ * returns a palette with colour forced on or off, so `color` is the only input
73
+ * to how this renders.
74
+ */
75
+ const painter = (color) => {
76
+ const palette = colors.createColors(color);
77
+ return {
78
+ bold: palette.bold,
79
+ dim: palette.dim,
80
+ code: palette.cyan,
81
+ green: palette.green,
82
+ yellow: palette.yellow,
83
+ blue: palette.blue,
84
+ };
85
+ };
86
+ const indent = " ";
87
+ const labelWidth = 8;
88
+ /** One `apply` row: dim label, wrapped body beside it. */
89
+ const field = (label, value, width, paint) => {
90
+ const gutter = indent + " ".repeat(labelWidth);
91
+ const body = wrap(inlineCode(value, paint), Math.max(width - gutter.length, 20));
92
+ return body.map((line, index) => index === 0
93
+ ? `${indent}${paint.dim(label.padEnd(labelWidth))}${line}`
94
+ : `${gutter}${line}`);
95
+ };
96
+ /**
97
+ * Whether `entry` shipped at or before `current` \u2014 selected because the
98
+ * gate no longer has a lower bound (`since <= target` alone, see
99
+ * `selectEntries`), not because the range newly crosses it. Only meaningful for
100
+ * a range-bounded list \u2014 the whole-catalogue browse has no `current` to
101
+ * compare against.
102
+ *
103
+ * This is what may already be done, not what is done: nothing records which
104
+ * migrations a project has actually performed, codemod or manual alike \u2014
105
+ * see `selectEntries`.
106
+ */
107
+ const isCatchUp = (entry, current) => current !== undefined && lte(entry.since, current);
108
+ const renderEntry = (entry, width, color, catchUp) => {
109
+ const paint = painter(color);
110
+ const action = actions[entry.action];
111
+ // Hollow vs filled: catch-up shipped at or before `current`, so it may
112
+ // already be handled, unlike something the upgrade is newly bringing in \u2014
113
+ // see the legend line in `renderContext`.
114
+ const symbol = catchUp ? "\u25CB" : "\u25CF";
115
+ const mark = color ? paint[action.tone](symbol) : catchUp ? "o" : "*";
116
+ const meta = [entry.kind, action.label];
117
+ if (entry.remotePackage) {
118
+ meta.push("also in flow-remote-react-components");
119
+ }
120
+ if (catchUp) {
121
+ meta.push("catch-up");
122
+ }
123
+ const lines = [
124
+ `${mark} ${paint.bold(entry.id)} ${paint.dim(entry.since)}`,
125
+ indent + paint.dim(meta.join(" \u00B7 ")),
126
+ "",
127
+ ...field("apply", entry.apply, width, paint),
128
+ ];
129
+ if (entry.action === "codemod") {
130
+ lines.push("", `${indent}${paint.dim("$")} ${paint.code(`npx @mittwald/flow-codemods@latest ${entry.id} src`)}`);
131
+ }
132
+ return lines.join("\n");
133
+ };
134
+ /**
135
+ * What a reader needs _before_ the entries: what range this is, and — when it
136
+ * explains a mark they're about to see below — the catch-up legend.
137
+ *
138
+ * "" for a bare `list` (no range): there is nothing to decode, so nothing
139
+ * renders. Otherwise "from X to Y", or for a zero-width range "nothing newer
140
+ * than X" (`from`/`to` is the wrong form once they're equal — that isn't a
141
+ * range, it's "you're already there"), plus — when any entry below is marked
142
+ * catch-up — the legend for the mark. The gate has no lower bound (see
143
+ * `selectEntries`), so a range-bounded list can show every entry in the
144
+ * catalogue, not just the ones the range newly crosses; the legend says what
145
+ * the mark means — and, just as much, what it does not: catch-up is "may
146
+ * already be done", never "already done".
147
+ */
148
+ const renderContext = (selected, range, color, width) => {
149
+ if (range === undefined) {
150
+ return "";
151
+ }
152
+ const catchUpCount = selected.filter((entry) => isCatchUp(entry, range.from)).length;
153
+ const rangeText = range.from === range.to
154
+ ? catchUpCount > 0
155
+ ? `nothing newer than ${range.to}; entries below are catch-up`
156
+ : `nothing newer than ${range.to}`
157
+ : `from ${range.from} to ${range.to}`;
158
+ if (catchUpCount === 0) {
159
+ return rangeText;
160
+ }
161
+ const paint = painter(color);
162
+ const mark = color ? paint[actions.codemod.tone]("○") : "o";
163
+ // Wrapped like every other line: unwrapped it ran to 150 characters and broke
164
+ // hard in any terminal narrower than that.
165
+ const legendGutter = " ";
166
+ const legend = wrap(`catch-up: shipped at or before ${range.from} — you may already have done this. Re-running a codemod is a safe no-op; a manual step needs your own check.`, Math.max(width - legendGutter.length, 20)).map((line, index) => index === 0 ? `${mark} ${line}` : `${legendGutter}${line}`);
167
+ return [rangeText, ...legend].join("\n");
168
+ };
169
+ /**
170
+ * What the reader has, after reading the entries: "N migrations · N codemods ·
171
+ * N by hand · N no code change".
172
+ *
173
+ * Nothing is hidden any more (see `selectEntries`), so this is just the entry
174
+ * count and the per-action breakdown — no separate hidden-count line.
175
+ */
176
+ const renderSummary = (selected, color) => {
177
+ const paint = painter(color);
178
+ const noun = selected.length === 1 ? "migration" : "migrations";
179
+ const counts = Object.keys(actions)
180
+ .map((action) => ({
181
+ action,
182
+ count: selected.filter((entry) => entry.action === action).length,
183
+ }))
184
+ .filter(({ count }) => count > 0)
185
+ .map(({ action, count }) => {
186
+ const { label, plural, tone } = actions[action];
187
+ const text = `${count} ${count === 1 ? label : plural}`;
188
+ return color ? paint[tone](text) : text;
189
+ });
190
+ // The counts carry their own colour, so no dim around them — nesting the two
191
+ // makes both weaker.
192
+ return [paint.bold(`${selected.length} ${noun}`), ...counts].join(paint.dim(" · "));
193
+ };
194
+ /**
195
+ * The migrations for a version range, as text or JSON.
196
+ *
197
+ * Read-only by design: this is what an agent can call to plan before it changes
198
+ * anything. `--json` carries `apply` through unchanged, because that is the
199
+ * field it acts on — and it returns before any styling, so no escape sequence
200
+ * can ever reach a parser.
201
+ *
202
+ * `color` and `width` are arguments rather than read from the environment here,
203
+ * so the rendering is deterministic in a test — `painter` builds its palette
204
+ * from `color` alone, never from the terminal. `cli.ts` supplies them.
205
+ */
206
+ export const renderList = ({ entries, range, json, color = false, width = 80, frame = true, }) => {
207
+ // The two paths differ in their bounds, deliberately: unbounded, this is a
208
+ // plain catalogue browse — every entry, sorted, regardless of whether it
209
+ // would apply to any given range. Bounded, it answers "what does this
210
+ // version range require of me", which `selectEntries` computes per entry
211
+ // from `since`. `range`'s two fields always arrive together (there is no
212
+ // partial bound any more — see `resolveRange`), so there is no sentinel to
213
+ // fill a missing side with; the branch below is the only thing that decides
214
+ // "whole catalogue" vs. "this range".
215
+ const selected = range === undefined
216
+ ? sortBySince(entries)
217
+ : selectEntries(entries, range.to);
218
+ if (json) {
219
+ // An object, not the bare array: the agent-facing form has to say which
220
+ // range it describes, or `list minor` and `list` are indistinguishable in
221
+ // it — and it carries `catchUp` per entry for the same reason the human
222
+ // form marks it.
223
+ return `${JSON.stringify({
224
+ range: range === undefined
225
+ ? null
226
+ : { current: range.from, target: range.to },
227
+ migrations: selected.map((entry) => ({
228
+ ...entry,
229
+ catchUp: isCatchUp(entry, range?.from),
230
+ })),
231
+ }, null, 2)}\n`;
232
+ }
233
+ if (selected.length === 0) {
234
+ return "Nothing to migrate in that range.\n";
235
+ }
236
+ const body = selected
237
+ .map((entry) => renderEntry(entry, width, color, isCatchUp(entry, range?.from)))
238
+ .join("\n\n");
239
+ if (!frame) {
240
+ return `${body}\n`;
241
+ }
242
+ // Context (range, legend) leads, because a reader needs both before the
243
+ // entries; the summary (counts) trails, because a 22-entry list scrolls the
244
+ // top out of sight well before the reader reaches the end — exactly when
245
+ // they want the counts. `context` is "" for a bare `list`, so no blank line
246
+ // gets left dangling above the first entry.
247
+ const context = renderContext(selected, range, color, width);
248
+ const summary = renderSummary(selected, color);
249
+ return `${context === "" ? "" : `${context}\n\n`}${body}\n\n${summary}\n`;
250
+ };
251
+ export const defaultListDeps = (cwd) => ({
252
+ ...defaultRangeDeps(cwd),
253
+ write: (text) => process.stdout.write(text),
254
+ });
255
+ /**
256
+ * `list [revision]` — the catalogue browser (no argument) or a dry run of
257
+ * `upgrade <revision>` (with one).
258
+ *
259
+ * The two forms are distinguished by whether a revision was given, not by a
260
+ * default: unlike `upgrade`, `list` has none, because a bare `list` is a
261
+ * deliberately different thing — the whole catalogue, offline, no manifest
262
+ * read. Given a revision, this shares `resolveRange` with `upgrade`, so the
263
+ * range shown is exactly what `upgrade <revision>` would act on. Unlike
264
+ * `upgrade`, an unresolved-but-not-an-upgrade target (e.g. an exact version at
265
+ * or below current) is not a refusal here — `resolveRange` reports it as `ok:
266
+ * true`, and `list` shows it like any other range (which is typically empty,
267
+ * and prints "Nothing to migrate in that range.").
268
+ */
269
+ export const runList = async (parsed, deps) => {
270
+ const { revision } = parsed;
271
+ if (revision === undefined) {
272
+ deps.write(renderList({
273
+ entries: allEntries,
274
+ json: parsed.json,
275
+ color: deps.color,
276
+ width: deps.width,
277
+ }));
278
+ return 0;
279
+ }
280
+ const resolved = await resolveRange(revision, deps);
281
+ if (!resolved.ok) {
282
+ deps.write(`${resolved.reason}\n`);
283
+ return 1;
284
+ }
285
+ deps.write(renderList({
286
+ entries: allEntries,
287
+ range: { from: resolved.current, to: resolved.target },
288
+ json: parsed.json,
289
+ color: deps.color,
290
+ width: deps.width,
291
+ }));
292
+ return 0;
293
+ };
@@ -0,0 +1,34 @@
1
+ import { type CatalogEntry } from "../catalog/entries.js";
2
+ import { type InstallRunner } from "../install.js";
3
+ import { fetchVersions } from "../resolve/registry.js";
4
+ import { runCodemod } from "../run/jscodeshift.js";
5
+ import type { ParsedCommand } from "./args.js";
6
+ export interface UpgradeDeps {
7
+ cwd: string;
8
+ fetchVersions: typeof fetchVersions;
9
+ install: InstallRunner;
10
+ runCodemod: typeof runCodemod;
11
+ /** Which codemods to apply. `-y` and a non-TTY pass everything through. */
12
+ choose: (entries: CatalogEntry[]) => Promise<CatalogEntry[]>;
13
+ isDirty: (cwd: string) => boolean;
14
+ readInstalledVersion: (cwd: string, name: string) => string | undefined;
15
+ log: (message: string) => void;
16
+ }
17
+ export declare const defaultUpgradeDeps: (cwd: string) => UpgradeDeps;
18
+ /**
19
+ * Bump every Flow dependency, install, then run the codemods the resolved range
20
+ * calls for — and end by naming what no codemod covers.
21
+ *
22
+ * The order is not cosmetic: the codemods run against the installed target, so
23
+ * `tsc` can be green when the command returns. `--dry` skips both the manifest
24
+ * write and the install, so the codemods it still runs act against whatever is
25
+ * currently installed rather than the target — their output is indicative, not
26
+ * exact.
27
+ *
28
+ * Selection has no lower bound at all (see `selectEntries`) — every entry with
29
+ * `since <= target` is in, codemod or manual — so a project already on `target`
30
+ * still gets a full pass: it skips only the write and the install, which
31
+ * genuinely have nothing to do.
32
+ */
33
+ export declare const runUpgrade: (parsed: ParsedCommand, deps: UpgradeDeps) => Promise<number>;
34
+ //# sourceMappingURL=upgrade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../../src/cli/upgrade.ts"],"names":[],"mappings":"AAGA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAItE,OAAO,EAGL,KAAK,aAAa,EACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAEvD,OAAO,EAAE,UAAU,EAAsB,MAAM,uBAAuB,CAAC;AACvE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAI/C,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,aAAa,EAAE,OAAO,aAAa,CAAC;IACpC,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,2EAA2E;IAC3E,MAAM,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC7D,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAClC,oBAAoB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACxE,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED,eAAO,MAAM,kBAAkB,QAAS,MAAM,KAAG,WAS/C,CAAC;AA2BH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,UAAU,WACb,aAAa,QACf,WAAW,KAChB,OAAO,CAAC,MAAM,CAwMhB,CAAC"}