44/* eslint-disable no-console */
55
66import * as path from 'node:path' ;
7+ import { createRequire } from 'node:module' ;
78
89import { Import , FileSystem } from '@rushstack/node-core-library' ;
910
@@ -62,41 +63,44 @@ function applyPatch(): void {
6263 useNodeJSResolver : true
6364 } ) ;
6465
65- const baseWorkerPoolPath : string = path . join ( jestWorkerFolder , 'build/base/BaseWorkerPool.js' ) ;
66- const baseWorkerPoolFilename : string = path . basename ( baseWorkerPoolPath ) ; // BaseWorkerPool.js
66+ // jest-worker 30.x switched to a webpack-bundled single-file architecture.
67+ // For 29.x and earlier, patch build/base/BaseWorkerPool.js directly.
68+ // For 30.x and later, patch build/index.js (the webpack bundle).
69+ const jestWorkerPackageJson : { version : string } = require ( path . join ( jestWorkerFolder , 'package.json' ) ) ;
70+ const jestWorkerMajorVersion : number = parseInt ( jestWorkerPackageJson . version . split ( '.' ) [ 0 ] , 10 ) ;
71+ const isBundled : boolean = jestWorkerMajorVersion >= 30 ;
6772
68- if ( ! FileSystem . exists ( baseWorkerPoolPath ) ) {
73+ const targetPath : string = isBundled
74+ ? path . join ( jestWorkerFolder , 'build/index.js' )
75+ : path . join ( jestWorkerFolder , 'build/base/BaseWorkerPool.js' ) ;
76+
77+ if ( ! FileSystem . exists ( targetPath ) ) {
6978 throw new Error (
70- ' The BaseWorkerPool.js file was not found in the expected location:\n' + baseWorkerPoolPath
79+ ` The ${ path . basename ( targetPath ) } file was not found in the expected location:\n` + targetPath
7180 ) ;
7281 }
7382
7483 // Load the module
75- const baseWorkerPoolModule : IBaseWorkerPoolModule = require ( baseWorkerPoolPath ) ;
84+ const targetModule : IBaseWorkerPoolModule = require ( targetPath ) ;
7685
7786 // Obtain the metadata for the module
78- let baseWorkerPoolModuleMetadata : NodeModule | undefined = undefined ;
87+ let targetModuleMetadata : NodeModule | undefined = undefined ;
7988 for ( const childModule of module . children ) {
80- if ( path . basename ( childModule . filename || '' ) . toUpperCase ( ) === baseWorkerPoolFilename . toUpperCase ( ) ) {
81- if ( baseWorkerPoolModuleMetadata ) {
89+ // Match by full path to avoid false positives (e.g. many modules named index.js)
90+ if ( childModule . filename === targetPath ) {
91+ if ( targetModuleMetadata ) {
8292 throw new Error ( 'More than one child module matched while detecting Node.js module metadata' ) ;
8393 }
84- baseWorkerPoolModuleMetadata = childModule ;
94+ targetModuleMetadata = childModule ;
8595 }
8696 }
8797
88- if ( ! baseWorkerPoolModuleMetadata ) {
89- throw new Error ( ' Failed to detect the Node.js module metadata for BaseWorkerPool.js' ) ;
98+ if ( ! targetModuleMetadata ) {
99+ throw new Error ( ` Failed to detect the Node.js module metadata for ${ path . basename ( targetPath ) } ` ) ;
90100 }
91101
92102 // Load the original file contents
93- const originalFileContent : string = FileSystem . readFile ( baseWorkerPoolPath ) ;
94-
95- // Add boilerplate so that eval() will return the exports
96- let patchedCode : string =
97- '// PATCHED BY HEFT USING eval()\n\nexports = {}\n' +
98- originalFileContent +
99- '\n// return value:\nexports' ;
103+ const originalFileContent : string = FileSystem . readFile ( targetPath ) ;
100104
101105 // Apply the patch. We will replace this:
102106 //
@@ -106,7 +110,7 @@ function applyPatch(): void {
106110 //
107111 // const FORCE_EXIT_DELAY = 7000;
108112 let matched : boolean = false ;
109- patchedCode = patchedCode . replace (
113+ const patchedCode : string = originalFileContent . replace (
110114 / ( c o n s t \s + F O R C E _ E X I T _ D E L A Y \s * = \s * ) ( \d + ) ( \s * \; ) / ,
111115 ( matchedString : string , leftPart : string , middlePart : string , rightPart : string ) : string => {
112116 matched = true ;
@@ -115,24 +119,47 @@ function applyPatch(): void {
115119 ) ;
116120
117121 if ( ! matched ) {
118- throw new Error ( 'The expected pattern was not found in the file:\n' + baseWorkerPoolPath ) ;
122+ throw new Error ( 'The expected pattern was not found in the file:\n' + targetPath ) ;
119123 }
120124
121- function evalInContext ( ) : IBaseWorkerPoolModule {
122- // Remap the require() function for the eval() context
125+ if ( isBundled ) {
126+ // jest-worker 30.x: webpack bundle uses `module.exports = __webpack_exports__` at the top level.
127+ // Shadow `module` with a shim so that assignment writes to our object, then copy the
128+ // resulting exports over the already-cached module exports in-place.
129+ function evalInContextBundled ( ) : Record < string , unknown > {
130+ // createRequire(targetPath) produces a proper require function with resolve/cache/etc.
131+ // and the right module-resolution context (resolves relative to jest-worker's build dir).
132+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
133+ const require : NodeRequire = createRequire ( targetPath ) ;
134+ // Shadow `module` so the bundle's `module.exports = ...` writes to our shim
135+ const module : { exports : Record < string , unknown > } = { exports : { } } ;
136+ // eslint-disable-next-line no-eval
137+ eval ( patchedCode ) ;
138+ return module . exports ;
139+ }
123140
124- // eslint-disable-next-line @typescript-eslint/no-unused-vars
125- function require ( modulePath : string ) : void {
126- return baseWorkerPoolModuleMetadata ! . require ( modulePath ) ;
141+ const patchedExports : Record < string , unknown > = evalInContextBundled ( ) ;
142+ // Can't mutate the cached exports in-place (webpack defines them as read-only getters).
143+ // Replace the exports object in the require cache entirely so future require() calls
144+ // return the patched version.
145+ require . cache [ targetModuleMetadata . filename ] ! . exports = patchedExports ;
146+ } else {
147+ // jest-worker < 30: BaseWorkerPool.js uses bare `exports`, wrap for eval return value
148+ const wrappedCode : string =
149+ '// PATCHED BY HEFT USING eval()\n\nexports = {}\n' + patchedCode + '\n// return value:\nexports' ;
150+
151+ function evalInContext ( ) : IBaseWorkerPoolModule {
152+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
153+ function require ( modulePath : string ) : void {
154+ return targetModuleMetadata ! . require ( modulePath ) ;
155+ }
156+ // eslint-disable-next-line no-eval
157+ return eval ( wrappedCode ) ;
127158 }
128159
129- // eslint-disable-next-line no-eval
130- return eval ( patchedCode ) ;
160+ const patchedModule : IBaseWorkerPoolModule = evalInContext ( ) ;
161+ targetModule . default = patchedModule . default ;
131162 }
132-
133- const patchedModule : IBaseWorkerPoolModule = evalInContext ( ) ;
134-
135- baseWorkerPoolModule . default = patchedModule . default ;
136163 } catch ( e ) {
137164 console . error ( ) ;
138165 console . error ( `ERROR: ${ patchName } failed to patch the "jest-worker" package:` ) ;
0 commit comments