Optimize the transformed output of JSXSpreadAttributes containing an ObjectLiteralExpression (#49100)

This commit is contained in:
Mateusz Burzyński
2023-03-21 15:47:35 -07:00
committed by GitHub
parent 01de7885a4
commit 3f90887a6e
21 changed files with 1318 additions and 39 deletions
+56 -21
View File
@@ -35,8 +35,12 @@ import {
isJsxSelfClosingElement,
isJsxSpreadAttribute,
isLineBreak,
isObjectLiteralExpression,
isPropertyAssignment,
isSourceFile,
isSpreadAssignment,
isStringDoubleQuoted,
isStringLiteral,
isWhiteSpaceSingleLine,
JsxAttribute,
JsxAttributeValue,
@@ -55,6 +59,8 @@ import {
mapDefined,
Node,
NodeFlags,
ObjectLiteralElementLike,
ObjectLiteralExpression,
PropertyAssignment,
ScriptTarget,
setIdentifierGeneratedImportReference,
@@ -63,7 +69,6 @@ import {
singleOrUndefined,
SourceFile,
spanMap,
SpreadAssignment,
startOnNewLine,
Statement,
StringLiteral,
@@ -243,13 +248,18 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
}
}
function hasProto(obj: ObjectLiteralExpression) {
return obj.properties.some(p => isPropertyAssignment(p) &&
(isIdentifier(p.name) && idText(p.name) === "__proto__" || isStringLiteral(p.name) && p.name.text === "__proto__"));
}
/**
* The react jsx/jsxs transform falls back to `createElement` when an explicit `key` argument comes after a spread
*/
function hasKeyAfterPropsSpread(node: JsxOpeningLikeElement) {
let spread = false;
for (const elem of node.attributes.properties) {
if (isJsxSpreadAttribute(elem)) {
if (isJsxSpreadAttribute(elem) && (!isObjectLiteralExpression(elem.expression) || elem.expression.properties.some(isSpreadAssignment))) {
spread = true;
}
else if (spread && isJsxAttribute(elem) && elem.name.escapedText === "key") {
@@ -427,7 +437,10 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
return element;
}
function transformJsxSpreadAttributeToSpreadAssignment(node: JsxSpreadAttribute) {
function transformJsxSpreadAttributeToProps(node: JsxSpreadAttribute) {
if (isObjectLiteralExpression(node.expression) && !hasProto(node.expression)) {
return node.expression.properties;
}
return factory.createSpreadAssignment(Debug.checkDefined(visitNode(node.expression, visitor, isExpression)));
}
@@ -438,8 +451,8 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
}
function transformJsxAttributesToProps(attrs: readonly(JsxSpreadAttribute | JsxAttribute)[], children?: PropertyAssignment) {
const props = flatten<SpreadAssignment | PropertyAssignment>(spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) =>
map(attrs, attr => isSpread ? transformJsxSpreadAttributeToSpreadAssignment(attr as JsxSpreadAttribute) : transformJsxAttributeToObjectLiteralElement(attr as JsxAttribute))));
const props = flatten(spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) =>
flatten(map(attrs, attr => isSpread ? transformJsxSpreadAttributeToProps(attr as JsxSpreadAttribute) : transformJsxAttributeToObjectLiteralElement(attr as JsxAttribute)))));
if (children) {
props.push(children);
}
@@ -447,30 +460,52 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
}
function transformJsxAttributesToExpression(attrs: readonly(JsxSpreadAttribute | JsxAttribute)[], children?: PropertyAssignment) {
// Map spans of JsxAttribute nodes into object literals and spans
// of JsxSpreadAttribute nodes into expressions.
const expressions = flatten(
spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) => isSpread
? map(attrs as JsxSpreadAttribute[], transformJsxSpreadAttributeToExpression)
: factory.createObjectLiteralExpression(map(attrs as JsxAttribute[], transformJsxAttributeToObjectLiteralElement))
)
);
const expressions: Expression[] = [];
let properties: ObjectLiteralElementLike[] = [];
if (isJsxSpreadAttribute(attrs[0])) {
// We must always emit at least one object literal before a spread
// argument.factory.createObjectLiteral
expressions.unshift(factory.createObjectLiteralExpression());
for (const attr of attrs) {
if (isJsxSpreadAttribute(attr)) {
// as an optimization we try to flatten the first level of spread inline object
// as if its props would be passed as JSX attributes
if (isObjectLiteralExpression(attr.expression) && !hasProto(attr.expression)) {
for (const prop of attr.expression.properties) {
if (isSpreadAssignment(prop)) {
finishObjectLiteralIfNeeded();
expressions.push(prop.expression);
continue;
}
properties.push(prop);
}
continue;
}
finishObjectLiteralIfNeeded();
expressions.push(attr.expression);
continue;
}
properties.push(transformJsxAttributeToObjectLiteralElement(attr));
}
if (children) {
expressions.push(factory.createObjectLiteralExpression([children]));
properties.push(children);
}
finishObjectLiteralIfNeeded();
if (expressions.length && !isObjectLiteralExpression(expressions[0])) {
// We must always emit at least one object literal before a spread attribute
// as the JSX always factory expects a fresh object, so we need to make a copy here
// we also avoid mutating an external reference by doing this (first expression is used as assign's target)
expressions.unshift(factory.createObjectLiteralExpression());
}
return singleOrUndefined(expressions) || emitHelpers().createAssignHelper(expressions);
}
function transformJsxSpreadAttributeToExpression(node: JsxSpreadAttribute) {
return Debug.checkDefined(visitNode(node.expression, visitor, isExpression));
function finishObjectLiteralIfNeeded() {
if (properties.length) {
expressions.push(factory.createObjectLiteralExpression(properties));
properties = [];
}
}
}
function transformJsxAttributeToObjectLiteralElement(node: JsxAttribute) {