67 lines
2.1 KiB
JavaScript
67 lines
2.1 KiB
JavaScript
export default function ({ types: t }) {
|
|
const immutabilityVisitor = {
|
|
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 expressionResult = path.evaluate();
|
|
if (expressionResult.confident) {
|
|
// We know the result; check its mutability.
|
|
const { value } = expressionResult;
|
|
const isMutable = (value && typeof value === "object") || (typeof value === "function");
|
|
if (!isMutable) {
|
|
// It evaluated to an immutable value, so we can hoist it.
|
|
return;
|
|
}
|
|
} else if (t.isIdentifier(expressionResult.deopt)) {
|
|
// It's safe to hoist here if the deopt reason is an identifier (e.g. func param).
|
|
// The hoister will take care of how high up it can be hoisted.
|
|
return;
|
|
}
|
|
}
|
|
stop();
|
|
}
|
|
},
|
|
};
|
|
|
|
return {
|
|
visitor: {
|
|
JSXElement(path) {
|
|
if (path.node._hoisted) return;
|
|
|
|
const state = { isImmutable: true };
|
|
path.traverse(immutabilityVisitor, state);
|
|
|
|
if (state.isImmutable) {
|
|
path.hoist();
|
|
} else {
|
|
path.node._hoisted = true;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
}
|