@@ -2,7 +2,7 @@ import { receiveMessageOnPort } from 'node:worker_threads';
2
2
const mockedModuleExports = new Map ( ) ;
3
3
let currentMockVersion = 0 ;
4
4
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
6
6
// swap module resolution results for mocking purposes. It uses this instead
7
7
// of import.meta so that CommonJS can still use the functionality.
8
8
//
@@ -22,7 +22,7 @@ let currentMockVersion = 0;
22
22
// it cannot be changed. So things like the following DO NOT WORK:
23
23
//
24
24
// ```mjs
25
- // import mock from 'node: mock';
25
+ // import mock from 'test-esm-loader- mock'; // See test-esm-loader-mock.mjs
26
26
// mock('file:///app.js', {x:1});
27
27
// const namespace1 = await import('file:///app.js');
28
28
// namespace1.x; // 1
@@ -34,17 +34,6 @@ let currentMockVersion = 0;
34
34
// assert(namespace1 === namespace2);
35
35
// ```
36
36
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
-
48
37
/**
49
38
* @param param0 message from the application context
50
39
*/
@@ -54,127 +43,31 @@ function onPreloadPortMessage({
54
43
currentMockVersion = mockVersion ;
55
44
mockedModuleExports . set ( resolved , exports ) ;
56
45
}
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 ( ) ;
68
46
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 ) ;
167
55
}
168
56
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
+ }
169
68
170
69
// Rewrites node: loading to mock-facade: so that it can be intercepted
171
70
export async function resolve ( specifier , context , defaultResolve ) {
172
- if ( specifier === 'node:mock' ) {
173
- return {
174
- shortCircuit : true ,
175
- url : specifier
176
- } ;
177
- }
178
71
doDrainPort ( ) ;
179
72
const def = await defaultResolve ( specifier , context ) ;
180
73
if ( context . parentURL ?. startsWith ( 'mock-facade:' ) ) {
@@ -193,55 +86,46 @@ export async function resolve(specifier, context, defaultResolve) {
193
86
194
87
export async function load ( url , context , defaultLoad ) {
195
88
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
- }
207
89
/**
208
90
* Mocked fake module, not going to be handled in default way so it
209
91
* generates the source text, then short circuits
210
92
*/
211
93
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 ) ;
216
95
return {
217
96
shortCircuit : true ,
218
- source : ret ,
219
- format : 'module'
97
+ source : generateModule ( encodedTargetURL ) ,
98
+ format : 'module' ,
220
99
} ;
221
100
}
222
101
return defaultLoad ( url , context ) ;
223
102
}
224
103
225
104
/**
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
228
107
* @returns {string }
229
108
*/
230
- function generateModule ( exports ) {
109
+ function generateModule ( encodedTargetURL ) {
110
+ const exports = mockedModuleExports . get (
111
+ decodeURIComponent ( encodedTargetURL )
112
+ ) ;
231
113
let body = [
114
+ `import { mockedModules } from ${ JSON . stringify ( mainImportURL ) } ;` ,
232
115
'export {};' ,
233
- 'let mapping = {__proto__: null};'
116
+ 'let mapping = {__proto__: null};' ,
117
+ `const mock = mockedModules.get(${ JSON . stringify ( encodedTargetURL ) } );` ,
234
118
] ;
235
119
for ( const [ i , name ] of Object . entries ( exports ) ) {
236
120
let key = JSON . stringify ( name ) ;
237
- body . push ( `var _${ i } = import.meta. mock.namespace[${ key } ];` ) ;
121
+ body . push ( `var _${ i } = mock.namespace[${ key } ];` ) ;
238
122
body . push ( `Object.defineProperty(mapping, ${ key } , { enumerable: true, set(v) {_${ i } = v;}, get() {return _${ i } ;} });` ) ;
239
123
body . push ( `export {_${ i } as ${ name } };` ) ;
240
124
}
241
- body . push ( `import.meta. mock.listeners.push(${
125
+ body . push ( `mock.listeners.push(${
242
126
( ) => {
243
127
for ( var k in mapping ) {
244
- mapping [ k ] = import . meta . mock . namespace [ k ] ;
128
+ mapping [ k ] = mock . namespace [ k ] ;
245
129
}
246
130
}
247
131
} );`) ;
0 commit comments