@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
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @mittwald/flow-codemods
2
+
3
+ Codemods and an upgrade CLI for consumers of [Flow](https://flow.mittwald.de),
4
+ mittwald's design system. Run it with `npx` — there is no reason to install it
5
+ as a dependency.
6
+
7
+ ```shell
8
+ npx @mittwald/flow-codemods@latest upgrade
9
+ ```
10
+
11
+ ## Commands
12
+
13
+ ### `upgrade [revision]`
14
+
15
+ Bumps every `@mittwald/flow-*` dependency in `package.json` to a resolved
16
+ target, installs, then runs the codemod of every migration up to that target.
17
+
18
+ `revision` is one of:
19
+
20
+ - `patch` — stays inside the current minor
21
+ - `minor` (default) — stays inside the current major
22
+ - `major` — no ceiling
23
+ - a dist-tag, e.g. `latest` or `next`
24
+ - an exact version, e.g. `1.4.0`
25
+
26
+ The target is resolved from the versions every declared Flow dependency has
27
+ actually published — not just one of them — so the command never writes a
28
+ version some dependency lacks.
29
+
30
+ `dependencies`, `devDependencies` and `optionalDependencies` are rewritten.
31
+ **`peerDependencies` are reported and left alone**: a peer range states which
32
+ versions your package supports, not one it installs, and narrowing `^1.0.0` to
33
+ `^1.0.14` would change what _your_ consumers are allowed to install. That is
34
+ your call, not the command's.
35
+
36
+ After installing, `upgrade` runs the codemod of every migration whose `since` is
37
+ at or below the target and prints the ones with no codemod, for you to apply by
38
+ hand.
39
+
40
+ **There is deliberately no lower bound.** Nothing records which migrations a
41
+ project already performed, so `upgrade` offers all of them rather than guessing
42
+ from the version you happen to be on — a project that never ran the command can
43
+ catch up. Re-running a codemod is safe: every transform is tested for it.
44
+
45
+ Options:
46
+
47
+ - `-y`, `--yes` — accept every default (which codemods to run). Implied when
48
+ stdin is not a TTY, so CI and agent runs never block on a prompt.
49
+ - `--dry` — resolve the target and print what would change, but write nothing
50
+ and skip the install. Codemods still run in this mode, against whatever is
51
+ currently installed rather than the target — their output is indicative, not
52
+ exact.
53
+ - `--allow-dirty` — run even though the working tree has uncommitted changes.
54
+ Codemods rewrite files in place; without this flag, `upgrade` refuses on a
55
+ dirty tree so a bad run is still `git checkout`-able.
56
+ - `--path <dir>` — sources to run the codemods against. Defaults to `./src` when
57
+ that directory exists, otherwise the project root. Give it explicitly if your
58
+ sources live somewhere else — an unrelated `src/` next to them would otherwise
59
+ win.
60
+ - `--print` — print each codemod's transformed output.
61
+
62
+ ### `list [revision]`
63
+
64
+ Shows migrations — codemod and by-hand alike — without touching the project. The
65
+ read-only planning entry point: run it before `upgrade` to see what a bump would
66
+ involve.
67
+
68
+ - `list` (no argument) — the whole catalogue. Offline: reads no manifest, hits
69
+ no network.
70
+ - `list [revision]` — the same manifest read, registry fetch, and revision
71
+ resolution `upgrade [revision]` does, showing exactly the range it would act
72
+ on, without writing anything. `revision` takes the same values as `upgrade`'s
73
+ — `patch` | `minor` | `major` | a dist-tag | an exact version — and there is
74
+ no default: only a given revision switches `list` into this form.
75
+
76
+ Options:
77
+
78
+ - `--json` — machine-readable output: an object with `range`
79
+ (`current`/`target`, or `null` for the offline whole-catalogue form) and
80
+ `migrations`, each entry carrying `catchUp`
81
+
82
+ ### `<id> [path]`
83
+
84
+ Runs a single codemod by its catalogue id (see `list`) against `path` — which
85
+ defaults to `./src` when that directory exists, otherwise the project root.
86
+
87
+ Options: `--dry`, `--print` — same meaning as under `upgrade`.
88
+
89
+ ### `to-remote-package`
90
+
91
+ A port, not a migration: it rewrites every `@mittwald/flow-react-components`
92
+ import to `@mittwald/flow-remote-react-components`. It has no catalogue entry —
93
+ no version range calls for it, and it never shows up in `list` or `upgrade`. Run
94
+ it deliberately when moving an app into an mStudio extension; on a normal app it
95
+ rewrites every Flow import.
96
+
97
+ ```shell
98
+ npx @mittwald/flow-codemods@latest to-remote-package src
99
+ ```
100
+
101
+ ## Exit codes
102
+
103
+ `0` on success, `1` on a refusal — dirty tree, unresolvable revision, failed
104
+ install, an unknown id, or an id whose migration has no codemod — and `1` when a
105
+ codemod reports an error, is declined for every file it looked at, or found
106
+ nothing under `path` to process.
@@ -0,0 +1,17 @@
1
+ import type { MigrationEntry } from "./types.js";
2
+ /**
3
+ * A catalogue entry as the CLI sees it — no body.
4
+ *
5
+ * This is the only module that imports the generated file, so nothing else
6
+ * depends on how it is shaped.
7
+ */
8
+ export type CatalogEntry = Omit<MigrationEntry, "body">;
9
+ export declare const allEntries: CatalogEntry[];
10
+ /**
11
+ * Shared refusal for an id that names no codemod — `runSingleCodemod` (against
12
+ * the catalogue) and `runCodemod` (against `src/migrations`/`src/tools` on
13
+ * disk) each hit this independently, so the message is defined once and reused
14
+ * rather than kept in sync by hand in both places.
15
+ */
16
+ export declare const unknownCodemodMessage: (id: string) => string;
17
+ //# sourceMappingURL=entries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entries.d.ts","sourceRoot":"","sources":["../../src/catalog/entries.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;AAExD,eAAO,MAAM,UAAU,EAAE,YAAY,EAAe,CAAC;AAErD;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,OAAQ,MAAM,KAAG,MAC+C,CAAC"}
@@ -0,0 +1,9 @@
1
+ import { migrations } from "../migrations.generated.js";
2
+ export const allEntries = migrations;
3
+ /**
4
+ * Shared refusal for an id that names no codemod — `runSingleCodemod` (against
5
+ * the catalogue) and `runCodemod` (against `src/migrations`/`src/tools` on
6
+ * disk) each hit this independently, so the message is defined once and reused
7
+ * rather than kept in sync by hand in both places.
8
+ */
9
+ export const unknownCodemodMessage = (id) => `"${id}" is not a codemod in this package. Run \`flow-codemods list\` to see the available ids.`;
@@ -0,0 +1,28 @@
1
+ import type { CatalogEntry } from "./entries.js";
2
+ /**
3
+ * Oldest first — the order the changes shipped, which is the order to apply
4
+ * them.
5
+ */
6
+ export declare const sortBySince: (entries: CatalogEntry[]) => CatalogEntry[];
7
+ /**
8
+ * The entries that a move to `target` calls for.
9
+ *
10
+ * The gate is the exact version, not a major/minor/patch granularity: a
11
+ * revision keyword bounds `target`, and the set falls out of that. So `upgrade
12
+ * patch` — target = highest patch of the current minor — selects exactly the
13
+ * entries of the active minor, and `upgrade minor` selects every entry of the
14
+ * active major.
15
+ *
16
+ * One rule, for every entry: `since <= target`. There used to be a second,
17
+ * lower bound (`current < since`) for entries with no codemod, on the
18
+ * assumption that a consumer already applied everything behind their version.
19
+ * That was a guess, not a fact — nothing records which migrations a project has
20
+ * performed — and it was wrong for the case this tool exists for: someone who
21
+ * never ran it never did the manual steps either. Dropping it for codemods
22
+ * (idempotent — see `transformCoverage.test.ts`) but keeping it for manual
23
+ * entries only hid exactly the migrations that consumer needed to see. The
24
+ * `catch-up` rendering in `src/cli/list.ts` now carries the distinction instead
25
+ * of the gate hiding it.
26
+ */
27
+ export declare const selectEntries: (entries: CatalogEntry[], target: string) => CatalogEntry[];
28
+ //# sourceMappingURL=select.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.d.ts","sourceRoot":"","sources":["../../src/catalog/select.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD;;;GAGG;AACH,eAAO,MAAM,WAAW,YAAa,YAAY,EAAE,KAAG,YAAY,EAG/D,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,YACf,YAAY,EAAE,UACf,MAAM,KACb,YAAY,EACmD,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { compare, lte } from "semver";
2
+ /**
3
+ * Oldest first — the order the changes shipped, which is the order to apply
4
+ * them.
5
+ */
6
+ export const sortBySince = (entries) => entries.toSorted((a, b) => compare(a.since, b.since) || a.id.localeCompare(b.id));
7
+ /**
8
+ * The entries that a move to `target` calls for.
9
+ *
10
+ * The gate is the exact version, not a major/minor/patch granularity: a
11
+ * revision keyword bounds `target`, and the set falls out of that. So `upgrade
12
+ * patch` — target = highest patch of the current minor — selects exactly the
13
+ * entries of the active minor, and `upgrade minor` selects every entry of the
14
+ * active major.
15
+ *
16
+ * One rule, for every entry: `since <= target`. There used to be a second,
17
+ * lower bound (`current < since`) for entries with no codemod, on the
18
+ * assumption that a consumer already applied everything behind their version.
19
+ * That was a guess, not a fact — nothing records which migrations a project has
20
+ * performed — and it was wrong for the case this tool exists for: someone who
21
+ * never ran it never did the manual steps either. Dropping it for codemods
22
+ * (idempotent — see `transformCoverage.test.ts`) but keeping it for manual
23
+ * entries only hid exactly the migrations that consumer needed to see. The
24
+ * `catch-up` rendering in `src/cli/list.ts` now carries the distinction instead
25
+ * of the gate hiding it.
26
+ */
27
+ export const selectEntries = (entries, target) => sortBySince(entries.filter((entry) => lte(entry.since, target)));
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What kind of change the entry describes.
3
+ *
4
+ * - `migration` — the old path is gone, at runtime or in the types.
5
+ * - `deprecation` — the old path still works and warns.
6
+ *
7
+ * Descriptive only: it is rendered as a label and does **not** affect
8
+ * selection. It used to — a `deprecation` was offered as soon as its
9
+ * replacement existed and a `migration` only once the consumer crossed `since`
10
+ * — but the gate is one rule now (`since <= target`, see `selectEntries`), so
11
+ * both behave alike. The distinction still tells a reader whether their code
12
+ * compiles today, which is why the field stays.
13
+ */
14
+ export type MigrationKind = "migration" | "deprecation";
15
+ /**
16
+ * What has to happen.
17
+ *
18
+ * - `codemod` — `src/migrations/<id>/transform.ts` does it.
19
+ * - `manual` — a person or an agent has to change code.
20
+ * - `none` — behaviour changed, no code change required. An agent needs this
21
+ * spelled out, or it keeps looking for something to edit.
22
+ */
23
+ export type MigrationAction = "codemod" | "manual" | "none";
24
+ export interface MigrationEntry {
25
+ /**
26
+ * Dashed and lowercase. Doubles as the `src/migrations/<id>` directory name
27
+ * and, when `action` is `codemod`, that directory's `transform.ts` file, and
28
+ * as the `MIGRATION.md` heading anchor.
29
+ */
30
+ id: string;
31
+ /** The exact version the change shipped in. */
32
+ since: string;
33
+ /** Heading text for the guide. */
34
+ title: string;
35
+ kind: MigrationKind;
36
+ action: MigrationAction;
37
+ /**
38
+ * Whether the entry also applies to `@mittwald/flow-remote-react-components`.
39
+ * Held to that package's real export surface by `remoteScope.test.ts` — do
40
+ * not guess it.
41
+ */
42
+ remotePackage: boolean;
43
+ /** What to change, imperative and specific enough to execute. */
44
+ apply: string;
45
+ /** The guide entry as Markdown. Headings inside it start at level 4. */
46
+ body: string;
47
+ }
48
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/catalog/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GAAG,WAAW,GAAG,aAAa,CAAC;AAExD;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B;;;;OAIG;IACH,EAAE,EAAE,MAAM,CAAC;IACX,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,eAAe,CAAC;IACxB;;;;OAIG;IACH,aAAa,EAAE,OAAO,CAAC;IACvB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,IAAI,EAAE,MAAM,CAAC;CACd"}
File without changes
@@ -0,0 +1,16 @@
1
+ import type { CheckContext } from "./types.js";
2
+ /**
3
+ * Ripgrep's `-t ts` type: `.ts`, `.tsx`, `.cts`, `.mts`. Named once here so the
4
+ * 21 detect/verify modules that translate a `rg -t ts '...'` field do not each
5
+ * repeat the list.
6
+ */
7
+ export declare const tsExtensions: string[];
8
+ /**
9
+ * The shared `CheckContext` implementation — the only place under `src/checks`,
10
+ * `src/detect` or `src/verify` that touches the filesystem. Pure Node
11
+ * (`node:fs/promises`, regexes): no `rg`, `grep`, `tsc`, or any other
12
+ * subprocess, so this runs the same on Windows, in a minimal CI container, or
13
+ * without ripgrep installed.
14
+ */
15
+ export declare const createCheckContext: (path: string) => CheckContext;
16
+ //# sourceMappingURL=context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/checks/context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAW,MAAM,YAAY,CAAC;AAExD;;;;GAIG;AACH,eAAO,MAAM,YAAY,UAAkC,CAAC;AA0B5D;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,SAAU,MAAM,KAAG,YA0CjD,CAAC"}
@@ -0,0 +1,72 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { extname, join } from "node:path";
3
+ /**
4
+ * Ripgrep's `-t ts` type: `.ts`, `.tsx`, `.cts`, `.mts`. Named once here so the
5
+ * 21 detect/verify modules that translate a `rg -t ts '...'` field do not each
6
+ * repeat the list.
7
+ */
8
+ export const tsExtensions = [".ts", ".tsx", ".cts", ".mts"];
9
+ /**
10
+ * Directories a check must never walk into.
11
+ *
12
+ * Not optional: `upgrade` runs `verify` after an install, so a walk without
13
+ * this excludes a freshly populated `node_modules` — the exact omission that
14
+ * was a Critical on this branch for the codemod runner (see `ignorePattern` in
15
+ * `src/run/jscodeshift.ts`). `dist` and `.git` are excluded for the same
16
+ * reason: build output and VCS internals are not the consumer's code.
17
+ */
18
+ const excludedDirs = new Set(["node_modules", "dist", ".git"]);
19
+ const walk = async (dir) => {
20
+ const entries = await readdir(dir, { withFileTypes: true });
21
+ const found = await Promise.all(entries.map(async (entry) => {
22
+ if (entry.isDirectory()) {
23
+ return excludedDirs.has(entry.name) ? [] : walk(join(dir, entry.name));
24
+ }
25
+ return entry.isFile() ? [join(dir, entry.name)] : [];
26
+ }));
27
+ return found.flat();
28
+ };
29
+ /**
30
+ * The shared `CheckContext` implementation — the only place under `src/checks`,
31
+ * `src/detect` or `src/verify` that touches the filesystem. Pure Node
32
+ * (`node:fs/promises`, regexes): no `rg`, `grep`, `tsc`, or any other
33
+ * subprocess, so this runs the same on Windows, in a minimal CI container, or
34
+ * without ripgrep installed.
35
+ */
36
+ export const createCheckContext = (path) => {
37
+ const files = async (extensions) => {
38
+ const all = await walk(path);
39
+ return extensions === undefined
40
+ ? all
41
+ : all.filter((file) => extensions.includes(extname(file)));
42
+ };
43
+ const read = (file) => readFile(file, "utf8");
44
+ const search = async (pattern, extensions) => {
45
+ const candidates = await files(extensions);
46
+ // A fresh, non-global copy of `pattern`: `.test()` on a `g`-flagged regex
47
+ // is stateful (it advances `lastIndex`), which would silently skip matches
48
+ // on later lines of the same file, or later files, depending on what the
49
+ // caller passed in.
50
+ const matcher = new RegExp(pattern.source, pattern.flags.replace("g", ""));
51
+ const findings = [];
52
+ for (const file of candidates) {
53
+ let content;
54
+ try {
55
+ content = await read(file);
56
+ }
57
+ catch {
58
+ // Unreadable — permissions, a broken symlink, binary content a strict
59
+ // decode chokes on. A check skips that one file rather than failing
60
+ // the whole run over it.
61
+ continue;
62
+ }
63
+ content.split("\n").forEach((line, index) => {
64
+ if (matcher.test(line)) {
65
+ findings.push({ file, line: index + 1, text: line.trim() });
66
+ }
67
+ });
68
+ }
69
+ return findings;
70
+ };
71
+ return { path, files, read, search };
72
+ };
@@ -0,0 +1,13 @@
1
+ import type { Detector, Verifier } from "./types.js";
2
+ /**
3
+ * Loads the detector for a catalogue id, or `undefined` when none exists yet.
4
+ *
5
+ * Only a fraction of the catalogue has a module so far — the rest follow in
6
+ * later work. `detect`/`verify` (the CLI commands) must treat a missing module
7
+ * as "nothing to run for this id" rather than a failure, so callers can loop
8
+ * over the whole catalogue without special-casing which ids are covered.
9
+ */
10
+ export declare const loadDetector: (id: string) => Promise<Detector | undefined>;
11
+ /** The `verify` counterpart of {@link loadDetector}. */
12
+ export declare const loadVerifier: (id: string) => Promise<Verifier | undefined>;
13
+ //# sourceMappingURL=load.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/checks/load.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAuBrD;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,OACnB,MAAM,KACT,OAAO,CAAC,QAAQ,GAAG,SAAS,CAiB9B,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,YAAY,OACnB,MAAM,KACT,OAAO,CAAC,QAAQ,GAAG,SAAS,CAW9B,CAAC"}
@@ -0,0 +1,53 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ /**
4
+ * `<packageRoot>/{src,dist}/detect` and `.../verify`, resolved from wherever
5
+ * this module itself runs — `src/checks/load.ts` under vitest (the `.ts`
6
+ * sources), `dist/checks/load.js` from the built binary (the compiled `.js`).
7
+ * Both mirror the same directory depth, so one relative path serves both.
8
+ *
9
+ * Unlike `src/transforms` (see the equivalent comment on `transformsDir` in
10
+ * `src/run/jscodeshift.ts`), detect and verify modules compile into `dist`
11
+ * normally: nothing here needs jscodeshift's babel pipeline, so there is no
12
+ * reason to keep them as raw `.ts`.
13
+ */
14
+ const detectDir = fileURLToPath(new URL("../detect", import.meta.url));
15
+ const verifyDir = fileURLToPath(new URL("../verify", import.meta.url));
16
+ /**
17
+ * Whether `<dir>/<id>` exists as either a compiled `.js` (the built binary) or
18
+ * a source `.ts` (running under vitest, straight from `src`).
19
+ */
20
+ const exists = (dir, id) => existsSync(`${dir}/${id}.js`) || existsSync(`${dir}/${id}.ts`);
21
+ /**
22
+ * Loads the detector for a catalogue id, or `undefined` when none exists yet.
23
+ *
24
+ * Only a fraction of the catalogue has a module so far — the rest follow in
25
+ * later work. `detect`/`verify` (the CLI commands) must treat a missing module
26
+ * as "nothing to run for this id" rather than a failure, so callers can loop
27
+ * over the whole catalogue without special-casing which ids are covered.
28
+ */
29
+ export const loadDetector = async (id) => {
30
+ if (!exists(detectDir, id)) {
31
+ return undefined;
32
+ }
33
+ // `@vite-ignore`: the id is only known at runtime, so this can never be a
34
+ // static specifier a bundler could pre-resolve — under plain Node (the
35
+ // built binary) that is a non-issue, but vitest runs test files through
36
+ // Vite's own module graph, and without the comment Vite tries to rewrite
37
+ // this into a glob import and fails with "Unknown variable dynamic
38
+ // import". The comment tells it to leave the import alone and let Node's
39
+ // loader resolve it, same as the built binary does.
40
+ const detectModule = (await import(
41
+ /* @vite-ignore */ `../detect/${id}.js`));
42
+ return detectModule.detector;
43
+ };
44
+ /** The `verify` counterpart of {@link loadDetector}. */
45
+ export const loadVerifier = async (id) => {
46
+ if (!exists(verifyDir, id)) {
47
+ return undefined;
48
+ }
49
+ // See the matching comment in `loadDetector` above.
50
+ const verifyModule = (await import(
51
+ /* @vite-ignore */ `../verify/${id}.js`));
52
+ return verifyModule.verifier;
53
+ };
@@ -0,0 +1,49 @@
1
+ /** Where a check found something, so the reader can go look. */
2
+ export interface Finding {
3
+ file: string;
4
+ line: number;
5
+ text: string;
6
+ }
7
+ /**
8
+ * What a check gets. Pure Node — no subprocess, so this works on Windows, in a
9
+ * minimal container, and without ripgrep installed.
10
+ */
11
+ export interface CheckContext {
12
+ /** Absolute path to the consumer's sources. */
13
+ path: string;
14
+ /** Files under `path`, with node_modules / dist / .git excluded. */
15
+ files(extensions?: string[]): Promise<string[]>;
16
+ read(file: string): Promise<string>;
17
+ /** Every match across those files. Most checks need only this. */
18
+ search(pattern: RegExp, extensions?: string[]): Promise<Finding[]>;
19
+ }
20
+ export interface Detector {
21
+ detect(context: CheckContext): Promise<Finding[]>;
22
+ }
23
+ export interface VerifyResult {
24
+ /** False when something the check _can_ decide is still wrong. */
25
+ ok: boolean;
26
+ findings: Finding[];
27
+ /**
28
+ * What a person must still judge — beyond just running the typechecker.
29
+ * Printed under the migration's name. An empty array means this module's own
30
+ * check is the whole story (aside from the typecheck below): nothing further
31
+ * for a person to weigh.
32
+ */
33
+ hints: string[];
34
+ /**
35
+ * The `tsc --noEmit` reminder, kept separate from `hints` so a command can
36
+ * tell "still needs a person's judgement" apart from "still needs the routine
37
+ * typecheck" instead of string-sniffing prose. Running it is a global check
38
+ * and this CLI wraps no commands, so it never becomes a `Finding`. Present
39
+ * whenever this migration's correctness depends in some way on the
40
+ * typechecker — including to explain that it does _not_ catch a particular
41
+ * class of mistake here, which is itself worth telling a person running `tsc
42
+ * --noEmit` out of habit.
43
+ */
44
+ typecheckHint?: string;
45
+ }
46
+ export interface Verifier {
47
+ verify(context: CheckContext): Promise<VerifyResult>;
48
+ }
49
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/checks/types.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAChD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,kEAAkE;IAClE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CACpE;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CACnD;AAED,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,EAAE,EAAE,OAAO,CAAC;IACZ,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB;;;;;OAKG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;CACtD"}
File without changes
@@ -0,0 +1,22 @@
1
+ export type Command = "upgrade" | "list" | "codemod" | "help" | "version";
2
+ export interface ParsedCommand {
3
+ command: Command;
4
+ /**
5
+ * `upgrade` always has one (defaulted below when not given explicitly).
6
+ * `list` has one only when given — that presence/absence is what
7
+ * distinguishes "the whole catalogue" from "the range this revision would
8
+ * touch". `patch` | `minor` | `major` | a dist-tag | an exact version.
9
+ */
10
+ revision?: string;
11
+ /** `codemod` only. */
12
+ id?: string;
13
+ /** Sources to transform. Unset means "decide at run time". */
14
+ path?: string;
15
+ json: boolean;
16
+ yes: boolean;
17
+ dry: boolean;
18
+ print: boolean;
19
+ allowDirty: boolean;
20
+ }
21
+ export declare const parseArguments: (argv: string[]) => ParsedCommand;
22
+ //# sourceMappingURL=args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"args.d.ts","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,OAAO,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAE1E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sBAAsB;IACtB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,GAAG,EAAE,OAAO,CAAC;IACb,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,OAAO,CAAC;CACrB;AAQD,eAAO,MAAM,cAAc,SAAU,MAAM,EAAE,KAAG,aAqD/C,CAAC"}
@@ -0,0 +1,56 @@
1
+ import { parseArgs } from "node:util";
2
+ /**
3
+ * The default revision. `minor` stays inside the current major, so the command
4
+ * never crosses a breaking boundary without being asked to.
5
+ */
6
+ const defaultRevision = "minor";
7
+ export const parseArguments = (argv) => {
8
+ const { values, positionals } = parseArgs({
9
+ args: argv,
10
+ allowPositionals: true,
11
+ strict: true,
12
+ options: {
13
+ help: { type: "boolean", short: "h" },
14
+ version: { type: "boolean", short: "V" },
15
+ yes: { type: "boolean", short: "y" },
16
+ json: { type: "boolean" },
17
+ dry: { type: "boolean" },
18
+ print: { type: "boolean" },
19
+ "allow-dirty": { type: "boolean" },
20
+ path: { type: "string" },
21
+ },
22
+ });
23
+ const flags = {
24
+ json: values.json === true,
25
+ yes: values.yes === true,
26
+ dry: values.dry === true,
27
+ print: values.print === true,
28
+ allowDirty: values["allow-dirty"] === true,
29
+ };
30
+ const [first, second] = positionals;
31
+ if (values.version === true) {
32
+ return { command: "version", ...flags };
33
+ }
34
+ if (values.help === true || first === undefined) {
35
+ return { command: "help", ...flags };
36
+ }
37
+ if (first === "upgrade") {
38
+ return {
39
+ command: "upgrade",
40
+ revision: second ?? defaultRevision,
41
+ path: values.path,
42
+ ...flags,
43
+ };
44
+ }
45
+ if (first === "list") {
46
+ // No default here, unlike `upgrade`: `revision` unset is what makes a
47
+ // bare `list` the catalogue browser rather than `list minor`.
48
+ return { command: "list", revision: second, ...flags };
49
+ }
50
+ return {
51
+ command: "codemod",
52
+ id: first,
53
+ path: second ?? values.path,
54
+ ...flags,
55
+ };
56
+ };
@@ -0,0 +1,31 @@
1
+ import type { CatalogEntry } from "../catalog/entries.js";
2
+ export interface CreateChooseInput {
3
+ /** `-y` — accept every default. */
4
+ yes: boolean;
5
+ /** Whether stdin is a TTY. */
6
+ isTTY: boolean;
7
+ /**
8
+ * Whether this is an unattended runner.
9
+ *
10
+ * Injected rather than read from `process.env` in here: a hidden environment
11
+ * read underneath the caller's explicit parameters makes the function behave
12
+ * differently in CI than in a test that sets every input it can see — which
13
+ * is exactly how the cancel case below passed locally and failed on the
14
+ * runner, where `CI` is set and the prompt therefore never ran.
15
+ */
16
+ isCI: boolean;
17
+ /** Resolves to the ids the user picked. */
18
+ prompt: (entries: CatalogEntry[]) => Promise<string[]>;
19
+ /** Reports that the prompt was cancelled. */
20
+ onCancel: (message: string) => void;
21
+ }
22
+ /**
23
+ * Which codemods to run — interactively, or with everything passed through.
24
+ *
25
+ * `-y` and a non-TTY stdin both skip the prompt: an agent or CI run has nobody
26
+ * to answer it. `isCI` is checked too, on top of `isTTY` — a `docker run -t`
27
+ * allocates a TTY with nobody watching it, so that flag is what actually
28
+ * distinguishes a human from an unattended runner there.
29
+ */
30
+ export declare const createChoose: ({ yes, isTTY, isCI, prompt, onCancel, }: CreateChooseInput) => ((entries: CatalogEntry[]) => Promise<CatalogEntry[]>);
31
+ //# sourceMappingURL=choose.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"choose.d.ts","sourceRoot":"","sources":["../../src/cli/choose.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,mCAAmC;IACnC,GAAG,EAAE,OAAO,CAAC;IACb,8BAA8B;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf;;;;;;;;OAQG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,2CAA2C;IAC3C,MAAM,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACvD,6CAA6C;IAC7C,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACrC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,4CAMtB,iBAAiB,KAAG,CAAC,CACtB,OAAO,EAAE,YAAY,EAAE,KACpB,OAAO,CAAC,YAAY,EAAE,CAAC,CAwB3B,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Which codemods to run — interactively, or with everything passed through.
3
+ *
4
+ * `-y` and a non-TTY stdin both skip the prompt: an agent or CI run has nobody
5
+ * to answer it. `isCI` is checked too, on top of `isTTY` — a `docker run -t`
6
+ * allocates a TTY with nobody watching it, so that flag is what actually
7
+ * distinguishes a human from an unattended runner there.
8
+ */
9
+ export const createChoose = ({ yes, isTTY, isCI, prompt, onCancel, }) => {
10
+ const interactive = !yes && isTTY && !isCI;
11
+ return async (entries) => {
12
+ if (!interactive || entries.length === 0) {
13
+ return entries;
14
+ }
15
+ let ids;
16
+ try {
17
+ ids = await prompt(entries);
18
+ }
19
+ catch {
20
+ // Ctrl+C here used to reject straight out of `runUpgrade`, so the last
21
+ // thing the user saw was an inquirer stack trace — after the manifest had
22
+ // been written and the install had run. Those cannot be taken back, so
23
+ // treat the cancel as "no codemods" and let the caller report what did
24
+ // happen. Any prompt failure lands here; none of them is a reason to lose
25
+ // the bump.
26
+ onCancel("Cancelled at the codemod prompt. The dependency bump and the install already happened — no codemods were run.");
27
+ return [];
28
+ }
29
+ return entries.filter((entry) => ids.includes(entry.id));
30
+ };
31
+ };