Skip to content

Commit

Permalink
feat: Add codemod to transform string refs to arrow-functions
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Apr 25, 2024
1 parent 243edf6 commit 63feb87
Show file tree
Hide file tree
Showing 12 changed files with 299 additions and 0 deletions.
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,76 @@ guide](https://github.com/airbnb/javascript/blob/7684892951ef663e1c4e62ad57d662e
npx react-codemod sort-comp <path>
```

#### `string-refs`

WARNING: Only apply this codemod if you've fixed all warnings like this:

```
Warning: Component "div" contains the string ref "inner". Support for string refs will be removed in a future major release. We recommend using useRef() or createRef() instead.
```

This codemod will convert deprecated string refs to callback refs.

Input:

```jsx
import * as React from "react";

class ParentComponent extends React.Component {
render() {
return <div ref="refComponent" />;
}
}
```

Output:

```jsx
import * as React from "react";

class ParentComponent extends React.Component {
render() {
return (
<div
ref={(current) => {
this.refs["refComponent"] = current;
}}
/>
);
}
}
```

Note that this only works for string literals.
Referring to the ref with a variable will not trigger the transform:
Input:

```jsx
import * as React from "react";

const refName = "refComponent";

class ParentComponent extends React.Component {
render() {
return <div ref={refName} />;
}
}
```

Output (nothing changed):

```jsx
import * as React from "react";

const refName = "refComponent";

class ParentComponent extends React.Component {
render() {
return <div ref={refName} />;
}
}
```

#### `update-react-imports`

[As of Babel 7.9.0](https://babeljs.io/blog/2020/03/16/7.9.0#a-new-jsx-transform-11154-https-githubcom-babel-babel-pull-11154), when using `runtime: automatic` in `@babel/preset-react` or `@babel/plugin-transform-react-jsx`, you will not need to explicitly import React for compiling jsx. This codemod removes the redundant import statements. It also converts default imports (`import React from 'react'`) to named imports (e.g. `import { useState } from 'react'`).
Expand Down
5 changes: 5 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,11 @@ const TRANSFORMER_INQUIRER_CHOICES = [
'Reorders React component methods to match the ESLint react/sort-comp rule.',
value: 'sort-comp'
},
{
name:
'string-refs: Converts deprecated string refs to callback refs.',
value: 'string-refs'
},
{
name: 'update-react-imports: Removes redundant import statements from explicitly importing React to compile JSX and converts default imports to destructured named imports',
value: 'update-react-imports',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from "react";

class ParentComponent extends React.Component {
render() {
return (
<div ref="P" id="P">
<div ref="P_P1" id="P_P1">
<span ref="P_P1_C1" id="P_P1_C1" />
<span ref="P_P1_C2" id="P_P1_C2" />
</div>
<div ref="P_OneOff" id="P_OneOff" />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as React from "react";

class ParentComponent extends React.Component {
render() {
return (
<div ref={current => {
this.refs['P'] = current;
}} id="P">
<div ref={current => {
this.refs['P_P1'] = current;
}} id="P_P1">
<span ref={current => {
this.refs['P_P1_C1'] = current;
}} id="P_P1_C1" />
<span ref={current => {
this.refs['P_P1_C2'] = current;
}} id="P_P1_C2" />
</div>
<div ref={current => {
this.refs['P_OneOff'] = current;
}} id="P_OneOff" />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as React from "react";

<div ref="bad" />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as React from "react";

<div ref={current => {
this.refs['bad'] = current;
}} />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import * as React from "react";

class ParentComponent extends React.Component {
// Actual code probably has more accurate types.
// Codemod might cause TypeScript errors but these are good errors since they reveal unsound code.
refs: Record<string, any>;

render() {
return (
<div ref="P" id="P">
<div ref="P_P1" id="P_P1">
<span ref="P_P1_C1" id="P_P1_C1" />
<span ref="P_P1_C2" id="P_P1_C2" />
</div>
<div ref="P_OneOff" id="P_OneOff" />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as React from "react";

class ParentComponent extends React.Component {
// Actual code probably has more accurate types.
// Codemod might cause TypeScript errors but these are good errors since they reveal unsound code.
refs: Record<string, any>;

render() {
return (
<div ref={current => {
this.refs['P'] = current;
}} id="P">
<div ref={current => {
this.refs['P_P1'] = current;
}} id="P_P1">
<span ref={current => {
this.refs['P_P1_C1'] = current;
}} id="P_P1_C1" />
<span ref={current => {
this.refs['P_P1_C2'] = current;
}} id="P_P1_C2" />
</div>
<div ref={current => {
this.refs['P_OneOff'] = current;
}} id="P_OneOff" />
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from "react";

class ParentComponent extends React.Component {
render() {
const refName = "P";
// Giving up. Would need to implement scope tracking.
return <div ref={refName} id="P"></div>;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as React from "react";

class ParentComponent extends React.Component {
render() {
const refName = "P";
// Giving up. Would need to implement scope tracking.
return <div ref={refName} id="P"></div>;
}
}
41 changes: 41 additions & 0 deletions transforms/__tests__/string-refs-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

"use strict";

const flowTests = [
"literal-with-owner",
"literal-without-owner",
"value-with-owner",
];

const typescriptTests = ["literal-with-owner"];

const defineTest = require("jscodeshift/dist/testUtils").defineTest;

describe("string-refs", () => {
describe("flow", () => {
flowTests.forEach((test) =>
defineTest(__dirname, "string-refs", null, `string-refs/${test}`, {
parser: "flow",
})
);
});

describe("typescript", () => {
typescriptTests.forEach((test) =>
defineTest(
__dirname,
"string-refs",
null,
`string-refs/typescript/${test}`,
{ parser: "tsx" }
)
);
});
});
69 changes: 69 additions & 0 deletions transforms/string-refs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* Copyright 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

"use strict";

export default (file, api, options) => {
const j = api.jscodeshift;

const printOptions = options.printOptions || {
quote: "single",
trailingComma: true,
};

const root = j(file.source);

let hasModifications = false;

root
.find(j.JSXAttribute, (node) => {
return node.name.name === "ref";
})
.forEach((jsxAttributePath) => {
const valuePath = jsxAttributePath.get("value");
if (
// Flow parser
valuePath.value.type === "Literal" ||
// TSX parser
valuePath.value.type === "StringLiteral"
) {
hasModifications = true;
// This might shadow existing variables.
// But this should be safe since we control what identifiers we're reading in this block.
// It will trigger ESLint's `no-shadow` though.
// Babel has a helper to get a identifier that doesn't shadow existing vars.
// Maybe JSCodeShift has such a helper as well?
const currentIdentifierName = "current";
valuePath.replace(
// {(current) => { this.refs[valuePath.node.value] = current }}
j.jsxExpressionContainer(
j.arrowFunctionExpression(
[j.identifier(currentIdentifierName)],
j.blockStatement([
j.expressionStatement(
j.assignmentExpression(
"=",
j.memberExpression(
j.memberExpression(
j.thisExpression(),
j.identifier("refs")
),
j.literal(valuePath.node.value)
),
j.identifier(currentIdentifierName)
)
),
])
)
)
);
}
});

return hasModifications ? root.toSource(printOptions) : file.source;
};

0 comments on commit 63feb87

Please sign in to comment.