Skip to content

Commit 85bae50

Browse files
authored
feat: Add deprecated-react-text and preset-19 (#44)
* wip * f * f * docs * Add prompt for react-19 preset
1 parent 1cddd71 commit 85bae50

File tree

9 files changed

+279
-6
lines changed

9 files changed

+279
-6
lines changed

.changeset/selfish-jars-film.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"types-react-codemod": patch
3+
---
4+
5+
Add `deprecated-react-text` and `preset-19`.

CONTRIBUTING.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Contributing
2+
3+
## Developing a codemod
4+
5+
[Base ASTExplorer config for JSCodeshift for TypeScript](https://astexplorer.net/#/gist/90d789c2762cd9a13f7e3164e464a2c7/8530b091d0fc90ecc84448c924e3ea4d929bf6ab).

README.md

+36-3
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,10 @@ $ npx types-react-codemod --help
3434
types-react-codemod <codemod> <paths...>
3535

3636
Positionals:
37-
codemod [string] [required] [choices: "context-any", "deprecated-react-type",
38-
"deprecated-sfc-element", "deprecated-sfc", "deprecated-stateless-component",
39-
"implicit-children", "preset-18", "useCallback-implicit-any"]
37+
codemod [string] [required] [choices: "context-any", "deprecated-react-text",
38+
"deprecated-react-type", "deprecated-sfc-element", "deprecated-sfc",
39+
"deprecated-stateless-component", "implicit-children", "preset-18",
40+
"preset-19", "useCallback-implicit-any"]
4041
paths [string] [required]
4142

4243
Options:
@@ -67,6 +68,8 @@ The reason being that a false-positive can be reverted easily (assuming you have
6768
- `context-any`
6869
- `implicit-children`
6970
- `useCallback-implicit-any`
71+
- `preset-19`
72+
- `deprecated-react-text`
7073

7174
### `preset-18`
7275

@@ -193,3 +196,33 @@ type CreateCallback = () => (event: Event) => void;
193196
-const createCallback: CreateCallback = () => useCallback((event) => {}, [])
194197
+const createCallback: CreateCallback = () => useCallback((event: any) => {}, [])
195198
```
199+
200+
### `preset-19`
201+
202+
This codemod combines all codemods for React 19 types.
203+
You can interactively pick the codemods included.
204+
By default, the codemods that are definitely required to upgrade to `@types/react@^19.0.0` are selected.
205+
The other codemods may or may not be required.
206+
You should select all and audit the changed files regardless.
207+
208+
### `deprecated-react-text`
209+
210+
```diff
211+
import * as React from "react";
212+
interface Props {
213+
- label?: React.ReactText;
214+
+ label?: number | string;
215+
}
216+
```
217+
218+
#### `deprecated-react-text` false-negative pattern A
219+
220+
Importing `ReactText` via aliased named import will result in the transform being skipped.
221+
222+
```tsx
223+
import { ReactText as MyReactText } from "react";
224+
interface Props {
225+
// not transformed
226+
label?: MyReactText;
227+
}
228+
```

bin/__tests__/__snapshots__/types-react-codemod.js.snap

+4-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ Object {
66
"stdout": "types-react-codemod <codemod> <paths...>
77
88
Positionals:
9-
codemod [string] [required] [choices: \\"context-any\\", \\"deprecated-react-type\\",
10-
\\"deprecated-sfc-element\\", \\"deprecated-sfc\\", \\"deprecated-stateless-component\\",
11-
\\"implicit-children\\", \\"preset-18\\", \\"useCallback-implicit-any\\"]
9+
codemod [string] [required] [choices: \\"context-any\\", \\"deprecated-react-text\\",
10+
\\"deprecated-react-type\\", \\"deprecated-sfc-element\\", \\"deprecated-sfc\\",
11+
\\"deprecated-stateless-component\\", \\"implicit-children\\", \\"preset-18\\",
12+
\\"preset-19\\", \\"useCallback-implicit-any\\"]
1213
paths [string] [required]
1314
1415
Options:

bin/types-react-codemod.cjs

+10
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@ async function main() {
8585
},
8686
]);
8787
args.push(`--preset18Transforms="${presets.join(",")}"`);
88+
} else if (codemod === "preset-19") {
89+
const { presets } = await inquirer.prompt([
90+
{
91+
message: "Pick transforms to apply",
92+
name: "presets",
93+
type: "checkbox",
94+
choices: [{ checked: true, value: "deprecated-react-text" }],
95+
},
96+
]);
97+
args.push(`--preset19Transforms="${presets.join(",")}"`);
8898
}
8999

90100
if (dry) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { describe, expect, test } from "@jest/globals";
2+
import dedent from "dedent";
3+
import * as JscodeshiftTestUtils from "jscodeshift/dist/testUtils";
4+
import deprecatedReactTextTransform from "../deprecated-react-text";
5+
6+
function applyTransform(source, options = {}) {
7+
return JscodeshiftTestUtils.applyTransform(
8+
deprecatedReactTextTransform,
9+
options,
10+
{
11+
path: "test.d.ts",
12+
source: dedent(source),
13+
}
14+
);
15+
}
16+
17+
describe("transform deprecated-react-text", () => {
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 { ReactText } from 'react';
38+
interface Props {
39+
children?: ReactText;
40+
}
41+
`)
42+
).toMatchInlineSnapshot(`
43+
"import { ReactText } from 'react';
44+
interface Props {
45+
children?: number | string;
46+
}"
47+
`);
48+
});
49+
50+
test("false-negative named renamed import", () => {
51+
expect(
52+
applyTransform(`
53+
import { ReactText as MyReactText } from 'react';
54+
interface Props {
55+
children?: MyReactText;
56+
}
57+
`)
58+
).toMatchInlineSnapshot(`
59+
"import { ReactText as MyReactText } from 'react';
60+
interface Props {
61+
children?: MyReactText;
62+
}"
63+
`);
64+
});
65+
66+
test("namespace import", () => {
67+
expect(
68+
applyTransform(`
69+
import * as React from 'react';
70+
interface Props {
71+
children?: React.ReactText;
72+
}
73+
`)
74+
).toMatchInlineSnapshot(`
75+
"import * as React from 'react';
76+
interface Props {
77+
children?: number | string;
78+
}"
79+
`);
80+
});
81+
});

