1- import { receiveMessageOnPort } from 'node:worker_threads' ;
21const mockedModuleExports = new Map ( ) ;
32let currentMockVersion = 0 ;
43
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
65// swap module resolution results for mocking purposes. It uses this instead
76// of import.meta so that CommonJS can still use the functionality.
87//
@@ -22,7 +21,7 @@ let currentMockVersion = 0;
2221// it cannot be changed. So things like the following DO NOT WORK:
2322//
2423// ```mjs
25- // import mock from 'node: mock';
24+ // import mock from 'test-esm-loader- mock'; // See test-esm-loader-mock.mjs
2625// mock('file:///app.js', {x:1});
2726// const namespace1 = await import('file:///app.js');
2827// namespace1.x; // 1
@@ -34,148 +33,16 @@ let currentMockVersion = 0;
3433// assert(namespace1 === namespace2);
3534// ```
3635
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- }
4736
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+ } ) ;
5642}
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-
16943
17044// Rewrites node: loading to mock-facade: so that it can be intercepted
17145export async function resolve ( specifier , context , defaultResolve ) {
172- if ( specifier === 'node:mock' ) {
173- return {
174- shortCircuit : true ,
175- url : specifier
176- } ;
177- }
178- doDrainPort ( ) ;
17946 const def = await defaultResolve ( specifier , context ) ;
18047 if ( context . parentURL ?. startsWith ( 'mock-facade:' ) ) {
18148 // Do nothing, let it get the "real" module
@@ -192,48 +59,38 @@ export async function resolve(specifier, context, defaultResolve) {
19259}
19360
19461export 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- }
20762 /**
20863 * Mocked fake module, not going to be handled in default way so it
20964 * generates the source text, then short circuits
21065 */
21166 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 ) ;
21669 return {
21770 shortCircuit : true ,
218- source : ret ,
71+ source,
21972 format : 'module'
22073 } ;
22174 }
22275 return defaultLoad ( url , context ) ;
22376}
22477
22578/**
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
22881 * @returns {string }
22982 */
230- function generateModule ( exports ) {
83+ function generateModule ( encodedTargetURL ) {
84+ const exports = mockedModuleExports . get (
85+ decodeURIComponent ( encodedTargetURL )
86+ ) ;
23187 let body = [
23288 'export {};' ,
23389 'let mapping = {__proto__: null};'
23490 ] ;
23591 for ( const [ i , name ] of Object . entries ( exports ) ) {
23692 let key = JSON . stringify ( name ) ;
93+ body . push ( `import.meta.mock = globalThis.mockedModules.get('${ encodedTargetURL } ');` ) ;
23794 body . push ( `var _${ i } = import.meta.mock.namespace[${ key } ];` ) ;
23895 body . push ( `Object.defineProperty(mapping, ${ key } , { enumerable: true, set(v) {_${ i } = v;}, get() {return _${ i } ;} });` ) ;
23996 body . push ( `export {_${ i } as ${ name } };` ) ;
0 commit comments