@mittwald/flow-codemods 1.1.0 → 1.1.2
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/dist/migrations/accent-box-color-to-background-color/transform.js +175 -0
- package/dist/migrations/action-prop-to-on-action/transform.js +82 -0
- package/dist/migrations/align-to-combine/transform.js +166 -0
- package/dist/migrations/button-color-accent-to-success/transform.js +126 -0
- package/dist/migrations/button-props-interfaces/transform.js +178 -0
- package/dist/migrations/color-primary-to-default/transform.js +135 -0
- package/dist/migrations/imports-to-package-root/transform.js +93 -0
- package/dist/migrations/muted-action-error-to-abort-action-error/transform.js +198 -0
- package/dist/migrations/package.json +3 -0
- package/dist/migrations/password-tools-rule/transform.js +173 -0
- package/dist/migrations/password-tools-subpath-renamed/transform.js +51 -0
- package/dist/migrations.generated.js +1 -1
- package/dist/run/jscodeshift.d.ts +4 -1
- package/dist/run/jscodeshift.d.ts.map +1 -1
- package/dist/run/jscodeshift.js +60 -22
- package/dist/tools/package.json +3 -0
- package/dist/tools/to-remote-package.js +29 -0
- package/package.json +4 -6
- package/src/migrations/accent-box-color-to-background-color/transform.ts +0 -218
- package/src/migrations/action-prop-to-on-action/transform.ts +0 -110
- package/src/migrations/align-to-combine/transform.ts +0 -212
- package/src/migrations/button-color-accent-to-success/transform.ts +0 -164
- package/src/migrations/button-props-interfaces/transform.ts +0 -229
- package/src/migrations/color-primary-to-default/transform.ts +0 -173
- package/src/migrations/imports-to-package-root/transform.ts +0 -107
- package/src/migrations/muted-action-error-to-abort-action-error/transform.ts +0 -260
- package/src/migrations/password-tools-rule/transform.ts +0 -219
- package/src/migrations/password-tools-subpath-renamed/transform.ts +0 -61
- package/src/tools/to-remote-package.ts +0 -37
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Replaces `AsyncRule` and `SyncRule` with `Rule` (alpha.802).
|
|
5
|
+
*
|
|
6
|
+
* `@mittwald/password-tools-js` merged both classes into a single abstract
|
|
7
|
+
* `Rule`, and the `mittwald-password-tools-js` entry stopped re-exporting the
|
|
8
|
+
* old names. A custom rule extends `Rule` and may return its result
|
|
9
|
+
* synchronously or as a promise — the distinction the two classes encoded is
|
|
10
|
+
* gone, so both names collapse onto the same one.
|
|
11
|
+
*
|
|
12
|
+
* Only names imported from the `mittwald-password-tools-js` entry of
|
|
13
|
+
* `@mittwald/flow-react-components` are touched, so a same-named import from
|
|
14
|
+
* another package is left alone. `@mittwald/flow-remote-react-components` has
|
|
15
|
+
* no such entry. A name imported under a local alias (`import { AsyncRule as
|
|
16
|
+
* Base }`) keeps its alias — only the imported name changes. Namespace usages
|
|
17
|
+
* (`Pw.AsyncRule`) are rewritten as well.
|
|
18
|
+
*
|
|
19
|
+
* Because both names collapse onto `Rule`, a file importing more than one of
|
|
20
|
+
* them would end up with a duplicate specifier. Those collapse onto one.
|
|
21
|
+
*/
|
|
22
|
+
const passwordToolsRuleTransform = (fileInfo, { j }) => {
|
|
23
|
+
const flowPackages = [
|
|
24
|
+
"@mittwald/flow-react-components/mittwald-password-tools-js",
|
|
25
|
+
];
|
|
26
|
+
const renames = new Map([
|
|
27
|
+
["AsyncRule", "Rule"],
|
|
28
|
+
["SyncRule", "Rule"],
|
|
29
|
+
]);
|
|
30
|
+
const isFlowImport = (source) => flowPackages.includes(source);
|
|
31
|
+
// ast-types models `importKind` on the declaration only, while babel also
|
|
32
|
+
// puts it on the specifier — which is where a per-specifier `type X` lives.
|
|
33
|
+
const isTypeOnly = (specifier) => specifier.importKind === "type";
|
|
34
|
+
const makeValueImport = (specifier) => {
|
|
35
|
+
specifier.importKind = "value";
|
|
36
|
+
};
|
|
37
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
38
|
+
/** Local identifiers that refer to a renamed export, mapped to the new name. */
|
|
39
|
+
const localRenames = new Map();
|
|
40
|
+
/**
|
|
41
|
+
* Whether any import specifier was rewritten. An aliased `Align as Row` adds
|
|
42
|
+
* nothing to `localRenames` — its local name stays `Row` — so counting those
|
|
43
|
+
* alone would treat an alias-only file as untouched and discard the rewrite.
|
|
44
|
+
*/
|
|
45
|
+
let renamedAnImport = false;
|
|
46
|
+
/** Local names of `import * as Flow` namespace imports from a Flow package. */
|
|
47
|
+
const flowNamespaces = new Set();
|
|
48
|
+
const flowImports = root
|
|
49
|
+
.find(j.ImportDeclaration)
|
|
50
|
+
.filter((path) => isFlowImport(String(path.node.source.value)));
|
|
51
|
+
/**
|
|
52
|
+
* `imported:local` pairs the file already binds without a rename. A rename
|
|
53
|
+
* that lands on one of these must not add a second specifier for it — the
|
|
54
|
+
* name is available from the import that already carries it, which is also
|
|
55
|
+
* the entry that really exports it.
|
|
56
|
+
*/
|
|
57
|
+
const claimed = new Set();
|
|
58
|
+
flowImports.forEach((path) => {
|
|
59
|
+
for (const specifier of path.node.specifiers ?? []) {
|
|
60
|
+
if (specifier.type === "ImportNamespaceSpecifier" &&
|
|
61
|
+
specifier.local?.name) {
|
|
62
|
+
flowNamespaces.add(String(specifier.local.name));
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (specifier.type !== "ImportSpecifier" ||
|
|
66
|
+
specifier.imported.type !== "Identifier" ||
|
|
67
|
+
renames.has(specifier.imported.name)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
claimed.add(`${specifier.imported.name}:${String(specifier.local?.name ?? specifier.imported.name)}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
flowImports.forEach((path) => {
|
|
74
|
+
const specifiers = path.node.specifiers ?? [];
|
|
75
|
+
const survivors = [];
|
|
76
|
+
for (const specifier of specifiers) {
|
|
77
|
+
if (specifier.type !== "ImportSpecifier" ||
|
|
78
|
+
specifier.imported.type !== "Identifier") {
|
|
79
|
+
survivors.push(specifier);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const imported = specifier.imported.name;
|
|
83
|
+
const renamed = renames.get(imported);
|
|
84
|
+
if (!renamed) {
|
|
85
|
+
survivors.push(specifier);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const local = String(specifier.local?.name ?? imported);
|
|
89
|
+
const isAlias = local !== imported;
|
|
90
|
+
const newLocal = isAlias ? local : renamed;
|
|
91
|
+
const key = `${renamed}:${newLocal}`;
|
|
92
|
+
renamedAnImport = true;
|
|
93
|
+
if (!isAlias) {
|
|
94
|
+
// An aliased import keeps its local name and needs no further change.
|
|
95
|
+
localRenames.set(local, renamed);
|
|
96
|
+
}
|
|
97
|
+
// Renaming can collide with a name the file already binds — directly, or
|
|
98
|
+
// because a second old name maps onto the same new one. Either way the
|
|
99
|
+
// binding already exists, so this specifier goes away instead of
|
|
100
|
+
// producing a duplicate declaration, which would not parse.
|
|
101
|
+
const collision = claimed.has(key);
|
|
102
|
+
claimed.add(key);
|
|
103
|
+
if (collision) {
|
|
104
|
+
// A value import must not lose out to a type-only one.
|
|
105
|
+
if (!isTypeOnly(specifier)) {
|
|
106
|
+
for (const kept of survivors) {
|
|
107
|
+
if (kept.type === "ImportSpecifier" &&
|
|
108
|
+
kept.imported.type === "Identifier" &&
|
|
109
|
+
`${kept.imported.name}:${String(kept.local?.name ?? kept.imported.name)}` === key) {
|
|
110
|
+
makeValueImport(kept);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// Mutate in place so an `import type` / `type X` modifier survives.
|
|
117
|
+
specifier.imported.name = renamed;
|
|
118
|
+
if (!isAlias && specifier.local) {
|
|
119
|
+
specifier.local.name = renamed;
|
|
120
|
+
}
|
|
121
|
+
survivors.push(specifier);
|
|
122
|
+
}
|
|
123
|
+
if (survivors.length === specifiers.length) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
// Every specifier moved to an import that already binds the name. Leaving
|
|
127
|
+
// the declaration behind would turn it into a side-effect import of an
|
|
128
|
+
// entry the file no longer uses.
|
|
129
|
+
if (survivors.length === 0) {
|
|
130
|
+
j(path).remove();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
path.node.specifiers = survivors;
|
|
134
|
+
});
|
|
135
|
+
if (!renamedAnImport && flowNamespaces.size === 0) {
|
|
136
|
+
return fileInfo.source;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* `JSXIdentifier` extends `Identifier`, so this one pass covers JSX tags,
|
|
140
|
+
* value references and type references alike. Positions where the name is not
|
|
141
|
+
* a reference to the import — an object key, a member's property, a JSX
|
|
142
|
+
* attribute name — are skipped.
|
|
143
|
+
*/
|
|
144
|
+
root.find(j.Identifier).forEach((path) => {
|
|
145
|
+
const parent = path.parent.node;
|
|
146
|
+
const isNamespaceMember = (parent.type === "MemberExpression" ||
|
|
147
|
+
parent.type === "JSXMemberExpression") &&
|
|
148
|
+
parent.property === path.node;
|
|
149
|
+
if (isNamespaceMember) {
|
|
150
|
+
const object = parent.object;
|
|
151
|
+
if (object.type === "Identifier" || object.type === "JSXIdentifier") {
|
|
152
|
+
const renamed = renames.get(path.node.name);
|
|
153
|
+
if (renamed && flowNamespaces.has(String(object.name))) {
|
|
154
|
+
path.node.name = renamed;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (parent.type === "ImportSpecifier" ||
|
|
160
|
+
parent.type === "JSXAttribute" ||
|
|
161
|
+
((parent.type === "ObjectProperty" || parent.type === "Property") &&
|
|
162
|
+
parent.key === path.node &&
|
|
163
|
+
!parent.computed)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const renamed = localRenames.get(path.node.name);
|
|
167
|
+
if (renamed) {
|
|
168
|
+
path.node.name = renamed;
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
return root.toSource();
|
|
172
|
+
};
|
|
173
|
+
exports.default = passwordToolsRuleTransform;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const oldPath = "@mittwald/flow-react-components/password-tools";
|
|
4
|
+
const newPath = "@mittwald/flow-react-components/mittwald-password-tools-js";
|
|
5
|
+
/**
|
|
6
|
+
* Rewrites the module specifier
|
|
7
|
+
* `@mittwald/flow-react-components/password-tools` to
|
|
8
|
+
* `@mittwald/flow-react-components/mittwald-password-tools-js` (alpha.1000).
|
|
9
|
+
*
|
|
10
|
+
* Every form that names a module is covered: `import`, `import type`, `export …
|
|
11
|
+
* from`, a side-effect import, `import()` and `require()`. Only that exact
|
|
12
|
+
* specifier is touched — a deeper path under it never existed, so there is
|
|
13
|
+
* nothing to prefix-match and no risk of rewriting an unrelated
|
|
14
|
+
* `password-tools` of someone else's.
|
|
15
|
+
*/
|
|
16
|
+
const passwordToolsSubpathRenamedTransform = (fileInfo, { j }) => {
|
|
17
|
+
const root = j(fileInfo.source, { parser: "tsx" });
|
|
18
|
+
let changed = false;
|
|
19
|
+
const rewrite = (node) => {
|
|
20
|
+
if (node.value === oldPath) {
|
|
21
|
+
node.value = newPath;
|
|
22
|
+
changed = true;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
// import … from "…", export … from "…", import "…"
|
|
26
|
+
root.find(j.ImportDeclaration).forEach((path) => rewrite(path.node.source));
|
|
27
|
+
root
|
|
28
|
+
.find(j.ExportNamedDeclaration)
|
|
29
|
+
.forEach((path) => path.node.source && rewrite(path.node.source));
|
|
30
|
+
root
|
|
31
|
+
.find(j.ExportAllDeclaration)
|
|
32
|
+
.forEach((path) => rewrite(path.node.source));
|
|
33
|
+
// import("…") and require("…")
|
|
34
|
+
root
|
|
35
|
+
.find(j.CallExpression)
|
|
36
|
+
.filter((path) => {
|
|
37
|
+
const callee = path.node.callee;
|
|
38
|
+
return (callee.type === "Import" ||
|
|
39
|
+
(callee.type === "Identifier" && callee.name === "require"));
|
|
40
|
+
})
|
|
41
|
+
.forEach((path) => {
|
|
42
|
+
const [argument] = path.node.arguments;
|
|
43
|
+
if (argument &&
|
|
44
|
+
(argument.type === "StringLiteral" || argument.type === "Literal")) {
|
|
45
|
+
rewrite(argument);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
// Returning the source untouched keeps this file out of the "changed" count.
|
|
49
|
+
return changed ? root.toSource() : fileInfo.source;
|
|
50
|
+
};
|
|
51
|
+
exports.default = passwordToolsSubpathRenamedTransform;
|
|
@@ -145,7 +145,7 @@ export const migrations = [
|
|
|
145
145
|
kind: "migration",
|
|
146
146
|
action: "manual",
|
|
147
147
|
remotePackage: true,
|
|
148
|
-
apply: "Check every `CodeBlock` usage against the current props (see the [CodeBlock documentation](https://flow.mittwald.de/
|
|
148
|
+
apply: "Check every `CodeBlock` usage against the current props (see the [CodeBlock documentation](https://flow.mittwald.de/components/content/code-block)) and remove or replace props the new implementation does not support.",
|
|
149
149
|
},
|
|
150
150
|
{
|
|
151
151
|
id: "muted-action-error-to-abort-action-error",
|
|
@@ -36,7 +36,10 @@ export interface CodemodResult {
|
|
|
36
36
|
* `processedNothing` exists because jscodeshift reports a path with no matching
|
|
37
37
|
* files and a worker that died before touching one the same way: every counter
|
|
38
38
|
* zero, no error. The caller must not render that as "0 files changed", which
|
|
39
|
-
* reads like success.
|
|
39
|
+
* reads like success. The load check below removes the most common cause of the
|
|
40
|
+
* second case, so `processedNothing` now means the path far more often than it
|
|
41
|
+
* used to — but not always: a worker can still die for a reason a successful
|
|
42
|
+
* load does not predict, so the flag keeps its deliberately vague name.
|
|
40
43
|
*/
|
|
41
44
|
export declare const runCodemod: ({ id, path, dry, print, }: CodemodOptions) => Promise<CodemodResult>;
|
|
42
45
|
//# sourceMappingURL=jscodeshift.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jscodeshift.d.ts","sourceRoot":"","sources":["../../src/run/jscodeshift.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"jscodeshift.d.ts","sourceRoot":"","sources":["../../src/run/jscodeshift.ts"],"names":[],"mappings":"AAwDA;;;;;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;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,8BAKpB,cAAc,KAAG,OAAO,CAAC,aAAa,CA6DxC,CAAC"}
|
package/dist/run/jscodeshift.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import { unknownCodemodMessage } from "../catalog/entries.js";
|
|
4
4
|
// jscodeshift ships no types for its Runner — `allowJs` in this repo's shared
|
|
5
5
|
// tsconfig (packages/typescript-config/base.json) lets a deep import of a
|
|
@@ -8,30 +8,45 @@ import { unknownCodemodMessage } from "../catalog/entries.js";
|
|
|
8
8
|
// directive").
|
|
9
9
|
import { run as runJscodeshift } from "jscodeshift/src/Runner.js";
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
11
|
+
* The package root, from either `src/run` or `dist/run` — both are two levels
|
|
12
|
+
* down, so one expression serves the test run and the published binary.
|
|
18
13
|
*/
|
|
19
|
-
const
|
|
20
|
-
const toolsDir = fileURLToPath(new URL("../../src/tools", import.meta.url));
|
|
14
|
+
const packageRoot = fileURLToPath(new URL("../../", import.meta.url));
|
|
21
15
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
16
|
+
* Where a transform for `id` may live, most preferred first.
|
|
17
|
+
*
|
|
18
|
+
* The compiled CommonJS in `dist` comes first, and in a published install it is
|
|
19
|
+
* the only one that works. jscodeshift's worker `require()`s this path; it
|
|
20
|
+
* installs `@babel/register` beforehand, but babel-register's `only` defaults
|
|
21
|
+
* to the current working directory, so a transform inside the consumer's
|
|
22
|
+
* `node_modules` is never claimed by babel. Node's own `.ts` handler takes over
|
|
23
|
+
* and refuses — `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`, no opt-out — and
|
|
24
|
+
* every codemod dies before touching a file. See `tsconfig.transforms.json`.
|
|
25
|
+
*
|
|
26
|
+
* The `.ts` sources stay as a fallback for running out of the repo (`tsx`, the
|
|
27
|
+
* unit tests, `dist` not built yet), where cwd _is_ inside the package and
|
|
28
|
+
* babel-register does claim them. They are no longer published: `files` ships
|
|
29
|
+
* `dist` only, so the path that cannot work is absent from the tarball rather
|
|
30
|
+
* than merely deprioritised.
|
|
24
31
|
*
|
|
25
|
-
*
|
|
26
|
-
* with no catalogue entry (
|
|
27
|
-
* in `src/tests/remoteScope.test.ts`)
|
|
28
|
-
*
|
|
29
|
-
* — there is no migration directory to put it beside.
|
|
32
|
+
* `src/tools` is searched alongside `src/migrations` because
|
|
33
|
+
* `to-remote-package` is a transform with no catalogue entry (a port, not a
|
|
34
|
+
* migration — see `notAMigration` in `src/tests/remoteScope.test.ts`) and still
|
|
35
|
+
* has to be runnable by id.
|
|
30
36
|
*/
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
37
|
+
const candidatePaths = (id) => [
|
|
38
|
+
`${packageRoot}dist/migrations/${id}/transform.js`,
|
|
39
|
+
`${packageRoot}dist/tools/${id}.js`,
|
|
40
|
+
`${packageRoot}src/migrations/${id}/transform.ts`,
|
|
41
|
+
`${packageRoot}src/tools/${id}.ts`,
|
|
42
|
+
];
|
|
43
|
+
/** The transform file for `id`, or `undefined` when no candidate exists. */
|
|
44
|
+
const findTransform = (id) => candidatePaths(id).find((candidate) => existsSync(candidate));
|
|
45
|
+
/**
|
|
46
|
+
* The transform file for `id`, falling back to the last candidate so callers
|
|
47
|
+
* that only report a path still have one to name.
|
|
48
|
+
*/
|
|
49
|
+
const transformPath = (id) => findTransform(id) ?? `${packageRoot}src/tools/${id}.ts`;
|
|
35
50
|
/**
|
|
36
51
|
* Whether `id` names a transform file on disk.
|
|
37
52
|
*
|
|
@@ -53,13 +68,36 @@ export const transformExists = (id) => existsSync(transformPath(id));
|
|
|
53
68
|
* `processedNothing` exists because jscodeshift reports a path with no matching
|
|
54
69
|
* files and a worker that died before touching one the same way: every counter
|
|
55
70
|
* zero, no error. The caller must not render that as "0 files changed", which
|
|
56
|
-
* reads like success.
|
|
71
|
+
* reads like success. The load check below removes the most common cause of the
|
|
72
|
+
* second case, so `processedNothing` now means the path far more often than it
|
|
73
|
+
* used to — but not always: a worker can still die for a reason a successful
|
|
74
|
+
* load does not predict, so the flag keeps its deliberately vague name.
|
|
57
75
|
*/
|
|
58
76
|
export const runCodemod = async ({ id, path, dry = false, print = false, }) => {
|
|
59
77
|
const transform = transformPath(id);
|
|
60
78
|
if (!existsSync(transform)) {
|
|
61
79
|
throw new Error(unknownCodemodMessage(id));
|
|
62
80
|
}
|
|
81
|
+
// Load the transform here, in this process, before handing its path to
|
|
82
|
+
// jscodeshift.
|
|
83
|
+
//
|
|
84
|
+
// A worker that cannot load the transform dies before it touches a file, and
|
|
85
|
+
// the Runner then resolves with every counter at zero — indistinguishable
|
|
86
|
+
// from a path that matched nothing (see `processedNothing`). The stack trace
|
|
87
|
+
// goes to the worker's stderr, where the summary line the caller prints
|
|
88
|
+
// contradicts it by guessing at the path instead. Loading it up front turns
|
|
89
|
+
// that class of failure into a thrown error naming the real cause, which is
|
|
90
|
+
// the only way a caller can tell "this migration got no run at all" from
|
|
91
|
+
// "this migration had nothing to do".
|
|
92
|
+
//
|
|
93
|
+
// Safe to do: every shipped transform imports nothing but types, so loading
|
|
94
|
+
// one has no side effects and costs a file read.
|
|
95
|
+
try {
|
|
96
|
+
await import(pathToFileURL(transform).href);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new Error(`${id} could not be loaded from ${transform}: ${error instanceof Error ? error.message : error}`, { cause: error });
|
|
100
|
+
}
|
|
63
101
|
let stats;
|
|
64
102
|
try {
|
|
65
103
|
stats = (await runJscodeshift(transform, [path], {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Ports imports from `@mittwald/flow-react-components` to
|
|
5
|
+
* `@mittwald/flow-remote-react-components`.
|
|
6
|
+
*
|
|
7
|
+
* Not a migration: no version range calls for it, so it has no catalogue entry
|
|
8
|
+
* (see `notAMigration` in `src/tests/remoteScope.test.ts`) and lives outside
|
|
9
|
+
* `src/migrations`, in `src/tools` alongside any other transform the CLI runs
|
|
10
|
+
* by id without a matching entry. It is still resolved and run the same way a
|
|
11
|
+
* migration's `transform.ts` is — `transformExists`/`runCodemod` in
|
|
12
|
+
* `src/run/jscodeshift.ts` fall back to this directory when an id names no
|
|
13
|
+
* migration.
|
|
14
|
+
*/
|
|
15
|
+
const toRemotePackageTransform = (fileInfo, { j }) => {
|
|
16
|
+
const flowPackage = "@mittwald/flow-react-components";
|
|
17
|
+
const root = j(fileInfo.source, {
|
|
18
|
+
parser: "ts",
|
|
19
|
+
});
|
|
20
|
+
root
|
|
21
|
+
.find(j.ImportDeclaration)
|
|
22
|
+
.filter((i) => String(i.node.source.value).startsWith(flowPackage))
|
|
23
|
+
.forEach((i) => {
|
|
24
|
+
const importPath = String(i.node.source.value);
|
|
25
|
+
i.node.source.value = importPath.replace(flowPackage, "@mittwald/flow-remote-react-components");
|
|
26
|
+
});
|
|
27
|
+
return root.toSource();
|
|
28
|
+
};
|
|
29
|
+
exports.default = toRemotePackageTransform;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mittwald/flow-codemods",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Codemods and an upgrade CLI for consumers of Flow, mittwald's design system",
|
|
6
6
|
"homepage": "https://flow.mittwald.de",
|
|
@@ -9,15 +9,13 @@
|
|
|
9
9
|
"flow-codemods": "dist/cli.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
|
-
"dist"
|
|
13
|
-
"src/migrations/**/transform.ts",
|
|
14
|
-
"src/tools/to-remote-package.ts"
|
|
12
|
+
"dist"
|
|
15
13
|
],
|
|
16
14
|
"engines": {
|
|
17
15
|
"node": ">=22.0.0"
|
|
18
16
|
},
|
|
19
17
|
"scripts": {
|
|
20
|
-
"build": "tsx dev/generateCli.ts && tsc -p tsconfig.build.json",
|
|
18
|
+
"build": "tsx dev/generateCli.ts && tsc -p tsconfig.build.json && tsx dev/buildTransforms.ts",
|
|
21
19
|
"test:compile": "tsc --noEmit",
|
|
22
20
|
"test:unit": "vitest run"
|
|
23
21
|
},
|
|
@@ -38,5 +36,5 @@
|
|
|
38
36
|
"vitest": "^4.1.11",
|
|
39
37
|
"yaml": "^2.8.1"
|
|
40
38
|
},
|
|
41
|
-
"gitHead": "
|
|
39
|
+
"gitHead": "82e51fc50883e0579996611df3d56bcc948cc2d8"
|
|
42
40
|
}
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
import type { Transform } from "jscodeshift";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Moves the background value of `AccentBox` from `color` to `backgroundColor`
|
|
5
|
-
* (alpha.786).
|
|
6
|
-
*
|
|
7
|
-
* `color` did not go away, it changed meaning: it used to set the background
|
|
8
|
-
* (`"blue" | "green" | "gradient" | "neutral"`) and now sets the content color
|
|
9
|
-
* (`"default" | "dark" | "light" | "dark-static" | "light-static"`). A blanket
|
|
10
|
-
* rename would therefore break every element that already uses the new meaning.
|
|
11
|
-
* The transform decides per value instead: a value from the new content-color
|
|
12
|
-
* union stays on `color`, every other literal moves to `backgroundColor`.
|
|
13
|
-
*
|
|
14
|
-
* The scope is deliberately narrow. Only JSX elements that resolve to
|
|
15
|
-
* `AccentBox` — imported (named or as a namespace) from
|
|
16
|
-
* `@mittwald/flow-react-components` or
|
|
17
|
-
* `@mittwald/flow-remote-react-components`, including their subpath entries —
|
|
18
|
-
* are touched.
|
|
19
|
-
*
|
|
20
|
-
* Two cases are left alone, because both are undecidable without knowing the
|
|
21
|
-
* value:
|
|
22
|
-
*
|
|
23
|
-
* - Values it cannot read (`color={expression}`), and expressions that mix both
|
|
24
|
-
* meanings (`color={flag ? "blue" : "dark"}`). The same expression means the
|
|
25
|
-
* background in old code and the content color in new code, and a mix has no
|
|
26
|
-
* single correct answer. An expression whose every value position is a
|
|
27
|
-
* literal on the same side of the split _is_ decidable and gets renamed:
|
|
28
|
-
* `color={flag ? "blue" : "green"}`.
|
|
29
|
-
* - An element that already carries `backgroundColor`. Moving `color` there would
|
|
30
|
-
* silently overwrite the explicit value.
|
|
31
|
-
*
|
|
32
|
-
* Both keep their `color` prop and need a look by hand.
|
|
33
|
-
*/
|
|
34
|
-
const accentBoxColorToBackgroundColorTransform: Transform = (
|
|
35
|
-
fileInfo,
|
|
36
|
-
{ j },
|
|
37
|
-
) => {
|
|
38
|
-
const flowPackages = [
|
|
39
|
-
"@mittwald/flow-react-components",
|
|
40
|
-
"@mittwald/flow-remote-react-components",
|
|
41
|
-
];
|
|
42
|
-
const affectedComponents = new Set(["AccentBox"]);
|
|
43
|
-
/** Values `color` still accepts — everything else was a background color. */
|
|
44
|
-
const contentColors = new Set([
|
|
45
|
-
"default",
|
|
46
|
-
"dark",
|
|
47
|
-
"light",
|
|
48
|
-
"dark-static",
|
|
49
|
-
"light-static",
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
const isFlowImport = (source: string): boolean =>
|
|
53
|
-
flowPackages.some((pkg) => source === pkg || source.startsWith(`${pkg}/`));
|
|
54
|
-
|
|
55
|
-
/** The value of a string literal, or `undefined` for anything dynamic. */
|
|
56
|
-
const literalValue = (node: unknown): string | undefined => {
|
|
57
|
-
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
const { type } = node as { type: string };
|
|
61
|
-
if (type !== "StringLiteral" && type !== "Literal") {
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
const { value } = node as { value?: unknown };
|
|
65
|
-
return typeof value === "string" ? value : undefined;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Every literal that could become this attribute's value, or `undefined` when
|
|
70
|
-
* any of those positions is something we cannot read. The positions are the
|
|
71
|
-
* expression itself, both ternary branches, and the operands of `??`/`||` —
|
|
72
|
-
* plus only the right operand of `&&`, since `&&` yields its left operand
|
|
73
|
-
* only when that operand is falsy and a non-empty string never is.
|
|
74
|
-
*
|
|
75
|
-
* A list, not a single value, because this transform decides per attribute,
|
|
76
|
-
* not per literal: `color={flag ? "blue" : "dark"}` mixes a background with a
|
|
77
|
-
* content colour, and no single rename is right for both.
|
|
78
|
-
*/
|
|
79
|
-
const valueLiterals = (node: unknown): string[] | undefined => {
|
|
80
|
-
const literal = literalValue(node);
|
|
81
|
-
if (literal !== undefined) {
|
|
82
|
-
return [literal];
|
|
83
|
-
}
|
|
84
|
-
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
const typed = node as {
|
|
88
|
-
type: string;
|
|
89
|
-
operator?: string;
|
|
90
|
-
consequent?: unknown;
|
|
91
|
-
alternate?: unknown;
|
|
92
|
-
left?: unknown;
|
|
93
|
-
right?: unknown;
|
|
94
|
-
};
|
|
95
|
-
if (typed.type === "ConditionalExpression") {
|
|
96
|
-
const consequent = valueLiterals(typed.consequent);
|
|
97
|
-
const alternate = valueLiterals(typed.alternate);
|
|
98
|
-
return consequent && alternate
|
|
99
|
-
? [...consequent, ...alternate]
|
|
100
|
-
: undefined;
|
|
101
|
-
}
|
|
102
|
-
if (typed.type === "LogicalExpression") {
|
|
103
|
-
const right = valueLiterals(typed.right);
|
|
104
|
-
if (right === undefined) {
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
if (typed.operator === "&&") {
|
|
108
|
-
return right;
|
|
109
|
-
}
|
|
110
|
-
const left = valueLiterals(typed.left);
|
|
111
|
-
return left ? [...left, ...right] : undefined;
|
|
112
|
-
}
|
|
113
|
-
return undefined;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
const root = j(fileInfo.source, { parser: "tsx" });
|
|
117
|
-
|
|
118
|
-
// Local JSX identifier -> canonical component name (resolves `as` aliases).
|
|
119
|
-
const localToComponent = new Map<string, string>();
|
|
120
|
-
// Local names of `import * as Flow` namespace imports from a Flow package.
|
|
121
|
-
const flowNamespaces = new Set<string>();
|
|
122
|
-
|
|
123
|
-
root
|
|
124
|
-
.find(j.ImportDeclaration)
|
|
125
|
-
.filter((path) => isFlowImport(String(path.node.source.value)))
|
|
126
|
-
.forEach((path) => {
|
|
127
|
-
for (const specifier of path.node.specifiers ?? []) {
|
|
128
|
-
if (
|
|
129
|
-
specifier.type === "ImportSpecifier" &&
|
|
130
|
-
specifier.imported.type === "Identifier" &&
|
|
131
|
-
affectedComponents.has(specifier.imported.name)
|
|
132
|
-
) {
|
|
133
|
-
localToComponent.set(
|
|
134
|
-
String(specifier.local?.name ?? specifier.imported.name),
|
|
135
|
-
String(specifier.imported.name),
|
|
136
|
-
);
|
|
137
|
-
} else if (
|
|
138
|
-
specifier.type === "ImportNamespaceSpecifier" &&
|
|
139
|
-
specifier.local
|
|
140
|
-
) {
|
|
141
|
-
flowNamespaces.add(String(specifier.local.name));
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
if (localToComponent.size === 0 && flowNamespaces.size === 0) {
|
|
147
|
-
return fileInfo.source;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
root.find(j.JSXOpeningElement).forEach((path) => {
|
|
151
|
-
const name = path.node.name;
|
|
152
|
-
|
|
153
|
-
let isAffected = false;
|
|
154
|
-
if (name.type === "JSXIdentifier") {
|
|
155
|
-
isAffected = localToComponent.has(name.name);
|
|
156
|
-
} else if (
|
|
157
|
-
name.type === "JSXMemberExpression" &&
|
|
158
|
-
name.object.type === "JSXIdentifier" &&
|
|
159
|
-
name.property.type === "JSXIdentifier"
|
|
160
|
-
) {
|
|
161
|
-
isAffected =
|
|
162
|
-
flowNamespaces.has(name.object.name) &&
|
|
163
|
-
affectedComponents.has(name.property.name);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (!isAffected) {
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const attributes = path.node.attributes ?? [];
|
|
171
|
-
|
|
172
|
-
const isNamed = (attribute: unknown, key: string): boolean =>
|
|
173
|
-
!!attribute &&
|
|
174
|
-
typeof attribute === "object" &&
|
|
175
|
-
(attribute as { type?: string }).type === "JSXAttribute" &&
|
|
176
|
-
(attribute as { name?: { type?: string; name?: string } }).name?.type ===
|
|
177
|
-
"JSXIdentifier" &&
|
|
178
|
-
(attribute as { name?: { name?: string } }).name?.name === key;
|
|
179
|
-
|
|
180
|
-
// An explicit `backgroundColor` is the new API already — never overwrite it.
|
|
181
|
-
if (attributes.some((attribute) => isNamed(attribute, "backgroundColor"))) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
for (const attribute of attributes) {
|
|
186
|
-
if (!isNamed(attribute, "color") || attribute.type !== "JSXAttribute") {
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const value = attribute.value;
|
|
191
|
-
|
|
192
|
-
const literals =
|
|
193
|
-
value?.type === "JSXExpressionContainer"
|
|
194
|
-
? valueLiterals(value.expression)
|
|
195
|
-
: valueLiterals(value);
|
|
196
|
-
|
|
197
|
-
if (literals === undefined || literals.length === 0) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// All-or-nothing. Every value has to be a background colour for the
|
|
202
|
-
// rename to be right; all content colours means the element already uses
|
|
203
|
-
// the new meaning, and a mix has no single correct answer.
|
|
204
|
-
const backgrounds = literals.filter(
|
|
205
|
-
(literal) => !contentColors.has(literal),
|
|
206
|
-
);
|
|
207
|
-
if (backgrounds.length !== literals.length) {
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
attribute.name = j.jsxIdentifier("backgroundColor");
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
return root.toSource();
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
export default accentBoxColorToBackgroundColorTransform;
|