transforms/__tests__/preset-19.js

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { beforeEach, describe, expect, jest, test } from "@jest/globals";
2+
import dedent from "dedent";
3+
import * as JscodeshiftTestUtils from "jscodeshift/dist/testUtils";
4+
5+
describe("preset-19", () => {
6+
let preset19Transform;
7+
let deprecatedReactTextTransform;
8+
9+
function applyTransform(source, options = {}) {
10+
return JscodeshiftTestUtils.applyTransform(preset19Transform, options, {
11+
path: "test.d.ts",
12+
source: dedent(source),
13+
});
14+
}
15+
16+
beforeEach(() => {
17+
jest.resetModules();
18+
19+
function mockTransform(moduleName) {
20+
const transform = jest.fn();
21+
22+
jest.doMock(moduleName, () => {
23+
return {
24+
__esModule: true,
25+
default: transform,
26+
};
27+
});
28+
29+
return transform;
30+
}
31+
32+
deprecatedReactTextTransform = mockTransform("../deprecated-react-text");
33+
34+
preset19Transform = require("../preset-19");
35+
});
36+
37+
test("applies subset", () => {
38+
applyTransform("", {
39+
preset19Transforms: "deprecated-react-text",
40+
});
41+
42+
expect(deprecatedReactTextTransform).toHaveBeenCalled();
43+
});
44+
45+
test("applies all", () => {
46+
applyTransform("", {
47+
preset19Transforms: ["deprecated-react-text"].join(","),
48+
});
49+
50+
applyTransform("", {
51+
preset19Transforms: "deprecated-react-text",
52+
});
53+
54+
expect(deprecatedReactTextTransform).toHaveBeenCalled();
55+
});
56+
});

transforms/deprecated-react-text.js

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const parseSync = require("./utils/parseSync");
2+
3+
/**
4+
* @type {import('jscodeshift').Transform}
5+
*/
6+
const deprecatedReactTextTransform = (file, api) => {
7+
const j = api.jscodeshift;
8+
const ast = parseSync(file);
9+
10+
const changedIdentifiers = ast
11+
.find(j.TSTypeReference, (node) => {
12+
const { typeName } = node;
13+
/**
14+
* @type {import('jscodeshift').Identifier | null}
15+
*/
16+
let identifier = null;
17+
if (typeName.type === "Identifier") {
18+
identifier = typeName;
19+
} else if (
20+
typeName.type === "TSQualifiedName" &&
21+
typeName.right.type === "Identifier"
22+
) {
23+
identifier = typeName.right;
24+
}
25+
26+
return identifier !== null && identifier.name === "ReactText";
27+
})
28+
.replaceWith(() => {
29+
// `number | string`
30+
return j.tsUnionType([j.tsNumberKeyword(), j.tsStringKeyword()]);
31+
});
32+
33+
// Otherwise some files will be marked as "modified" because formatting changed
34+
if (changedIdentifiers.length > 0) {
35+
return ast.toSource();
36+
}
37+
return file.source;
38+
};
39+
40+
export default deprecatedReactTextTransform;

transforms/preset-19.js

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import deprecatedReactTextTransform from "./deprecated-react-text";
2+
3+
/**
4+
* @type {import('jscodeshift').Transform}
5+
*/
6+
const transform = (file, api, options) => {
7+
const { preset19Transforms } = options;
8+
9+
const transformNames = new Set(preset19Transforms.split(","));
10+
/**
11+
* @type {import('jscodeshift').Transform[]}
12+
*/
13+
const transforms = [];
14+
if (transformNames.has("deprecated-react-text")) {
15+
transforms.push(deprecatedReactTextTransform);
16+
}
17+
18+
let wasAlwaysSkipped = true;
19+
const newSource = transforms.reduce((currentFileSource, transform) => {
20+
// TODO: currently we parse -> transform -> print on every transform
21+
// Instead, we could parse and prince once in the preset and the transformers just deal with an AST.
22+
// That requires refactoring of every transform into source-transformer and ast-transformer
23+
const transformResult = transform(
24+
{ path: file.path, source: currentFileSource },
25+
api,
26+
options
27+
);
28+
29+
if (transformResult == null) {
30+
return currentFileSource;
31+
} else {
32+
wasAlwaysSkipped = false;
33+
return transformResult;
34+
}
35+
}, file.source);
36+
37+
if (!wasAlwaysSkipped) {
38+
return newSource;
39+
}
40+
};
41+
42+
export default transform;

0 commit comments

Comments
 (0)