@@ -2,7 +2,7 @@ import { receiveMessageOnPort } from 'node:worker_threads';
22const mockedModuleExports = new Map ( ) ;
33let currentMockVersion = 0 ;
44
5- // This loader causes a new module `node:mock` to become available as a way to
5+ // These hooks enable code running on the application thread to
66// swap module resolution results for mocking purposes. It uses this instead
77// of import.meta so that CommonJS can still use the functionality.
88//
@@ -22,7 +22,7 @@ let currentMockVersion = 0;
2222// it cannot be changed. So things like the following DO NOT WORK:
2323//
2424// ```mjs
25- // import mock from 'node: mock';
25+ // import mock from 'test-esm-loader- mock'; // See test-esm-loader-mock.mjs
2626// mock('file:///app.js', {x:1});
2727// const namespace1 = await import('file:///app.js');
2828// namespace1.x; // 1
@@ -34,17 +34,6 @@ let currentMockVersion = 0;
3434// assert(namespace1 === namespace2);
3535// ```
3636
37- /**
38- * FIXME: this is a hack to workaround loaders being
39- * single threaded for now, just ensures that the MessagePort drains
40- */
41- function doDrainPort ( ) {
42- let msg ;
43- while ( msg = receiveMessageOnPort ( preloadPort ) ) {
44- onPreloadPortMessage ( msg . message ) ;
45- }
46- }
47-
4837/**
4938 * @param param0 message from the application context
5039 */
@@ -54,127 +43,31 @@ function onPreloadPortMessage({
5443 currentMockVersion = mockVersion ;
5544 mockedModuleExports . set ( resolved , exports ) ;
5645}
57- let preloadPort ;
58- export function globalPreload ( { port} ) {
59- // Save the communication port to the application context to send messages
60- // to it later
61- preloadPort = port ;
62- // Every time the application context sends a message over the port
63- port . on ( 'message' , onPreloadPortMessage ) ;
64- // This prevents the port that the Loader/application talk over
65- // from keeping the process alive, without this, an application would be kept
66- // alive just because a loader is waiting for messages
67- port . unref ( ) ;
6846
69- const insideAppContext = ( getBuiltin , port , setImportMetaCallback ) => {
70- /**
71- * This is the Map that saves *all* the mocked URL -> replacement Module
72- * mappings
73- * @type {Map<string, {namespace, listeners}> }
74- */
75- let mockedModules = new Map ( ) ;
76- let mockVersion = 0 ;
77- /**
78- * This is the value that is placed into the `node:mock` default export
79- *
80- * @example
81- * ```mjs
82- * import mock from 'node:mock';
83- * const mutator = mock('file:///app.js', {x:1});
84- * const namespace = await import('file:///app.js');
85- * namespace.x; // 1;
86- * mutator.x = 2;
87- * namespace.x; // 2;
88- * ```
89- *
90- * @param {string } resolved an absolute URL HREF string
91- * @param {object } replacementProperties an object to pick properties from
92- * to act as a module namespace
93- * @returns {object } a mutator object that can update the module namespace
94- * since we can't do something like old Object.observe
95- */
96- const doMock = ( resolved , replacementProperties ) => {
97- let exportNames = Object . keys ( replacementProperties ) ;
98- let namespace = Object . create ( null ) ;
99- /**
100- * @type {Array<(name: string)=>void> } functions to call whenever an
101- * export name is updated
102- */
103- let listeners = [ ] ;
104- for ( const name of exportNames ) {
105- let currentValueForPropertyName = replacementProperties [ name ] ;
106- Object . defineProperty ( namespace , name , {
107- enumerable : true ,
108- get ( ) {
109- return currentValueForPropertyName ;
110- } ,
111- set ( v ) {
112- currentValueForPropertyName = v ;
113- for ( let fn of listeners ) {
114- try {
115- fn ( name ) ;
116- } catch {
117- }
118- }
119- }
120- } ) ;
121- }
122- mockedModules . set ( resolved , {
123- namespace,
124- listeners
125- } ) ;
126- mockVersion ++ ;
127- // Inform the loader that the `resolved` URL should now use the specific
128- // `mockVersion` and has export names of `exportNames`
129- //
130- // This allows the loader to generate a fake module for that version
131- // and names the next time it resolves a specifier to equal `resolved`
132- port . postMessage ( { mockVersion, resolved, exports : exportNames } ) ;
133- return namespace ;
134- }
135- // Sets the import.meta properties up
136- // has the normal chaining workflow with `defaultImportMetaInitializer`
137- setImportMetaCallback ( ( meta , context , defaultImportMetaInitializer ) => {
138- /**
139- * 'node:mock' creates its default export by plucking off of import.meta
140- * and must do so in order to get the communications channel from inside
141- * preloadCode
142- */
143- if ( context . url === 'node:mock' ) {
144- meta . doMock = doMock ;
145- return ;
146- }
147- /**
148- * Fake modules created by `node:mock` get their meta.mock utility set
149- * to the corresponding value keyed off `mockedModules` and use this
150- * to setup their exports/listeners properly
151- */
152- if ( context . url . startsWith ( 'mock-facade:' ) ) {
153- let [ proto , version , encodedTargetURL ] = context . url . split ( ':' ) ;
154- let decodedTargetURL = decodeURIComponent ( encodedTargetURL ) ;
155- if ( mockedModules . has ( decodedTargetURL ) ) {
156- meta . mock = mockedModules . get ( decodedTargetURL ) ;
157- return ;
158- }
159- }
160- /**
161- * Ensure we still get things like `import.meta.url`
162- */
163- defaultImportMetaInitializer ( meta , context ) ;
164- } ) ;
165- } ;
166- return `(${ insideAppContext } )(getBuiltin, port, setImportMetaCallback)`
47+ /** @type {URL['href'] } */
48+ let mainImportURL ;
49+ /** @type {MessagePort } */
50+ let preloadPort ;
51+ export async function initialize ( data ) {
52+ ( { mainImportURL, port : preloadPort } = data ) ;
53+
54+ data . port . on ( 'message' , onPreloadPortMessage ) ;
16755}
16856
57+ /**
58+ * Because Node.js internals use a separate MessagePort for cross-thread
59+ * communication, there could be some messages pending that we should handle
60+ * before continuing.
61+ */
62+ function doDrainPort ( ) {
63+ let msg ;
64+ while ( msg = receiveMessageOnPort ( preloadPort ) ) {
65+ onPreloadPortMessage ( msg . message ) ;
66+ }
67+ }
16968
17069// Rewrites node: loading to mock-facade: so that it can be intercepted
17170export async function resolve ( specifier , context , defaultResolve ) {
172- if ( specifier === 'node:mock' ) {
173- return {
174- shortCircuit : true ,
175- url : specifier
176- } ;
177- }
17871 doDrainPort ( ) ;
17972 const def = await defaultResolve ( specifier , context ) ;
18073 if ( context . parentURL ?. startsWith ( 'mock-facade:' ) ) {
@@ -193,55 +86,46 @@ export async function resolve(specifier, context, defaultResolve) {
19386
19487export async function load ( url , context , defaultLoad ) {
19588 doDrainPort ( ) ;
196- if ( url === 'node:mock' ) {
197- /**
198- * Simply grab the import.meta.doMock to establish the communication
199- * channel with preloadCode
200- */
201- return {
202- shortCircuit : true ,
203- source : 'export default import.meta.doMock' ,
204- format : 'module'
205- } ;
206- }
20789 /**
20890 * Mocked fake module, not going to be handled in default way so it
20991 * generates the source text, then short circuits
21092 */
21193 if ( url . startsWith ( 'mock-facade:' ) ) {
212- let [ proto , version , encodedTargetURL ] = url . split ( ':' ) ;
213- let ret = generateModule ( mockedModuleExports . get (
214- decodeURIComponent ( encodedTargetURL )
215- ) ) ;
94+ const encodedTargetURL = url . slice ( url . lastIndexOf ( ':' ) + 1 ) ;
21695 return {
21796 shortCircuit : true ,
218- source : ret ,
219- format : 'module'
97+ source : generateModule ( encodedTargetURL ) ,
98+ format : 'module' ,
22099 } ;
221100 }
222101 return defaultLoad ( url , context ) ;
223102}
224103
225104/**
226- *
227- * @param {Array< string> } exports name of the exports of the module
105+ * Generate the source code for a mocked module.
106+ * @param {string } encodedTargetURL the module being mocked
228107 * @returns {string }
229108 */
230- function generateModule ( exports ) {
109+ function generateModule ( encodedTargetURL ) {
110+ const exports = mockedModuleExports . get (
111+ decodeURIComponent ( encodedTargetURL )
112+ ) ;
231113 let body = [
114+ `import { mockedModules } from ${ JSON . stringify ( mainImportURL ) } ;` ,
232115 'export {};' ,
233- 'let mapping = {__proto__: null};'
116+ 'let mapping = {__proto__: null};' ,
117+ `const mock = mockedModules.get(${ JSON . stringify ( encodedTargetURL ) } );` ,
234118 ] ;
235119 for ( const [ i , name ] of Object . entries ( exports ) ) {
236120 let key = JSON . stringify ( name ) ;
237- body . push ( `var _${ i } = import.meta. mock.namespace[${ key } ];` ) ;
121+ body . push ( `var _${ i } = mock.namespace[${ key } ];` ) ;
238122 body . push ( `Object.defineProperty(mapping, ${ key } , { enumerable: true, set(v) {_${ i } = v;}, get() {return _${ i } ;} });` ) ;
239123 body . push ( `export {_${ i } as ${ name } };` ) ;
240124 }
241- body . push ( `import.meta. mock.listeners.push(${
125+ body . push ( `mock.listeners.push(${
242126 ( ) => {
243127 for ( var k in mapping ) {
244- mapping [ k ] = import . meta . mock . namespace [ k ] ;
128+ mapping [ k ] = mock . namespace [ k ] ;
245129 }
246130 }
247131 } );`) ;
0 commit comments