Skip to content

Commit

Permalink
feat: Add deprecated-react-node-array transform (#325)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
eps1lon committed Dec 26, 2023
1 parent 80fe29c commit b7f757c
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 12 deletions.
12 changes: 12 additions & 0 deletions .changeset/three-bananas-lick.md
@@ -0,0 +1,12 @@
---
"types-react-codemod": minor
---

Add codemod to replace deprecated `ReactNodeArray` by inlining its actual type.

```diff
import * as React from 'react';

-const node: React.ReactNodeArray
+const node: ReadonlyArray<React.ReactNode>
```
35 changes: 28 additions & 7 deletions README.md
Expand Up @@ -30,16 +30,15 @@ Time elapsed: 0.229seconds
## Usage

```bash
$ npx types-react-codemod --help
types-react-codemod <codemod> <paths...>
$ npx types-react-codemod <codemod> <paths...>

Positionals:
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
"deprecated-sfc", "deprecated-stateless-component",
"deprecated-void-function-component", "implicit-children", "preset-18",
"preset-19", "refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
"useRef-required-initial"]
"deprecated-react-node-array", "deprecated-react-text",
"deprecated-react-type", "deprecated-sfc-element", "deprecated-sfc",
"deprecated-stateless-component", "deprecated-void-function-component",
"implicit-children", "preset-18", "preset-19", "refobject-defaults",
"scoped-jsx", "useCallback-implicit-any", "useRef-required-initial"]
paths [string] [required]

Options:
Expand Down Expand Up @@ -234,6 +233,28 @@ interface Props {
}
```

### `deprecated-react-node-array`

```diff
import * as React from "react";
interface Props {
- children?: React.ReactNodeArray;
+ children?: ReadonlyArray<React.ReactNode>;
}
```

#### `deprecated-react-node-array` false-negative pattern A

Importing `ReactNodeArray` via aliased named import will result in the transform being skipped.

```tsx
import { ReactNodeArray as MyReactNodeArray } from "react";
interface Props {
// not transformed
children?: MyReactNodeArray;
}
```

### `deprecated-react-text`

```diff
Expand Down
10 changes: 5 additions & 5 deletions bin/__tests__/types-react-codemod.js
Expand Up @@ -22,11 +22,11 @@ describe("types-react-codemod", () => {
Positionals:
codemod [string] [required] [choices: "context-any", "deprecated-react-child",
"deprecated-react-text", "deprecated-react-type", "deprecated-sfc-element",
"deprecated-sfc", "deprecated-stateless-component",
"deprecated-void-function-component", "implicit-children", "preset-18",
"preset-19", "refobject-defaults", "scoped-jsx", "useCallback-implicit-any",
"useRef-required-initial"]
"deprecated-react-node-array", "deprecated-react-text",
"deprecated-react-type", "deprecated-sfc-element", "deprecated-sfc",
"deprecated-stateless-component", "deprecated-void-function-component",
"implicit-children", "preset-18", "preset-19", "refobject-defaults",
"scoped-jsx", "useCallback-implicit-any", "useRef-required-initial"]
paths [string] [required]
Options:
Expand Down
97 changes: 97 additions & 0 deletions transforms/__tests__/deprecated-react-node-array.js
@@ -0,0 +1,97 @@
const { describe, expect, test } = require("@jest/globals");
const dedent = require("dedent");
const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
const deprecatedReactNodeArrayTransform = require("../deprecated-react-node-array");

function applyTransform(source, options = {}) {
return JscodeshiftTestUtils.applyTransform(
deprecatedReactNodeArrayTransform,
options,
{
path: "test.d.ts",
source: dedent(source),
},
);
}

describe("transform deprecated-react-node-array", () => {
test("not modified", () => {
expect(
applyTransform(`
import * as React from 'react';
interface Props {
children?: ReactNode;
}
`),
).toMatchInlineSnapshot(`
"import * as React from 'react';
interface Props {
children?: ReactNode;
}"
`);
});

test("named import", () => {
expect(
applyTransform(`
import { ReactNodeArray } from 'react';
interface Props {
children?: ReactNodeArray;
}
`),
).toMatchInlineSnapshot(`
"import { ReactNode } from 'react';
interface Props {
children?: ReadonlyArray<ReactNode>;
}"
`);
});

test("named import with existing ReactNode import", () => {
expect(
applyTransform(`
import { ReactNodeArray, ReactNode } from 'react';
interface Props {
children?: ReactNodeArray;
}
`),
).toMatchInlineSnapshot(`
"import { ReactNode } from 'react';
interface Props {
children?: ReadonlyArray<ReactNode>;
}"
`);
});

test("false-negative named renamed import", () => {
expect(
applyTransform(`
import { ReactNodeArray as MyReactNodeArray } from 'react';
interface Props {
children?: MyReactNodeArray;
}
`),
).toMatchInlineSnapshot(`
"import { ReactNodeArray as MyReactNodeArray } from 'react';
interface Props {
children?: MyReactNodeArray;
}"
`);
});

test("namespace import", () => {
expect(
applyTransform(`
import * as React from 'react';
interface Props {
children?: React.ReactNodeArray;
}
`),
).toMatchInlineSnapshot(`
"import * as React from 'react';
interface Props {
children?: ReadonlyArray<React.ReactNode>;
}"
`);
});
});
6 changes: 6 additions & 0 deletions transforms/__tests__/preset-19.js
Expand Up @@ -5,6 +5,7 @@ const JscodeshiftTestUtils = require("jscodeshift/dist/testUtils");
describe("preset-19", () => {
let preset19Transform;
let deprecatedReactChildTransform;
let deprecatedReactNodeArrayTransform;
let deprecatedReactTextTransform;
let deprecatedVoidFunctionComponentTransform;
let refobjectDefaultsTransform;
Expand Down Expand Up @@ -32,6 +33,9 @@ describe("preset-19", () => {
}

deprecatedReactChildTransform = mockTransform("../deprecated-react-child");
deprecatedReactNodeArrayTransform = mockTransform(
"../deprecated-react-node-array",
);
deprecatedReactTextTransform = mockTransform("../deprecated-react-text");
deprecatedVoidFunctionComponentTransform = mockTransform(
"../deprecated-void-function-component",
Expand Down Expand Up @@ -59,6 +63,7 @@ describe("preset-19", () => {
applyTransform("", {
preset19Transforms: [
"deprecated-react-child",
"deprecated-react-node-array",
"deprecated-react-text",
"deprecated-void-function-component",
"refobject-defaults",
Expand All @@ -68,6 +73,7 @@ describe("preset-19", () => {
});

expect(deprecatedReactChildTransform).toHaveBeenCalled();
expect(deprecatedReactNodeArrayTransform).toHaveBeenCalled();
expect(deprecatedReactTextTransform).toHaveBeenCalled();
expect(deprecatedVoidFunctionComponentTransform).toHaveBeenCalled();
expect(refobjectDefaultsTransform).toHaveBeenCalled();
Expand Down
92 changes: 92 additions & 0 deletions transforms/deprecated-react-node-array.js
@@ -0,0 +1,92 @@
const parseSync = require("./utils/parseSync");

/**
* @type {import('jscodeshift').Transform}
*/
const deprecatedReactNodeArrayTransform = (file, api) => {
const j = api.jscodeshift;
const ast = parseSync(file);

const hasReactNodeImport = ast.find(j.ImportSpecifier, (node) => {
const { imported, local } = node;
return (
imported.type === "Identifier" &&
imported.name === "ReactNode" &&
// We don't support renames generally, so we don't handle them here
(local == null || local.name === "ReactNode")
);
});
const reactNodeArrayImports = ast.find(j.ImportSpecifier, (node) => {
const { imported, local } = node;
return (
imported.type === "Identifier" &&
imported.name === "ReactNodeArray" &&
// We don't support renames generally, so we don't handle them here
(local == null || local.name === "ReactNodeArray")
);
});

if (hasReactNodeImport.length > 0) {
reactNodeArrayImports.remove();
} else {
reactNodeArrayImports.replaceWith(() => {
return j.importSpecifier(j.identifier("ReactNode"));
});
}

const changedIdentifiers = ast
.find(j.TSTypeReference, (node) => {
const { typeName } = node;

return (
typeName.type === "Identifier" && typeName.name === "ReactNodeArray"
);
})
.replaceWith(() => {
// `ReadonlyArray<ReactNode>`
return j.tsTypeReference(
j.identifier("ReadonlyArray"),
j.tsTypeParameterInstantiation([
j.tsTypeReference(j.identifier("ReactNode")),
]),
);
});

const changedQualifiedNames = ast
.find(j.TSTypeReference, (node) => {
const { typeName } = node;

return (
typeName.type === "TSQualifiedName" &&
typeName.right.type === "Identifier" &&
typeName.right.name === "ReactNodeArray"
);
})
.replaceWith((path) => {
const { node } = path;
const typeName = /** @type {import('jscodeshift').TSQualifiedName} */ (
node.typeName
);
// `ReadonlyArray<*.ReactNode>`
return j.tsTypeReference(
j.identifier("ReadonlyArray"),
j.tsTypeParameterInstantiation([
j.tsTypeReference(
j.tsQualifiedName(typeName.left, j.identifier("ReactNode")),
),
]),
);
});

// Otherwise some files will be marked as "modified" because formatting changed
if (
changedIdentifiers.length > 0 ||
changedQualifiedNames.length > 0 ||
reactNodeArrayImports.length > 0
) {
return ast.toSource();
}
return file.source;
};

module.exports = deprecatedReactNodeArrayTransform;
4 changes: 4 additions & 0 deletions transforms/preset-19.js
@@ -1,4 +1,5 @@
const deprecatedReactChildTransform = require("./deprecated-react-child");
const deprecatedReactNodeArrayTransform = require("./deprecated-react-node-array");
const deprecatedReactTextTransform = require("./deprecated-react-text");
const deprecatedVoidFunctionComponentTransform = require("./deprecated-void-function-component");
const refobjectDefaultsTransform = require("./refobject-defaults");
Expand All @@ -19,6 +20,9 @@ const transform = (file, api, options) => {
if (transformNames.has("deprecated-react-child")) {
transforms.push(deprecatedReactChildTransform);
}
if (transformNames.has("deprecated-react-node-array")) {
transforms.push(deprecatedReactNodeArrayTransform);
}
if (transformNames.has("deprecated-react-text")) {
transforms.push(deprecatedReactTextTransform);
}
Expand Down

0 comments on commit b7f757c

Please sign in to comment.