@@ -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) {
636694function 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 / ^ m o c k / 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+
11321316function 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
11961388function 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