Skip to content

Commit e928761

Browse files
authored
Add codemod to replace LegacyRef with Ref (#347)
* Add codemod to replace `LegacyRef` with `Ref` * Preserve type import
1 parent 8c27551 commit e928761

8 files changed

Lines changed: 289 additions & 14 deletions

File tree

.changeset/ninety-elephants-jam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"types-react-codemod": minor
3+
---
4+
5+
Add codemod to replace `LegacyRef` with `Ref`

README.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ Time elapsed: 0.229seconds
3333
$ npx types-react-codemod <codemod> <paths...>
3434

3535
Positionals:
36-
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
37-
"deprecated-react-fragment", "deprecated-react-node-array",
38-
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
39-
"deprecated-sfc", "deprecated-stateless-component",
40-
"deprecated-void-function-component", "implicit-children", "preset-18",
41-
"preset-19", "refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
42-
"useRef-required-initial"]
36+
codemod [string] [required] [choices: "context-any", "deprecated-legacy-ref",
37+
"deprecated-react-child", "deprecated-react-fragment",
38+
"deprecated-react-node-array", "deprecated-react-text",
39+
"deprecated-react-type", "deprecated-sfc-element", "deprecated-sfc",
40+
"deprecated-stateless-component", "deprecated-void-function-component",
41+
"implicit-children", "preset-18", "preset-19", "refobject-defaults",
42+
"scoped-jsx", "useCallback-implicit-any", "useRef-required-initial"]
4343
paths [string] [required]
4444

