/** * https://github.com/flying-sheep/babel-plugin-transform-react-createelement-to-jsx * @license GNU General Public License v3.0 */ import type * as babel from '@babel/core' interface PluginOptions { reactAlias: string } /** * Visitor factory for babel, converting React.createElement(...) to ... * * What we want to handle here is this CallExpression: * * React.createElement( * type: StringLiteral|Identifier|MemberExpression, * [props: ObjectExpression|Expression], * [...children: StringLiteral|Expression] * ) * * Any of those arguments might also be missing (undefined) and/or invalid. */ export default function ( { types: t }: typeof babel, { reactAlias = 'React' }: PluginOptions ): babel.PluginObj { /** * Get a `JSXElement` from a `CallExpression`. * Returns `null` if this impossible. */ function getJSXNode(node: any): any { if (!isReactCreateElement(node)) { return null } //nameNode and propsNode may be undefined, getJSX* need to handle that const [nameNode, propsNode, ...childNodes] = node.arguments const name = getJSXName(nameNode) if (name == null) { return null //name is required } const props = getJSXProps(propsNode) if (props == null) { return null //no props → [], invalid → null } const children = getJSXChildren(childNodes) if (children == null) { return null //no children → [], invalid → null } if ( t.isJSXMemberExpression(name) && t.isJSXIdentifier(name.object) && name.object.name === reactAlias && name.property.name === 'Fragment' ) { return t.jsxFragment( t.jsxOpeningFragment(), t.jsxClosingFragment(), children ) } // self-closing tag if no children const selfClosing = children.length === 0 const startTag = t.jsxOpeningElement(name, props, selfClosing) startTag.loc = node.loc const endTag = selfClosing ? null : t.jsxClosingElement(name) return t.jsxElement(startTag, endTag, children, selfClosing) } /** * Get a JSXIdentifier or JSXMemberExpression from a Node of known type. * Returns null if an unknown node type, null or undefined is passed. */ function getJSXName(node: any): any { if (node == null) { return null } const name = getJSXIdentifier(node, true) if (name != null) { return name } if (!t.isMemberExpression(node)) { return null } const object = getJSXName(node.object) const property = getJSXName(node.property) if (object == null || property == null) { return null } return t.jsxMemberExpression(object, property) } /** * Get an array of JSX(Spread)Attribute from a props ObjectExpression. * Handles the _extends Expression babel creates from SpreadElement nodes. * Returns null if a validation error occurs. */ function getJSXProps(node: any): any[] | null { if (node == null || isNullLikeNode(node)) { return [] } if ( t.isCallExpression(node) && t.isIdentifier(node.callee, { name: '_extends' }) ) { const props: any[] = node.arguments.map(getJSXProps) //if calling this recursively works, flatten. if (props.every((prop) => prop != null)) { return [].concat(...props) } } if (!t.isObjectExpression(node) && t.isExpression(node)) return [t.jsxSpreadAttribute(node)] if (!isPlainObjectExpression(node)) { return null } return node.properties .map((prop: any) => t.isObjectProperty(prop) ? t.jsxAttribute( getJSXIdentifier(prop.key)!, getJSXAttributeValue(prop.value) ) : t.jsxSpreadAttribute(prop.argument) ) .filter((prop: any) => t.isJSXIdentifier(prop.name) ? prop.name.name !== '__self' && prop.name.name !== '__source' : true ) } function getJSXChild(node: any) { if (t.isStringLiteral(node)) { return t.jsxText(node.value) } if (isReactCreateElement(node)) { return getJSXNode(node) } if (t.isExpression(node)) { return t.jsxExpressionContainer(node) } return null } function getJSXChildren(nodes: any[]) { const children = nodes .filter((node) => !isNullLikeNode(node)) .map(getJSXChild) if (children.some((child) => child == null)) { return null } return children } function getJSXIdentifier(node: any, tag = false) { //TODO: JSXNamespacedName if (t.isIdentifier(node) && (!tag || node.name.match(/^[A-Z]/))) { return t.jsxIdentifier(node.name) } if (t.isStringLiteral(node)) { return t.jsxIdentifier(node.value) } return null } function getJSXAttributeValue(node: any) { if (t.isStringLiteral(node)) { return node } if (t.isJSXElement(node)) { return node } if (t.isExpression(node)) { return t.jsxExpressionContainer(node) } return null } /** * Tests if a node is a CallExpression with callee `React.createElement` */ const isReactCreateElement = (node: any) => t.isCallExpression(node) && t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.object, { name: reactAlias }) && t.isIdentifier(node.callee.property, { name: 'createElement' }) && !node.callee.computed /** * Tests if a node is `null` or `undefined` */ const isNullLikeNode = (node: any) => t.isNullLiteral(node) || t.isIdentifier(node, { name: 'undefined' }) /** * Tests if a node is an object expression with noncomputed, nonmethod attrs */ const isPlainObjectExpression = (node: any) => t.isObjectExpression(node) && node.properties.every( (property) => t.isSpreadElement(property) || (t.isObjectProperty(property, { computed: false }) && getJSXIdentifier(property.key) != null && getJSXAttributeValue(property.value) != null) ) return { visitor: { CallExpression(path) { const node = getJSXNode(path.node) if (node == null) { return null } path.replaceWith(node) } } } }