@onrails/codemod 0.1.1 → 0.1.3
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 +2 -1
- package/src/ast.ts +58 -0
- package/src/chains.ts +228 -0
- package/src/cli.ts +132 -0
- package/src/constants.ts +63 -0
- package/src/file-change.ts +57 -0
- package/src/imports.ts +77 -0
- package/src/index.ts +8 -630
- package/src/io.ts +10 -0
- package/src/pkg.ts +62 -0
- package/src/types.ts +46 -0
- package/src/warnings.ts +61 -0
package/src/pkg.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { type Maybe, match, none, some } from "@onrails/maybe";
|
|
3
|
+
import { type Result, ResultAsync, trySync } from "@onrails/result";
|
|
4
|
+
import { DEP_KEYS } from "./constants.js";
|
|
5
|
+
import { readFileText, toError, writeFileText } from "./io.js";
|
|
6
|
+
import type { ComputedPkg, PkgChange, PkgUpdate } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const reorderDeps = (deps: Record<string, string>): Record<string, string> =>
|
|
9
|
+
Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b)));
|
|
10
|
+
|
|
11
|
+
export const parsePackageJson = (raw: string): Result<Record<string, unknown>, Error> =>
|
|
12
|
+
trySync(() => JSON.parse(raw) as Record<string, unknown>, toError)();
|
|
13
|
+
|
|
14
|
+
const applyDepRewrite =
|
|
15
|
+
(fileSpec: string) =>
|
|
16
|
+
(acc: PkgUpdate, key: string): PkgUpdate => {
|
|
17
|
+
const deps = acc.json[key] as Record<string, string> | undefined;
|
|
18
|
+
if (!deps || typeof deps !== "object" || !("neverthrow" in deps)) return acc;
|
|
19
|
+
const { neverthrow: _drop, ...rest } = deps;
|
|
20
|
+
return {
|
|
21
|
+
json: { ...acc.json, [key]: reorderDeps({ ...rest, "@onrails/result": fileSpec }) },
|
|
22
|
+
removed: [...acc.removed, key],
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const computePkgRewrite = (
|
|
27
|
+
json: Record<string, unknown>,
|
|
28
|
+
path: string,
|
|
29
|
+
onrailsAbs: string,
|
|
30
|
+
): Maybe<ComputedPkg> => {
|
|
31
|
+
const fileSpec = `file:${relative(path.replace(/\/package\.json$/, ""), onrailsAbs)}`;
|
|
32
|
+
const updated = DEP_KEYS.reduce(applyDepRewrite(fileSpec), { json, removed: [] } as PkgUpdate);
|
|
33
|
+
return updated.removed.length === 0
|
|
34
|
+
? none()
|
|
35
|
+
: some({ json: updated.json, removed: updated.removed, fileSpec });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const toPkgChange = (path: string, c: ComputedPkg): PkgChange => ({
|
|
39
|
+
path,
|
|
40
|
+
removed: [...c.removed],
|
|
41
|
+
addedAs: c.fileSpec,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export const rewritePkg = (
|
|
45
|
+
path: string,
|
|
46
|
+
onrailsAbs: string,
|
|
47
|
+
dry: boolean,
|
|
48
|
+
): ResultAsync<Maybe<PkgChange>, Error> =>
|
|
49
|
+
readFileText(path)
|
|
50
|
+
.flatMap((raw) => ResultAsync.fromResult(parsePackageJson(raw)))
|
|
51
|
+
.flatMap((json) =>
|
|
52
|
+
match(
|
|
53
|
+
computePkgRewrite(json, path, onrailsAbs),
|
|
54
|
+
(c) => {
|
|
55
|
+
const change = toPkgChange(path, c);
|
|
56
|
+
return dry
|
|
57
|
+
? ResultAsync.ok<Maybe<PkgChange>, Error>(some(change))
|
|
58
|
+
: writeFileText(path, `${JSON.stringify(c.json, null, 2)}\n`).map(() => some(change));
|
|
59
|
+
},
|
|
60
|
+
() => ResultAsync.ok<Maybe<PkgChange>, Error>(none()),
|
|
61
|
+
),
|
|
62
|
+
);
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export type Mode = "compat" | "native";
|
|
2
|
+
export type Args = { target: string; dry: boolean; onrails: string; mode: Mode };
|
|
3
|
+
|
|
4
|
+
export type Warning = { line: number; label: string; text: string };
|
|
5
|
+
export type FileChange = {
|
|
6
|
+
path: string;
|
|
7
|
+
before: number;
|
|
8
|
+
after: number;
|
|
9
|
+
changed: boolean;
|
|
10
|
+
warnings: readonly Warning[];
|
|
11
|
+
};
|
|
12
|
+
export type Edit = { start: number; end: number; text: string; imports: readonly string[] };
|
|
13
|
+
export type ChainStep = { method: string; argsText: string; argCount: number };
|
|
14
|
+
export type PipelinePart = { importName: string; text: string };
|
|
15
|
+
|
|
16
|
+
export type ComputedChange = {
|
|
17
|
+
next: string;
|
|
18
|
+
before: number;
|
|
19
|
+
after: number;
|
|
20
|
+
changed: boolean;
|
|
21
|
+
warnings: readonly Warning[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type ModeStrategy = {
|
|
25
|
+
countBefore: (src: string) => number;
|
|
26
|
+
earlyExit: (src: string, before: number) => boolean;
|
|
27
|
+
transform: (src: string) => string;
|
|
28
|
+
warnings: (next: string) => readonly Warning[];
|
|
29
|
+
countAfter: (next: string) => number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type PkgChange = { path: string; removed: string[]; addedAs: string };
|
|
33
|
+
export type PkgUpdate = { json: Record<string, unknown>; removed: readonly string[] };
|
|
34
|
+
export type ComputedPkg = {
|
|
35
|
+
json: Record<string, unknown>;
|
|
36
|
+
removed: readonly string[];
|
|
37
|
+
fileSpec: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ArgState = {
|
|
41
|
+
positional: readonly string[];
|
|
42
|
+
dry: boolean;
|
|
43
|
+
mode: Mode;
|
|
44
|
+
onrails: string;
|
|
45
|
+
};
|
|
46
|
+
export type EditAcc = { src: string; imports: Set<string> };
|
package/src/warnings.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { concatCollectors, walkSource } from "./ast.js";
|
|
3
|
+
import { COMPAT_ONLY_PATTERNS, COMPAT_SPEC } from "./constants.js";
|
|
4
|
+
import type { Warning } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const collectRegexLineWarnings = (src: string): readonly Warning[] =>
|
|
7
|
+
src
|
|
8
|
+
.split(/\r?\n/)
|
|
9
|
+
.flatMap((line, i) =>
|
|
10
|
+
COMPAT_ONLY_PATTERNS.flatMap(({ pattern, label }) =>
|
|
11
|
+
pattern.test(line) ? [{ line: i + 1, label, text: line.trim() }] : [],
|
|
12
|
+
),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const collectAstCompatWarnings = (src: string): readonly Warning[] => {
|
|
16
|
+
const warnings: Warning[] = [];
|
|
17
|
+
const lines = src.split(/\r?\n/);
|
|
18
|
+
const lineTextAt = (sf: ts.SourceFile, node: ts.Node) => {
|
|
19
|
+
const line = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
20
|
+
return { line, text: lines[line - 1]?.trim() ?? node.getText(sf) };
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
walkSource(src, (node, sf) => {
|
|
24
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
25
|
+
const method = node.expression.name.text;
|
|
26
|
+
if (method === "isOk" || method === "isErr") {
|
|
27
|
+
warnings.push({ ...lineTextAt(sf, node), label: "compat predicate method" });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
32
|
+
const property = node.name.text;
|
|
33
|
+
if (property === "value" || property === "error") {
|
|
34
|
+
const receiver = node.expression.getText(sf);
|
|
35
|
+
if (/result$/i.test(receiver) || /^result$/i.test(receiver)) {
|
|
36
|
+
warnings.push({ ...lineTextAt(sf, node), label: "compat value/error property" });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return warnings;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const collectNativeMigrationWarnings = concatCollectors(
|
|
46
|
+
collectRegexLineWarnings,
|
|
47
|
+
collectAstCompatWarnings,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
export const collectUnsupportedCompatImportWarnings = (src: string): readonly Warning[] =>
|
|
51
|
+
src.split(/\r?\n/).flatMap((line, i) =>
|
|
52
|
+
line.includes(COMPAT_SPEC)
|
|
53
|
+
? [
|
|
54
|
+
{
|
|
55
|
+
line: i + 1,
|
|
56
|
+
label: "unsupported compat import",
|
|
57
|
+
text: line.trim(),
|
|
58
|
+
},
|
|
59
|
+
]
|
|
60
|
+
: [],
|
|
61
|
+
);
|