mirror of
https://github.com/zhigang1992/babel-plugin-react-anonymous-display-name.git
synced 2026-04-29 04:15:41 +08:00
Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
lib/
|
||||
.vscode/
|
||||
9
.npmignore
Normal file
9
.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
# by default, ignore everything
|
||||
*
|
||||
*/**
|
||||
|
||||
# allow these files to be published
|
||||
!dist/**
|
||||
!LICENSE
|
||||
!README.md
|
||||
!package.json
|
||||
2
.prettierignore
Normal file
2
.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
dist/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 patrykkopycinski
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
64
README.md
Executable file
64
README.md
Executable file
@@ -0,0 +1,64 @@
|
||||
# `babel-plugin-react-anonymous-display-name`
|
||||
|
||||
## Motivation
|
||||
|
||||

|
||||
|
||||
Babel plugin that fixes displaying, in react devtools, components wrapped by `React.memo` and `forwardRef` as `Anonymous`.
|
||||
|
||||
## Install
|
||||
|
||||
Using npm:
|
||||
|
||||
```sh
|
||||
npm install --save-dev babel-plugin-react-anonymous-display-name
|
||||
```
|
||||
|
||||
or using yarn:
|
||||
|
||||
```sh
|
||||
yarn add babel-plugin-react-anonymous-display-name --dev
|
||||
```
|
||||
|
||||
## How does this work?
|
||||
|
||||
If you also prefer using arrow functions the only way to get proper component names in react devtools right now is:
|
||||
|
||||
```js
|
||||
// doesn't work :(
|
||||
export const Memo = React.memo(() => <div />);
|
||||
Memo.displayName = 'Memo';
|
||||
|
||||
// works
|
||||
const MyComponent = React.memo(function MyComponent(props) {
|
||||
return <div />;
|
||||
});
|
||||
|
||||
// works too
|
||||
const MemoComponent = () => <div />;
|
||||
export const Memo = React.memo(MemoComponent);
|
||||
```
|
||||
|
||||
But it leads to the unnecessary code and in bigger projects I can be an issue. This plugin fixes the issue by transforming anonymous arrow function into named function with name taken from the variable
|
||||
|
||||
```js
|
||||
const Memo = React.memo(() => <div />);
|
||||
```
|
||||
|
||||
into:
|
||||
|
||||
```js
|
||||
const Memo = React.memo(function Memo() {
|
||||
return <div />;
|
||||
});
|
||||
```
|
||||
|
||||
### Eslint plugin
|
||||
|
||||
As you don't have to set `displayName` manually anymore, here is Eslint plugin that will help you to find places where you defined `displayName` on `memo()` components:
|
||||
|
||||
- [eslint-plugin-no-memo-displayname](https://github.com/patrykkopycinski/eslint-plugin-no-memo-displayname)
|
||||
|
||||
### License
|
||||
|
||||
MIT
|
||||
BIN
assets/anonymous-memo.png
Normal file
BIN
assets/anonymous-memo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
20
babel.config.js
Normal file
20
babel.config.js
Normal file
@@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
shippedProposals: true,
|
||||
useBuiltIns: 'usage',
|
||||
corejs: '3',
|
||||
targets: 'maintained node versions'
|
||||
}
|
||||
],
|
||||
[
|
||||
'@babel/preset-typescript',
|
||||
{
|
||||
isTSX: true,
|
||||
allExtensions: true
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
3
jest.config.js
Normal file
3
jest.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
testMatch: ['<rootDir>/tests/**/*.(ts|tsx|js|jsx)']
|
||||
};
|
||||
40
package.json
Executable file
40
package.json
Executable file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "babel-plugin-react-anonymous-display-name",
|
||||
"version": "0.0.1",
|
||||
"description": "Automatically add displayName properties to your React project.",
|
||||
"main": "lib/index.js",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/patrykkopycinski/babel-plugin-react-display-name"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "babel src --extensions .ts --out-dir lib --delete-dir-on-start",
|
||||
"test": "jest",
|
||||
"lint": "tsc -p src && tsc -p tests && prettier --loglevel warn --write \"**/*.{ts,tsx,js,json,md}\"",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"babel-plugin",
|
||||
"react",
|
||||
"display",
|
||||
"name",
|
||||
"anonymous",
|
||||
"memo",
|
||||
"forwardRef"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.8.3",
|
||||
"@babel/core": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.3",
|
||||
"@babel/preset-typescript": "^7.8.3",
|
||||
"@types/jest": "^24.9.1",
|
||||
"@types/node": "^10.0.0",
|
||||
"jest": "^25.1.0",
|
||||
"prettier": "^1.19.1",
|
||||
"typescript": "^3.7.5"
|
||||
}
|
||||
}
|
||||
4
prettier.config.js
Normal file
4
prettier.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
arrowParens: 'always',
|
||||
singleQuote: true
|
||||
};
|
||||
44
src/index.ts
Executable file
44
src/index.ts
Executable file
@@ -0,0 +1,44 @@
|
||||
import babel, { PluginObj, types } from '@babel/core';
|
||||
|
||||
const SUPPORTED_HOCS = ['forwardRef', 'memo'];
|
||||
|
||||
const isAnonymousComponent = (
|
||||
t: typeof types,
|
||||
callee: types.Expression | types.V8IntrinsicIdentifier
|
||||
) =>
|
||||
// memo((props) => *) case
|
||||
(t.isIdentifier(callee) && SUPPORTED_HOCS.includes(callee.name)) ||
|
||||
// React.memo((props) => *) case
|
||||
(t.isMemberExpression(callee) &&
|
||||
SUPPORTED_HOCS.includes(callee.property.name));
|
||||
|
||||
export default ({ types: t }: typeof babel): PluginObj => ({
|
||||
visitor: {
|
||||
VariableDeclarator(path) {
|
||||
if (
|
||||
t.isIdentifier(path.node.id) &&
|
||||
t.isCallExpression(path.node.init) &&
|
||||
t.isArrowFunctionExpression(path.node.init.arguments[0]) &&
|
||||
isAnonymousComponent(t, path.node.init.callee)
|
||||
) {
|
||||
path.replaceWith(
|
||||
t.variableDeclarator(
|
||||
t.identifier(path.node.id.name),
|
||||
t.callExpression(path.node.init.callee, [
|
||||
t.functionExpression(
|
||||
t.identifier(path.node.id.name),
|
||||
path.node.init.arguments[0].params,
|
||||
// is memo((props) => { return *; }) case
|
||||
t.isBlockStatement(path.node.init.arguments[0].body)
|
||||
? path.node.init.arguments[0].body
|
||||
: t.blockStatement([
|
||||
t.returnStatement(path.node.init.arguments[0].body)
|
||||
])
|
||||
)
|
||||
])
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
6
src/tsconfig.json
Normal file
6
src/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
269
tests/index.ts
Executable file
269
tests/index.ts
Executable file
@@ -0,0 +1,269 @@
|
||||
import { transform } from '@babel/core';
|
||||
import plugin from '../src';
|
||||
|
||||
function run(source: string) {
|
||||
const { code } = transform(source, {
|
||||
filename: 'test.ts',
|
||||
plugins: [plugin]
|
||||
})!;
|
||||
return code;
|
||||
}
|
||||
|
||||
describe('supported HOC', () => {
|
||||
test('anonymous arrow function', () => {
|
||||
const source = `
|
||||
const Hello4 = React.memo(() => null);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4 = React.memo(function Hello4() {
|
||||
return null;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
test('anonymous arrow function memo()', () => {
|
||||
const source = `
|
||||
const Hello4 = memo(() => null);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4 = memo(function Hello4() {
|
||||
return null;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
test('anonymous function with return', () => {
|
||||
const source = `
|
||||
const Hello4 = memo(() => {
|
||||
const testVariable = true;
|
||||
|
||||
return <div />;
|
||||
});
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4 = memo(function Hello4() {
|
||||
const testVariable = true;
|
||||
return <div />;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
test('predefined Component', () => {
|
||||
const source = `
|
||||
const Hello4Component = () => null;
|
||||
const Hello4 = memo(Hello4Component);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4Component = () => null;
|
||||
|
||||
const Hello4 = memo(Hello4Component);"
|
||||
`);
|
||||
});
|
||||
|
||||
test('handle "forwardRef" usage with export', () => {
|
||||
const source = `
|
||||
export const Hello3 = forwardRef(() => null);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
Object.defineProperty(exports, \\"__esModule\\", {
|
||||
value: true
|
||||
});
|
||||
exports.Hello3 = void 0;
|
||||
const Hello3 = forwardRef(function Hello3() {
|
||||
return null;
|
||||
});
|
||||
exports.Hello3 = Hello3;"
|
||||
`);
|
||||
});
|
||||
|
||||
test('handle multiple rewrites', () => {
|
||||
const source = `
|
||||
const Hello2 = React.memo(() => null);
|
||||
const Hello4 = memo(() => null);
|
||||
const Hello6 = forwardRef<{}>(() => {
|
||||
return null
|
||||
});
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello2 = React.memo(function Hello2() {
|
||||
return null;
|
||||
});
|
||||
const Hello4 = memo(function Hello4() {
|
||||
return null;
|
||||
});
|
||||
const Hello6 = forwardRef(function Hello6() {
|
||||
return null;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('React.memo', () => {
|
||||
test('predefined areEqual function', () => {
|
||||
const source = `
|
||||
function areEqual(prevProps, nextProps) {}
|
||||
|
||||
const Hello4Component = () => null;
|
||||
const Hello4 = memo(Hello4Component, areEqual);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
function areEqual(prevProps, nextProps) {}
|
||||
|
||||
const Hello4Component = () => null;
|
||||
|
||||
const Hello4 = memo(Hello4Component, areEqual);"
|
||||
`);
|
||||
});
|
||||
|
||||
test('inline areEqual function', () => {
|
||||
const source = `
|
||||
const Hello4Component = () => null;
|
||||
|
||||
const Hello4 = memo(Hello4Component, function areEqual(prevProps, nextProps) {});
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4Component = () => null;
|
||||
|
||||
const Hello4 = memo(Hello4Component, function areEqual(prevProps, nextProps) {});"
|
||||
`);
|
||||
});
|
||||
|
||||
test('inline areEqual arrow function', () => {
|
||||
const source = `
|
||||
const Hello4Component = () => null;
|
||||
|
||||
const Hello4 = memo(Hello4Component, (prevProps, nextProps) => ({}));
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4Component = () => null;
|
||||
|
||||
const Hello4 = memo(Hello4Component, (prevProps, nextProps) => ({}));"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('React.forwardRef', () => {
|
||||
test('handle "forwardRef" usage', () => {
|
||||
const source = `
|
||||
const Hello3 = forwardRef(() => null);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello3 = forwardRef(function Hello3() {
|
||||
return null;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsupported HOC', () => {
|
||||
test('handle "withRouter" usage', () => {
|
||||
const source = `
|
||||
const Hello4 = withRouter(Hello4Component);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4 = withRouter(Hello4Component);"
|
||||
`);
|
||||
});
|
||||
|
||||
test('handle "withRouter" usage', () => {
|
||||
const source = `
|
||||
const Hello4 = withRouter(() => <RouterComponent />);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4 = withRouter(() => <RouterComponent />);"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
test('handle "memo" usage', () => {
|
||||
const source = `
|
||||
const Hello4 = memo(() => {
|
||||
const testVariable = true;
|
||||
|
||||
return <div />;
|
||||
});
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4 = memo(function Hello4() {
|
||||
const testVariable = true;
|
||||
return <div />;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
test('handle TS "memo" usage', () => {
|
||||
const source = `
|
||||
interface Props {}
|
||||
|
||||
const Hello4 = memo<Props>(() => {
|
||||
const testVariable = true;
|
||||
|
||||
return <div />;
|
||||
});
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4 = memo(function Hello4() {
|
||||
const testVariable = true;
|
||||
return <div />;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
|
||||
test('handle "memo" with params usage', () => {
|
||||
const source = `
|
||||
const Hello4 = memo(({ show, hide }) => <Loader show={show} hide={hide} />);
|
||||
`;
|
||||
|
||||
expect(run(source)).toMatchInlineSnapshot(`
|
||||
"\\"use strict\\";
|
||||
|
||||
const Hello4 = memo(function Hello4({
|
||||
show,
|
||||
hide
|
||||
}) {
|
||||
return <Loader show={show} hide={hide} />;
|
||||
});"
|
||||
`);
|
||||
});
|
||||
6
tests/tsconfig.json
Normal file
6
tests/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"]
|
||||
}
|
||||
}
|
||||
12
tsconfig.base.json
Normal file
12
tsconfig.base.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"target": "es2017",
|
||||
"types": []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user