UNPKG

@ungap/babel-plugin-transform-hinted-jsx

Version:

A JSX transformer with extra hints around interpolations and outer templates

161 lines (145 loc) 4.87 kB
'use strict'; const _pluginJSX = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require("@babel/plugin-transform-react-jsx")); // _pluginJSX.default when using native ESM; // _pluginJSX when using the version compiled by ascjs. const pluginJSX = _pluginJSX.default || _pluginJSX; const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/; const JSX_FRAG_ANNOTATION_REGEX = /\*?\s*@jsxFrag\s+([^\s]+)/; const JSX_INTERPOLATION_ANNOTATION_REGEX = /\*?\s*@jsxInterpolation\s+([^\s]+)/; module.exports = ({types: t}, options) => { let pragma = '', pragmaFrag = '', pragmaPrefix = '', pragmaInterplt = ''; const injectedContainers = new WeakSet; const getCalleeName = ({object, property, name}) => { if (name) return name; const whole = [property.name]; while (object.object) { whole.push(object.property.name); object = object.object; } whole.push(object.name); return whole.reverse().join('.'); }; const interpolation = () => ( pragmaInterplt || ((pragmaPrefix || 'React') + '.interpolation')) ; const interpolation2ME = () => toMemberExpression( interpolation(), 'identifier', 'memberExpression' ); const fragment2ME = () => toMemberExpression( pragmaFrag || 'React.Fragment', 'jsxIdentifier', 'jsxMemberExpression' ); const toMemberExpression = (id, identifier, memberExpression) => ( id.split('.') .map(name => t[identifier](name)) .reduce( (object, property) => t[memberExpression](object, property) ) ); // Force the JSX plugin to use object spread instead of _extends. options.useSpread = true; return { inherits: pluginJSX, visitor: { // intercepts comments directive to name pragma and utils Program: { enter(_, state) { const {file: {ast: {comments}}} = state; if (comments) { for (const comment of comments) { if (JSX_ANNOTATION_REGEX.test(comment.value)) { pragma = RegExp.$1; [pragmaPrefix] = pragma.split('.'); } else if (JSX_FRAG_ANNOTATION_REGEX.test(comment.value)) pragmaFrag = RegExp.$1; else if (JSX_INTERPOLATION_ANNOTATION_REGEX.test(comment.value)) pragmaInterplt = RegExp.$1; } } } }, // add a unique token to outer most JSX templates JSXElement(path) { if (path.parentPath.isJSXElement()) return; const tokenId = path.scope.generateUidIdentifier("token"); path.scope.getProgramParent().push({ id: tokenId, init: t.objectExpression([]) }); const expr = t.jsxExpressionContainer(t.cloneNode(tokenId)); injectedContainers.add(expr); path.node.openingElement.attributes.unshift( t.jsxAttribute( t.jsxIdentifier("__token"), expr ) ); }, // augment interpolations with an explicit call // to its React.interpolation equivalent JSXExpressionContainer({node, parentPath}) { if ( injectedContainers.has(node) || ( parentPath.isJSXAttribute() && parentPath.parent.attributes.some( attr => t.isJSXSpreadAttribute(attr) ) ) ) return; injectedContainers.add(node); node.expression = t.callExpression( interpolation2ME(), [node.expression] ); }, // transform a fragment into a JSXExpressionContainer // where checks around its top most definition are performed JSXFragment(path) { path.replaceWith( t.jsxElement( t.jsxOpeningElement( fragment2ME(), [] ), t.jsxClosingElement( fragment2ME(), [] ), path.node.children ) ) }, // makes spread operations around attributes pollute the whole // attributes handling as dynamic interpolation SpreadElement(path) { const {parentPath} = path.parentPath; if (parentPath && parentPath.isCallExpression()) { const name = getCalleeName(parentPath.node.callee); if ( name === pragma || name === 'React.createElement' ) { const {callee} = path.parentPath.node; if (callee && getCalleeName(callee) === interpolation()) return; path.parentPath.replaceWith( t.inherits( t.callExpression( interpolation2ME(), [path.parentPath.node] ), path.parentPath ) ); } } } } }; };