@onrails/codemod 0.2.0 → 0.2.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onrails/codemod",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Codemods for migrating onto @onrails/result (neverthrow → compat shim, and beyond)",
5
5
  "license": "MIT",
6
6
  "author": "Alan R. Soares",
package/src/ast.ts CHANGED
@@ -21,8 +21,19 @@ export const concatCollectors =
21
21
  export const walkSource = (
22
22
  src: string,
23
23
  visit: (node: ts.Node, sf: ts.SourceFile) => unknown,
24
+ jsx = false,
24
25
  ): void => {
25
- const sf = ts.createSourceFile("codemod.ts", src, ts.ScriptTarget.Latest, true);
26
+ // Parse JSX files in TSX mode — otherwise `<`/`>` are read as comparison
27
+ // operators and `<T>expr` as a cast, which truncates node boundaries and
28
+ // corrupts span-based edits. The scriptKind is set explicitly (not just via
29
+ // the filename) so it can't drift from the extension.
30
+ const sf = ts.createSourceFile(
31
+ jsx ? "codemod.tsx" : "codemod.ts",
32
+ src,
33
+ ts.ScriptTarget.Latest,
34
+ true,
35
+ jsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
36
+ );
26
37
  const go = (n: ts.Node) => {
27
38
  if (visit(n, sf)) return;
28
39
  ts.forEachChild(n, go);
package/src/chains.ts CHANGED
@@ -213,28 +213,32 @@ export const applyEditStep = (acc: EditAcc, e: Edit): EditAcc => ({
213
213
  imports: new Set([...acc.imports, ...e.imports]),
214
214
  });
215
215
 
216
- export function rewriteCompatMethodChainsToNative(src: string): string {
216
+ export function rewriteCompatMethodChainsToNative(src: string, jsx = false): string {
217
217
  const edits: Edit[] = [];
218
218
 
219
- walkSource(src, (node, sf) => {
220
- if (ts.isCallExpression(node)) {
221
- const helper = helperCallToNative(node);
222
- if (isSome(helper)) {
223
- edits.push(spanEdit(node, sf, helper.value));
224
- return true;
219
+ walkSource(
220
+ src,
221
+ (node, sf) => {
222
+ if (ts.isCallExpression(node)) {
223
+ const helper = helperCallToNative(node);
224
+ if (isSome(helper)) {
225
+ edits.push(spanEdit(node, sf, helper.value));
226
+ return true;
227
+ }
225
228
  }
226
- }
227
229
 
228
- if (
229
- isSupportedChainCall(node) &&
230
- !isNestedInSupportedChain(node) &&
231
- !ts.isPropertyAccessExpression(node.parent)
232
- ) {
233
- const chain = collectChain(node);
234
- const chained = chainToNative(chain.base, chain.steps);
235
- if (isSome(chained)) edits.push(spanEdit(node, sf, chained.value));
236
- }
237
- });
230
+ if (
231
+ isSupportedChainCall(node) &&
232
+ !isNestedInSupportedChain(node) &&
233
+ !ts.isPropertyAccessExpression(node.parent)
234
+ ) {
235
+ const chain = collectChain(node);
236
+ const chained = chainToNative(chain.base, chain.steps);
237
+ if (isSome(chained)) edits.push(spanEdit(node, sf, chained.value));
238
+ }
239
+ },
240
+ jsx,
241
+ );
238
242
 
239
243
  if (edits.length === 0) return src;
240
244
 
package/src/cli.ts CHANGED
@@ -3,7 +3,7 @@ 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";
6
- import { CODE_EXT, SKIP } from "./constants.js";
6
+ import { CODE_EXT, JSX_EXT, SKIP } from "./constants.js";
7
7
  import { computeFileChange, toFileChange } from "./file-change.js";
8
8
  import { readFileText, writeFileText } from "./io.js";
9
9
  import { rewritePkg } from "./pkg.js";
@@ -64,7 +64,7 @@ const rewriteCode = (
64
64
  ): ResultAsync<Maybe.Maybe<FileChange>, Error> =>
65
65
  readFileText(path).flatMap((src) =>
66
66
  Maybe.match(
67
- computeFileChange(src, mode),
67
+ computeFileChange(src, mode, JSX_EXT.test(path)),
68
68
  (c) => {
69
69
  const change = toFileChange(path, c);
70
70
  return c.changed && !dry
package/src/constants.ts CHANGED
@@ -8,6 +8,7 @@ export const SKIP = new Set([
8
8
  "build",
9
9
  ]);
10
10
  export const CODE_EXT = /\.(ts|tsx|mts|cts)$/;
11
+ export const JSX_EXT = /\.(tsx|jsx)$/;
11
12
  export const IMPORT_RE = /(from\s+|import\s*\(\s*)(['"])neverthrow\2/g;
12
13
  export const COMPAT_SPEC = "@onrails/result/compat/neverthrow";
13
14
  export const NATIVE_SPEC = "@onrails/result";
@@ -1,25 +1,25 @@
1
1
  import { type Maybe, none, some } from "@onrails/maybe";
2
- import { flow } from "@onrails/result";
3
- import { concatCollectors, countOccurrences } from "./ast.js";
2
+ import { countOccurrences } from "./ast.js";
4
3
  import { rewriteCompatMethodChainsToNative } from "./chains.js";
5
4
  import { COMPAT_SPEC, IMPORT_RE, NATIVE_SPEC } from "./constants.js";
6
5
  import { rewriteCompatImportsToNative } from "./imports.js";
7
6
  import { tersify } from "./tersify.js";
8
- import type { ComputedChange, FileChange, Mode, ModeStrategy } from "./types.js";
7
+ import type { ComputedChange, FileChange, Mode, ModeStrategy, Warning } from "./types.js";
9
8
  import {
10
9
  collectNativeMigrationWarnings,
11
10
  collectUnsupportedCompatImportWarnings,
12
11
  } from "./warnings.js";
13
12
 
14
- const rewriteCompatToNative = flow(rewriteCompatImportsToNative, rewriteCompatMethodChainsToNative);
13
+ const rewriteCompatToNative = (src: string, jsx: boolean): string =>
14
+ rewriteCompatMethodChainsToNative(rewriteCompatImportsToNative(src), jsx);
15
15
 
16
16
  const rewriteNeverthrowToCompat = (src: string): string =>
17
17
  src.replace(IMPORT_RE, (_, lead, quote) => `${lead}${quote}${COMPAT_SPEC}${quote}`);
18
18
 
19
- const collectAllNativeWarnings = concatCollectors(
20
- collectUnsupportedCompatImportWarnings,
21
- collectNativeMigrationWarnings,
22
- );
19
+ const collectAllNativeWarnings = (next: string, jsx: boolean): readonly Warning[] => [
20
+ ...collectUnsupportedCompatImportWarnings(next),
21
+ ...collectNativeMigrationWarnings(next, jsx),
22
+ ];
23
23
 
24
24
  const MODES: Record<Mode, ModeStrategy> = {
25
25
  compat: {
@@ -45,13 +45,13 @@ const MODES: Record<Mode, ModeStrategy> = {
45
45
  },
46
46
  };
47
47
 
48
- export const computeFileChange = (src: string, mode: Mode): Maybe<ComputedChange> => {
48
+ export const computeFileChange = (src: string, mode: Mode, jsx = false): Maybe<ComputedChange> => {
49
49
  const strat = MODES[mode];
50
50
  const before = strat.countBefore(src);
51
51
  if (strat.earlyExit(src, before)) return none();
52
- const next = strat.transform(src);
52
+ const next = strat.transform(src, jsx);
53
53
  const changed = next !== src;
54
- const warnings = strat.warnings(next);
54
+ const warnings = strat.warnings(next, jsx);
55
55
  return !changed && warnings.length === 0
56
56
  ? none()
57
57
  : some({ next, before, after: strat.countAfter(next), changed, warnings });
package/src/tersify.ts CHANGED
@@ -422,29 +422,33 @@ const TRANSFORMS: ReadonlyArray<(node: ts.Node, sf: ts.SourceFile) => Maybe<Edit
422
422
  tryTersifyIdentityFilter,
423
423
  ];
424
424
 
425
- function tersifyOnce(src: string): string {
425
+ function tersifyOnce(src: string, jsx: boolean): string {
426
426
  const edits: Edit[] = [];
427
427
  const skippedNodes = new Set<ts.Node>();
428
428
 
429
- walkSource(src, (node, sf) => {
430
- if (skippedNodes.has(node)) {
431
- return true;
432
- }
429
+ walkSource(
430
+ src,
431
+ (node, sf) => {
432
+ if (skippedNodes.has(node)) {
433
+ return true;
434
+ }
433
435
 
434
- scanIfReturnSequences(node, sf, skippedNodes, edits);
435
- scanIfToSwitchSequences(node, sf, skippedNodes, edits);
436
+ scanIfReturnSequences(node, sf, skippedNodes, edits);
437
+ scanIfToSwitchSequences(node, sf, skippedNodes, edits);
436
438
 
437
- for (const transform of TRANSFORMS) {
438
- const editResult = transform(node, sf);
439
- if (editResult._tag === "Some") {
440
- edits.push(editResult.value);
441
- skippedNodes.add(node);
442
- return true;
439
+ for (const transform of TRANSFORMS) {
440
+ const editResult = transform(node, sf);
441
+ if (editResult._tag === "Some") {
442
+ edits.push(editResult.value);
443
+ skippedNodes.add(node);
444
+ return true;
445
+ }
443
446
  }
444
- }
445
447
 
446
- return false;
447
- });
448
+ return false;
449
+ },
450
+ jsx,
451
+ );
448
452
 
449
453
  if (edits.length === 0) return src;
450
454
 
@@ -455,11 +459,11 @@ function tersifyOnce(src: string): string {
455
459
  return next;
456
460
  }
457
461
 
458
- export function tersify(src: string): string {
462
+ export function tersify(src: string, jsx = false): string {
459
463
  let current = src;
460
464
  let iterations = 0;
461
465
  while (iterations < 10) {
462
- const next = tersifyOnce(current);
466
+ const next = tersifyOnce(current, jsx);
463
467
  if (next === current) {
464
468
  break;
465
469
  }
package/src/types.ts CHANGED
@@ -24,8 +24,8 @@ export type ComputedChange = {
24
24
  export type ModeStrategy = {
25
25
  countBefore: (src: string) => number;
26
26
  earlyExit: (src: string, before: number) => boolean;
27
- transform: (src: string) => string;
28
- warnings: (next: string) => readonly Warning[];
27
+ transform: (src: string, jsx: boolean) => string;
28
+ warnings: (next: string, jsx: boolean) => readonly Warning[];
29
29
  countAfter: (next: string) => number;
30
30
  };
31
31
 
package/src/warnings.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { concatCollectors, walkSource } from "./ast.js";
2
+ import { walkSource } from "./ast.js";
3
3
  import { COMPAT_ONLY_PATTERNS, COMPAT_SPEC } from "./constants.js";
4
4
  import type { Warning } from "./types.js";
5
5
 
@@ -12,7 +12,7 @@ export const collectRegexLineWarnings = (src: string): readonly Warning[] =>
12
12
  ),
13
13
  );
14
14
 
15
- export const collectAstCompatWarnings = (src: string): readonly Warning[] => {
15
+ export const collectAstCompatWarnings = (src: string, jsx = false): readonly Warning[] => {
16
16
  const warnings: Warning[] = [];
17
17
  const lines = src.split(/\r?\n/);
18
18
  const lineTextAt = (sf: ts.SourceFile, node: ts.Node) => {
@@ -20,32 +20,36 @@ export const collectAstCompatWarnings = (src: string): readonly Warning[] => {
20
20
  return { line, text: lines[line - 1]?.trim() ?? node.getText(sf) };
21
21
  };
22
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" });
23
+ walkSource(
24
+ src,
25
+ (node, sf) => {
26
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
27
+ const method = node.expression.name.text;
28
+ if (method === "isOk" || method === "isErr") {
29
+ warnings.push({ ...lineTextAt(sf, node), label: "compat predicate method" });
30
+ }
28
31
  }
29
- }
30
32
 
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" });
33
+ if (ts.isPropertyAccessExpression(node)) {
34
+ const property = node.name.text;
35
+ if (property === "value" || property === "error") {
36
+ const receiver = node.expression.getText(sf);
37
+ if (/result$/i.test(receiver) || /^result$/i.test(receiver)) {
38
+ warnings.push({ ...lineTextAt(sf, node), label: "compat value/error property" });
39
+ }
37
40
  }
38
41
  }
39
- }
40
- });
42
+ },
43
+ jsx,
44
+ );
41
45
 
42
46
  return warnings;
43
47
  };
44
48
 
45
- export const collectNativeMigrationWarnings = concatCollectors(
46
- collectRegexLineWarnings,
47
- collectAstCompatWarnings,
48
- );
49
+ export const collectNativeMigrationWarnings = (src: string, jsx = false): readonly Warning[] => [
50
+ ...collectRegexLineWarnings(src),
51
+ ...collectAstCompatWarnings(src, jsx),
52
+ ];
49
53
 
50
54
  export const collectUnsupportedCompatImportWarnings = (src: string): readonly Warning[] =>
51
55
  src.split(/\r?\n/).flatMap((line, i) =>