Skip to content

Commit d335e0b

Browse files
committed
Adds support for more React imports and fixes a bunch of bugs
1 parent f472a2a commit d335e0b

File tree

7 files changed

+117
-17
lines changed

7 files changed

+117
-17
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
sandbox.js

packages/babel-plugin-optimize-react/__tests__/__snapshots__/createElement-test.js.snap

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ export function MyComponent() {
2121
}"
2222
`;
2323

24+
exports[`React createElement transforms should transform React.createElement calls #4 1`] = `
25+
"import * as React from \\"react\\";
26+
const __reactCreateElement__ = React.createElement;
27+
28+
const node = __reactCreateElement__(\\"div\\", null, __reactCreateElement__(\\"span\\", null, \\"Hello world!\\"));
29+
30+
export function MyComponent() {
31+
return node;
32+
}"
33+
`;
34+
2435
exports[`React createElement transforms should transform React.createElement calls 1`] = `
2536
"import React from \\"react\\";
2637
const __reactCreateElement__ = React.createElement;

packages/babel-plugin-optimize-react/__tests__/__snapshots__/hooks-test.js.snap

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,18 +109,32 @@ const {
109109

110110
exports[`React hook transforms should support mixed hook imports 1`] = `
111111
"import React from \\"react\\";
112-
import { memo } from \\"react\\";
112+
import \\"react\\";
113113
const {
114+
memo,
114115
useState
115116
} = React;"
116117
`;
117118

119+
exports[`React hook transforms should support mixed hook imports with no default #2 1`] = `
120+
"import React from \\"react\\";
121+
const {
122+
memo,
123+
useRef,
124+
useState
125+
} = React;
126+
export const Portal = memo(() => null);"
127+
`;
128+
118129
exports[`React hook transforms should support mixed hook imports with no default 1`] = `
119130
"import React from \\"react\\";
120131
const {
121132
useState
122133
} = React;
123-
import { memo } from \\"react\\";"
134+
import \\"react\\";
135+
const {
136+
memo
137+
} = React;"
124138
`;
125139

126140
exports[`React hook transforms should support transform hook imports 1`] = `

packages/babel-plugin-optimize-react/__tests__/createElement-test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,18 @@ describe('React createElement transforms', () => {
4747
const output = transform(test);
4848
expect(output).toMatchSnapshot();
4949
});
50+
51+
it('should transform React.createElement calls #4', () => {
52+
const test = `
53+
import * as React from "react";
54+
55+
const node = React.createElement("div", null, React.createElement("span", null, "Hello world!"));
56+
57+
export function MyComponent() {
58+
return node;
59+
}
60+
`;
61+
const output = transform(test);
62+
expect(output).toMatchSnapshot();
63+
});
5064
});

packages/babel-plugin-optimize-react/__tests__/hooks-test.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,4 +147,14 @@ describe('React hook transforms', () => {
147147
const output = transform(test);
148148
expect(output).toMatchSnapshot();
149149
});
150+
151+
it('should support mixed hook imports with no default #2', () => {
152+
const test = `
153+
import {memo, useRef, useState} from "react";
154+
155+
export const Portal = memo(() => null);
156+
`;
157+
const output = transform(test);
158+
expect(output).toMatchSnapshot();
159+
});
150160
});

packages/babel-plugin-optimize-react/index.js

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,47 @@ const reactHooks = new Set([
1313
'useState',
1414
]);
1515

16+
const reactNamedImports = new Set([
17+
'Children',
18+
'cloneElement',
19+
'Component',
20+
'ConcurrentMode',
21+
'createContext',
22+
'createElement',
23+
'createFactory',
24+
'forwardRef',
25+
'Fragment',
26+
'isValidElement',
27+
'lazy',
28+
'memo',
29+
'Profiler',
30+
'PureComponent',
31+
'StrictMode',
32+
'Suspense',
33+
'useCallback',
34+
'useContext',
35+
'useDebugValue',
36+
'useEffect',
37+
'useImperativeHandle',
38+
'useLayoutEffect',
39+
'useMemo',
40+
'useReducer',
41+
'useRef',
42+
'useState',
43+
'version',
44+
]);
45+
1646
module.exports = function(babel) {
1747
const { types: t } = babel;
1848

19-
// Collects named imports of React hooks from the "react" package
20-
function collectReactHooksAndRemoveTheirNamedImports(path, state) {
49+
// Collects named imports from the "react" package
50+
function collectAllReactImportalsAndRemoveTheirNamedImports(path, state) {
2151
const node = path.node;
2252
const hooks = [];
2353
if (t.isStringLiteral(node.source) && node.source.value === 'react') {
2454
const specifiers = path.get('specifiers');
25-
if (state.hasDefaultSpecifier === undefined) {
26-
state.hasDefaultSpecifier = false;
55+
if (state.hasDefaultOrNamespaceSpecifier === undefined) {
56+
state.hasDefaultOrNamespaceSpecifier = false;
2757
}
2858

2959
for (let specifier of specifiers) {
@@ -32,7 +62,7 @@ module.exports = function(babel) {
3262
const localNode = specifier.node.local;
3363

3464
if (t.isIdentifier(importedNode) && t.isIdentifier(localNode)) {
35-
if (reactHooks.has(importedNode.name)) {
65+
if (reactNamedImports.has(importedNode.name)) {
3666
hooks.push({
3767
imported: importedNode.name,
3868
local: localNode.name,
@@ -41,17 +71,33 @@ module.exports = function(babel) {
4171
}
4272
}
4373
} else if (t.isImportDefaultSpecifier(specifier)) {
44-
state.hasDefaultSpecifier = true;
74+
const local = specifier.get('local');
75+
76+
if (t.isIdentifier(local) && local.node.name === 'React') {
77+
state.hasDefaultOrNamespaceSpecifier = true;
78+
}
79+
} else if (t.isImportNamespaceSpecifier(specifier)) {
80+
const local = specifier.get('local');
81+
82+
if (t.isIdentifier(local) && local.node.name === 'React') {
83+
state.hasDefaultOrNamespaceSpecifier = true;
84+
}
4585
}
4686
}
4787
// If there is no default specifier for React, add one
48-
if (state.hasDefaultSpecifier === false && specifiers.length > 0) {
88+
if (
89+
state.hasDefaultOrNamespaceSpecifier === false &&
90+
specifiers.length > 0
91+
) {
4992
const defaultSpecifierNode = t.importDefaultSpecifier(
5093
t.identifier('React')
5194
);
5295

53-
path.pushContainer('specifiers', defaultSpecifierNode);
54-
state.hasDefaultSpecifier = true;
96+
// We unshift so it goes to the beginning
97+
path.unshiftContainer('specifiers', defaultSpecifierNode);
98+
state.hasDefaultOrNamespaceSpecifier = true;
99+
// Make sure we register the binding, so tracking continues to work
100+
path.scope.registerDeclaration(path);
55101
}
56102
}
57103
return hooks;
@@ -65,7 +111,10 @@ module.exports = function(babel) {
65111
if (binding !== undefined) {
66112
const bindingPath = binding.path;
67113

68-
if (t.isImportDefaultSpecifier(bindingPath)) {
114+
if (
115+
t.isImportDefaultSpecifier(bindingPath) ||
116+
t.isImportNamespaceSpecifier(bindingPath)
117+
) {
69118
const parentPath = bindingPath.parentPath;
70119

71120
if (
@@ -187,6 +236,7 @@ module.exports = function(babel) {
187236

188237
if (
189238
t.isImportDefaultSpecifier(bindingPath) ||
239+
t.isImportNamespaceSpecifier(bindingPath) ||
190240
t.isVariableDeclarator(bindingPath)
191241
) {
192242
bindingPath.parentPath.insertAfter(createElementDeclaration);
@@ -205,18 +255,18 @@ module.exports = function(babel) {
205255
// import React, {useState} from "react";
206256
// As we collection them, we also remove the imports from the declaration.
207257

208-
const importedHooks = collectReactHooksAndRemoveTheirNamedImports(
258+
const reactNamedImports = collectAllReactImportalsAndRemoveTheirNamedImports(
209259
path,
210260
state
211261
);
212-
if (importedHooks.length > 0) {
262+
if (reactNamedImports.length > 0) {
213263
// Create a destructured variable declaration. i.e.:
214-
// const {useEffect, useState} = React;
264+
// const {memo, useEffect, useState} = React;
215265
// Then insert it below the import declaration node.
216266

217267
const declarations = t.variableDeclarator(
218268
t.objectPattern(
219-
importedHooks.map(({ imported, local }) =>
269+
reactNamedImports.map(({ imported, local }) =>
220270
t.objectProperty(
221271
t.identifier(imported),
222272
t.identifier(local),

packages/babel-plugin-optimize-react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "babel-plugin-optimize-react",
3-
"version": "0.0.3",
3+
"version": "0.0.4",
44
"description": "Babel plugin for optimizing common React patterns",
55
"repository": "facebookincubator/create-react-app",
66
"license": "MIT",

0 commit comments

Comments
 (0)