Skip to content

Commit e9e0534

Browse files
committed
[Jest] Fix SWC compatibility edge cases
1 parent e709622 commit e9e0534

2 files changed

Lines changed: 323 additions & 25 deletions

File tree

src/platform/packages/shared/kbn-test/src/jest/transforms/swc/index.js

Lines changed: 220 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -409,13 +409,15 @@ function inlineComputedEnumValues(parsed, source) {
409409
const object = new Map();
410410
for (const property of unwrapped.properties) {
411411
if (property.type !== 'KeyValueProperty') {
412-
continue;
412+
return;
413413
}
414414

415415
const propertyName = getPropertyName(property.key);
416-
if (propertyName !== undefined) {
417-
object.set(propertyName, resolveConstant(property.value, seen));
416+
if (propertyName === undefined) {
417+
return;
418418
}
419+
420+
object.set(propertyName, resolveConstant(property.value, seen));
419421
}
420422
return object;
421423
}
@@ -507,6 +509,29 @@ function createPurityChecker(parsed) {
507509
return kind === 'const' || kind === 'import' || !reassigned.has(key);
508510
};
509511

512+
const isPureClassMember = (member, isPureExpression) => {
513+
const keyIsPure = member.key?.type !== 'Computed' || isPureExpression(member.key.expression);
514+
const decorators = member.decorators ?? member.function?.decorators ?? [];
515+
516+
if (!keyIsPure || decorators.length > 0) {
517+
return false;
518+
}
519+
520+
switch (member.type) {
521+
case 'Constructor':
522+
case 'ClassMethod':
523+
case 'PrivateMethod':
524+
case 'TsIndexSignature':
525+
case 'EmptyStatement':
526+
return true;
527+
case 'ClassProperty':
528+
case 'PrivateProperty':
529+
return !member.isStatic || !member.value || isPureExpression(member.value);
530+
default:
531+
return false;
532+
}
533+
};
534+
510535
const isPure = (node) => {
511536
const expression = unwrapExpression(node);
512537

@@ -557,7 +582,8 @@ function createPurityChecker(parsed) {
557582
case 'ClassExpression':
558583
return (
559584
(expression.decorators?.length ?? 0) === 0 &&
560-
(!expression.superClass || isPure(expression.superClass))
585+
(!expression.superClass || isPure(expression.superClass)) &&
586+
expression.body.every((member) => isPureClassMember(member, isPure))
561587
);
562588
default:
563589
return false;
@@ -625,6 +651,38 @@ function forEachStatementList(ast, callback) {
625651
});
626652
}
627653

654+
function moveVariableDeclarators(parsed, source, declaration, declarators, insertion) {
655+
const orderedDeclarators = [...declarators].sort(
656+
(left, right) => left.span.start - right.span.start
657+
);
658+
const selected = new Set(orderedDeclarators);
659+
660+
if (selected.size === declaration.declarations.length) {
661+
const end = parsed.end(declaration);
662+
source.move(parsed.start(declaration), end, insertion);
663+
source.appendLeft(end, '\n');
664+
return;
665+
}
666+
667+
const remaining = declaration.declarations.filter((declarator) => !selected.has(declarator));
668+
const lastRemaining = remaining.at(-1);
669+
670+
for (const declarator of orderedDeclarators) {
671+
const start = parsed.start(declarator);
672+
const end = parsed.end(declarator);
673+
source.prependRight(start, `${declaration.kind} `);
674+
source.appendLeft(end, ';\n');
675+
source.move(start, end, insertion);
676+
}
677+
678+
for (let index = 0; index < declaration.declarations.length - 1; index++) {
679+
const declarator = declaration.declarations[index];
680+
if (selected.has(declarator) || declarator === lastRemaining) {
681+
source.remove(parsed.end(declarator), parsed.start(declaration.declarations[index + 1]));
682+
}
683+
}
684+
}
685+
628686
/**
629687
* Babel's Jest hoist inlined nothing, but only hoisted mocks with literal module names. SWC hoists
630688
* every jest.mock() call, so resolve identifier module names to the literal visible at the call
@@ -636,7 +694,7 @@ function forEachStatementList(ast, callback) {
636694
function rewriteJestMocks(parsed, source) {
637695
const { isPure, isConstantBinding } = createPurityChecker(parsed);
638696
const hoistedModuleNames = new Set();
639-
const movedStatements = new Set();
697+
const movedDeclarators = new Set();
640698
const moduleNameDeclarations = new Map();
641699

642700
visitAst(parsed.ast, (node) => {
@@ -708,31 +766,45 @@ function rewriteJestMocks(parsed, source) {
708766
!binding ||
709767
/^mock/i.test(reference.value) ||
710768
!binding.declarator.init ||
711-
binding.declaration.declarations.length !== 1 ||
712-
(block !== parsed.ast && binding.statement === firstStatement) ||
713-
movedStatements.has(binding.statement) ||
769+
(block !== parsed.ast &&
770+
binding.statement === firstStatement &&
771+
binding.declaration.declarations.length === 1) ||
772+
movedDeclarators.has(binding.declarator) ||
714773
!isConstantBinding(binding.declarator.id) ||
715774
!isPure(binding.declarator.init)
716775
) {
717776
continue;
718777
}
719778

720-
movedStatements.add(binding.statement);
779+
movedDeclarators.add(binding.declarator);
721780

722781
if (block === parsed.ast) {
723782
hoistedModuleNames.add(reference.value);
724783
} else {
725-
blockMoves.push(binding.statement);
784+
blockMoves.push(binding);
726785
}
727786
}
728787
}
729788
}
730789

731790
// Keep source order so a hoisted constant can still reference an earlier hoisted one.
732-
for (const statement of blockMoves.sort((left, right) => left.span.start - right.span.start)) {
733-
const end = parsed.end(statement);
734-
source.move(parsed.start(statement), end, parsed.start(firstStatement));
735-
source.appendLeft(end, ';\n');
791+
const movesByDeclaration = new Map();
792+
for (const binding of blockMoves) {
793+
const declarators = movesByDeclaration.get(binding.declaration) ?? [];
794+
declarators.push(binding.declarator);
795+
movesByDeclaration.set(binding.declaration, declarators);
796+
}
797+
798+
for (const [declaration, declarators] of [...movesByDeclaration].sort(
799+
([left], [right]) => left.span.start - right.span.start
800+
)) {
801+
moveVariableDeclarators(
802+
parsed,
803+
source,
804+
declaration,
805+
declarators,
806+
parsed.start(firstStatement)
807+
);
736808
}
737809
});
738810

@@ -931,18 +1003,16 @@ function hoistModuleDeclarations(code, hoistedModuleNames) {
9311003
const insertion = parsed.start(insertionTarget);
9321004

9331005
for (const statement of body.slice(insertionIndex + 1)) {
934-
if (
935-
statement.type !== 'VariableDeclaration' ||
936-
!statement.declarations.some(
937-
({ id }) => id.type === 'Identifier' && hoistedModuleNames.has(id.value)
938-
)
939-
) {
1006+
if (statement.type !== 'VariableDeclaration') {
9401007
continue;
9411008
}
9421009

943-
const end = parsed.end(statement);
944-
source.move(parsed.start(statement), end, insertion);
945-
source.appendLeft(end, '\n');
1010+
const declarators = statement.declarations.filter(
1011+
({ id }) => id.type === 'Identifier' && hoistedModuleNames.has(id.value)
1012+
);
1013+
if (declarators.length > 0) {
1014+
moveVariableDeclarators(parsed, source, statement, declarators, insertion);
1015+
}
9461016
}
9471017

9481018
if (!source.hasChanged()) {
@@ -1129,6 +1199,120 @@ function getExportName(rawName) {
11291199
return rawName.startsWith('"') ? JSON.parse(rawName) : rawName;
11301200
}
11311201

1202+
function escapeRegExp(value) {
1203+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1204+
}
1205+
1206+
function republishMutableLocalExports(code, mutableLocalExports) {
1207+
if (mutableLocalExports.length === 0) {
1208+
return { code };
1209+
}
1210+
1211+
const parsed = new ParsedSource(code, GENERATED_PATH);
1212+
const source = new MagicString(code);
1213+
const exportsByBinding = new Map();
1214+
const identifiers = new Set();
1215+
1216+
visitAst(parsed.ast, (node) => {
1217+
if (node.type === 'Identifier') {
1218+
identifiers.add(node.value);
1219+
}
1220+
});
1221+
1222+
for (const statement of parsed.ast.body) {
1223+
if (statement.type !== 'VariableDeclaration') {
1224+
continue;
1225+
}
1226+
1227+
for (const { id } of statement.declarations) {
1228+
if (id.type !== 'Identifier') {
1229+
continue;
1230+
}
1231+
1232+
const matchingExports = mutableLocalExports.filter(({ binding }) => binding === id.value);
1233+
if (matchingExports.length > 0) {
1234+
exportsByBinding.set(getIdentifierKey(id), matchingExports);
1235+
}
1236+
}
1237+
}
1238+
1239+
let resultName = '__kbnExportUpdateResult';
1240+
while (identifiers.has(resultName)) {
1241+
resultName += '_';
1242+
}
1243+
1244+
const getAffectedExports = (assignedIdentifiers) => {
1245+
const affectedExports = new Map();
1246+
1247+
for (const identifier of assignedIdentifiers) {
1248+
for (const exported of exportsByBinding.get(getIdentifierKey(identifier)) ?? []) {
1249+
affectedExports.set(JSON.stringify([exported.name, exported.binding]), exported);
1250+
}
1251+
}
1252+
1253+
return [...affectedExports.values()];
1254+
};
1255+
const getUpdates = (affectedExports) =>
1256+
affectedExports
1257+
.map(({ name, binding }) => {
1258+
const rawName = JSON.stringify(name);
1259+
return (
1260+
`Object.getOwnPropertyDescriptor(exports, ${rawName})?.get === undefined && ` +
1261+
`(exports[${rawName}] = ${binding})`
1262+
);
1263+
})
1264+
.join(', ');
1265+
1266+
visitAst(parsed.ast, (node) => {
1267+
let assignedIdentifiers = [];
1268+
if (node.type === 'AssignmentExpression') {
1269+
assignedIdentifiers = getBindingIdentifiers(node.left);
1270+
} else if (node.type === 'UpdateExpression') {
1271+
assignedIdentifiers = getBindingIdentifiers(node.argument);
1272+
}
1273+
1274+
const affectedExports = getAffectedExports(assignedIdentifiers);
1275+
if (affectedExports.length === 0) {
1276+
return;
1277+
}
1278+
1279+
const updates = getUpdates(affectedExports);
1280+
source.prependRight(parsed.start(node), `((${resultName}) => (${updates}, ${resultName}))(`);
1281+
source.appendLeft(parsed.end(node), ')');
1282+
});
1283+
1284+
visitAst(parsed.ast, (node) => {
1285+
if (
1286+
(node.type !== 'ForInStatement' && node.type !== 'ForOfStatement') ||
1287+
node.left.type === 'VariableDeclaration'
1288+
) {
1289+
return;
1290+
}
1291+
1292+
const affectedExports = getAffectedExports(getBindingIdentifiers(node.left));
1293+
if (affectedExports.length === 0) {
1294+
return;
1295+
}
1296+
1297+
const updates = `${getUpdates(affectedExports)};`;
1298+
if (node.body.type === 'BlockStatement') {
1299+
source.appendLeft(parsed.start(node.body) + 1, updates);
1300+
} else {
1301+
source.prependRight(parsed.start(node.body), `{ ${updates} `);
1302+
source.appendLeft(parsed.end(node.body), ' }');
1303+
}
1304+
});
1305+
1306+
if (!source.hasChanged()) {
1307+
return { code };
1308+
}
1309+
1310+
return {
1311+
code: source.toString(),
1312+
map: JSON.parse(source.generateMap({ hires: true, source: GENERATED_PATH }).toString()),
1313+
};
1314+
}
1315+
11321316
function makeExportsReplaceable(code) {
11331317
if (!code.includes('exports')) {
11341318
return { code, localExportNames: [] };
@@ -1185,12 +1369,20 @@ function makeExportsReplaceable(code) {
11851369
// module has initialized so Sinon can wrap them, keeping `let`/`var` exports as live getters.
11861370
// SWC prints class declarations as `let Name = class Name`, which are still immutable.
11871371
const isMutableBinding = (binding) =>
1188-
new RegExp(String.raw`^(?:let|var) ${binding}\b(?! = class\b)`, 'm').test(rewritten);
1372+
new RegExp(String.raw`^(?:let|var) ${escapeRegExp(binding)}(?![\w$])(?! = class\b)`, 'm').test(
1373+
rewritten
1374+
);
1375+
const mutableLocalExports = localExports.filter(({ binding }) => isMutableBinding(binding));
11891376
const localExportNames = localExports
11901377
.filter(({ name, binding }) => name !== 'default' && !isMutableBinding(binding))
11911378
.map(({ name }) => name);
1379+
const republished = republishMutableLocalExports(rewritten, mutableLocalExports);
11921380

1193-
return { code: rewritten, localExportNames };
1381+
return {
1382+
code: republished.code,
1383+
map: republished.map,
1384+
localExportNames,
1385+
};
11941386
}
11951387

11961388
function materializeLocalExports(code, localExportNames) {
@@ -1276,6 +1468,9 @@ function finalizeResult(result, prepared, transformOptions) {
12761468
if (!transformOptions?.supportsStaticESM) {
12771469
const replaceable = makeExportsReplaceable(code);
12781470
code = materializeLocalExports(replaceable.code, replaceable.localExportNames);
1471+
if (replaceable.map) {
1472+
maps.unshift(replaceable.map);
1473+
}
12791474

12801475
if (prepared.soleDefaultExport) {
12811476
code = appendStatement(code, 'module.exports = exports.default;');

0 commit comments

Comments
 (0)