@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.
- package/README.md +106 -0
- package/dist/catalog/entries.d.ts +17 -0
- package/dist/catalog/entries.d.ts.map +1 -0
- package/dist/catalog/entries.js +9 -0
- package/dist/catalog/select.d.ts +28 -0
- package/dist/catalog/select.d.ts.map +1 -0
- package/dist/catalog/select.js +27 -0
- package/dist/catalog/types.d.ts +48 -0
- package/dist/catalog/types.d.ts.map +1 -0
- package/dist/catalog/types.js +0 -0
- package/dist/checks/context.d.ts +16 -0
- package/dist/checks/context.d.ts.map +1 -0
- package/dist/checks/context.js +72 -0
- package/dist/checks/load.d.ts +13 -0
- package/dist/checks/load.d.ts.map +1 -0
- package/dist/checks/load.js +53 -0
- package/dist/checks/types.d.ts +49 -0
- package/dist/checks/types.d.ts.map +1 -0
- package/dist/checks/types.js +0 -0
- package/dist/cli/args.d.ts +22 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +56 -0
- package/dist/cli/choose.d.ts +31 -0
- package/dist/cli/choose.d.ts.map +1 -0
- package/dist/cli/choose.js +31 -0
- package/dist/cli/codemod.d.ts +24 -0
- package/dist/cli/codemod.d.ts.map +1 -0
- package/dist/cli/codemod.js +76 -0
- package/dist/cli/detect.d.ts +35 -0
- package/dist/cli/detect.d.ts.map +1 -0
- package/dist/cli/detect.js +62 -0
- package/dist/cli/list.d.ts +71 -0
- package/dist/cli/list.d.ts.map +1 -0
- package/dist/cli/list.js +293 -0
- package/dist/cli/upgrade.d.ts +34 -0
- package/dist/cli/upgrade.d.ts.map +1 -0
- package/dist/cli/upgrade.js +217 -0
- package/dist/cli/verify.d.ts +77 -0
- package/dist/cli/verify.d.ts.map +1 -0
- package/dist/cli/verify.js +148 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +117 -0
- package/dist/detect/align-to-combine.d.ts +8 -0
- package/dist/detect/align-to-combine.d.ts.map +1 -0
- package/dist/detect/align-to-combine.js +9 -0
- package/dist/detect/overlay-controller-add-on-close-return-type.d.ts +9 -0
- package/dist/detect/overlay-controller-add-on-close-return-type.d.ts.map +1 -0
- package/dist/detect/overlay-controller-add-on-close-return-type.js +10 -0
- package/dist/detect/tooltip-trigger-delay-type.d.ts +11 -0
- package/dist/detect/tooltip-trigger-delay-type.d.ts.map +1 -0
- package/dist/detect/tooltip-trigger-delay-type.js +12 -0
- package/dist/flowPackages.generated.d.ts +3 -0
- package/dist/flowPackages.generated.d.ts.map +1 -0
- package/dist/flowPackages.generated.js +18 -0
- package/dist/git.d.ts +13 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +41 -0
- package/dist/install.d.ts +37 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +71 -0
- package/dist/manifest.d.ts +45 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +88 -0
- package/dist/migrations.generated.d.ts +4 -0
- package/dist/migrations.generated.d.ts.map +1 -0
- package/dist/migrations.generated.js +213 -0
- package/dist/resolve/range.d.ts +50 -0
- package/dist/resolve/range.d.ts.map +1 -0
- package/dist/resolve/range.js +173 -0
- package/dist/resolve/registry.d.ts +33 -0
- package/dist/resolve/registry.d.ts.map +1 -0
- package/dist/resolve/registry.js +69 -0
- package/dist/resolve/target.d.ts +63 -0
- package/dist/resolve/target.d.ts.map +1 -0
- package/dist/resolve/target.js +115 -0
- package/dist/run/jscodeshift.d.ts +42 -0
- package/dist/run/jscodeshift.d.ts.map +1 -0
- package/dist/run/jscodeshift.js +88 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/verify/align-to-combine.d.ts +11 -0
- package/dist/verify/align-to-combine.d.ts.map +1 -0
- package/dist/verify/align-to-combine.js +22 -0
- package/dist/verify/overlay-controller-add-on-close-return-type.d.ts +14 -0
- package/dist/verify/overlay-controller-add-on-close-return-type.d.ts.map +1 -0
- package/dist/verify/overlay-controller-add-on-close-return-type.js +21 -0
- package/dist/verify/tooltip-trigger-delay-type.d.ts +13 -0
- package/dist/verify/tooltip-trigger-delay-type.d.ts.map +1 -0
- package/dist/verify/tooltip-trigger-delay-type.js +18 -0
- package/package.json +41 -0
- package/src/migrations/accent-box-color-to-background-color/transform.ts +218 -0
- package/src/migrations/action-prop-to-on-action/transform.ts +110 -0
- package/src/migrations/align-to-combine/transform.ts +212 -0
- package/src/migrations/button-color-accent-to-success/transform.ts +164 -0
- package/src/migrations/button-props-interfaces/transform.ts +229 -0
- package/src/migrations/color-primary-to-default/transform.ts +173 -0
- package/src/migrations/imports-to-package-root/transform.ts +107 -0
- package/src/migrations/muted-action-error-to-abort-action-error/transform.ts +260 -0
- package/src/migrations/password-tools-rule/transform.ts +219 -0
- package/src/migrations/password-tools-subpath-renamed/transform.ts +61 -0
- 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
|
+
};
|