101 lines
3.3 KiB
TypeScript
101 lines
3.3 KiB
TypeScript
// 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<PluginState> = {
|
|
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)
|
|
}
|
|
},
|
|
})
|
|
},
|
|
},
|
|
}
|
|
}
|