// https://github.com/babel/babel/blob/c38bf12f010520ea7abe8a286f62922b2d1e1f1b/packages/babel-plugin-transform-react-constant-elements/src/index.ts import type { Visitor } from "@babel/core" import type { PluginObj, types as t } from "@babel/core" interface PluginState { isImmutable: boolean mutablePropsAllowed?: boolean } export default (allowMutablePropsOnTags?: string[]): PluginObj => { const HOISTED = new WeakSet() const immutabilityVisitor: Visitor = { enter(path, state) { const stop = () => { state.isImmutable = false path.stop() } if (path.isJSXClosingElement()) { path.skip() return } // Elements with refs are not safe to hoist. if ( path.isJSXIdentifier({ name: "ref" }) && path.parentPath.isJSXAttribute({ name: path.node }) ) { return stop() } // Ignore identifiers & JSX expressions. if (path.isJSXIdentifier() || path.isIdentifier() || path.isJSXMemberExpression()) { return } if (!path.isImmutable()) { // If it's not immutable, it may still be a pure expression, such as string concatenation. // It is still safe to hoist that, so long as its result is immutable. // If not, it is not safe to replace as mutable values (like objects) could be mutated after render. // https://github.com/facebook/react/issues/3226 if (path.isPure()) { const { confident, value } = path.evaluate() if (confident) { // We know the result; check its mutability. const isMutable = (!state.mutablePropsAllowed && value && typeof value === "object") || typeof value === "function" if (!isMutable) { // It evaluated to an immutable value, so we can hoist it. path.skip() return } } } stop() } }, } return { name: "transform-react-constant-elements", visitor: { Program(program) { program.traverse({ JSXElement(path) { if (HOISTED.has(path.node)) return HOISTED.add(path.node) const state: PluginState = { isImmutable: true } // This transform takes the option `allowMutablePropsOnTags`, which is an array // of JSX tags to allow mutable props (such as objects, functions) on. Use sparingly // and only on tags you know will never modify their own props. if (allowMutablePropsOnTags != null) { // Get the element's name. If it's a member expression, we use the last part of the path. // So the option ["FormattedMessage"] would match "Intl.FormattedMessage". let namePath = path.get("openingElement").get("name") while (namePath.isJSXMemberExpression()) { namePath = namePath.get("property") } const elementName = (namePath.node as t.JSXIdentifier).name state.mutablePropsAllowed = allowMutablePropsOnTags.includes(elementName) } // Traverse all props passed to this element for immutability. path.traverse(immutabilityVisitor, state) if (state.isImmutable) { path.hoist(path.scope) } }, }) }, }, } }