@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,173 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { rsort } from "semver";
4
+ import { flowPackages } from "../flowPackages.generated.js";
5
+ import { detectCurrentVersion, findFlowDependencies, } from "../manifest.js";
6
+ import { fetchAllVersions, fetchVersions, intersectVersions, } from "./registry.js";
7
+ import { resolveTarget } from "./target.js";
8
+ /**
9
+ * The installed version of `name`, read from `cwd`'s `node_modules`.
10
+ *
11
+ * Lives here rather than in `upgrade.ts` (where it used to) because `list` now
12
+ * resolves a range too, and both commands need the identical "what's actually
13
+ * installed" answer — this is `resolveRange`'s default reader.
14
+ */
15
+ export const readInstalledVersion = (cwd, name) => {
16
+ try {
17
+ const manifest = JSON.parse(readFileSync(join(cwd, "node_modules", name, "package.json"), "utf8"));
18
+ return manifest.version;
19
+ }
20
+ catch {
21
+ return undefined;
22
+ }
23
+ };
24
+ export const defaultRangeDeps = (cwd) => ({
25
+ cwd,
26
+ fetchVersions,
27
+ readInstalledVersion,
28
+ });
29
+ const isEnoent = (error) => error instanceof Error &&
30
+ "code" in error &&
31
+ error.code === "ENOENT";
32
+ /**
33
+ * Turns a structured `UnresolvedTarget` into the one- or two-sentence message
34
+ * `resolveRange` returns as `reason`.
35
+ *
36
+ * `resolveTarget` reports _why_ it could not resolve `revision`; this is the
37
+ * "let the caller say the right thing" half. `unrecognized` covers both an
38
+ * unknown keyword and an unknown dist-tag — nothing about the string itself
39
+ * says which the caller meant, so the message lists both the keywords and the
40
+ * real dist-tags rather than guessing.
41
+ */
42
+ const describeUnresolvedTarget = (reason) => {
43
+ switch (reason.kind) {
44
+ case "unrecognized": {
45
+ const tags = reason.distTags.length > 0
46
+ ? `, or one of these dist-tags: ${reason.distTags.join(", ")}`
47
+ : "";
48
+ return `"${reason.revision}" is not a recognised revision. Use patch, minor, major, an exact version${tags}.`;
49
+ }
50
+ case "no-candidate": {
51
+ const suggestion = reason.broader !== undefined
52
+ ? `Use "${reason.broader.keyword}" (→ ${reason.broader.target}), or name an exact version.`
53
+ : "No stable version has been published yet — name a dist-tag or an exact version.";
54
+ return `"${reason.revision}" looks for ${reason.describes}; none is published. ${suggestion}`;
55
+ }
56
+ case "unpublished": {
57
+ const closest = reason.closest !== undefined
58
+ ? ` The closest published version is ${reason.closest}.`
59
+ : "";
60
+ return `"${reason.revision}" has not been published.${closest}`;
61
+ }
62
+ }
63
+ };
64
+ /**
65
+ * What `revision` resolves to for the project at `cwd`.
66
+ *
67
+ * Reads the manifest, finds its Flow dependencies, detects the current version,
68
+ * fetches what every dependency has actually published, and resolves `revision`
69
+ * against the intersection — the same sequence `upgrade` always needed, now
70
+ * shared with `list <revision>`, which uses it to answer "what would `upgrade
71
+ * <revision>` touch?" without writing anything.
72
+ *
73
+ * Reports the resolution rather than judging it: an exact version or a stale
74
+ * dist-tag can resolve at or below `current`, and this function returns that
75
+ * target just the same. Whether that is a refusal (`upgrade`, about to write
76
+ * files) or a legitimate answer ("nothing between here and there" — `list`) is
77
+ * for the caller to decide. Two callers judging the same fact differently is
78
+ * fine; two callers computing the fact differently is how they drift.
79
+ */
80
+ export const resolveRange = async (revision, deps) => {
81
+ const manifestPath = join(deps.cwd, "package.json");
82
+ let manifestRaw;
83
+ try {
84
+ manifestRaw = readFileSync(manifestPath, "utf8");
85
+ }
86
+ catch (error) {
87
+ if (isEnoent(error)) {
88
+ return {
89
+ ok: false,
90
+ reason: `No package.json found in ${deps.cwd}. Resolving "${revision}" needs one to read the current Flow version from — a bare "list" does not.`,
91
+ };
92
+ }
93
+ throw error;
94
+ }
95
+ let manifest;
96
+ try {
97
+ manifest = JSON.parse(manifestRaw);
98
+ }
99
+ catch (error) {
100
+ return {
101
+ ok: false,
102
+ reason: `Could not parse ${manifestPath} as JSON: ${error instanceof Error ? error.message : error}`,
103
+ };
104
+ }
105
+ const dependencies = findFlowDependencies(manifest, flowPackages);
106
+ // Destructuring the emptiness check (rather than `dependencies.length ===
107
+ // 0`) sidesteps a `no-non-null-assertion` lint error at every later read of
108
+ // `dependencies[0]` — `no-non-null-assertion` is an error in this repo's
109
+ // eslint config.
110
+ const [firstDependency] = dependencies;
111
+ if (firstDependency === undefined) {
112
+ return {
113
+ ok: false,
114
+ reason: `No Flow dependency found in ${manifestPath}. Nothing to upgrade.`,
115
+ };
116
+ }
117
+ const current = detectCurrentVersion(dependencies, (name) => deps.readInstalledVersion(deps.cwd, name));
118
+ if (current === undefined) {
119
+ return {
120
+ ok: false,
121
+ reason: "Could not determine the Flow version this project is on.",
122
+ };
123
+ }
124
+ // Fixed versioning keeps every Flow package's package.json version equal,
125
+ // but Lerna publishes only the packages that actually changed, so what
126
+ // reaches the registry diverges per package (#2887, accepted risk).
127
+ // Resolving from one "anchor" dependency and writing it onto all of them can
128
+ // pick a version some of the others never published. Resolve instead from
129
+ // the intersection of what every declared dependency has actually
130
+ // published, so the version this returns is always installable.
131
+ const fetched = await fetchAllVersions(dependencies.map(({ name }) => name), deps.fetchVersions);
132
+ const versions = intersectVersions(fetched);
133
+ if (versions.length === 0) {
134
+ return {
135
+ ok: false,
136
+ reason: `${dependencies
137
+ .map(({ name }) => name)
138
+ .join(", ")} have no published version in common. Nothing to upgrade to.`,
139
+ };
140
+ }
141
+ // Dist-tags come from the first declared dependency, but a tag is only kept
142
+ // when the version it points at is one every dependency has published — an
143
+ // unvalidated tag could otherwise resolve outside the intersection above.
144
+ const [firstFetched] = fetched;
145
+ const distTags = Object.fromEntries(Object.entries(firstFetched?.distTags ?? {}).filter(([, version]) => versions.includes(version)));
146
+ const resolved = resolveTarget({ revision, current, versions, distTags });
147
+ if (!resolved.ok) {
148
+ return { ok: false, reason: describeUnresolvedTarget(resolved.reason) };
149
+ }
150
+ const { target } = resolved;
151
+ // Defence in depth: `target` is drawn from `versions` (the intersection) or
152
+ // from a dist-tag already validated against it, so this should never find a
153
+ // gap. If it ever does, refuse rather than report a version some package
154
+ // never published — naming which package lacks it and the highest version
155
+ // every declared dependency has actually published.
156
+ const missingFrom = fetched.find((pkg) => !pkg.versions.includes(target));
157
+ if (missingFrom !== undefined) {
158
+ return {
159
+ ok: false,
160
+ reason: `${missingFrom.name} has not published ${target}. The highest version every declared Flow dependency has published is ${rsort(versions)[0]}.`,
161
+ };
162
+ }
163
+ return {
164
+ ok: true,
165
+ manifestPath,
166
+ manifestRaw,
167
+ manifest,
168
+ dependencies,
169
+ current,
170
+ target,
171
+ versions,
172
+ };
173
+ };
@@ -0,0 +1,33 @@
1
+ export interface RegistryVersions {
2
+ versions: string[];
3
+ distTags: Record<string, string>;
4
+ }
5
+ /**
6
+ * Every published version of a package, plus its dist-tags.
7
+ *
8
+ * Uses the abbreviated packument media type: the full document for a package
9
+ * with hundreds of releases is megabytes, and none of it is needed here.
10
+ */
11
+ export declare const fetchVersions: (packageName: string) => Promise<RegistryVersions>;
12
+ export interface PackageVersions extends RegistryVersions {
13
+ name: string;
14
+ }
15
+ /**
16
+ * Every declared Flow dependency's packument, fetched concurrently.
17
+ *
18
+ * `fetchVersions` is injected rather than imported directly so this stays
19
+ * testable without a network stub — `UpgradeDeps.fetchVersions` is what
20
+ * production and tests both pass in here.
21
+ */
22
+ export declare const fetchAllVersions: (names: string[], fetchOne: (name: string) => Promise<RegistryVersions>) => Promise<PackageVersions[]>;
23
+ /**
24
+ * The versions every one of `packages` has published.
25
+ *
26
+ * Fixed versioning keeps every Flow package's `package.json` version equal, but
27
+ * Lerna publishes only the packages that actually changed, so what reaches the
28
+ * registry diverges per package (#2887, accepted risk). A target version is
29
+ * only safe to write onto every declared dependency when it is one of these —
30
+ * the intersection, not any single package's list.
31
+ */
32
+ export declare const intersectVersions: (packages: PackageVersions[]) => string[];
33
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/resolve/registry.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAwBD;;;;;GAKG;AACH,eAAO,MAAM,aAAa,gBACX,MAAM,KAClB,OAAO,CAAC,gBAAgB,CA6B1B,CAAC;AAEF,MAAM,WAAW,eAAgB,SAAQ,gBAAgB;IACvD,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,UACpB,MAAM,EAAE,YACL,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,gBAAgB,CAAC,KACpD,OAAO,CAAC,eAAe,EAAE,CACmD,CAAC;AAEhF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,aAAc,eAAe,EAAE,KAAG,MAAM,EAQrE,CAAC"}
@@ -0,0 +1,69 @@
1
+ import registryUrl from "registry-url";
2
+ /**
3
+ * The registry that actually serves `@mittwald`, from the consumer's npm
4
+ * config.
5
+ *
6
+ * Hardcoding `registry.npmjs.org` would be wrong for anyone behind a corporate
7
+ * mirror or a scoped private registry — `npm view` works for them because npm
8
+ * reads their `.npmrc`, and a CLI that ignores it would fail or, worse, answer
9
+ * from the wrong source.
10
+ */
11
+ const registryFor = (packageName) => {
12
+ const scope = packageName.startsWith("@")
13
+ ? packageName.slice(0, packageName.indexOf("/"))
14
+ : undefined;
15
+ return registryUrl(scope).replace(/\/$/, "");
16
+ };
17
+ /**
18
+ * Every published version of a package, plus its dist-tags.
19
+ *
20
+ * Uses the abbreviated packument media type: the full document for a package
21
+ * with hundreds of releases is megabytes, and none of it is needed here.
22
+ */
23
+ export const fetchVersions = async (packageName) => {
24
+ // One wrapper for every failure, not just the HTTP one: `fetch` itself rejects
25
+ // on DNS failure or no connection, and `json()` rejects on a malformed body.
26
+ // Without this those surface as a bare "fetch failed", which says nothing
27
+ // about what the CLI was trying to do.
28
+ let packument;
29
+ try {
30
+ const response = await fetch(`${registryFor(packageName)}/${packageName}`, {
31
+ headers: { accept: "application/vnd.npm.install-v1+json" },
32
+ });
33
+ if (!response.ok) {
34
+ throw new Error(`the registry answered ${response.status}`);
35
+ }
36
+ packument = (await response.json());
37
+ }
38
+ catch (error) {
39
+ throw new Error(`Could not read ${packageName} from the npm registry: ${error instanceof Error ? error.message : error}`, { cause: error });
40
+ }
41
+ return {
42
+ versions: Object.keys(packument.versions ?? {}),
43
+ distTags: packument["dist-tags"] ?? {},
44
+ };
45
+ };
46
+ /**
47
+ * Every declared Flow dependency's packument, fetched concurrently.
48
+ *
49
+ * `fetchVersions` is injected rather than imported directly so this stays
50
+ * testable without a network stub — `UpgradeDeps.fetchVersions` is what
51
+ * production and tests both pass in here.
52
+ */
53
+ export const fetchAllVersions = async (names, fetchOne) => Promise.all(names.map(async (name) => ({ name, ...(await fetchOne(name)) })));
54
+ /**
55
+ * The versions every one of `packages` has published.
56
+ *
57
+ * Fixed versioning keeps every Flow package's `package.json` version equal, but
58
+ * Lerna publishes only the packages that actually changed, so what reaches the
59
+ * registry diverges per package (#2887, accepted risk). A target version is
60
+ * only safe to write onto every declared dependency when it is one of these —
61
+ * the intersection, not any single package's list.
62
+ */
63
+ export const intersectVersions = (packages) => {
64
+ const [first, ...rest] = packages;
65
+ if (first === undefined) {
66
+ return [];
67
+ }
68
+ return first.versions.filter((version) => rest.every((pkg) => pkg.versions.includes(version)));
69
+ };
@@ -0,0 +1,63 @@
1
+ export interface ResolveTargetInput {
2
+ /** `patch` | `minor` | `major` | a dist-tag | an exact version. */
3
+ revision: string;
4
+ /** The version the consumer is on. */
5
+ current: string;
6
+ /** Every published version of the anchor package. */
7
+ versions: string[];
8
+ distTags: Record<string, string>;
9
+ }
10
+ export type KeywordRevision = "patch" | "minor" | "major";
11
+ /**
12
+ * Why `resolveTarget` could not turn `revision` into a version — the caller
13
+ * needs this to say something more useful than "could not resolve", because the
14
+ * four cases below call for genuinely different messages (unknown input, a real
15
+ * keyword with nothing to point at, an unknown dist-tag, an unpublished exact
16
+ * version).
17
+ */
18
+ export type UnresolvedTarget = {
19
+ kind: "unrecognized";
20
+ revision: string;
21
+ /** The dist-tags that do exist, so the caller can name them. */
22
+ distTags: string[];
23
+ } | {
24
+ kind: "no-candidate";
25
+ revision: KeywordRevision;
26
+ /** What the keyword was looking for, in prose ("the highest …"). */
27
+ describes: string;
28
+ /**
29
+ * The next broader keyword and what it resolves to, when one exists —
30
+ * `major` has none, it is already unbounded. Naming this beats hardcoding
31
+ * "use major": a `patch` failure's real escape is `minor`.
32
+ */
33
+ broader?: {
34
+ keyword: KeywordRevision;
35
+ target: string;
36
+ };
37
+ } | {
38
+ kind: "unpublished";
39
+ revision: string;
40
+ /** The nearest published version, when `versions` is non-empty. */
41
+ closest: string | undefined;
42
+ };
43
+ export type ResolveTargetResult = {
44
+ ok: true;
45
+ target: string;
46
+ } | {
47
+ ok: false;
48
+ reason: UnresolvedTarget;
49
+ };
50
+ /**
51
+ * What a revision means, as a concrete published version — or a structured
52
+ * reason it does not.
53
+ *
54
+ * Reports the resolution rather than judging it: it does **not** judge whether
55
+ * the result is an upgrade; the caller compares against `current` and refuses a
56
+ * sideways or downward move.
57
+ *
58
+ * Keyword resolution skips prereleases (see `highestStable`). Only an explicit
59
+ * dist-tag or an exact version reaches a `-next.N`, so `upgrade minor` on the
60
+ * stable line never drifts onto the collection branch.
61
+ */
62
+ export declare const resolveTarget: ({ revision, current, versions, distTags, }: ResolveTargetInput) => ResolveTargetResult;
63
+ //# sourceMappingURL=target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target.d.ts","sourceRoot":"","sources":["../../src/resolve/target.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;AAO1D;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB;IACE,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,GACD;IACE,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,eAAe,CAAC;IAC1B,oEAAoE;IACpE,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE;QAAE,OAAO,EAAE,eAAe,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACxD,GACD;IACE,IAAI,EAAE,aAAa,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B,CAAC;AAEN,MAAM,MAAM,mBAAmB,GAC7B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAiFzE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,+CAKvB,kBAAkB,KAAG,mBA0CvB,CAAC"}
@@ -0,0 +1,115 @@
1
+ import { lt, major, minor, prerelease, rsort, satisfies, valid } from "semver";
2
+ const KEYWORDS = ["patch", "minor", "major"];
3
+ const isKeyword = (revision) => KEYWORDS.includes(revision);
4
+ /**
5
+ * The range a revision keyword bounds the target to.
6
+ *
7
+ * `patch` keeps the current minor, `minor` keeps the current major, `major`
8
+ * accepts anything.
9
+ */
10
+ const keywordRange = (revision, current) => {
11
+ switch (revision) {
12
+ case "patch":
13
+ return `${major(current)}.${minor(current)}.x`;
14
+ case "minor":
15
+ return `${major(current)}.x`;
16
+ case "major":
17
+ return "*";
18
+ }
19
+ };
20
+ /** Prose for what a keyword looks for — used in the "no-candidate" message. */
21
+ const describeKeyword = (revision, current) => {
22
+ switch (revision) {
23
+ case "patch":
24
+ return `the highest published patch release on ${major(current)}.${minor(current)}.x`;
25
+ case "minor":
26
+ return `the highest published stable release in ${major(current)}.x`;
27
+ case "major":
28
+ return "the highest published stable release";
29
+ }
30
+ };
31
+ /**
32
+ * The highest stable version satisfying `range`, or `undefined`.
33
+ *
34
+ * The `prerelease(...) === null` filter is defence in depth rather than the
35
+ * mechanism: none of the ranges built here embeds a prerelease tag, and
36
+ * node-semver only matches a prerelease when a comparator carries a matching
37
+ * one — so `satisfies` already excludes them. The filter keeps that true if a
38
+ * range shape or an option ever changes.
39
+ */
40
+ const highestStable = (range, versions) => {
41
+ const candidates = versions.filter((version) => prerelease(version) === null && satisfies(version, range));
42
+ return rsort(candidates)[0];
43
+ };
44
+ /**
45
+ * The next keyword broader than `revision`, and what it resolves to.
46
+ *
47
+ * `patch` is bounded to the current minor, `minor` to the current major — each
48
+ * has exactly one broader step. `major` is already unbounded, so it has none.
49
+ * This is what turns "use major" into a suggestion the caller can act on
50
+ * directly, and what stops a `patch` failure suggesting `major` when `minor`
51
+ * would already have found something.
52
+ */
53
+ const broaderCandidate = (revision, current, versions) => {
54
+ const index = KEYWORDS.indexOf(revision);
55
+ for (let i = index + 1; i < KEYWORDS.length; i++) {
56
+ const keyword = KEYWORDS[i];
57
+ if (keyword === undefined) {
58
+ continue;
59
+ }
60
+ const target = highestStable(keywordRange(keyword, current), versions);
61
+ if (target !== undefined) {
62
+ return { keyword, target };
63
+ }
64
+ }
65
+ return undefined;
66
+ };
67
+ /**
68
+ * What a revision means, as a concrete published version — or a structured
69
+ * reason it does not.
70
+ *
71
+ * Reports the resolution rather than judging it: it does **not** judge whether
72
+ * the result is an upgrade; the caller compares against `current` and refuses a
73
+ * sideways or downward move.
74
+ *
75
+ * Keyword resolution skips prereleases (see `highestStable`). Only an explicit
76
+ * dist-tag or an exact version reaches a `-next.N`, so `upgrade minor` on the
77
+ * stable line never drifts onto the collection branch.
78
+ */
79
+ export const resolveTarget = ({ revision, current, versions, distTags, }) => {
80
+ const tagged = distTags[revision];
81
+ if (tagged !== undefined) {
82
+ return { ok: true, target: tagged };
83
+ }
84
+ if (valid(revision) !== null) {
85
+ if (versions.includes(revision)) {
86
+ return { ok: true, target: revision };
87
+ }
88
+ const sorted = rsort(versions);
89
+ const closest = sorted.find((version) => lt(version, revision)) ?? sorted.at(-1);
90
+ return { ok: false, reason: { kind: "unpublished", revision, closest } };
91
+ }
92
+ if (!isKeyword(revision)) {
93
+ return {
94
+ ok: false,
95
+ reason: {
96
+ kind: "unrecognized",
97
+ revision,
98
+ distTags: Object.keys(distTags),
99
+ },
100
+ };
101
+ }
102
+ const target = highestStable(keywordRange(revision, current), versions);
103
+ if (target !== undefined) {
104
+ return { ok: true, target };
105
+ }
106
+ return {
107
+ ok: false,
108
+ reason: {
109
+ kind: "no-candidate",
110
+ revision,
111
+ describes: describeKeyword(revision, current),
112
+ broader: broaderCandidate(revision, current, versions),
113
+ },
114
+ };
115
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Whether `id` names a transform file on disk.
3
+ *
4
+ * `runSingleCodemod` calls this to decide that before it ever reaches this
5
+ * module's own `existsSync` check below.
6
+ */
7
+ export declare const transformExists: (id: string) => boolean;
8
+ export interface CodemodOptions {
9
+ /** A catalogue id — the transform file name without its extension. */
10
+ id: string;
11
+ /** File or directory to transform. */
12
+ path: string;
13
+ dry?: boolean;
14
+ print?: boolean;
15
+ }
16
+ export interface CodemodResult {
17
+ changed: number;
18
+ unmodified: number;
19
+ /** Files the transform declined by returning nothing. */
20
+ skipped: number;
21
+ errors: number;
22
+ /** True when jscodeshift accounted for no file at all — see below. */
23
+ processedNothing: boolean;
24
+ }
25
+ /**
26
+ * Runs one codemod over a path.
27
+ *
28
+ * This drives jscodeshift's `Runner` directly rather than its CLI. The CLI only
29
+ * reports its counts as text, and scraping that text is not safe: `--print`
30
+ * writes the transformed source to stdout _before_ the summary, so a source
31
+ * comment like `// 42 ok` wins a regex looking for `(\d+) ok`. The Runner
32
+ * resolves with the counters as numbers, and it distinguishes `skip` (the
33
+ * transform returned nothing) from `nochange` (it returned identical source) —
34
+ * a difference the CLI's summary and any regex over it both lose.
35
+ *
36
+ * `processedNothing` exists because jscodeshift reports a path with no matching
37
+ * files and a worker that died before touching one the same way: every counter
38
+ * zero, no error. The caller must not render that as "0 files changed", which
39
+ * reads like success.
40
+ */
41
+ export declare const runCodemod: ({ id, path, dry, print, }: CodemodOptions) => Promise<CodemodResult>;
42
+ //# sourceMappingURL=jscodeshift.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jscodeshift.d.ts","sourceRoot":"","sources":["../../src/run/jscodeshift.ts"],"names":[],"mappings":"AAuCA;;;;;GAKG;AACH,eAAO,MAAM,eAAe,OAAQ,MAAM,KAAG,OACd,CAAC;AAEhC,MAAM,WAAW,cAAc;IAC7B,sEAAsE;IACtE,EAAE,EAAE,MAAM,CAAC;IACX,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAUD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,8BAKpB,cAAc,KAAG,OAAO,CAAC,aAAa,CAoCxC,CAAC"}
@@ -0,0 +1,88 @@
1
+ import { existsSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ import { unknownCodemodMessage } from "../catalog/entries.js";
4
+ // jscodeshift ships no types for its Runner — `allowJs` in this repo's shared
5
+ // tsconfig (packages/typescript-config/base.json) lets a deep import of a
6
+ // plain `.js` file resolve without a type error, so no `@ts-expect-error` is
7
+ // needed (and adding one here fails the build: "Unused '@ts-expect-error'
8
+ // directive").
9
+ import { run as runJscodeshift } from "jscodeshift/src/Runner.js";
10
+ /**
11
+ * `<packageRoot>/src/migrations` and `<packageRoot>/src/tools`, from either
12
+ * `src/run` or `dist/run`.
13
+ *
14
+ * `dist` mirrors `src`'s directory depth, so one pair of relative paths serves
15
+ * the test run and the published binary. The transforms are not compiled into
16
+ * `dist`: jscodeshift puts a transform through its own babel pipeline, so it
17
+ * wants the `.ts` file.
18
+ */
19
+ const migrationsDir = fileURLToPath(new URL("../../src/migrations", import.meta.url));
20
+ const toolsDir = fileURLToPath(new URL("../../src/tools", import.meta.url));
21
+ /**
22
+ * The transform file for `id`: `src/migrations/<id>/transform.ts` when `id`
23
+ * names a migration, otherwise `src/tools/<id>.ts`.
24
+ *
25
+ * Deliberately independent of the catalogue: `to-remote-package` is a transform
26
+ * with no catalogue entry (it is a port, not a migration — see `notAMigration`
27
+ * in `src/tests/remoteScope.test.ts`), and it still has to be runnable by id.
28
+ * It lives in `src/tools` rather than `src/migrations` for exactly that reason
29
+ * — there is no migration directory to put it beside.
30
+ */
31
+ const transformPath = (id) => {
32
+ const migrationPath = `${migrationsDir}/${id}/transform.ts`;
33
+ return existsSync(migrationPath) ? migrationPath : `${toolsDir}/${id}.ts`;
34
+ };
35
+ /**
36
+ * Whether `id` names a transform file on disk.
37
+ *
38
+ * `runSingleCodemod` calls this to decide that before it ever reaches this
39
+ * module's own `existsSync` check below.
40
+ */
41
+ export const transformExists = (id) => existsSync(transformPath(id));
42
+ /**
43
+ * Runs one codemod over a path.
44
+ *
45
+ * This drives jscodeshift's `Runner` directly rather than its CLI. The CLI only
46
+ * reports its counts as text, and scraping that text is not safe: `--print`
47
+ * writes the transformed source to stdout _before_ the summary, so a source
48
+ * comment like `// 42 ok` wins a regex looking for `(\d+) ok`. The Runner
49
+ * resolves with the counters as numbers, and it distinguishes `skip` (the
50
+ * transform returned nothing) from `nochange` (it returned identical source) —
51
+ * a difference the CLI's summary and any regex over it both lose.
52
+ *
53
+ * `processedNothing` exists because jscodeshift reports a path with no matching
54
+ * files and a worker that died before touching one the same way: every counter
55
+ * zero, no error. The caller must not render that as "0 files changed", which
56
+ * reads like success.
57
+ */
58
+ export const runCodemod = async ({ id, path, dry = false, print = false, }) => {
59
+ const transform = transformPath(id);
60
+ if (!existsSync(transform)) {
61
+ throw new Error(unknownCodemodMessage(id));
62
+ }
63
+ let stats;
64
+ try {
65
+ stats = (await runJscodeshift(transform, [path], {
66
+ parser: "tsx",
67
+ silent: true,
68
+ dry,
69
+ print,
70
+ // Without these the walk takes every file under the path. `upgrade` runs
71
+ // codemods after the install, so `node_modules` is freshly populated
72
+ // underneath — and non-JS files fail to parse, which shows up as
73
+ // `errors > 0` and hides the real change count.
74
+ extensions: "js,jsx,ts,tsx,cjs,mjs,cts,mts",
75
+ ignorePattern: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
76
+ }));
77
+ }
78
+ catch (error) {
79
+ throw new Error(`Running ${id} failed: ${error instanceof Error ? error.message : error}`, { cause: error });
80
+ }
81
+ return {
82
+ changed: stats.ok,
83
+ unmodified: stats.nochange,
84
+ skipped: stats.skip,
85
+ errors: stats.error,
86
+ processedNothing: stats.ok + stats.nochange + stats.skip + stats.error === 0,
87
+ };
88
+ };