stylebot-harmony/scripts/plugins/babel-constant-element.ts
2023-08-03 20:09:32 -04:00

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)
}
},
})
},
},
}
}