| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,9 +1,6 @@ | |||
| 1 | - // Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs | ||
| 2 | 1 | import '../common/index.mjs'; | |
| 3 | - import assert from 'assert/strict'; | ||
| 4 | - | ||
| 5 | - // This is provided by test/fixtures/es-module-loaders/mock-loader.mjs | ||
| 6 | - import mock from 'node:mock'; | ||
| 2 | + import assert from 'node:assert/strict'; | ||
| 3 | + import { mock } from '../fixtures/es-module-loaders/mock.mjs'; | ||
| 7 | 4 | ||
| 8 | 5 | mock('node:events', { | |
| 9 | 6 | EventEmitter: 'This is mocked!' | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -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 | });`); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,70 @@ | |||
| 1 | + import { register } from 'node:module'; | ||
| 2 | + import { MessageChannel } from 'node:worker_threads'; | ||
| 3 | + | ||
| 4 | + | ||
| 5 | + const { port1, port2 } = new MessageChannel(); | ||
| 6 | + | ||
| 7 | + register('./mock-loader.mjs', import.meta.url, { | ||
| 8 | + data: { | ||
| 9 | + port: port2, | ||
| 10 | + mainImportURL: import.meta.url, | ||
| 11 | + }, | ||
| 12 | + transferList: [port2], | ||
| 13 | + }); | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * This is the Map that saves *all* the mocked URL -> replacement Module | ||
| 17 | + * mappings | ||
| 18 | + * @type {Map<string, {namespace, listeners}>} | ||
| 19 | + */ | ||
| 20 | + export const mockedModules = new Map(); | ||
| 21 | + let mockVersion = 0; | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * @param {string} resolved an absolute URL HREF string | ||
| 25 | + * @param {object} replacementProperties an object to pick properties from | ||
| 26 | + * to act as a module namespace | ||
| 27 | + * @returns {object} a mutator object that can update the module namespace | ||
| 28 | + * since we can't do something like old Object.observe | ||
| 29 | + */ | ||
| 30 | + export function mock(resolved, replacementProperties) { | ||
| 31 | + const exportNames = Object.keys(replacementProperties); | ||
| 32 | + const namespace = { __proto__: null }; | ||
| 33 | + /** | ||
| 34 | + * @type {Array<(name: string)=>void>} functions to call whenever an | ||
| 35 | + * export name is updated | ||
| 36 | + */ | ||
| 37 | + const listeners = []; | ||
| 38 | + for (const name of exportNames) { | ||
| 39 | + let currentValueForPropertyName = replacementProperties[name]; | ||
| 40 | + Object.defineProperty(namespace, name, { | ||
| 41 | + __proto__: null, | ||
| 42 | + enumerable: true, | ||
| 43 | + get() { | ||
| 44 | + return currentValueForPropertyName; | ||
| 45 | + }, | ||
| 46 | + set(v) { | ||
| 47 | + currentValueForPropertyName = v; | ||
| 48 | + for (const fn of listeners) { | ||
| 49 | + try { | ||
| 50 | + fn(name); | ||
| 51 | + } catch { | ||
| 52 | + /* noop */ | ||
| 53 | + } | ||
| 54 | + } | ||
| 55 | + }, | ||
| 56 | + }); | ||
| 57 | + } | ||
| 58 | + mockedModules.set(encodeURIComponent(resolved), { | ||
| 59 | + namespace, | ||
| 60 | + listeners, | ||
| 61 | + }); | ||
| 62 | + mockVersion++; | ||
| 63 | + // Inform the loader that the `resolved` URL should now use the specific | ||
| 64 | + // `mockVersion` and has export names of `exportNames` | ||
| 65 | + // | ||
| 66 | + // This allows the loader to generate a fake module for that version | ||
| 67 | + // and names the next time it resolves a specifier to equal `resolved` | ||
| 68 | + port1.postMessage({ mockVersion, resolved, exports: exportNames }); | ||
| 69 | + return namespace; | ||
| 70 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments