Skip to content

Commit fa7741b

Browse files
committed
feat: first commit. added codemod
1 parent 86c9359 commit fa7741b

13 files changed

+8911
-2
lines changed

Diff for: .eslintignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
./dist
2+
husky.config.js
3+
commitlint.config.js

Diff for: .eslintrc

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"env": {
3+
"commonjs": true,
4+
"es6": true,
5+
"node": true
6+
},
7+
"extends": [
8+
"plugin:@typescript-eslint/eslint-recommended",
9+
"plugin:@typescript-eslint/recommended",
10+
"prettier/@typescript-eslint"
11+
],
12+
"plugins": ["@typescript-eslint"],
13+
"parser": "@typescript-eslint/parser",
14+
"rules": {
15+
"@typescript-eslint/no-non-null-assertion": "off",
16+
"@typescript-eslint/no-explicit-any": "off",
17+
"@typescript-eslint/explicit-module-boundary-types": "off",
18+
"@typescript-eslint/no-unused-vars": ["error", { "ignoreRestSiblings": true }]
19+
}
20+
}

Diff for: .gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules

Diff for: README.md

+81-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,81 @@
1-
# codemod-replace-reactFC-typescript
2-
jscodeshift's codemod to replace React.FC when using React with Typescript
1+
# codemod-replace-react-fc-typescript
2+
3+
A codemod using [jscodeshift](https://github.com/facebook/jscodeshift) to remove `React.FC` and `React.SFC` from your codebase
4+
5+
## Motivation
6+
7+
IF you use React and Typescript, you might have come across this [GitHub PR in Create React App's repo](https://github.com/facebook/create-react-app/pull/8177) about removing `React.FC` from their base template of a Typescript project.
8+
9+
The three main points that made me buy this was the fact that:
10+
11+
- There's an implicit definition of `children` - all your components will have `children` typed!
12+
- They don't support generics
13+
- It does not correctly work with `defaultProps`
14+
15+
as well as other downsides (check out the PR description for that)
16+
17+
Motivated by that PR, and a lot of blog posts who also shared similar conclusions (and this [ADR](https://backstage.io/docs/architecture-decisions/adrs-adr006) from Spotify's team in which they recorded the decision of removing `React.FC` from they codebase too), I wrote this little codemod that drops `React.FC` and `React.SFC` (which was also deprecated) and replaces the Props as the type of the unique argument in the component definition.
18+
19+
Let's see it with code
20+
21+
```tsx
22+
// before codemod runs
23+
type Props2 = { id: number };
24+
export const MyComponent2: React.FC<Props2> = (props) => {
25+
return <span>{props.id}</span>
26+
}
27+
28+
// after codemod runs
29+
type Props2 = { id: number };
30+
export const MyComponent2 = (props: Props2) => {
31+
return <span>{props.id}</span>
32+
}
33+
34+
```
35+
36+
It also works if the Props are defined inline
37+
38+
```tsx
39+
// before codemod runs
40+
export const MyComponent4: React.FC<{ inlineProp: number, disabled?: boolean }> = (props) => <span>foo</span>
41+
42+
// after codemod runs
43+
export const MyComponent4 = (
44+
props: {
45+
inlineProp: number,
46+
disabled?: boolean
47+
}
48+
) => <span>foo</span>
49+
```
50+
51+
It works with generics too!
52+
53+
```tsx
54+
// before codemod runs
55+
type GenericsProps<T extends any> = { config: T }
56+
export const MyComponentWithGenerics: React.FC<GenericsProps<string>> = (props) => <span>{props.config}</span>
57+
export const MyComponentWithGenerics2: React.FC<GenericsProps<{ text: string }>> = ({ config: { text }}) => <span>{text}</span>
58+
59+
// after codemod runs
60+
type GenericsProps<T extends any> = { config: T }
61+
export const MyComponentWithGenerics = (props: GenericsProps<string>) => <span>{props.config}</span>
62+
export const MyComponentWithGenerics2 = (
63+
{
64+
config: { text }
65+
}: GenericsProps<{ text: string }>
66+
) => <span>{text}</span>
67+
```
68+
69+
## How to use
70+
71+
1- Install jscodeshift
72+
73+
```
74+
npm install -g jscodeshift
75+
```
76+
77+
2- TBD the command to run
78+
79+
## Notes
80+
81+
The codemod focuses in replacing the nodes but does not do styling. You might want to run Prettier or your favorite formatting tool after the code has been modified.

Diff for: babel.config.json

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"presets": [
3+
[
4+
"@babel/preset-env",
5+
{
6+
"targets": {
7+
"node": "current"
8+
},
9+
"modules": "commonjs"
10+
}
11+
],
12+
["@babel/preset-typescript"]
13+
],
14+
"plugins": []
15+
}

Diff for: commitlint.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = { extends: ['@commitlint/config-conventional'] };

Diff for: dist/index.js

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
module.exports =
2+
/******/ (() => { // webpackBootstrap
3+
/******/ "use strict";
4+
/******/ var __webpack_modules__ = ({
5+
6+
/***/ 580:
7+
/***/ ((__unused_webpack_module, exports) => {
8+
9+
10+
Object.defineProperty(exports, "__esModule", ({ value: true }));
11+
exports.parser = void 0;
12+
const isIdentifier = (x) => x.type === 'Identifier';
13+
const isTsTypeReference = (x) => x.type === 'TSTypeReference';
14+
const isObjectPattern = (x) => x.type === 'ObjectPattern';
15+
exports.default = (fileInfo, { j }) => {
16+
function addPropsTypeToComponentBody(n) {
17+
// extract the Prop's type text
18+
const typeParameterParam = n.node.id.typeAnnotation.typeAnnotation
19+
.typeParameters.params[0];
20+
let newTypeAnnotation;
21+
// form of React.FC<Props> or React.SFC<Props>
22+
if (isTsTypeReference(typeParameterParam)) {
23+
const { loc, ...rest } = typeParameterParam;
24+
newTypeAnnotation = j.tsTypeAnnotation.from({ typeAnnotation: j.tsTypeReference.from({ ...rest }) });
25+
}
26+
else {
27+
// form of React.FC<{ foo: number }> or React.SFC<{ foo: number }>
28+
const inlineTypeDeclaration = typeParameterParam;
29+
// remove locations to avoid messing up with commans
30+
const newMembers = inlineTypeDeclaration.members.map(({ loc, ...rest }) => {
31+
// dynamically call the api method to build the proper node. For example TSPropertySignature becomes tsPropertySignature
32+
const key = rest.type.slice(0, 2).toLowerCase() + rest.type.slice(2);
33+
return j[key].from({ ...rest });
34+
});
35+
newTypeAnnotation = j.tsTypeAnnotation.from({ typeAnnotation: j.tsTypeLiteral.from({ members: newMembers }) });
36+
}
37+
// build the new nodes
38+
const arrowFunctionNode = n.node.init;
39+
const firstParam = arrowFunctionNode.params[0];
40+
let arrowFunctionFirstParameter;
41+
// form of (props) =>
42+
if (isIdentifier(firstParam)) {
43+
arrowFunctionFirstParameter = j.identifier.from({
44+
...firstParam,
45+
typeAnnotation: newTypeAnnotation,
46+
});
47+
}
48+
// form of ({ foo }) =>
49+
if (isObjectPattern(firstParam)) {
50+
arrowFunctionFirstParameter = j.objectPattern.from({
51+
...firstParam,
52+
typeAnnotation: newTypeAnnotation,
53+
});
54+
}
55+
const newVariableDeclarator = j.variableDeclarator.from({
56+
...n.node,
57+
init: j.arrowFunctionExpression.from({
58+
...arrowFunctionNode,
59+
params: [arrowFunctionFirstParameter],
60+
}),
61+
});
62+
n.replace(newVariableDeclarator);
63+
return;
64+
}
65+
function removeReactFCorSFCdeclaration(n) {
66+
const { id, ...restOfNode } = n.node;
67+
const { typeAnnotation, ...restOfId } = id;
68+
const newId = j.identifier.from({ ...restOfId });
69+
const newVariableDeclarator = j.variableDeclarator.from({
70+
...restOfNode,
71+
id: newId,
72+
});
73+
n.replace(newVariableDeclarator);
74+
}
75+
try {
76+
const root = j(fileInfo.source);
77+
let hasModifications = false;
78+
const newSource = root
79+
.find(j.VariableDeclarator, (n) => {
80+
const identifier = n?.id;
81+
const typeName = identifier?.typeAnnotation?.typeAnnotation?.typeName;
82+
const genericParamsType = identifier?.typeAnnotation?.typeAnnotation?.typeParameters?.type;
83+
// verify it is the shape of React.FC<Props> React.SFC<Props>, React.FC<{ type: string }>
84+
return (typeName?.left?.name === 'React' &&
85+
['FC', 'SFC'].includes(typeName?.right?.name) &&
86+
['TSQualifiedName', 'TSTypeParameterInstantiation'].includes(genericParamsType));
87+
})
88+
.forEach((n) => {
89+
hasModifications = true;
90+
addPropsTypeToComponentBody(n);
91+
removeReactFCorSFCdeclaration(n);
92+
})
93+
.toSource();
94+
return hasModifications ? newSource : null;
95+
}
96+
catch (e) {
97+
console.log(e);
98+
}
99+
};
100+
exports.parser = 'tsx';
101+
102+
103+
/***/ })
104+
105+
/******/ });
106+
/************************************************************************/
107+
/******/ // The module cache
108+
/******/ var __webpack_module_cache__ = {};
109+
/******/
110+
/******/ // The require function
111+
/******/ function __webpack_require__(moduleId) {
112+
/******/ // Check if module is in cache
113+
/******/ if(__webpack_module_cache__[moduleId]) {
114+
/******/ return __webpack_module_cache__[moduleId].exports;
115+
/******/ }
116+
/******/ // Create a new module (and put it into the cache)
117+
/******/ var module = __webpack_module_cache__[moduleId] = {
118+
/******/ // no module.id needed
119+
/******/ // no module.loaded needed
120+
/******/ exports: {}
121+
/******/ };
122+
/******/
123+
/******/ // Execute the module function
124+
/******/ var threw = true;
125+
/******/ try {
126+
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
127+
/******/ threw = false;
128+
/******/ } finally {
129+
/******/ if(threw) delete __webpack_module_cache__[moduleId];
130+
/******/ }
131+
/******/
132+
/******/ // Return the exports of the module
133+
/******/ return module.exports;
134+
/******/ }
135+
/******/
136+
/************************************************************************/
137+
/******/ /* webpack/runtime/compat */
138+
/******/
139+
/******/ __webpack_require__.ab = __dirname + "/";/************************************************************************/
140+
/******/ // module exports must be returned from runtime so entry inlining is disabled
141+
/******/ // startup
142+
/******/ // Load entry module and return exports
143+
/******/ return __webpack_require__(580);
144+
/******/ })()
145+
;

Diff for: husky.config.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const tasks = arr => arr.join(' && ')
2+
3+
module.exports = {
4+
hooks: {
5+
'pre-commit': tasks([
6+
'lint-staged'
7+
]),
8+
'commit-msg': 'commitlint -E HUSKY_GIT_PARAMS'
9+
}
10+
}

0 commit comments

Comments
 (0)