Initial commit

This commit is contained in:
Patryk Kopyciński
2020-01-26 15:06:40 +01:00
commit 49bfa40d51
16 changed files with 4898 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
lib/
.vscode/

9
.npmignore Normal file
View 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
View File

@@ -0,0 +1,2 @@
node_modules/
dist/

21
LICENSE Normal file
View 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
View File

@@ -0,0 +1,64 @@
# `babel-plugin-react-anonymous-display-name`
## Motivation
![alt text](https://raw.githubusercontent.com/patrykkopycinski/eslint-plugin-no-memo-displayname/master/assets/anonymous-memo.png '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

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

20
babel.config.js Normal file
View 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
View File

@@ -0,0 +1,3 @@
module.exports = {
testMatch: ['<rootDir>/tests/**/*.(ts|tsx|js|jsx)']
};

40
package.json Executable file
View 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
View File

@@ -0,0 +1,4 @@
module.exports = {
arrowParens: 'always',
singleQuote: true
};

44
src/index.ts Executable file
View 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
View File

@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"types": ["node"]
}
}

269
tests/index.ts Executable file
View 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
View File

@@ -0,0 +1,6 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"types": ["jest", "node"]
}
}

12
tsconfig.base.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"target": "es2017",
"types": []
}
}

4395
yarn.lock Normal file

File diff suppressed because it is too large Load Diff