1
- import { receiveMessageOnPort } from 'node:worker_threads' ;
2
1
const mockedModuleExports = new Map ( ) ;
3
2
let currentMockVersion = 0 ;
4
3
5
- // This loader causes a new module `node:mock` to become available as a way to
4
+ // This loader enables code running on the application thread to
6
5
// swap module resolution results for mocking purposes. It uses this instead
7
6
// of import.meta so that CommonJS can still use the functionality.
8
7
//
@@ -22,7 +21,7 @@ let currentMockVersion = 0;
22
21
// it cannot be changed. So things like the following DO NOT WORK:
23
22
//
24
23
// ```mjs
25
- // import mock from 'node: mock';
24
+ // import mock from 'test-esm-loader- mock'; // See test-esm-loader-mock.mjs
26
25
// mock('file:///app.js', {x:1});
27
26
// const namespace1 = await import('file:///app.js');
28
27
// namespace1.x; // 1
@@ -34,148 +33,16 @@ let currentMockVersion = 0;
34
33
// assert(namespace1 === namespace2);
35
34
// ```
36
35
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
36
48
- /**
49
- * @param param0 message from the application context
50
- */
51
- function onPreloadPortMessage ( {
52
- mockVersion, resolved, exports
53
- } ) {
54
- currentMockVersion = mockVersion ;
55
- mockedModuleExports . set ( resolved , exports ) ;
37
+ export async function initialize ( { port } ) {
38
+ port . on ( 'message' , ( { mockVersion, resolved, exports } ) => {
39
+ currentMockVersion = mockVersion ;
40
+ mockedModuleExports . set ( resolved , exports ) ;
41
+ } ) ;
56
42
}
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
-
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)`
167
- }
168
-
169
43
170
44
// Rewrites node: loading to mock-facade: so that it can be intercepted
171
45
export async function resolve ( specifier , context , defaultResolve ) {
172
- if ( specifier === 'node:mock' ) {
173
- return {
174
- shortCircuit : true ,
175
- url : specifier
176
- } ;
177
- }
178
- doDrainPort ( ) ;
179
46
const def = await defaultResolve ( specifier , context ) ;
180
47
if ( context . parentURL ?. startsWith ( 'mock-facade:' ) ) {
181
48
// Do nothing, let it get the "real" module
@@ -192,48 +59,38 @@ export async function resolve(specifier, context, defaultResolve) {
192
59
}
193
60
194
61
export async function load ( url , context , defaultLoad ) {
195
- 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
62
/**
208
63
* Mocked fake module, not going to be handled in default way so it
209
64
* generates the source text, then short circuits
210
65
*/
211
66
if ( url . startsWith ( 'mock-facade:' ) ) {
212
- let [ proto , version , encodedTargetURL ] = url . split ( ':' ) ;
213
- let ret = generateModule ( mockedModuleExports . get (
214
- decodeURIComponent ( encodedTargetURL )
215
- ) ) ;
67
+ let [ _proto , _version , encodedTargetURL ] = url . split ( ':' ) ;
68
+ let source = generateModule ( encodedTargetURL ) ;
216
69
return {
217
70
shortCircuit : true ,
218
- source : ret ,
71
+ source,
219
72
format : 'module'
220
73
} ;
221
74
}
222
75
return defaultLoad ( url , context ) ;
223
76
}
224
77
225
78
/**
226
- *
227
- * @param {Array< string> } exports name of the exports of the module
79
+ * Generate the source code for a mocked module.
80
+ * @param {string } encodedTargetURL the module being mocked
228
81
* @returns {string }
229
82
*/
230
- function generateModule ( exports ) {
83
+ function generateModule ( encodedTargetURL ) {
84
+ const exports = mockedModuleExports . get (
85
+ decodeURIComponent ( encodedTargetURL )
86
+ ) ;
231
87
let body = [
232
88
'export {};' ,
233
89
'let mapping = {__proto__: null};'
234
90
] ;
235
91
for ( const [ i , name ] of Object . entries ( exports ) ) {
236
92
let key = JSON . stringify ( name ) ;
93
+ body . push ( `import.meta.mock = globalThis.mockedModules.get('${ encodedTargetURL } ');` ) ;
237
94
body . push ( `var _${ i } = import.meta.mock.namespace[${ key } ];` ) ;
238
95
body . push ( `Object.defineProperty(mapping, ${ key } , { enumerable: true, set(v) {_${ i } = v;}, get() {return _${ i } ;} });` ) ;
239
96
body . push ( `export {_${ i } as ${ name } };` ) ;
0 commit comments