@@ -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 | });`); |
|