Skip to content

Commit

Permalink
Adds support for more React imports and fixes a bunch of bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
trueadm committed Jan 21, 2019
1 parent f472a2a commit d335e0b
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 17 deletions.
1 change: 1 addition & 0 deletions packages/babel-plugin-optimize-react/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ export function MyComponent() {
}"
`;

exports[`React createElement transforms should transform React.createElement calls #4 1`] = `
"import * as React from \\"react\\";
const __reactCreateElement__ = React.createElement;
const node = __reactCreateElement__(\\"div\\", null, __reactCreateElement__(\\"span\\", null, \\"Hello world!\\"));
export function MyComponent() {
return node;
}"
`;

exports[`React createElement transforms should transform React.createElement calls 1`] = `
"import React from \\"react\\";
const __reactCreateElement__ = React.createElement;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,32 @@ const {

exports[`React hook transforms should support mixed hook imports 1`] = `
"import React from \\"react\\";
import { memo } from \\"react\\";
import \\"react\\";
const {
memo,
useState
} = React;"
`;

exports[`React hook transforms should support mixed hook imports with no default #2 1`] = `
"import React from \\"react\\";
const {
memo,
useRef,
useState
} = React;
export const Portal = memo(() => null);"
`;

exports[`React hook transforms should support mixed hook imports with no default 1`] = `
"import React from \\"react\\";
const {
useState
} = React;
import { memo } from \\"react\\";"
import \\"react\\";
const {
memo
} = React;"
`;

exports[`React hook transforms should support transform hook imports 1`] = `
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,18 @@ describe('React createElement transforms', () => {
const output = transform(test);
expect(output).toMatchSnapshot();
});

it('should transform React.createElement calls #4', () => {
const test = `
import * as React from "react";
const node = React.createElement("div", null, React.createElement("span", null, "Hello world!"));
export function MyComponent() {
return node;
}
`;
const output = transform(test);
expect(output).toMatchSnapshot();
});
});
10 changes: 10 additions & 0 deletions packages/babel-plugin-optimize-react/__tests__/hooks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,14 @@ describe('React hook transforms', () => {
const output = transform(test);
expect(output).toMatchSnapshot();
});

it('should support mixed hook imports with no default #2', () => {
const test = `
import {memo, useRef, useState} from "react";
export const Portal = memo(() => null);
`;
const output = transform(test);
expect(output).toMatchSnapshot();
});
});
78 changes: 64 additions & 14 deletions packages/babel-plugin-optimize-react/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,47 @@ const reactHooks = new Set([
'useState',
]);

const reactNamedImports = new Set([
'Children',
'cloneElement',
'Component',
'ConcurrentMode',
'createContext',
'createElement',
'createFactory',
'forwardRef',
'Fragment',
'isValidElement',
'lazy',
'memo',
'Profiler',
'PureComponent',
'StrictMode',
'Suspense',
'useCallback',
'useContext',
'useDebugValue',
'useEffect',
'useImperativeHandle',
'useLayoutEffect',
'useMemo',
'useReducer',
'useRef',
'useState',
'version',
]);

module.exports = function(babel) {
const { types: t } = babel;

// Collects named imports of React hooks from the "react" package
function collectReactHooksAndRemoveTheirNamedImports(path, state) {
// Collects named imports from the "react" package
function collectAllReactImportalsAndRemoveTheirNamedImports(path, state) {
const node = path.node;
const hooks = [];
if (t.isStringLiteral(node.source) && node.source.value === 'react') {
const specifiers = path.get('specifiers');
if (state.hasDefaultSpecifier === undefined) {
state.hasDefaultSpecifier = false;
if (state.hasDefaultOrNamespaceSpecifier === undefined) {
state.hasDefaultOrNamespaceSpecifier = false;
}

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

if (t.isIdentifier(importedNode) && t.isIdentifier(localNode)) {
if (reactHooks.has(importedNode.name)) {
if (reactNamedImports.has(importedNode.name)) {
hooks.push({
imported: importedNode.name,
local: localNode.name,
Expand All @@ -41,17 +71,33 @@ module.exports = function(babel) {
}
}
} else if (t.isImportDefaultSpecifier(specifier)) {
state.hasDefaultSpecifier = true;
const local = specifier.get('local');

if (t.isIdentifier(local) && local.node.name === 'React') {
state.hasDefaultOrNamespaceSpecifier = true;
}
} else if (t.isImportNamespaceSpecifier(specifier)) {
const local = specifier.get('local');

if (t.isIdentifier(local) && local.node.name === 'React') {
state.hasDefaultOrNamespaceSpecifier = true;
}
}
}
// If there is no default specifier for React, add one
if (state.hasDefaultSpecifier === false && specifiers.length > 0) {
if (
state.hasDefaultOrNamespaceSpecifier === false &&
specifiers.length > 0
) {
const defaultSpecifierNode = t.importDefaultSpecifier(
t.identifier('React')
);

path.pushContainer('specifiers', defaultSpecifierNode);
state.hasDefaultSpecifier = true;
// We unshift so it goes to the beginning
path.unshiftContainer('specifiers', defaultSpecifierNode);
state.hasDefaultOrNamespaceSpecifier = true;
// Make sure we register the binding, so tracking continues to work
path.scope.registerDeclaration(path);
}
}
return hooks;
Expand All @@ -65,7 +111,10 @@ module.exports = function(babel) {
if (binding !== undefined) {
const bindingPath = binding.path;

if (t.isImportDefaultSpecifier(bindingPath)) {
if (
t.isImportDefaultSpecifier(bindingPath) ||
t.isImportNamespaceSpecifier(bindingPath)
) {
const parentPath = bindingPath.parentPath;

if (
Expand Down Expand Up @@ -187,6 +236,7 @@ module.exports = function(babel) {

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

const importedHooks = collectReactHooksAndRemoveTheirNamedImports(
const reactNamedImports = collectAllReactImportalsAndRemoveTheirNamedImports(
path,
state
);
if (importedHooks.length > 0) {
if (reactNamedImports.length > 0) {
// Create a destructured variable declaration. i.e.:
// const {useEffect, useState} = React;
// const {memo, useEffect, useState} = React;
// Then insert it below the import declaration node.

const declarations = t.variableDeclarator(
t.objectPattern(
importedHooks.map(({ imported, local }) =>
reactNamedImports.map(({ imported, local }) =>
t.objectProperty(
t.identifier(imported),
t.identifier(local),
Expand Down
2 changes: 1 addition & 1 deletion packages/babel-plugin-optimize-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "babel-plugin-optimize-react",
"version": "0.0.3",
"version": "0.0.4",
"description": "Babel plugin for optimizing common React patterns",
"repository": "facebookincubator/create-react-app",
"license": "MIT",
Expand Down

0 comments on commit d335e0b

Please sign in to comment.