@onrails/codemod 0.1.2 → 0.1.4
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/package.json +1 -1
- package/src/ast.ts +2 -2
- package/src/chains.ts +75 -4
- package/src/cli.ts +8 -8
- package/src/imports.ts +26 -5
- package/src/pkg.ts +2 -2
package/package.json
CHANGED
package/src/ast.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fromNullable, type Maybe, match
|
|
1
|
+
import { fromNullable, type Maybe, match, none, some } from "@onrails/maybe";
|
|
2
2
|
import ts from "typescript";
|
|
3
3
|
import { TYPE_ONLY_NATIVE } from "./constants.js";
|
|
4
4
|
import type { Edit } from "./types.js";
|
|
@@ -11,7 +11,7 @@ export const edit = (text: string, imports: readonly string[] = []): Edit => ({
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
export const lookupMap = <K, V, R>(m: Map<K, V>, k: K, f: (v: V) => R): Maybe<R> =>
|
|
14
|
-
|
|
14
|
+
match(fromNullable(m.get(k)), (v) => some(f(v)), none);
|
|
15
15
|
|
|
16
16
|
export const concatCollectors =
|
|
17
17
|
<T, R>(...fns: Array<(t: T) => readonly R[]>) =>
|
package/src/chains.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { compactMap, isSome, type Maybe, none, some } from "@onrails/maybe";
|
|
1
|
+
import { compactMap, isSome, type Maybe, map, none, some } from "@onrails/maybe";
|
|
2
2
|
import ts from "typescript";
|
|
3
3
|
import { argsToText, edit, lookupMap, spanEdit, walkSource } from "./ast.js";
|
|
4
4
|
import {
|
|
@@ -22,10 +22,81 @@ export function isSupportedChainCall(node: ts.Node): node is ts.CallExpression {
|
|
|
22
22
|
return CHAIN_METHODS.has(method) || TERMINAL_METHODS.has(method);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
if (ts.isIdentifier(node.expression)
|
|
27
|
-
|
|
25
|
+
function identifierCallToNative(node: ts.CallExpression): Maybe<Edit> {
|
|
26
|
+
if (!ts.isIdentifier(node.expression)) return none();
|
|
27
|
+
const name = node.expression.text;
|
|
28
|
+
if (node.arguments.length === 0) {
|
|
29
|
+
const zero = lookupMap(ZERO_ARG_HELPERS, name, (text) => edit(text));
|
|
30
|
+
if (isSome(zero)) return zero;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (name === "sequenceTupleAsync") {
|
|
34
|
+
return some(edit(`ResultAsync.combineTuple(${argsToText(node.arguments)})`, ["ResultAsync"]));
|
|
35
|
+
}
|
|
36
|
+
if (name === "getOrElse") {
|
|
37
|
+
return some(edit(`unwrapOr(${argsToText(node.arguments)})`, ["unwrapOr"]));
|
|
38
|
+
}
|
|
39
|
+
if (name === "collect") {
|
|
40
|
+
return some(edit(`combine(${argsToText(node.arguments)})`, ["combine"]));
|
|
41
|
+
}
|
|
42
|
+
if (name === "matchResult" || name === "matchMaybe") {
|
|
43
|
+
return some(edit(`match(${argsToText(node.arguments)})`, ["match"]));
|
|
28
44
|
}
|
|
45
|
+
if (name === "fold") {
|
|
46
|
+
return map(foldHandlerTexts(node.arguments[0]), ({ okText, errText }) =>
|
|
47
|
+
edit(`match(${okText}, ${errText})`, ["match"]),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return none();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type FoldHandlerTexts = { okText: string; errText: string };
|
|
54
|
+
|
|
55
|
+
// Bails (none) unless both `ok` and `err` handlers are plain or shorthand
|
|
56
|
+
// properties — spread/computed handlers can't be rewritten textually.
|
|
57
|
+
function foldHandlerTexts(objArg: ts.Expression | undefined): Maybe<FoldHandlerTexts> {
|
|
58
|
+
if (!objArg || !ts.isObjectLiteralExpression(objArg)) return none();
|
|
59
|
+
let okText: string | undefined;
|
|
60
|
+
let errText: string | undefined;
|
|
61
|
+
for (const prop of objArg.properties) {
|
|
62
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
63
|
+
if (prop.name.text === "ok") {
|
|
64
|
+
okText = prop.initializer.getText();
|
|
65
|
+
} else if (prop.name.text === "err") {
|
|
66
|
+
errText = prop.initializer.getText();
|
|
67
|
+
}
|
|
68
|
+
} else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
69
|
+
if (prop.name.text === "ok") {
|
|
70
|
+
okText = "ok";
|
|
71
|
+
} else if (prop.name.text === "err") {
|
|
72
|
+
errText = "err";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return okText !== undefined && errText !== undefined ? some({ okText, errText }) : none();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function curriedFoldToNative(node: ts.CallExpression): Maybe<Edit> {
|
|
80
|
+
if (
|
|
81
|
+
!ts.isCallExpression(node.expression) ||
|
|
82
|
+
!ts.isIdentifier(node.expression.expression) ||
|
|
83
|
+
node.expression.expression.text !== "fold"
|
|
84
|
+
) {
|
|
85
|
+
return none();
|
|
86
|
+
}
|
|
87
|
+
const receiver = node.arguments[0];
|
|
88
|
+
if (!receiver) return none();
|
|
89
|
+
return map(foldHandlerTexts(node.expression.arguments[0]), ({ okText, errText }) =>
|
|
90
|
+
edit(`match(${okText}, ${errText})(${receiver.getText()})`, ["match"]),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function helperCallToNative(node: ts.CallExpression): Maybe<Edit> {
|
|
95
|
+
const ident = identifierCallToNative(node);
|
|
96
|
+
if (isSome(ident)) return ident;
|
|
97
|
+
|
|
98
|
+
const curried = curriedFoldToNative(node);
|
|
99
|
+
if (isSome(curried)) return curried;
|
|
29
100
|
|
|
30
101
|
if (!ts.isPropertyAccessExpression(node.expression)) return none();
|
|
31
102
|
const method = node.expression.name.text;
|
package/src/cli.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import * as Maybe from "@onrails/maybe";
|
|
3
3
|
import { match } from "@onrails/pattern";
|
|
4
4
|
import { err, isErr, ok, type Result, ResultAsync } from "@onrails/result";
|
|
5
5
|
import { Glob } from "bun";
|
|
@@ -61,25 +61,25 @@ const rewriteCode = (
|
|
|
61
61
|
path: string,
|
|
62
62
|
dry: boolean,
|
|
63
63
|
mode: Mode,
|
|
64
|
-
): ResultAsync<Maybe<FileChange>, Error> =>
|
|
64
|
+
): ResultAsync<Maybe.Maybe<FileChange>, Error> =>
|
|
65
65
|
readFileText(path).flatMap((src) =>
|
|
66
|
-
|
|
66
|
+
Maybe.match(
|
|
67
67
|
computeFileChange(src, mode),
|
|
68
68
|
(c) => {
|
|
69
69
|
const change = toFileChange(path, c);
|
|
70
70
|
return c.changed && !dry
|
|
71
|
-
? writeFileText(path, c.next).map(() => some(change))
|
|
72
|
-
: ResultAsync.ok<Maybe<FileChange>, Error>(some(change));
|
|
71
|
+
? writeFileText(path, c.next).map(() => Maybe.some(change))
|
|
72
|
+
: ResultAsync.ok<Maybe.Maybe<FileChange>, Error>(Maybe.some(change));
|
|
73
73
|
},
|
|
74
|
-
() => ResultAsync.ok<Maybe<FileChange>, Error>(none()),
|
|
74
|
+
() => ResultAsync.ok<Maybe.Maybe<FileChange>, Error>(Maybe.none()),
|
|
75
75
|
),
|
|
76
76
|
);
|
|
77
77
|
|
|
78
78
|
const collectInto =
|
|
79
79
|
<T>(into: T[], file: string) =>
|
|
80
|
-
(r: ResultAsync<Maybe<T>, Error>): Promise<void> =>
|
|
80
|
+
(r: ResultAsync<Maybe.Maybe<T>, Error>): Promise<void> =>
|
|
81
81
|
r.match(
|
|
82
|
-
(m) => void tap(m, (c) => into.push(c)),
|
|
82
|
+
(m) => void Maybe.tap(m, (c) => into.push(c)),
|
|
83
83
|
(e) => console.error(`error processing ${file}:`, e.message),
|
|
84
84
|
);
|
|
85
85
|
|
package/src/imports.ts
CHANGED
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
import { isTypeOnlyNative, isValueImport, splitImportNames, stripInlineType } from "./ast.js";
|
|
2
2
|
import { COMPAT_SPEC, NATIVE_SPEC } from "./constants.js";
|
|
3
3
|
|
|
4
|
+
// Every entry must have a matching call-site rewrite in chains.ts, or the
|
|
5
|
+
// renamed import strands the old name at call sites. `of` is intentionally
|
|
6
|
+
// absent: bare `of(...)` calls collide with other libraries (e.g. RxJS) and
|
|
7
|
+
// the canonical target differs per carrier (result `ok` vs maybe `some`) —
|
|
8
|
+
// the native package still exports the deprecated alias, so imports of `of`
|
|
9
|
+
// pass through unchanged and the lint rules flag them for manual migration.
|
|
10
|
+
const IMPORT_RENAMES = new Map([
|
|
11
|
+
["sequenceTupleAsync", "ResultAsync"],
|
|
12
|
+
["getOrElse", "unwrapOr"],
|
|
13
|
+
["collect", "combine"],
|
|
14
|
+
["matchResult", "match"],
|
|
15
|
+
["matchMaybe", "match"],
|
|
16
|
+
["fold", "match"],
|
|
17
|
+
]);
|
|
18
|
+
|
|
4
19
|
export function toNativeImport(full: string, specifiers: string, quote: string): string {
|
|
5
20
|
const imports = splitImportNames(specifiers);
|
|
6
21
|
const typeNames = imports.filter(isTypeOnlyNative).map(stripInlineType);
|
|
7
|
-
const
|
|
22
|
+
const rawValueNames = imports.filter(isValueImport);
|
|
23
|
+
const valueNames = rawValueNames.map((name) => IMPORT_RENAMES.get(name) ?? name);
|
|
24
|
+
|
|
25
|
+
const uniqueValueNames = [...new Set(valueNames)];
|
|
26
|
+
const uniqueTypeNames = [...new Set(typeNames)];
|
|
8
27
|
const chunks: string[] = [];
|
|
9
28
|
|
|
10
|
-
if (
|
|
11
|
-
chunks.push(`import { ${
|
|
29
|
+
if (uniqueValueNames.length > 0) {
|
|
30
|
+
chunks.push(`import { ${uniqueValueNames.join(", ")} } from ${quote}${NATIVE_SPEC}${quote};`);
|
|
12
31
|
}
|
|
13
32
|
|
|
14
|
-
if (
|
|
15
|
-
chunks.push(
|
|
33
|
+
if (uniqueTypeNames.length > 0) {
|
|
34
|
+
chunks.push(
|
|
35
|
+
`import type { ${uniqueTypeNames.join(", ")} } from ${quote}${NATIVE_SPEC}${quote};`,
|
|
36
|
+
);
|
|
16
37
|
}
|
|
17
38
|
|
|
18
39
|
return chunks.length > 0 ? chunks.join("\n") : full;
|
package/src/pkg.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { relative } from "node:path";
|
|
2
|
-
import { type Maybe, match
|
|
2
|
+
import { type Maybe, match, none, some } from "@onrails/maybe";
|
|
3
3
|
import { type Result, ResultAsync, trySync } from "@onrails/result";
|
|
4
4
|
import { DEP_KEYS } from "./constants.js";
|
|
5
5
|
import { readFileText, toError, writeFileText } from "./io.js";
|
|
@@ -49,7 +49,7 @@ export const rewritePkg = (
|
|
|
49
49
|
readFileText(path)
|
|
50
50
|
.flatMap((raw) => ResultAsync.fromResult(parsePackageJson(raw)))
|
|
51
51
|
.flatMap((json) =>
|
|
52
|
-
|
|
52
|
+
match(
|
|
53
53
|
computePkgRewrite(json, path, onrailsAbs),
|
|
54
54
|
(c) => {
|
|
55
55
|
const change = toPkgChange(path, c);
|