Skip to content

Commit ed97a70

Browse files
authored
feat: Add deprecated-react-fragment transform (#326)
* feat: Add deprecated-react-fragment transform * fixup! feat: Add deprecated-react-fragment transform * fixup! fixup! feat: Add deprecated-react-fragment transform
1 parent 7a94e7f commit ed97a70

7 files changed

Lines changed: 245 additions & 10 deletions

File tree

.changeset/chilly-lamps-greet.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 `ReactFragment` by inlining its actual type
6+
7+
```diff
8+
import * as React from 'react';
9+
10+
-const node: React.ReactFragment
11+
+const node: Iterable<React.ReactNode>
12+
```

README.md

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ $ npx types-react-codemod <codemod> <paths...>
3434

3535
Positionals:
3636
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
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"]
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"]
4243
paths [string] [required]
4344

4445
Options:
@@ -255,6 +256,28 @@ interface Props {
255256
}
256257
```
257258

259+
### `deprecated-react-fragment`
260+
261+
```diff
262+
import * as React from "react";
263+
interface Props {
264+
- children?: React.ReactFragment;
265+
+ children?: Iterable<React.ReactNode>;
266+
}
267+
```
268+
269+
#### `deprecated-react-fragment` false-negative pattern A
270+
271+
Importing `ReactFragment` via aliased named import will result in the transform being skipped.
272+
273+
```tsx
274+
import { ReactFragment as MyReactFragment } from "react";
275+
interface Props {
276+
// not transformed
277+
children?: MyReactFragment;
278+
}
279+
```
280+
258281
### `deprecated-react-text`
259282

260283
```diff

bin/__tests__/types-react-codemod.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ describe("types-react-codemod", () => {
2222
2323
Positionals:
2424
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
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"]
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"]
3031
paths [string] [required]
3132
3233
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 deprecatedReactFragmentTransform = require("../deprecated-react-fragment");
5+
6+
function applyTransform(source, options = {}) {
7+
return JscodeshiftTestUtils.applyTransform(
8+
deprecatedReactFragmentTransform,
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 { ReactFragment } from 'react';
38+
interface Props {
39+
children?: ReactFragment;
40+
}
41+
`),
42+
).toMatchInlineSnapshot(`
43+
"import { ReactNode } from 'react';
44+
interface Props {
45+
children?: Iterable<ReactNode>;
46+
}"
47+
`);
48+
});
49+
50+
test("named import with existing ReactNode import", () => {
51+
expect(
52+
applyTransform(`
53+
import { ReactFragment, ReactNode } from 'react';
54+
interface Props {
55+
children?: ReactFragment;
56+
}
57+
`),
58+
).toMatchInlineSnapshot(`
59+
"import { ReactNode } from 'react';
60+
interface Props {
61+
children?: Iterable<ReactNode>;
62+
}"
63+
`);
64+
});
65+
66+
test("false-negative named renamed import", () => {
67+
expect(
68+
applyTransform(`
69+
import { ReactFragment as MyReactFragment } from 'react';
70+
interface Props {
71+
children?: MyReactFragment;
72+
}
73+
`),
74+
).toMatchInlineSnapshot(`
75+
"import { ReactFragment as MyReactFragment } from 'react';
76+
interface Props {
77+
children?: MyReactFragment;
78+
}"
79+
`);
80+
});
81+
82+
test("namespace import", () => {
83+
expect(
84+
applyTransform(`
85+
import * as React from 'react';
86+
interface Props {
87+
children?: React.ReactFragment;
88+
}
89+
`),
90+
).toMatchInlineSnapshot(`
91+
"import * as React from 'react';
92+
interface Props {
93+
children?: Iterable<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
@@ -6,6 +6,7 @@ describe("preset-19", () => {
66
let preset19Transform;
77
let deprecatedReactChildTransform;
88
let deprecatedReactNodeArrayTransform;
9+
let deprecatedReactFragmentTransform;
910
let deprecatedReactTextTransform;
1011
let deprecatedVoidFunctionComponentTransform;
1112
let refobjectDefaultsTransform;
@@ -36,6 +37,9 @@ describe("preset-19", () => {
3637
deprecatedReactNodeArrayTransform = mockTransform(
3738
"../deprecated-react-node-array",
3839
);
40+
deprecatedReactFragmentTransform = mockTransform(
41+
"../deprecated-react-fragment",
42+
);
3943
deprecatedReactTextTransform = mockTransform("../deprecated-react-text");
4044
deprecatedVoidFunctionComponentTransform = mockTransform(
4145
"../deprecated-void-function-component",
@@ -63,6 +67,7 @@ describe("preset-19", () => {
6367
applyTransform("", {
6468
preset19Transforms: [
6569
"deprecated-react-child",
70+
"deprecated-react-fragment",
6671
"deprecated-react-node-array",
6772
"deprecated-react-text",
6873
"deprecated-void-function-component",
@@ -74,6 +79,7 @@ describe("preset-19", () => {
7479

7580
expect(deprecatedReactChildTransform).toHaveBeenCalled();
7681
expect(deprecatedReactNodeArrayTransform).toHaveBeenCalled();
82+
expect(deprecatedReactFragmentTransform).toHaveBeenCalled();
7783
expect(deprecatedReactTextTransform).toHaveBeenCalled();
7884
expect(deprecatedVoidFunctionComponentTransform).toHaveBeenCalled();
7985
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 deprecatedReactFragmentTransform = (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 reactFragmentImports = ast.find(j.ImportSpecifier, (node) => {
20+
const { imported, local } = node;
21+
return (
22+
imported.type === "Identifier" &&
23+
imported.name === "ReactFragment" &&
24+
// We don't support renames generally, so we don't handle them here
25+
(local == null || local.name === "ReactFragment")
26+
);
27+
});
28+
29+
if (hasReactNodeImport.length > 0) {
30+
reactFragmentImports.remove();
31+
} else {
32+
reactFragmentImports.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 === "ReactFragment"
43+
);
44+
})
45+
.replaceWith(() => {
46+
// `Iterable<ReactNode>`
47+
return j.tsTypeReference(
48+
j.identifier("Iterable"),
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 === "ReactFragment"
63+
);
64+
})
65+
.replaceWith((path) => {
66+
const { node } = path;
67+
const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ (
68+
node.typeName
69+
);
70+
// `Iterable<*.ReactNode>`
71+
return j.tsTypeReference(
72+
j.identifier("Iterable"),
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+
reactFragmentImports.length > 0
86+
) {
87+
return ast.toSource();
88+
}
89+
return file.source;
90+
};
91+
92+
module.exports = deprecatedReactFragmentTransform;

transforms/preset-19.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const deprecatedReactChildTransform = require("./deprecated-react-child");
22
const deprecatedReactNodeArrayTransform = require("./deprecated-react-node-array");
3+
const deprecatedReactFragmentTransform = require("./deprecated-react-fragment");
34
const deprecatedReactTextTransform = require("./deprecated-react-text");
45
const deprecatedVoidFunctionComponentTransform = require("./deprecated-void-function-component");
56
const refobjectDefaultsTransform = require("./refobject-defaults");
@@ -23,6 +24,9 @@ const transform = (file, api, options) => {
2324
if (transformNames.has("deprecated-react-node-array")) {
2425
transforms.push(deprecatedReactNodeArrayTransform);
2526
}
27+
if (transformNames.has("deprecated-react-fragment")) {
28+
transforms.push(deprecatedReactFragmentTransform);
29+
}
2630
if (transformNames.has("deprecated-react-text")) {
2731
transforms.push(deprecatedReactTextTransform);
2832
}

0 commit comments

Comments
 (0)