From 13ab19e1392e9cfcd3fef97ed9049ad5fd1a2156 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 2 Jul 2021 15:57:01 -0500 Subject: [PATCH 01/19] esm: working mock test --- doc/api/esm.md | 24 ++++ .../modules/esm/initialize_import_meta.js | 32 +++++ lib/internal/modules/esm/loader.js | 52 +++++-- lib/internal/modules/esm/translators.js | 28 +--- test/es-module/test-esm-loader-mock.mjs | 39 +++++ .../es-module-loaders/mock-loader.mjs | 136 ++++++++++++++++++ test/parallel/test-bootstrap-modules.js | 1 + 7 files changed, 279 insertions(+), 33 deletions(-) create mode 100644 lib/internal/modules/esm/initialize_import_meta.js create mode 100644 test/es-module/test-esm-loader-mock.mjs create mode 100644 test/fixtures/es-module-loaders/mock-loader.mjs diff --git a/doc/api/esm.md b/doc/api/esm.md index efe22b49970156..a1a91bc2e50332 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -806,6 +806,30 @@ const require = createRequire(cwd() + '/'); } ``` +In order to allow communication between the application and the loader another +argument is provided to the preload code `port`. This is available as a +parameter to the loader hook and inside of the source text returned by the hook. +Some care must be taken in order to properly `ref()` and `unref()` the +`MessagePort` to prevent a process from being in a state where it won't close +normally. + +```js +/** + * This example causes + * @param {object} utilities + * @param {MessagePort} utilities.port + */ +export function getGlobalPreloadCode({ port }) { + port.onmessage = (evt) => { + // ... + }; + return `\ +port.postMessage('I went to the Loader and back'); +port.onmessage = eval; +`; +} +``` + ### Examples The various loader hooks can be used together to accomplish wide-ranging diff --git a/lib/internal/modules/esm/initialize_import_meta.js b/lib/internal/modules/esm/initialize_import_meta.js new file mode 100644 index 00000000000000..322b4c59be1561 --- /dev/null +++ b/lib/internal/modules/esm/initialize_import_meta.js @@ -0,0 +1,32 @@ +'use strict'; + +const { getOptionValue } = require('internal/options'); +const experimentalImportMetaResolve = +getOptionValue('--experimental-import-meta-resolve'); +const { PromisePrototypeThen, PromiseReject } = primordials; +const asyncESM = require('internal/process/esm_loader'); + +function createImportMetaResolve(defaultParentUrl) { + return async function resolve(specifier, parentUrl = defaultParentUrl) { + return PromisePrototypeThen( + asyncESM.esmLoader.resolve(specifier, parentUrl), + ({ url }) => url, + (error) => ( + error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ? + error.url : PromiseReject(error)) + ); + }; +} + +function initializeImportMeta(meta, context) { + const url = context.url; + + // Alphabetical + if (experimentalImportMetaResolve) + meta.resolve = createImportMetaResolve(url); + meta.url = url; +} + +module.exports = { + initializeImportMeta +}; diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index b12a87a9021242..c50e976e699eef 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -18,6 +18,7 @@ const { SafeWeakMap, globalThis, } = primordials; +const { MessageChannel } = require('internal/worker/io'); const { ERR_INVALID_ARG_TYPE, @@ -39,6 +40,9 @@ const { defaultResolve, DEFAULT_CONDITIONS, } = require('internal/modules/esm/resolve'); +const { + initializeImportMeta +} = require('internal/modules/esm/initialize_import_meta'); const { defaultLoad } = require('internal/modules/esm/load'); const { translators } = require( 'internal/modules/esm/translators'); @@ -76,6 +80,8 @@ class ESMLoader { defaultResolve, ]; + #importMetaInitializer = initializeImportMeta; + /** * Map of already-loaded CJS modules to use */ @@ -359,7 +365,15 @@ class ESMLoader { if (!count) return; for (let i = 0; i < count; i++) { - const preload = this.#globalPreloaders[i](); + const channel = new MessageChannel(); + const insidePreload = channel.port1; + insidePreload.unref(); + const insideLoader = channel.port2; + insideLoader.unref(); + + const preload = this.#globalPreloaders[i]({ + port: insideLoader + }); if (preload == null) return; @@ -373,22 +387,44 @@ class ESMLoader { const { compileFunction } = require('vm'); const preloadInit = compileFunction( preload, - ['getBuiltin'], + ['getBuiltin', 'port', 'setImportMetaCallback'], { filename: '', } ); const { NativeModule } = require('internal/bootstrap/loaders'); - - FunctionPrototypeCall(preloadInit, globalThis, (builtinName) => { - if (NativeModule.canBeRequiredByUsers(builtinName)) { - return require(builtinName); + let finished = false; + let replacedImportMetaInitializer = false; + let next = this.#importMetaInitializer; + try { + FunctionPrototypeCall(preloadInit, globalThis, (builtinName) => { + if (NativeModule.canBeRequiredByUsers(builtinName)) { + return require(builtinName); + } + throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); + }, insidePreload, (fn) => { + if (finished || typeof fn !== 'function') { + throw new ERR_INVALID_ARG_TYPE('fn', fn); + } + replacedImportMetaInitializer = true; + const parent = next; + next = (meta, context) => { + return fn(meta, context, parent); + }; + }); + } finally { + finished = true; + if (replacedImportMetaInitializer) { + this.#importMetaInitializer = next; } - throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); - }); + } } } + importMetaInitialize(meta, context) { + this.#importMetaInitializer(meta, context); + } + /** * Resolve the location of the module. * diff --git a/lib/internal/modules/esm/translators.js b/lib/internal/modules/esm/translators.js index ba00041c417706..0e9044c6a0e69d 100644 --- a/lib/internal/modules/esm/translators.js +++ b/lib/internal/modules/esm/translators.js @@ -8,8 +8,6 @@ const { ObjectGetPrototypeOf, ObjectPrototypeHasOwnProperty, ObjectKeys, - PromisePrototypeThen, - PromiseReject, SafeArrayIterator, SafeMap, SafeSet, @@ -52,9 +50,6 @@ const { const { maybeCacheSourceMap } = require('internal/source_map/source_map_cache'); const moduleWrap = internalBinding('module_wrap'); const { ModuleWrap } = moduleWrap; -const { getOptionValue } = require('internal/options'); -const experimentalImportMetaResolve = - getOptionValue('--experimental-import-meta-resolve'); const asyncESM = require('internal/process/esm_loader'); const { emitWarningSync } = require('internal/process/warning'); const { TextDecoder } = require('internal/encoding'); @@ -111,25 +106,6 @@ async function importModuleDynamically(specifier, { url }) { return asyncESM.esmLoader.import(specifier, url); } -function createImportMetaResolve(defaultParentUrl) { - return async function resolve(specifier, parentUrl = defaultParentUrl) { - return PromisePrototypeThen( - asyncESM.esmLoader.resolve(specifier, parentUrl), - ({ url }) => url, - (error) => ( - error.code === 'ERR_UNSUPPORTED_DIR_IMPORT' ? - error.url : PromiseReject(error)) - ); - }; -} - -function initializeImportMeta(meta, { url }) { - // Alphabetical - if (experimentalImportMetaResolve) - meta.resolve = createImportMetaResolve(url); - meta.url = url; -} - // Strategy for loading a standard JavaScript module. translators.set('module', async function moduleStrategy(url, source, isMain) { assertBufferSource(source, true, 'load'); @@ -138,7 +114,9 @@ translators.set('module', async function moduleStrategy(url, source, isMain) { debug(`Translating StandardModule ${url}`); const module = new ModuleWrap(url, undefined, source, 0, 0); moduleWrap.callbackMap.set(module, { - initializeImportMeta, + initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { + url: wrap.url + }), importModuleDynamically, }); return module; diff --git a/test/es-module/test-esm-loader-mock.mjs b/test/es-module/test-esm-loader-mock.mjs new file mode 100644 index 00000000000000..5172ea73c7d90c --- /dev/null +++ b/test/es-module/test-esm-loader-mock.mjs @@ -0,0 +1,39 @@ +// Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs +import '../common/index.mjs'; +import assert from 'assert/strict'; +import mock from 'node:mock'; + +mock('node:events', { + EventEmitter: 'This is mocked!' +}); + +// This resolves to node:events +assert.deepStrictEqual(await import('events'), Object.defineProperty({ + __proto__: null, + EventEmitter: 'This is mocked!' +}, Symbol.toStringTag, { + enumerable: false, + value: 'Module' +})); + +const mutator = mock('node:events', { + EventEmitter: 'This is mocked v2!' +}); + +const mockedV2 = await import('node:events'); +assert.deepStrictEqual(mockedV2, Object.defineProperty({ + __proto__: null, + EventEmitter: 'This is mocked v2!' +}, Symbol.toStringTag, { + enumerable: false, + value: 'Module' +})); + +mutator.EventEmitter = 'This is mocked v3!'; +assert.deepStrictEqual(mockedV2, Object.defineProperty({ + __proto__: null, + EventEmitter: 'This is mocked v3!' +}, Symbol.toStringTag, { + enumerable: false, + value: 'Module' +})); diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs new file mode 100644 index 00000000000000..4d2c613e3fd29a --- /dev/null +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -0,0 +1,136 @@ +import {receiveMessageOnPort} from 'worker_threads'; +const mockedModuleExports = new Map(); +let currentMockVersion = 0; + +/** + * FIXME: this is a hack to workaround loaders being + * single threaded for now + */ +function doDrainPort() { + let msg; + while (msg = receiveMessageOnPort(preloadPort)) { + onPreloadPortMessage(msg.message); + } +} +function onPreloadPortMessage({ + mockVersion, resolved, exports +}) { + currentMockVersion = mockVersion; + mockedModuleExports.set(resolved, exports); +} +let preloadPort; +export function globalPreload({port}) { + preloadPort = port; + port.on('message', onPreloadPortMessage); + port.unref(); + return `(${()=>{ + let mockedModules = new Map(); + let mockVersion = 0; + const doMock = (resolved, replacementProperties) => { + let exports = Object.keys(replacementProperties); + let namespace = Object.create(null); + let listeners = []; + for (const name of exports) { + let currentValue = replacementProperties[name]; + Object.defineProperty(namespace, name, { + enumerable: true, + get() { + return currentValue; + }, + set(v) { + currentValue = v; + for (let fn of listeners) { + try { + fn(name); + } catch { + } + } + } + }); + } + mockedModules.set(resolved, { + namespace, + listeners + }); + mockVersion++; + port.postMessage({mockVersion, resolved, exports }); + return namespace; + } + setImportMetaCallback((meta, context, parent) => { + if (context.url === 'node:mock') { + meta.doMock = doMock; + return; + } + if (context.url.startsWith('mock:')) { + let [proto, version, encodedTargetURL] = context.url.split(':'); + let decodedTargetURL = decodeURIComponent(encodedTargetURL); + if (mockedModules.has(decodedTargetURL)) { + meta.mock = mockedModules.get(decodedTargetURL); + return; + } + } + parent(meta, context); + }); + }})()`; +} + + +// rewrites node: loading to mock: so that it can be intercepted +export function resolve(specifier, context, defaultResolve) { + if (specifier === 'node:mock') { + return { + url: specifier + }; + } + doDrainPort(); + const def = defaultResolve(specifier, context); + if (context.parentURL?.startsWith('mock:')) { + // do nothing, let it get the "real" module + } else if (mockedModuleExports.has(def.url)) { + return { + url: `mock:${currentMockVersion}:${encodeURIComponent(def.url)}` + }; + }; + return { + url: `${def.url}` + }; +} + +export function load(url, context, defaultLoad) { + doDrainPort(); + if (url === 'node:mock') { + return { + source: 'export default import.meta.doMock', + format: 'module' + }; + } + if (url.startsWith('mock:')) { + let [proto, version, encodedTargetURL] = url.split(':'); + let ret = generateModule(mockedModuleExports.get( + decodeURIComponent(encodedTargetURL) + )); + return { + source: ret, + format: 'module' + }; + } + return defaultLoad(url, context); +} + +function generateModule(exports) { + let body = 'export {};let mapping = {__proto__: null};' + for (const [i, name] of Object.entries(exports)) { + let key = JSON.stringify(name); + body += `var _${i} = import.meta.mock.namespace[${key}];` + body += `Object.defineProperty(mapping, ${key}, {enumerable: true,set(v) {_${i} = v;}});` + body += `export {_${i} as ${name}};`; + } + body += `import.meta.mock.listeners.push(${ + () => { + for (var k in mapping) { + mapping[k] = import.meta.mock.namespace[k]; + } + } + });` + return body; +} diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 9ed1b90e318e7e..9ed839a0bc70b0 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -76,6 +76,7 @@ const expectedModules = new Set([ 'NativeModule internal/modules/esm/module_job', 'NativeModule internal/modules/esm/module_map', 'NativeModule internal/modules/esm/resolve', + 'NativeModule internal/modules/esm/initialize_import_meta', 'NativeModule internal/modules/esm/translators', 'NativeModule internal/process/esm_loader', 'NativeModule internal/options', From ffdaa30bec4ab8f740bff2e682d41ac495c00d35 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Wed, 27 Oct 2021 16:36:37 -0500 Subject: [PATCH 02/19] fixup: Update doc/api/esm.md --- doc/api/esm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index a1a91bc2e50332..cdb55e5052d944 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -819,7 +819,7 @@ normally. * @param {object} utilities * @param {MessagePort} utilities.port */ -export function getGlobalPreloadCode({ port }) { +export function globalPreloadCode({ port }) { port.onmessage = (evt) => { // ... }; From f5e96f3f002b5a814aaec1a95d3176d01da10373 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 29 Oct 2021 09:03:19 -0500 Subject: [PATCH 03/19] fixup: Apply suggestions from code review Co-authored-by: Derek Lewis --- doc/api/esm.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index cdb55e5052d944..4fa41ea3e42a2a 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -806,7 +806,7 @@ const require = createRequire(cwd() + '/'); } ``` -In order to allow communication between the application and the loader another +In order to allow communication between the application and the loader, another argument is provided to the preload code `port`. This is available as a parameter to the loader hook and inside of the source text returned by the hook. Some care must be taken in order to properly `ref()` and `unref()` the From f923cce3c1decbe36feee6da9ad014dad886b536 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 29 Oct 2021 09:28:35 -0500 Subject: [PATCH 04/19] fixup: rebase fixes --- doc/api/esm.md | 17 ++++++++++------- .../es-module-loaders/loader-side-effect.mjs | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 4fa41ea3e42a2a..16af5e5452eb24 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -773,7 +773,7 @@ source to a supported one (see [Examples](#examples) below). > signature may change. Do not rely on the API described below. > Note: In a previous version of this API, this hook was named -> `getGlobalPreloadCode`. +> `globalPreload`. * Returns: {string} @@ -815,18 +815,21 @@ normally. ```js /** - * This example causes + * This example has the application context send a message to the loader + * and sends the message back to the application context * @param {object} utilities * @param {MessagePort} utilities.port */ -export function globalPreloadCode({ port }) { +export function globalPreload({ port }) { port.onmessage = (evt) => { - // ... + port.postMessage(evt.data); }; return `\ -port.postMessage('I went to the Loader and back'); -port.onmessage = eval; -`; + port.postMessage('console.log("I went to the Loader and back");'); + port.onmessage = (evt) => { + eval(evt.data); + }; + `; } ``` diff --git a/test/fixtures/es-module-loaders/loader-side-effect.mjs b/test/fixtures/es-module-loaders/loader-side-effect.mjs index 5c80724fbb95f6..ae018f95412173 100644 --- a/test/fixtures/es-module-loaders/loader-side-effect.mjs +++ b/test/fixtures/es-module-loaders/loader-side-effect.mjs @@ -24,7 +24,7 @@ const implicitGlobalConst = 42 * 42; globalThis.explicitGlobalProperty = 42 * 42 * 42; } -export function getGlobalPreloadCode() { +export function globalPreload() { return `\ (${globalPreload.toString()})(); From 0e1e124ebcd2bdf5b0b96e4073621e499bc1d890 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Fri, 29 Oct 2021 15:29:54 -0500 Subject: [PATCH 05/19] fixup: rebasing --- test/fixtures/es-module-loaders/loader-side-effect.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/es-module-loaders/loader-side-effect.mjs b/test/fixtures/es-module-loaders/loader-side-effect.mjs index ae018f95412173..e91cdea0527881 100644 --- a/test/fixtures/es-module-loaders/loader-side-effect.mjs +++ b/test/fixtures/es-module-loaders/loader-side-effect.mjs @@ -1,5 +1,5 @@ // Arrow function so it closes over the this-value of the preload scope. -const globalPreload = () => { +const globalPreloadSrc = () => { /* global getBuiltin */ const assert = getBuiltin('assert'); const vm = getBuiltin('vm'); @@ -27,6 +27,6 @@ const implicitGlobalConst = 42 * 42; export function globalPreload() { return `\ -(${globalPreload.toString()})(); +(${globalPreloadSrc.toString()})(); `; } From 4e4ed137c589a520d5b5efe1eb21f7397dc50f73 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Thu, 4 Nov 2021 07:59:32 -0500 Subject: [PATCH 06/19] fixup: Apply suggestions from code review Co-authored-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- doc/api/esm.md | 2 +- lib/internal/modules/esm/loader.js | 7 +++++-- test/fixtures/es-module-loaders/mock-loader.mjs | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 16af5e5452eb24..81406aadd79b09 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -807,7 +807,7 @@ const require = createRequire(cwd() + '/'); ``` In order to allow communication between the application and the loader, another -argument is provided to the preload code `port`. This is available as a +argument is provided to the preload code: `port`. This is available as a parameter to the loader hook and inside of the source text returned by the hook. Some care must be taken in order to properly `ref()` and `unref()` the `MessagePort` to prevent a process from being in a state where it won't close diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index c50e976e699eef..9e82fa15205fe8 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -366,9 +366,12 @@ class ESMLoader { for (let i = 0; i < count; i++) { const channel = new MessageChannel(); - const insidePreload = channel.port1; + const { + port1: insidePreload, + port2: insideLoader, + } = channel; + insidePreload.unref(); - const insideLoader = channel.port2; insideLoader.unref(); const preload = this.#globalPreloaders[i]({ diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index 4d2c613e3fd29a..1296b443232fc3 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -53,7 +53,7 @@ export function globalPreload({port}) { listeners }); mockVersion++; - port.postMessage({mockVersion, resolved, exports }); + port.postMessage({ mockVersion, resolved, exports }); return namespace; } setImportMetaCallback((meta, context, parent) => { From 8369c4a2e0ff3367104ed5b00673ee1e2486cf32 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Thu, 4 Nov 2021 08:01:14 -0500 Subject: [PATCH 07/19] fixup: Apply code review suggestion to avoid string template --- test/fixtures/es-module-loaders/mock-loader.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index 1296b443232fc3..25a8c2c34f543b 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -23,7 +23,7 @@ export function globalPreload({port}) { preloadPort = port; port.on('message', onPreloadPortMessage); port.unref(); - return `(${()=>{ + return String(()=>{ let mockedModules = new Map(); let mockVersion = 0; const doMock = (resolved, replacementProperties) => { @@ -71,7 +71,7 @@ export function globalPreload({port}) { } parent(meta, context); }); - }})()`; + }); } From 57ace81418a86bff1be5e13b0d8a88fe46f3abdb Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Thu, 4 Nov 2021 08:42:20 -0500 Subject: [PATCH 08/19] fixup: lint --- lib/internal/modules/esm/loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 9e82fa15205fe8..216fe902c056b4 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -370,7 +370,7 @@ class ESMLoader { port1: insidePreload, port2: insideLoader, } = channel; - + insidePreload.unref(); insideLoader.unref(); From 7416b2740c4cec30c1b4c29f975b09e1d436c759 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Wed, 10 Nov 2021 21:26:36 -0600 Subject: [PATCH 09/19] fixup: bad review change --- test/fixtures/es-module-loaders/mock-loader.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index 25a8c2c34f543b..77435662911ced 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -23,7 +23,7 @@ export function globalPreload({port}) { preloadPort = port; port.on('message', onPreloadPortMessage); port.unref(); - return String(()=>{ + const insideAppContext = (getBuiltin, port, setImportMetaCallback) => { let mockedModules = new Map(); let mockVersion = 0; const doMock = (resolved, replacementProperties) => { @@ -71,7 +71,8 @@ export function globalPreload({port}) { } parent(meta, context); }); - }); + }; + return `(${insideAppContext})(getBuiltin, port, setImportMetaCallback)` } From d0276c611371723dd251a29fe1413357710aa156 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Mon, 15 Nov 2021 14:12:23 -0600 Subject: [PATCH 10/19] fixup: Update test/fixtures/es-module-loaders/mock-loader.mjs Co-authored-by: Geoffrey Booth <456802+GeoffreyBooth@users.noreply.github.com> --- test/fixtures/es-module-loaders/mock-loader.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index 77435662911ced..ffae39e90a75f6 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -1,4 +1,4 @@ -import {receiveMessageOnPort} from 'worker_threads'; +import { receiveMessageOnPort } from 'node:worker_threads'; const mockedModuleExports = new Map(); let currentMockVersion = 0; From 0e11bde2408e058e7dc86b71359adbefe7684787 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Mon, 15 Nov 2021 14:47:03 -0600 Subject: [PATCH 11/19] fixup: pr comments --- doc/api/esm.md | 6 ++++-- test/es-module/test-esm-loader-mock.mjs | 6 ++++++ test/fixtures/es-module-loaders/mock-loader.mjs | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 81406aadd79b09..299146f1b6aa8b 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -809,8 +809,8 @@ const require = createRequire(cwd() + '/'); In order to allow communication between the application and the loader, another argument is provided to the preload code: `port`. This is available as a parameter to the loader hook and inside of the source text returned by the hook. -Some care must be taken in order to properly `ref()` and `unref()` the -`MessagePort` to prevent a process from being in a state where it won't close +Some care must be taken in order to properly call [`port.ref()`][] and +[`port.unref()`][] to prevent a process from being in a state where it won't close normally. ```js @@ -1404,6 +1404,8 @@ success! [`module.createRequire()`]: module.md#modulecreaterequirefilename [`module.syncBuiltinESMExports()`]: module.md#modulesyncbuiltinesmexports [`package.json`]: packages.md#nodejs-packagejson-field-definitions +[`port.ref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portref +[`port.unref()`]: https://nodejs.org/dist/latest-v17.x/docs/api/worker_threads.html#portunref [`process.dlopen`]: process.md#processdlopenmodule-filename-flags [`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String [`util.TextDecoder`]: util.md#class-utiltextdecoder diff --git a/test/es-module/test-esm-loader-mock.mjs b/test/es-module/test-esm-loader-mock.mjs index 5172ea73c7d90c..2783bf694d239a 100644 --- a/test/es-module/test-esm-loader-mock.mjs +++ b/test/es-module/test-esm-loader-mock.mjs @@ -1,6 +1,8 @@ // Flags: --loader ./test/fixtures/es-module-loaders/mock-loader.mjs import '../common/index.mjs'; import assert from 'assert/strict'; + +// This is provided by test/fixtures/es-module-loaders/mock-loader.mjs import mock from 'node:mock'; mock('node:events', { @@ -8,6 +10,7 @@ mock('node:events', { }); // This resolves to node:events +// It is intercepted by mock-loader and doesn't return the normal value assert.deepStrictEqual(await import('events'), Object.defineProperty({ __proto__: null, EventEmitter: 'This is mocked!' @@ -20,6 +23,9 @@ const mutator = mock('node:events', { EventEmitter: 'This is mocked v2!' }); +// It is intercepted by mock-loader and doesn't return the normal value. +// This is resolved separately from the import above since the specifiers +// are different. const mockedV2 = await import('node:events'); assert.deepStrictEqual(mockedV2, Object.defineProperty({ __proto__: null, diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index ffae39e90a75f6..ce24de28badeb4 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -56,7 +56,7 @@ export function globalPreload({port}) { port.postMessage({ mockVersion, resolved, exports }); return namespace; } - setImportMetaCallback((meta, context, parent) => { + setImportMetaCallback((meta, context, defaultImportMetaInitializer) => { if (context.url === 'node:mock') { meta.doMock = doMock; return; @@ -69,7 +69,7 @@ export function globalPreload({port}) { return; } } - parent(meta, context); + defaultImportMetaInitializer(meta, context); }); }; return `(${insideAppContext})(getBuiltin, port, setImportMetaCallback)` From 172c038d20ef01a6ff0665280756696f83cb185a Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Mon, 15 Nov 2021 15:07:50 -0600 Subject: [PATCH 12/19] fixup: linter --- doc/api/esm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 299146f1b6aa8b..b65f688ae958b2 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -810,8 +810,8 @@ In order to allow communication between the application and the loader, another argument is provided to the preload code: `port`. This is available as a parameter to the loader hook and inside of the source text returned by the hook. Some care must be taken in order to properly call [`port.ref()`][] and -[`port.unref()`][] to prevent a process from being in a state where it won't close -normally. +[`port.unref()`][] to prevent a process from being in a state where it won't +close normally. ```js /** From b9e13801a1218b88acef81b6f808a9a0aa234a9d Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Mon, 22 Nov 2021 09:39:44 -0600 Subject: [PATCH 13/19] fixup: review nit, inspector debuggability --- .../es-module-loaders/mock-loader.mjs | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index ce24de28badeb4..d368bbfc34a8a9 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -2,6 +2,11 @@ import { receiveMessageOnPort } from 'node:worker_threads'; const mockedModuleExports = new Map(); let currentMockVersion = 0; +// This loader causes import.meta.mock to become available as a way to swap +// module resolution. +// +// + /** * FIXME: this is a hack to workaround loaders being * single threaded for now @@ -61,7 +66,7 @@ export function globalPreload({port}) { meta.doMock = doMock; return; } - if (context.url.startsWith('mock:')) { + if (context.url.startsWith('mock-facade:')) { let [proto, version, encodedTargetURL] = context.url.split(':'); let decodedTargetURL = decodeURIComponent(encodedTargetURL); if (mockedModules.has(decodedTargetURL)) { @@ -76,7 +81,7 @@ export function globalPreload({port}) { } -// rewrites node: loading to mock: so that it can be intercepted +// rewrites node: loading to mock-facade: so that it can be intercepted export function resolve(specifier, context, defaultResolve) { if (specifier === 'node:mock') { return { @@ -85,11 +90,11 @@ export function resolve(specifier, context, defaultResolve) { } doDrainPort(); const def = defaultResolve(specifier, context); - if (context.parentURL?.startsWith('mock:')) { + if (context.parentURL?.startsWith('mock-facade:')) { // do nothing, let it get the "real" module } else if (mockedModuleExports.has(def.url)) { return { - url: `mock:${currentMockVersion}:${encodeURIComponent(def.url)}` + url: `mock-facade:${currentMockVersion}:${encodeURIComponent(def.url)}` }; }; return { @@ -105,7 +110,7 @@ export function load(url, context, defaultLoad) { format: 'module' }; } - if (url.startsWith('mock:')) { + if (url.startsWith('mock-facade:')) { let [proto, version, encodedTargetURL] = url.split(':'); let ret = generateModule(mockedModuleExports.get( decodeURIComponent(encodedTargetURL) @@ -119,19 +124,22 @@ export function load(url, context, defaultLoad) { } function generateModule(exports) { - let body = 'export {};let mapping = {__proto__: null};' + let body = [ + 'export {};', + 'let mapping = {__proto__: null};' + ]; for (const [i, name] of Object.entries(exports)) { let key = JSON.stringify(name); - body += `var _${i} = import.meta.mock.namespace[${key}];` - body += `Object.defineProperty(mapping, ${key}, {enumerable: true,set(v) {_${i} = v;}});` - body += `export {_${i} as ${name}};`; + body.push(`var _${i} = import.meta.mock.namespace[${key}];`); + body.push(`Object.defineProperty(mapping, ${key}, { enumerable: true, set(v) {_${i} = v;}, get() {return _${i};} });`); + body.push(`export {_${i} as ${name}};`); } - body += `import.meta.mock.listeners.push(${ + body.push(`import.meta.mock.listeners.push(${ () => { for (var k in mapping) { mapping[k] = import.meta.mock.namespace[k]; } } - });` - return body; + });`); + return body.join('\n'); } From 5ad4c1b6a1a88a56ced729ba4cd6d402d34c5056 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Mon, 22 Nov 2021 10:42:10 -0600 Subject: [PATCH 14/19] fixup: doc the mock --- .../es-module-loaders/mock-loader.mjs | 116 ++++++++++++++++-- 1 file changed, 105 insertions(+), 11 deletions(-) diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index d368bbfc34a8a9..069ae31aa68c41 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -2,14 +2,36 @@ import { receiveMessageOnPort } from 'node:worker_threads'; const mockedModuleExports = new Map(); let currentMockVersion = 0; -// This loader causes import.meta.mock to become available as a way to swap -// module resolution. +// This loader causes a new module `node:mock` to become available as a way to +// swap module resolution results for mocking purposes. It uses this instead +// of import.meta so that CJS can still use the functionality // +// It does so by allowing non-mocked modules to live in normal URL cache +// locations but creates 'mock-facade:' URL cache location for every time a +// module location is mocked. Since a single URL can be mocked multiple +// times but it cannot be removed from the cache `mock-facade:` URLs have a +// form of mock-facade:$VERSION:$REPLACING_URL with the parameters being URL +// percent encoded every time a module is resolved // +// NOTE: due to ESM spec, once a specifier has been resolved in a source text +// it cannot be changed. so things like the following DO NOT WORK +// +// ```mjs +// import mock from 'node:mock'; +// mock('file:///app.js', {x:1}); +// const namespace1 = await import('file:///app.js'); +// namespace1.x; // 1 +// mock('file:///app.js', {x:2}); +// const namespace2 = await import('file:///app.js'); +// namespace2.x; // STILL 1, because this source text already set the specifier +// // for 'file:///app.js', a different specifier that resolves +// // to that could still get a new namespace though +// assert(namespace1 === namespace2); +// ``` /** * FIXME: this is a hack to workaround loaders being - * single threaded for now + * single threaded for now, just ensures that the MessagePort drains */ function doDrainPort() { let msg; @@ -17,6 +39,10 @@ function doDrainPort() { onPreloadPortMessage(msg.message); } } + +/** + * @param param0 message from the application context + */ function onPreloadPortMessage({ mockVersion, resolved, exports }) { @@ -25,25 +51,60 @@ function onPreloadPortMessage({ } let preloadPort; export function globalPreload({port}) { + // Save the communication port to the application context to send messages + // to it later preloadPort = port; + // Every time the application context sends a message over the port port.on('message', onPreloadPortMessage); + // This prevents the port that the Loader/application talk over + // from keeping the process alive, without this, an application would be kept + // alive just because a loader is waiting for messages port.unref(); + const insideAppContext = (getBuiltin, port, setImportMetaCallback) => { + /** + * This is the Map that saves *all* the mocked URL -> replacement Module + * mappings + * @type {Map} + */ let mockedModules = new Map(); let mockVersion = 0; + /** + * This is the value that is placed into the `node:mock` default export + * + * @example + * ```mjs + * import mock from 'node:mock'; + * const mutator = mock('file:///app.js', {x:1}); + * const namespace = await import('file:///app.js'); + * namespace.x; // 1; + * mutator.x = 2; + * namespace.x; // 2; + * ``` + * + * @param {string} resolved an absolute URL HREF string + * @param {object} replacementProperties an object to pick properties from + * to act as a module namespace + * @returns {object} a mutator object that can update the module namespace + * since we can't do something like old Object.observe + */ const doMock = (resolved, replacementProperties) => { - let exports = Object.keys(replacementProperties); + let exportNames = Object.keys(replacementProperties); let namespace = Object.create(null); + /** + * @type {Array<(name: string)=>void>} functions to call whenever an + * export name is updated + */ let listeners = []; - for (const name of exports) { - let currentValue = replacementProperties[name]; + for (const name of exportNames) { + let currentValueForPropertyName = replacementProperties[name]; Object.defineProperty(namespace, name, { enumerable: true, get() { - return currentValue; + return currentValueForPropertyName; }, set(v) { - currentValue = v; + currentValueForPropertyName = v; for (let fn of listeners) { try { fn(name); @@ -58,14 +119,31 @@ export function globalPreload({port}) { listeners }); mockVersion++; - port.postMessage({ mockVersion, resolved, exports }); + // Inform the loader that the `resolved` URL should now use the specific + // `mockVersion` and has export names of `exportNames` + // + // This allows the loader to generate a fake module for that version + // and names the next time it resolves a specifier to equal `resolved` + port.postMessage({ mockVersion, resolved, exports: exportNames }); return namespace; } + // Sets the import.meta properties up + // has the normal chaining workflow with `defaultImportMetaInitializer` setImportMetaCallback((meta, context, defaultImportMetaInitializer) => { + /** + * 'node:mock' creates its default export by plucking off of import.meta + * and must do so in order to get the communications channel from inside + * preloadCode + */ if (context.url === 'node:mock') { meta.doMock = doMock; return; } + /** + * Fake modules created by `node:mock` get their meta.mock utility set + * to the corresponding value keyed off `mockedModules` and use this + * to setup their exports/listeners properly + */ if (context.url.startsWith('mock-facade:')) { let [proto, version, encodedTargetURL] = context.url.split(':'); let decodedTargetURL = decodeURIComponent(encodedTargetURL); @@ -74,6 +152,9 @@ export function globalPreload({port}) { return; } } + /** + * Ensure we still get things like `import.meta.url` + */ defaultImportMetaInitializer(meta, context); }); }; @@ -81,7 +162,7 @@ export function globalPreload({port}) { } -// rewrites node: loading to mock-facade: so that it can be intercepted +// Rewrites node: loading to mock-facade: so that it can be intercepted export function resolve(specifier, context, defaultResolve) { if (specifier === 'node:mock') { return { @@ -91,7 +172,7 @@ export function resolve(specifier, context, defaultResolve) { doDrainPort(); const def = defaultResolve(specifier, context); if (context.parentURL?.startsWith('mock-facade:')) { - // do nothing, let it get the "real" module + // Do nothing, let it get the "real" module } else if (mockedModuleExports.has(def.url)) { return { url: `mock-facade:${currentMockVersion}:${encodeURIComponent(def.url)}` @@ -105,11 +186,19 @@ export function resolve(specifier, context, defaultResolve) { export function load(url, context, defaultLoad) { doDrainPort(); if (url === 'node:mock') { + /** + * Simply grab the import.meta.doMock to establish the communication + * channel with preloadCode + */ return { source: 'export default import.meta.doMock', format: 'module' }; } + /** + * Mocked fake module, not going to be handled in default way so it + * generates the source text, then short circuits + */ if (url.startsWith('mock-facade:')) { let [proto, version, encodedTargetURL] = url.split(':'); let ret = generateModule(mockedModuleExports.get( @@ -123,6 +212,11 @@ export function load(url, context, defaultLoad) { return defaultLoad(url, context); } +/** + * + * @param {Array} exports name of the exports of the module + * @returns {string} + */ function generateModule(exports) { let body = [ 'export {};', From d61b7d4ebab3fef840cf4d820f3b38d92b55f131 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Tue, 23 Nov 2021 09:36:44 -0600 Subject: [PATCH 15/19] fixup: doc nit --- doc/api/esm.md | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index b65f688ae958b2..03e313610c3e78 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -788,37 +788,13 @@ builtins like "fs": `getBuiltin(request: string)`. If the code needs more advanced `require` features, it has to construct its own `require` using `module.createRequire()`. -```js -/** - * @returns {string} Code to run before application startup - */ -export function globalPreload() { - return `\ -globalThis.someInjectedProperty = 42; -console.log('I just set some globals!'); - -const { createRequire } = getBuiltin('module'); -const { cwd } = getBuiltin('process'); - -const require = createRequire(cwd() + '/'); -// [...] -`; -} -``` - -In order to allow communication between the application and the loader, another -argument is provided to the preload code: `port`. This is available as a -parameter to the loader hook and inside of the source text returned by the hook. -Some care must be taken in order to properly call [`port.ref()`][] and -[`port.unref()`][] to prevent a process from being in a state where it won't -close normally. - ```js /** * This example has the application context send a message to the loader * and sends the message back to the application context * @param {object} utilities * @param {MessagePort} utilities.port + * @returns {string} */ export function globalPreload({ port }) { port.onmessage = (evt) => { @@ -833,6 +809,13 @@ export function globalPreload({ port }) { } ``` +In order to allow communication between the application and the loader, another +argument is provided to the preload code: `port`. This is available as a +parameter to the loader hook and inside of the source text returned by the hook. +Some care must be taken in order to properly call [`port.ref()`][] and +[`port.unref()`][] to prevent a process from being in a state where it won't +close normally. + ### Examples The various loader hooks can be used together to accomplish wide-ranging From 93ba88ad8bd1b4224695935ca1b2d3eb9c9aee29 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Tue, 23 Nov 2021 19:08:14 -0600 Subject: [PATCH 16/19] fixup: Apply suggestions from code review Co-authored-by: Geoffrey Booth <456802+GeoffreyBooth@users.noreply.github.com> --- doc/api/esm.md | 9 +++++---- test/fixtures/es-module-loaders/mock-loader.mjs | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 03e313610c3e78..f5bc4ab9021f5f 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -773,7 +773,7 @@ source to a supported one (see [Examples](#examples) below). > signature may change. Do not rely on the API described below. > Note: In a previous version of this API, this hook was named -> `globalPreload`. +> `getGlobalPreloadCode`. * Returns: {string} @@ -792,9 +792,10 @@ its own `require` using `module.createRequire()`. /** * This example has the application context send a message to the loader * and sends the message back to the application context - * @param {object} utilities - * @param {MessagePort} utilities.port - * @returns {string} + * @param {{ + port: MessagePort, + }} utilities Functions that preload code might find useful + * @returns {string} Code to execute in the preload context */ export function globalPreload({ port }) { port.onmessage = (evt) => { diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index 069ae31aa68c41..641d0155cc4bff 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -4,17 +4,17 @@ let currentMockVersion = 0; // This loader causes a new module `node:mock` to become available as a way to // swap module resolution results for mocking purposes. It uses this instead -// of import.meta so that CJS can still use the functionality +// of import.meta so that CommonJS can still use the functionality. // // It does so by allowing non-mocked modules to live in normal URL cache // locations but creates 'mock-facade:' URL cache location for every time a // module location is mocked. Since a single URL can be mocked multiple -// times but it cannot be removed from the cache `mock-facade:` URLs have a +// times but it cannot be removed from the cache, `mock-facade:` URLs have a // form of mock-facade:$VERSION:$REPLACING_URL with the parameters being URL // percent encoded every time a module is resolved // // NOTE: due to ESM spec, once a specifier has been resolved in a source text -// it cannot be changed. so things like the following DO NOT WORK +// it cannot be changed. So things like the following DO NOT WORK: // // ```mjs // import mock from 'node:mock'; From 1c5996dbacdc35c4cab597c695acdfc74710fb15 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Wed, 24 Nov 2021 07:36:20 -0600 Subject: [PATCH 17/19] fixup: review nits --- doc/api/esm.md | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index f5bc4ab9021f5f..92f39bc2d1d533 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -789,13 +789,39 @@ If the code needs more advanced `require` features, it has to construct its own `require` using `module.createRequire()`. ```js +/** + * @param {{ + port: MessagePort, + }} utilities Things that preload code might find useful + * @returns {string} Code to run before application startup + */ +export function globalPreload() { + return `\ +globalThis.someInjectedProperty = 42; +console.log('I just set some globals!'); + +const { createRequire } = getBuiltin('module'); +const { cwd } = getBuiltin('process'); + +const require = createRequire(cwd() + '/'); +// [...] +`; +} + +In order to allow communication between the application and the loader, another +argument is provided to the preload code: `port`. This is available as a +parameter to the loader hook and inside of the source text returned by the hook. +Some care must be taken in order to properly call [`port.ref()`][] and +[`port.unref()`][] to prevent a process from being in a state where it won't +close normally. + /** * This example has the application context send a message to the loader * and sends the message back to the application context * @param {{ port: MessagePort, - }} utilities Functions that preload code might find useful - * @returns {string} Code to execute in the preload context + }} utilities Things that preload code might find useful + * @returns {string} Code to run before application startup */ export function globalPreload({ port }) { port.onmessage = (evt) => { @@ -810,13 +836,6 @@ export function globalPreload({ port }) { } ``` -In order to allow communication between the application and the loader, another -argument is provided to the preload code: `port`. This is available as a -parameter to the loader hook and inside of the source text returned by the hook. -Some care must be taken in order to properly call [`port.ref()`][] and -[`port.unref()`][] to prevent a process from being in a state where it won't -close normally. - ### Examples The various loader hooks can be used together to accomplish wide-ranging From b9f339ff702aad99b70bc0b4039b011c6ed830b7 Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Wed, 24 Nov 2021 08:58:42 -0600 Subject: [PATCH 18/19] fixup: lint --- doc/api/esm.md | 2 ++ lib/internal/modules/esm/loader.js | 46 ++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/doc/api/esm.md b/doc/api/esm.md index 92f39bc2d1d533..2fcbacd689d968 100644 --- a/doc/api/esm.md +++ b/doc/api/esm.md @@ -807,6 +807,7 @@ const require = createRequire(cwd() + '/'); // [...] `; } +``` In order to allow communication between the application and the loader, another argument is provided to the preload code: `port`. This is available as a @@ -815,6 +816,7 @@ Some care must be taken in order to properly call [`port.ref()`][] and [`port.unref()`][] to prevent a process from being in a state where it won't close normally. +```js /** * This example has the application context send a message to the loader * and sends the message back to the application context diff --git a/lib/internal/modules/esm/loader.js b/lib/internal/modules/esm/loader.js index 216fe902c056b4..99aac82815fa3e 100644 --- a/lib/internal/modules/esm/loader.js +++ b/lib/internal/modules/esm/loader.js @@ -396,25 +396,41 @@ class ESMLoader { } ); const { NativeModule } = require('internal/bootstrap/loaders'); + // We only allow replacing the importMetaInitializer during preload, + // after preload is finished, we disable the ability to replace it + // + // This exposes accidentally setting the initializer too late by + // throwing an error. let finished = false; let replacedImportMetaInitializer = false; let next = this.#importMetaInitializer; try { - FunctionPrototypeCall(preloadInit, globalThis, (builtinName) => { - if (NativeModule.canBeRequiredByUsers(builtinName)) { - return require(builtinName); - } - throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); - }, insidePreload, (fn) => { - if (finished || typeof fn !== 'function') { - throw new ERR_INVALID_ARG_TYPE('fn', fn); - } - replacedImportMetaInitializer = true; - const parent = next; - next = (meta, context) => { - return fn(meta, context, parent); - }; - }); + // Calls the compiled preload source text gotten from the hook + // Since the parameters are named we use positional parameters + // see compileFunction above to cross reference the names + FunctionPrototypeCall( + preloadInit, + globalThis, + // Param getBuiltin + (builtinName) => { + if (NativeModule.canBeRequiredByUsers(builtinName)) { + return require(builtinName); + } + throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); + }, + // Param port + insidePreload, + // Param setImportMetaCallback + (fn) => { + if (finished || typeof fn !== 'function') { + throw new ERR_INVALID_ARG_TYPE('fn', fn); + } + replacedImportMetaInitializer = true; + const parent = next; + next = (meta, context) => { + return fn(meta, context, parent); + }; + }); } finally { finished = true; if (replacedImportMetaInitializer) { From 05e71c9a04f4f4283e50605e8a1ba86e85628bfe Mon Sep 17 00:00:00 2001 From: Bradley Farias Date: Wed, 24 Nov 2021 10:09:05 -0600 Subject: [PATCH 19/19] fixup: test doc clarification --- test/fixtures/es-module-loaders/mock-loader.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/fixtures/es-module-loaders/mock-loader.mjs b/test/fixtures/es-module-loaders/mock-loader.mjs index 641d0155cc4bff..4187137b105616 100644 --- a/test/fixtures/es-module-loaders/mock-loader.mjs +++ b/test/fixtures/es-module-loaders/mock-loader.mjs @@ -11,7 +11,12 @@ let currentMockVersion = 0; // module location is mocked. Since a single URL can be mocked multiple // times but it cannot be removed from the cache, `mock-facade:` URLs have a // form of mock-facade:$VERSION:$REPLACING_URL with the parameters being URL -// percent encoded every time a module is resolved +// percent encoded every time a module is resolved. So if a module for +// 'file:///app.js' is mocked it might look like +// 'mock-facade:12:file%3A%2F%2F%2Fapp.js'. This encoding is done to prevent +// problems like mocking URLs with special URL characters like '#' or '?' from +// accidentally being picked up as part of the 'mock-facade:' URL containing +// the mocked URL. // // NOTE: due to ESM spec, once a specifier has been resolved in a source text // it cannot be changed. So things like the following DO NOT WORK: