UNPKG

babel-plugin-transform-react-pure-components

Version:

Optimize React code by transforming pure components into stateless functional components.

248 lines (212 loc) 7.07 kB
export default function ({ types: t }) { function isReactClass (path, pureComponents) { const superClass = path.node.superClass; const isDirectReactClass = ( t.isMemberExpression(superClass) && t.isIdentifier(superClass.object, { name: 'React' }) && ( t.isIdentifier(superClass.property, { name: 'Component' }) || ( pureComponents && t.isIdentifier(superClass.property, { name: 'PureComponent' }) ) ) ); if (isDirectReactClass) { return true; } const state = { localComponentNames: [] }; const importVisitor = { ImportDeclaration (nestedPath) { const node = nestedPath.node; if (t.isStringLiteral(node.source, { value: 'react' })) { this.localComponentNames = node.specifiers .filter(specifier => ( t.isImportSpecifier(specifier) && ( specifier.imported.name === 'Component' || ( pureComponents && specifier.imported.name === 'PureComponent' ) ) )) .map(specifier => specifier.local.name); } } }; // Check for imports as local variable names. path.findParent(p => t.isProgram(p)).traverse(importVisitor, state); if (state.localComponentNames.length === 0) { return false; } return state.localComponentNames.indexOf(superClass.name) !== -1; } const bodyVisitor = { ClassMethod (path) { if (path.node.key.name === 'render') { this.renderMethod = path; } else { this.isPure = false; path.stop(); } }, ClassProperty (path) { const name = path.node.key.name; if (path.node.static && ( name === 'propTypes' || name === 'defaultProps' )) { this.properties.push(path); } else if (!path.node.static && ( name === 'props' && path.node.typeAnnotation )) { // Skip. } else { this.isPure = false; } }, MemberExpression (path) { const { node } = path; // Non-this member expressions dont matter. if (!t.isThisExpression(node.object)) { return; } // Don't allow this.<anything other than props>. if (!t.isIdentifier(node.property, { name: 'props' })) { this.isPure = false; path.stop(); return; } // Rewrite this.props.foo => props.foo. this.thisProps.push(path); }, JSXIdentifier (path) { if (path.node.name === 'ref') { this.isPure = false; path.stop(); } } }; return { visitor: { Class (path, options) { // Apply only to React.Component or React.PureComponent classes. if (!isReactClass(path, options.opts.pureComponents)) { return; } const state = { renderMethod: null, properties: [], thisProps: [], isPure: true }; // Get the render method and make sure it doesn't have any other // methods. path.traverse(bodyVisitor, state); if (!state.isPure || !state.renderMethod) { // Not a class that can be converted to a functional component. return; } const id = t.identifier(path.node.id.name); const replacement = []; const renameProps = state.renderMethod.node.body.body.some((statement) => { const isVariableDeclaration = statement.type === 'VariableDeclaration'; return isVariableDeclaration && statement.declarations.filter(declr => declr.id.name === 'props').length; }); state.thisProps.forEach((thisProp) => { thisProp.replaceWith(t.identifier(renameProps ? '__props' : 'props')); }); const functionalComponent = t.functionDeclaration( id, [t.identifier(renameProps ? '__props' : 'props')], state.renderMethod.node.body ); // Replace defaultProps with an Object.assign on entry. if (options.opts.assignDefaultProps) { const defaultProps = state.properties.find( prop => prop.node.key.name === 'defaultProps' ); if (defaultProps) { state.properties = state.properties.filter( prop => prop.node.key.name !== 'defaultProps' ); if (defaultProps.node.value.properties.length) { // One option is to hoist the default props to the parent scope, // so they are not allocated each time you instantiate the method. let temp; if (options.opts.assignDefaultProps === 'hoist') { const tempId = path.scope.generateUidIdentifier('defaultProps'); path.scope.parent.push({ id: tempId, kind: 'const', init: defaultProps.node.value }); temp = [ t.objectExpression([]), tempId ]; } else { temp = [ defaultProps.node.value ]; } functionalComponent.body.body.unshift( t.assignmentExpression( '=', t.identifier(renameProps ? '__props' : 'props'), t.callExpression( t.memberExpression( t.identifier('Object'), t.identifier('assign') ), [ ...temp, t.identifier(renameProps ? '__props' : 'props') ] ) ) ); } } } replacement.push(functionalComponent); const staticProps = state.properties.map(prop => t.expressionStatement( t.assignmentExpression( '=', t.memberExpression(id, prop.node.key), prop.node.value ) )); if (t.isExpression(path.node)) { // Wrap with IIFE for expressions. const iife = [ functionalComponent, ...staticProps, t.returnStatement(id) ]; path.replaceWith( t.callExpression( t.functionExpression( null, [], t.blockStatement(iife) ), [] ) ); } else if (t.isExportDeclaration(path.parent)) { // Fix "We don't know what to do with this node type" errors // for ES6 default/named exports. path.replaceWith(functionalComponent); path.parentPath.insertAfter(staticProps); } else { // Everything else const replacement = [functionalComponent, ...staticProps]; path.replaceWithMultiple(replacement); } } } }; }