4545
Options:
@@ -71,6 +71,7 @@ The reason being that a false-positive can be reverted easily (assuming you have
7171
- `implicit-children`
7272
- `useCallback-implicit-any`
7373
- `preset-19`
74+
- `deprecated-legacy-ref`
7475
- `deprecated-react-child`
7576
- `deprecated-react-text`
7677
- `deprecated-void-function-component`
@@ -212,6 +213,28 @@ By default, the codemods that are definitely required to upgrade to `@types/reac
212213
The other codemods may or may not be required.
213214
You should select all and audit the changed files regardless.
214215

216+
### `deprecated-legacy-ref`
217+
218+
```diff
219+
import * as React from "react";
220+
interface Props {
221+
- ref?: React.LegacyRef;
222+
+ ref?: React.Ref;
223+
}
224+
```
225+
226+
#### `deprecated-legacy-ref false-negative pattern A
227+
228+
Importing `LegacyRef` via aliased named import will result in the transform being skipped.
229+
230+
```tsx
231+
import { LegacyRef as MyLegacyRef } from "react";
232+
interface Props {
233+
// not transformed
234+
ref?: MyLegacyRef;
235+
}
236+
```
237+
215238
### `deprecated-react-child`
216239

217240
```diff

bin/__tests__/types-react-codemod.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ describe("types-react-codemod", () => {
2121
"stdout": "types-react-codemod <codemod> <paths...>
2222
2323
Positionals:
24-
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
25-
"deprecated-react-fragment", "deprecated-react-node-array",
26-
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
27-
"deprecated-sfc", "deprecated-stateless-component",
28-
"deprecated-void-function-component", "implicit-children", "preset-18",
29-
"preset-19", "refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
30-
"useRef-required-initial"]
24+
codemod [string] [required] [choices: "context-any", "deprecated-legacy-ref",
25+
"deprecated-react-child", "deprecated-react-fragment",
26+
"deprecated-react-node-array", "deprecated-react-text",
27+
"deprecated-react-type", "deprecated-sfc-element", "deprecated-sfc",
28+
"deprecated-stateless-component", "deprecated-void-function-component",
29+
"implicit-children", "preset-18", "preset-19", "refobject-defaults",
30+
"scoped-jsx", "useCallback-implicit-any", "useRef-required-initial"]
3131
paths [string] [required]
3232
3333
Options:

bin/types-react-codemod.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ async function main() {
9898
name: "presets",
9999
type: "checkbox",
100100
choices: [
101+
{ checked: true, value: "deprecated-legacy-ref" },
101102
{ checked: true, value: "deprecated-react-child" },
102103
{ checked: true, value: "deprecated-react-node-array" },
103104
{ checked: true, value: "deprecated-react-fragment" },
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
const { expect, test } = require("@jest/globals");
2+
const dedent = require("dedent");
3+
const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
4+
const deprecatedLegacyRefTransform = require("../deprecated-legacy-ref");
5+
6+
function applyTransform(source, options = {}) {
7+
return JscodeshiftTestUtils.applyTransform(
8+
deprecatedLegacyRefTransform,
9+
options,
10+
{
11+
path: "test.d.ts",
12+
source: dedent(source),
13+
},
14+
);
15+
}
16+
17+
test("not modified", () => {
18+
expect(
19+
applyTransform(`
20+
import * as React from 'react';
21+
interface Props {
22+
children?: ReactNode;
23+
}
24+
`),
25+
).toMatchInlineSnapshot(`
26+
"import * as React from 'react';
27+
interface Props {
28+
children?: ReactNode;
29+
}"
30+
`);
31+
});
32+
33+
test("named import", () => {
34+
expect(
35+
applyTransform(`
36+
import { LegacyRef } from 'react';
37+
interface Props<T> {
38+
legacyRef?: LegacyRef;
39+
legacyRefTyped?: LegacyRef<T>;
40+
}
41+
`),
42+
).toMatchInlineSnapshot(`
43+
"import { Ref } from 'react';
44+
interface Props<T> {
45+
legacyRef?: Ref;
46+
legacyRefTyped?: Ref<T>;
47+
}"
48+
`);
49+
});
50+
51+
test("named type import", () => {
52+
expect(
53+
applyTransform(`
54+
import { type LegacyRef } from 'react';
55+
interface Props<T> {
56+
legacyRef?: LegacyRef;
57+
legacyRefTyped?: LegacyRef<T>;
58+
}
59+
`),
60+
).toMatchInlineSnapshot(`
61+
"import { type Ref } from 'react';
62+
interface Props<T> {
63+
legacyRef?: Ref;
64+
legacyRefTyped?: Ref<T>;
65+
}"
66+
`);
67+
});
68+
69+
test("named import with existing target import", () => {
70+
expect(
71+
applyTransform(`
72+
import { LegacyRef, Ref } from 'react';
73+
interface Props {
74+
legacyRef?: LegacyRef;
75+
ref?: Ref;
76+
}
77+
`),
78+
).toMatchInlineSnapshot(`
79+
"import { Ref } from 'react';
80+
interface Props {
81+
legacyRef?: Ref;
82+
ref?: Ref;
83+
}"
84+
`);
85+
});
86+
87+
test("false-negative named renamed import", () => {
88+
expect(
89+
applyTransform(`
90+
import { LegacyRef as MyLegacyRef } from 'react';
91+
interface Props {
92+
ref?: MyLegacyRef;
93+
}
94+
`),
95+
).toMatchInlineSnapshot(`
96+
"import { LegacyRef as MyLegacyRef } from 'react';
97+
interface Props {
98+
ref?: MyLegacyRef;
99+
}"
100+
`);
101+
});
102+
103+
test("namespace import", () => {
104+
expect(
105+
applyTransform(`
106+
import * as React from 'react';
107+
interface Props<T> {
108+
ref?: React.LegacyRef;
109+
refTyped?: React.LegacyRef<T>;
110+
}
111+
`),
112+
).toMatchInlineSnapshot(`
113+
"import * as React from 'react';
114+
interface Props<T> {
115+
ref?: React.Ref;
116+
refTyped?: React.Ref<T>;
117+
}"
118+
`);
119+
});
120+
121+
test("as type parameter", () => {
122+
expect(
123+
applyTransform(`
124+
import * as React from 'react';
125+
createAction<React.LegacyRef>()
126+
createAction<React.LegacyRef<unknown>>()
127+
`),
128+
).toMatchInlineSnapshot(`
129+
"import * as React from 'react';
130+
createAction<React.Ref>()
131+
createAction<React.Ref<unknown>>()"
132+
`);
133+
});

transforms/__tests__/preset-19.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
44

55
describe("preset-19", () => {
66
let preset19Transform;
7+
let deprecatedLegacyRefTransform;
78
let deprecatedReactChildTransform;
89
let deprecatedReactNodeArrayTransform;
910
let deprecatedReactFragmentTransform;
@@ -33,6 +34,7 @@ describe("preset-19", () => {
3334
return transform;
3435
}
3536

37+
deprecatedLegacyRefTransform = mockTransform("../deprecated-legacy-ref");
3638
deprecatedReactChildTransform = mockTransform("../deprecated-react-child");
3739
deprecatedReactNodeArrayTransform = mockTransform(
3840
"../deprecated-react-node-array",
@@ -66,6 +68,7 @@ describe("preset-19", () => {
6668
test("applies all", () => {
6769
applyTransform("", {
6870
preset19Transforms: [
71+
"deprecated-legacy-ref",
6972
"deprecated-react-child",
7073
"deprecated-react-fragment",
7174
"deprecated-react-node-array",
@@ -77,6 +80,7 @@ describe("preset-19", () => {
7780
].join(","),
7881
});
7982

83+
expect(deprecatedLegacyRefTransform).toHaveBeenCalled();
8084
expect(deprecatedReactChildTransform).toHaveBeenCalled();
8185
expect(deprecatedReactNodeArrayTransform).toHaveBeenCalled();
8286
expect(deprecatedReactFragmentTransform).toHaveBeenCalled();
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
const parseSync = require("./utils/parseSync");
2+
const {
3+
findTSTypeReferenceCollections,
4+
} = require("./utils/jscodeshift-bugfixes");
5+
6+
/**
7+
* @type {import('jscodeshift').Transform}
8+
*/
9+
const deprecatedLegacyRefTransform = (file, api) => {
10+
const j = api.jscodeshift;
11+
const ast = parseSync(file);
12+
13+
let hasChanges = false;
14+
15+
const targetIdentifierImports = ast.find(j.ImportSpecifier, (node) => {
16+
const { imported, local } = node;
17+
return (
18+
imported.type === "Identifier" &&
19+
imported.name === "Ref" &&
20+
// We don't support renames generally, so we don't handle them here
21+
(local == null || local.name === "Ref")
22+
);
23+
});
24+
const sourceIdentifierImports = ast.find(j.ImportSpecifier, (node) => {
25+
const { imported, local } = node;
26+
return (
27+
imported.type === "Identifier" &&
28+
imported.name === "LegacyRef" &&
29+
// We don't support renames generally, so we don't handle them here
30+
(local == null || local.name === "LegacyRef")
31+
);
32+
});
33+
if (targetIdentifierImports.length > 0) {
34+
hasChanges = true;
35+
sourceIdentifierImports.remove();
36+
} else if (sourceIdentifierImports.length > 0) {
37+
hasChanges = true;
38+
sourceIdentifierImports.replaceWith((path) => {
39+
const importSpecifier = j.importSpecifier(j.identifier("Ref"));
40+
if ("importKind" in path.node) {
41+
// @ts-expect-error -- Missing types in jscodeshift. Babel uses `importKind`: https://astexplorer.net/#/gist/a76bd35f28483a467fef29d3c63aac9b/0e7ba6688fc09bd11b92197349b2384bb4c94574
42+
importSpecifier.importKind = path.node.importKind;
43+
}
44+
45+
return importSpecifier;
46+
});
47+
}
48+
49+
const sourceIdentifierTypeReferences = findTSTypeReferenceCollections(
50+
j,
51+
ast,
52+
(node) => {
53+
const { typeName } = node;
54+
55+
return typeName.type === "Identifier" && typeName.name === "LegacyRef";
56+
},
57+
);
58+
for (const typeReferences of sourceIdentifierTypeReferences) {
59+
const changedIdentifiers = typeReferences.replaceWith((path) => {
60+
// `Ref<T>`
61+
return j.tsTypeReference(
62+
j.identifier("Ref"),
63+
path.get("typeParameters").value,
64+
);
65+
});
66+
if (changedIdentifiers.length > 0) {
67+
hasChanges = true;
68+
}
69+
}
70+
71+
const sourceIdentifierQualifiedNamesReferences =
72+
findTSTypeReferenceCollections(j, ast, (node) => {
73+
const { typeName } = node;
74+
75+
return (
76+
typeName.type === "TSQualifiedName" &&
77+
typeName.right.type === "Identifier" &&
78+
typeName.right.name === "LegacyRef"
79+
);
80+
});
81+
for (const typeReferences of sourceIdentifierQualifiedNamesReferences) {
82+
const changedQualifiedNames = typeReferences.replaceWith((path) => {
83+
const { node } = path;
84+
const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ (
85+
node.typeName
86+
);
87+
// `*.Ref<T>`
88+
return j.tsTypeReference(
89+
j.tsQualifiedName(typeName.left, j.identifier("Ref")),
90+
path.get("typeParameters").value,
91+
);
92+
});
93+
if (changedQualifiedNames.length > 0) {
94+
hasChanges = true;
95+
}
96+
}
97+
98+
// Otherwise some files will be marked as "modified" because formatting changed
99+
if (hasChanges) {
100+
return ast.toSource();
101+
}
102+
return file.source;
103+
};
104+
105+
module.exports = deprecatedLegacyRefTransform;

transforms/preset-19.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const deprecatedLegacyRefTransform = require("./deprecated-legacy-ref");
12
const deprecatedReactChildTransform = require("./deprecated-react-child");
23
const deprecatedReactNodeArrayTransform = require("./deprecated-react-node-array");
34
const deprecatedReactFragmentTransform = require("./deprecated-react-fragment");
@@ -18,6 +19,9 @@ const transform = (file, api, options) => {
1819
* @type {import('jscodeshift').Transform[]}
1920
*/
2021
const transforms = [];
22+
if (transformNames.has("deprecated-legacy-ref")) {
23+
transforms.push(deprecatedLegacyRefTransform);
24+
}
2125
if (transformNames.has("deprecated-react-child")) {
2226
transforms.push(deprecatedReactChildTransform);
2327
}

0 commit comments

Comments
 (0)