Skip to content

Commit b7f757c

Browse files
authored
feat: Add deprecated-react-node-array transform (#325)
* feat: Add `deprecated-react-node-array` transform * fixup! feat: Add `deprecated-react-node-array` transform * fixup! fixup! feat: Add `deprecated-react-node-array` transform * fixup! fixup! fixup! feat: Add `deprecated-react-node-array` transform * Improve coverage * fixup! Improve coverage
1 parent 80fe29c commit b7f757c

File tree

7 files changed

+244
-12
lines changed

7 files changed

+244
-12
lines changed

.changeset/three-bananas-lick.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"types-react-codemod": minor
3+
---
4+
5+
Add codemod to replace deprecated `ReactNodeArray` by inlining its actual type.
6+
7+
```diff
8+
import * as React from 'react';
9+
10+
-const node: React.ReactNodeArray
11+
+const node: ReadonlyArray<React.ReactNode>
12+
```

README.md

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,15 @@ Time elapsed: 0.229seconds
3030
## Usage
3131

3232
```bash
33-
$ npx types-react-codemod --help
34-
types-react-codemod <codemod> <paths...>
33+
$ npx types-react-codemod <codemod> <paths...>
3534

3635
Positionals:
3736
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
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"]
37+
"deprecated-react-node-array", "deprecated-react-text",
38+
"deprecated-react-type", "deprecated-sfc-element", "deprecated-sfc",
39+
"deprecated-stateless-component", "deprecated-void-function-component",
40+
"implicit-children", "preset-18", "preset-19", "refobject-defaults",
41+
"scoped-jsx", "useCallback-implicit-any", "useRef-required-initial"]
4342
paths [string] [required]
4443

4544
Options:
@@ -234,6 +233,28 @@ interface Props {
234233
}
235234
```
236235

236+
### `deprecated-react-node-array`
237+
238+
```diff
239+
import * as React from "react";
240+
interface Props {
241+
- children?: React.ReactNodeArray;
242+
+ children?: ReadonlyArray<React.ReactNode>;
243+
}
244+
```
245+
246+
#### `deprecated-react-node-array` false-negative pattern A
247+
248+
Importing `ReactNodeArray` via aliased named import will result in the transform being skipped.
249+
250+
```tsx
251+
import { ReactNodeArray as MyReactNodeArray } from "react";
252+
interface Props {
253+
// not transformed
254+
children?: MyReactNodeArray;
255+
}
256+
```
257+
237258
### `deprecated-react-text`
238259

239260
```diff

bin/__tests__/types-react-codemod.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ describe("types-react-codemod", () => {
2222
2323
Positionals:
2424
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
25-
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
26-
"deprecated-sfc", "deprecated-stateless-component",
27-
"deprecated-void-function-component", "implicit-children", "preset-18",
28-
"preset-19", "refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
29-
"useRef-required-initial"]
25+
"deprecated-react-node-array", "deprecated-react-text",
26+
"deprecated-react-type", "deprecated-sfc-element", "deprecated-sfc",
27+
"deprecated-stateless-component", "deprecated-void-function-component",
28+
"implicit-children", "preset-18", "preset-19", "refobject-defaults",
29+
"scoped-jsx", "useCallback-implicit-any", "useRef-required-initial"]
3030
paths [string] [required]
3131
3232
Options:
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
const { describe, expect, test } = require("@jest/globals");
2+
const dedent = require("dedent");
3+
const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
4+
const deprecatedReactNodeArrayTransform = require("../deprecated-react-node-array");
5+
6+
function applyTransform(source, options = {}) {
7+
return JscodeshiftTestUtils.applyTransform(
8+
deprecatedReactNodeArrayTransform,
9+
options,
10+
{
11+
path: "test.d.ts",
12+
source: dedent(source),
13+
},
14+
);
15+
}
16+
17+
describe("transform deprecated-react-node-array", () => {
18+
test("not modified", () => {
19+
expect(
20+
applyTransform(`
21+
import * as React from 'react';
22+
interface Props {
23+
children?: ReactNode;
24+
}
25+
`),
26+
).toMatchInlineSnapshot(`
27+
"import * as React from 'react';
28+
interface Props {
29+
children?: ReactNode;
30+
}"
31+
`);
32+
});
33+
34+
test("named import", () => {
35+
expect(
36+
applyTransform(`
37+
import { ReactNodeArray } from 'react';
38+
interface Props {
39+
children?: ReactNodeArray;
40+
}
41+
`),
42+
).toMatchInlineSnapshot(`
43+
"import { ReactNode } from 'react';
44+
interface Props {
45+
children?: ReadonlyArray<ReactNode>;
46+
}"
47+
`);
48+
});
49+
50+
test("named import with existing ReactNode import", () => {
51+
expect(
52+
applyTransform(`
53+
import { ReactNodeArray, ReactNode } from 'react';
54+
interface Props {
55+
children?: ReactNodeArray;
56+
}
57+
`),
58+
).toMatchInlineSnapshot(`
59+
"import { ReactNode } from 'react';
60+
interface Props {
61+
children?: ReadonlyArray<ReactNode>;
62+
}"
63+
`);
64+
});
65+
66+
test("false-negative named renamed import", () => {
67+
expect(
68+
applyTransform(`
69+
import { ReactNodeArray as MyReactNodeArray } from 'react';
70+
interface Props {
71+
children?: MyReactNodeArray;
72+
}
73+
`),
74+
).toMatchInlineSnapshot(`
75+
"import { ReactNodeArray as MyReactNodeArray } from 'react';
76+
interface Props {
77+
children?: MyReactNodeArray;
78+
}"
79+
`);
80+
});
81+
82+
test("namespace import", () => {
83+
expect(
84+
applyTransform(`
85+
import * as React from 'react';
86+
interface Props {
87+
children?: React.ReactNodeArray;
88+
}
89+
`),
90+
).toMatchInlineSnapshot(`
91+
"import * as React from 'react';
92+
interface Props {
93+
children?: ReadonlyArray<React.ReactNode>;
94+
}"
95+
`);
96+
});
97+
});

transforms/__tests__/preset-19.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
55
describe("preset-19", () => {
66
let preset19Transform;
77
let deprecatedReactChildTransform;
8+
let deprecatedReactNodeArrayTransform;
89
let deprecatedReactTextTransform;
910
let deprecatedVoidFunctionComponentTransform;
1011
let refobjectDefaultsTransform;
@@ -32,6 +33,9 @@ describe("preset-19", () => {
3233
}
3334

3435
deprecatedReactChildTransform = mockTransform("../deprecated-react-child");
36+
deprecatedReactNodeArrayTransform = mockTransform(
37+
"../deprecated-react-node-array",
38+
);
3539
deprecatedReactTextTransform = mockTransform("../deprecated-react-text");
3640
deprecatedVoidFunctionComponentTransform = mockTransform(
3741
"../deprecated-void-function-component",
@@ -59,6 +63,7 @@ describe("preset-19", () => {
5963
applyTransform("", {
6064
preset19Transforms: [
6165
"deprecated-react-child",
66+
"deprecated-react-node-array",
6267
"deprecated-react-text",
6368
"deprecated-void-function-component",
6469
"refobject-defaults",
@@ -68,6 +73,7 @@ describe("preset-19", () => {
6873
});
6974

7075
expect(deprecatedReactChildTransform).toHaveBeenCalled();
76+
expect(deprecatedReactNodeArrayTransform).toHaveBeenCalled();
7177
expect(deprecatedReactTextTransform).toHaveBeenCalled();
7278
expect(deprecatedVoidFunctionComponentTransform).toHaveBeenCalled();
7379
expect(refobjectDefaultsTransform).toHaveBeenCalled();
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
const parseSync = require("./utils/parseSync");
2+
3+
/**
4+
* @type {import('jscodeshift').Transform}
5+
*/
6+
const deprecatedReactNodeArrayTransform = (file, api) => {
7+
const j = api.jscodeshift;
8+
const ast = parseSync(file);
9+
10+
const hasReactNodeImport = ast.find(j.ImportSpecifier, (node) => {
11+
const { imported, local } = node;
12+
return (
13+
imported.type === "Identifier" &&
14+
imported.name === "ReactNode" &&
15+
// We don't support renames generally, so we don't handle them here
16+
(local == null || local.name === "ReactNode")
17+
);
18+
});
19+
const reactNodeArrayImports = ast.find(j.ImportSpecifier, (node) => {
20+
const { imported, local } = node;
21+
return (
22+
imported.type === "Identifier" &&
23+
imported.name === "ReactNodeArray" &&
24+
// We don't support renames generally, so we don't handle them here
25+
(local == null || local.name === "ReactNodeArray")
26+
);
27+
});
28+
29+
if (hasReactNodeImport.length > 0) {
30+
reactNodeArrayImports.remove();
31+
} else {
32+
reactNodeArrayImports.replaceWith(() => {
33+
return j.importSpecifier(j.identifier("ReactNode"));
34+
});
35+
}
36+
37+
const changedIdentifiers = ast
38+
.find(j.TSTypeReference, (node) => {
39+
const { typeName } = node;
40+
41+
return (
42+
typeName.type === "Identifier" && typeName.name === "ReactNodeArray"
43+
);
44+
})
45+
.replaceWith(() => {
46+
// `ReadonlyArray<ReactNode>`
47+
return j.tsTypeReference(
48+
j.identifier("ReadonlyArray"),
49+
j.tsTypeParameterInstantiation([
50+
j.tsTypeReference(j.identifier("ReactNode")),
51+
]),
52+
);
53+
});
54+
55+
const changedQualifiedNames = ast
56+
.find(j.TSTypeReference, (node) => {
57+
const { typeName } = node;
58+
59+
return (
60+
typeName.type === "TSQualifiedName" &&
61+
typeName.right.type === "Identifier" &&
62+
typeName.right.name === "ReactNodeArray"
63+
);
64+
})
65+
.replaceWith((path) => {
66+
const { node } = path;
67+
const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ (
68+
node.typeName
69+
);
70+
// `ReadonlyArray<*.ReactNode>`
71+
return j.tsTypeReference(
72+
j.identifier("ReadonlyArray"),
73+
j.tsTypeParameterInstantiation([
74+
j.tsTypeReference(
75+
j.tsQualifiedName(typeName.left, j.identifier("ReactNode")),
76+
),
77+
]),
78+
);
79+
});
80+
81+
// Otherwise some files will be marked as "modified" because formatting changed
82+
if (
83+
changedIdentifiers.length > 0 ||
84+
changedQualifiedNames.length > 0 ||
85+
reactNodeArrayImports.length > 0
86+
) {
87+
return ast.toSource();
88+
}
89+
return file.source;
90+
};
91+
92+
module.exports = deprecatedReactNodeArrayTransform;

transforms/preset-19.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const deprecatedReactChildTransform = require("./deprecated-react-child");
2+
const deprecatedReactNodeArrayTransform = require("./deprecated-react-node-array");
23
const deprecatedReactTextTransform = require("./deprecated-react-text");
34
const deprecatedVoidFunctionComponentTransform = require("./deprecated-void-function-component");
45
const refobjectDefaultsTransform = require("./refobject-defaults");
@@ -19,6 +20,9 @@ const transform = (file, api, options) => {
1920
if (transformNames.has("deprecated-react-child")) {
2021
transforms.push(deprecatedReactChildTransform);
2122
}
23+
if (transformNames.has("deprecated-react-node-array")) {
24+
transforms.push(deprecatedReactNodeArrayTransform);
25+
}
2226
if (transformNames.has("deprecated-react-text")) {
2327
transforms.push(deprecatedReactTextTransform);
2428
}

0 commit comments

Comments
 (0)