UNPKG

babel-plugin-transform-react-pure-components

Version:

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

197 lines (160 loc) 6.83 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = function (_ref) { var t = _ref.types; function isReactClass(path, pureComponents) { var superClass = path.node.superClass; var 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; } var state = { localComponentNames: [] }; var importVisitor = { ImportDeclaration: function ImportDeclaration(nestedPath) { var node = nestedPath.node; if (t.isStringLiteral(node.source, { value: 'react' })) { this.localComponentNames = node.specifiers.filter(function (specifier) { return t.isImportSpecifier(specifier) && (specifier.imported.name === 'Component' || pureComponents && specifier.imported.name === 'PureComponent'); }).map(function (specifier) { return specifier.local.name; }); } } }; // Check for imports as local variable names. path.findParent(function (p) { return t.isProgram(p); }).traverse(importVisitor, state); if (state.localComponentNames.length === 0) { return false; } return state.localComponentNames.indexOf(superClass.name) !== -1; } var bodyVisitor = { ClassMethod: function ClassMethod(path) { if (path.node.key.name === 'render') { this.renderMethod = path; } else { this.isPure = false; path.stop(); } }, ClassProperty: function ClassProperty(path) { var 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: function MemberExpression(path) { var node = path.node; // 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: function JSXIdentifier(path) { if (path.node.name === 'ref') { this.isPure = false; path.stop(); } } }; return { visitor: { Class: function Class(path, options) { // Apply only to React.Component or React.PureComponent classes. if (!isReactClass(path, options.opts.pureComponents)) { return; } var 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; } var id = t.identifier(path.node.id.name); var replacement = []; var renameProps = state.renderMethod.node.body.body.some(function (statement) { var isVariableDeclaration = statement.type === 'VariableDeclaration'; return isVariableDeclaration && statement.declarations.filter(function (declr) { return declr.id.name === 'props'; }).length; }); state.thisProps.forEach(function (thisProp) { thisProp.replaceWith(t.identifier(renameProps ? '__props' : 'props')); }); var 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) { var defaultProps = state.properties.find(function (prop) { return prop.node.key.name === 'defaultProps'; }); if (defaultProps) { state.properties = state.properties.filter(function (prop) { return 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. var temp = void 0; if (options.opts.assignDefaultProps === 'hoist') { var 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')), [].concat(_toConsumableArray(temp), [t.identifier(renameProps ? '__props' : 'props')])))); } } } replacement.push(functionalComponent); var staticProps = state.properties.map(function (prop) { return t.expressionStatement(t.assignmentExpression('=', t.memberExpression(id, prop.node.key), prop.node.value)); }); if (t.isExpression(path.node)) { // Wrap with IIFE for expressions. var iife = [functionalComponent].concat(_toConsumableArray(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 var _replacement = [functionalComponent].concat(_toConsumableArray(staticProps)); path.replaceWithMultiple(_replacement); } } } }; }; function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }