diff --git a/doc/api/module.md b/doc/api/module.md index 0c20a90b43f52c..7cc5acb52793ae 100644 --- a/doc/api/module.md +++ b/doc/api/module.md @@ -149,6 +149,9 @@ import('node:fs').then((esmFS) => { - -> Stability: 1.0 - Early development - -> **Warning:** This hook will be removed in a future version. Use -> [`initialize`][] instead. When a hooks module has an `initialize` export, -> `globalPreload` will be ignored. - -* `context` {Object} Information to assist the preload code - * `port` {MessagePort} -* Returns: {string} Code to run before application startup - -Sometimes it might be necessary to run some code inside of the same global -scope that the application runs in. This hook allows the return of a string -that is run as a sloppy-mode script on startup. - -Similar to how CommonJS wrappers work, the code runs in an implicit function -scope. The only argument is a `require`-like function that can be used to load -builtins like "fs": `getBuiltin(request: string)`. - -If the code needs more advanced `require` features, it has to construct -its own `require` using `module.createRequire()`. - -```mjs -export function globalPreload(context) { - return `\ -globalThis.someInjectedProperty = 42; -console.log('I just set some globals!'); - -const { createRequire } = getBuiltin('module'); -const { cwd } = getBuiltin('process'); - -const require = createRequire(cwd() + '/'); -// [...] -`; -} -``` - -Another argument is provided to the preload code: `port`. This is available as a -parameter to the hook and inside of the source text returned by the hook. This -functionality has been moved to the `initialize` hook. - -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. - -```mjs -/** - * This example has the application context send a message to the hook - * and sends the message back to the application context - */ -export function globalPreload({ port }) { - port.onmessage = (evt) => { - port.postMessage(evt.data); - }; - return `\ - port.postMessage('console.log("I went to the hook and back");'); - port.onmessage = (evt) => { - eval(evt.data); - }; - `; -} -``` - ### Examples The various module customization hooks can be used together to accomplish @@ -1105,8 +1035,6 @@ returned object contains the following keys: [`Uint8Array`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array [`initialize`]: #initialize [`module`]: modules.md#the-module-object -[`port.ref()`]: worker_threads.md#portref -[`port.unref()`]: worker_threads.md#portunref [`register`]: #moduleregisterspecifier-parenturl-options [`string`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String [`util.TextDecoder`]: util.md#class-utiltextdecoder diff --git a/lib/internal/modules/esm/hooks.js b/lib/internal/modules/esm/hooks.js index 05885050b82fc9..87325ef4f71edf 100644 --- a/lib/internal/modules/esm/hooks.js +++ b/lib/internal/modules/esm/hooks.js @@ -3,7 +3,6 @@ const { ArrayPrototypePush, ArrayPrototypePushApply, - FunctionPrototypeCall, Int32Array, ObjectAssign, ObjectDefineProperty, @@ -11,7 +10,6 @@ const { Promise, SafeSet, StringPrototypeSlice, - StringPrototypeStartsWith, StringPrototypeToUpperCase, globalThis, } = primordials; @@ -33,7 +31,6 @@ const { ERR_INVALID_RETURN_VALUE, ERR_LOADER_CHAIN_INCOMPLETE, ERR_METHOD_NOT_IMPLEMENTED, - ERR_UNKNOWN_BUILTIN_MODULE, ERR_WORKER_UNSERIALIZABLE_ERROR, } = require('internal/errors').codes; const { exitCodes: { kUnfinishedTopLevelAwait } } = internalBinding('errors'); @@ -49,7 +46,6 @@ const { validateString, } = require('internal/validators'); const { - emitExperimentalWarning, kEmptyObject, } = require('internal/util'); @@ -73,8 +69,6 @@ let importMetaInitializer; /** * @typedef {object} ExportedHooks - * @property {Function} initialize Customizations setup hook. - * @property {Function} globalPreload Global preload hook. * @property {Function} resolve Resolve hook. * @property {Function} load Load hook. */ @@ -89,13 +83,6 @@ let importMetaInitializer; class Hooks { #chains = { - /** - * Prior to ESM loading. These are called once before any modules are started. - * @private - * @property {KeyedHook[]} globalPreload Last-in-first-out list of preload hooks. - */ - globalPreload: [], - /** * Phase 1 of 2 in ESM loading. * The output of the `resolve` chain of hooks is passed into the `load` chain of hooks. @@ -146,7 +133,6 @@ class Hooks { /** * Collect custom/user-defined module loader hook(s). - * After all hooks have been collected, the global preload hook(s) must be initialized. * @param {string} url Custom loader specifier * @param {Record} exports * @param {any} [data] Arbitrary data to be passed from the custom loader (user-land) @@ -155,18 +141,11 @@ class Hooks { */ addCustomLoader(url, exports, data) { const { - globalPreload, initialize, resolve, load, } = pluckHooks(exports); - if (globalPreload && !initialize) { - emitExperimentalWarning( - '`globalPreload` is planned for removal in favor of `initialize`. `globalPreload`', - ); - ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url }); - } if (resolve) { const next = this.#chains.resolve[this.#chains.resolve.length - 1]; ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next }); @@ -178,49 +157,6 @@ class Hooks { return initialize?.(data); } - /** - * Initialize `globalPreload` hooks. - */ - initializeGlobalPreload() { - const preloadScripts = []; - for (let i = this.#chains.globalPreload.length - 1; i >= 0; i--) { - const { MessageChannel } = require('internal/worker/io'); - const channel = new MessageChannel(); - const { - port1: insidePreload, - port2: insideLoader, - } = channel; - - insidePreload.unref(); - insideLoader.unref(); - - const { - fn: preload, - url: specifier, - } = this.#chains.globalPreload[i]; - - const preloaded = preload({ - port: insideLoader, - }); - - if (preloaded == null) { continue; } - - if (typeof preloaded !== 'string') { // [2] - throw new ERR_INVALID_RETURN_VALUE( - 'a string', - `${specifier} globalPreload`, - preload, - ); - } - - ArrayPrototypePush(preloadScripts, { - code: preloaded, - port: insidePreload, - }); - } - return preloadScripts; - } - /** * Resolve the location of the module. * @@ -559,8 +495,9 @@ class HooksProxy { AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0); const response = this.#worker.receiveMessageSync(); if (response == null || response.message.status === 'exit') { return; } - const { preloadScripts } = this.#unwrapMessage(response); - this.#executePreloadScripts(preloadScripts); + + // ! This line catches initialization errors in the worker thread. + this.#unwrapMessage(response); } this.#isReady = true; @@ -677,66 +614,12 @@ class HooksProxy { importMetaInitialize(meta, context, loader) { this.#importMetaInitializer(meta, context, loader); } - - #executePreloadScripts(preloadScripts) { - for (let i = 0; i < preloadScripts.length; i++) { - const { code, port } = preloadScripts[i]; - const { compileFunction } = require('vm'); - const preloadInit = compileFunction( - code, - ['getBuiltin', 'port', 'setImportMetaCallback'], - { - filename: '', - }, - ); - let finished = false; - let replacedImportMetaInitializer = false; - let next = this.#importMetaInitializer; - const { BuiltinModule } = require('internal/bootstrap/realm'); - // 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 - try { - FunctionPrototypeCall( - preloadInit, - globalThis, - // Param getBuiltin - (builtinName) => { - if (StringPrototypeStartsWith(builtinName, 'node:')) { - builtinName = StringPrototypeSlice(builtinName, 5); - } else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { - throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); - } - if (BuiltinModule.canBeRequiredByUsers(builtinName)) { - return require(builtinName); - } - throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); - }, - // Param port - port, - // 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) { - this.#importMetaInitializer = next; - } - } - } - } } ObjectSetPrototypeOf(HooksProxy.prototype, null); +// TODO(JakobJingleheimer): Remove this when loaders go "stable". +let globalPreloadWarningWasEmitted = false; + /** * A utility function to pluck the hooks from a user-defined loader. * @param {import('./loader.js).ModuleExports} exports @@ -750,9 +633,6 @@ function pluckHooks({ }) { const acceptedHooks = { __proto__: null }; - if (globalPreload) { - acceptedHooks.globalPreload = globalPreload; - } if (resolve) { acceptedHooks.resolve = resolve; } @@ -762,6 +642,12 @@ function pluckHooks({ if (initialize) { acceptedHooks.initialize = initialize; + } else if (globalPreload && !globalPreloadWarningWasEmitted) { + process.emitWarning( + '`globalPreload` has been removed; use `initialize` instead.', + 'UnsupportedWarning', + ); + globalPreloadWarningWasEmitted = true; } return acceptedHooks; diff --git a/lib/internal/modules/esm/utils.js b/lib/internal/modules/esm/utils.js index 5014c99b2a9eb3..74756857f70044 100644 --- a/lib/internal/modules/esm/utils.js +++ b/lib/internal/modules/esm/utils.js @@ -145,9 +145,7 @@ async function initializeHooks() { ); } - const preloadScripts = hooks.initializeGlobalPreload(); - - return { __proto__: null, hooks, preloadScripts }; + return hooks; } module.exports = { diff --git a/lib/internal/modules/esm/worker.js b/lib/internal/modules/esm/worker.js index f31820aa8f8d29..0750a80a56ab78 100644 --- a/lib/internal/modules/esm/worker.js +++ b/lib/internal/modules/esm/worker.js @@ -74,7 +74,8 @@ function wrapMessage(status, body) { } async function customizedModuleWorker(lock, syncCommPort, errorHandler) { - let hooks, preloadScripts, initializationError; + let hooks; + let initializationError; let hasInitializationError = false; { @@ -91,9 +92,7 @@ async function customizedModuleWorker(lock, syncCommPort, errorHandler) { try { - const initResult = await initializeHooks(); - hooks = initResult.hooks; - preloadScripts = initResult.preloadScripts; + hooks = await initializeHooks(); } catch (exception) { // If there was an error while parsing and executing a user loader, for example if because a // loader contained a syntax error, then we need to send the error to the main thread so it can @@ -107,7 +106,7 @@ async function customizedModuleWorker(lock, syncCommPort, errorHandler) { if (hasInitializationError) { syncCommPort.postMessage(wrapMessage('error', initializationError)); } else { - syncCommPort.postMessage(wrapMessage('success', { preloadScripts }), preloadScripts.map(({ port }) => port)); + syncCommPort.postMessage(wrapMessage('success')); } // We're ready, so unlock the main thread. diff --git a/test/es-module/test-esm-loader-globalpreload-hook.mjs b/test/es-module/test-esm-loader-globalpreload-hook.mjs deleted file mode 100644 index 87def31fb3d0ea..00000000000000 --- a/test/es-module/test-esm-loader-globalpreload-hook.mjs +++ /dev/null @@ -1,149 +0,0 @@ -import { spawnPromisified } from '../common/index.mjs'; -import * as fixtures from '../common/fixtures.mjs'; -import assert from 'node:assert'; -import os from 'node:os'; -import { execPath } from 'node:process'; -import { describe, it } from 'node:test'; - -describe('globalPreload hook', () => { - it('should not emit deprecation warning when initialize is supplied', async () => { - const { stderr } = await spawnPromisified(execPath, [ - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){}export function initialize(){}', - fixtures.path('empty.js'), - ]); - - assert.doesNotMatch(stderr, /`globalPreload` is an experimental feature/); - }); - - it('should handle globalPreload returning undefined', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){}', - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should handle loading node:test', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){return `getBuiltin("node:test")()`}', - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.match(stdout, /\n# pass 1\r?\n/); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should handle loading node:os with node: prefix', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("node:os").arch())`}', - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout.trim(), os.arch()); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - // `os` is used here because it's simple and not mocked (the builtin module otherwise doesn't matter). - it('should handle loading builtin module without node: prefix', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){return `console.log(getBuiltin("os").arch())`}', - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout.trim(), os.arch()); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should throw when loading node:test without node: prefix', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){return `getBuiltin("test")()`}', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /ERR_UNKNOWN_BUILTIN_MODULE/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should register globals set from globalPreload', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){return "this.myGlobal=4"}', - '--print', 'myGlobal', - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout.trim(), '4'); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should log console.log calls returned from globalPreload', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){return `console.log("Hello from globalPreload")`}', - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout.trim(), 'Hello from globalPreload'); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); - - it('should crash if globalPreload returns code that throws', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - 'data:text/javascript,export function globalPreload(){return `throw new Error("error from globalPreload")`}', - fixtures.path('empty.js'), - ]); - - assert.match(stderr, /error from globalPreload/); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 1); - assert.strictEqual(signal, null); - }); - - it('should have a `this` value that is not bound to the loader instance', async () => { - const { code, signal, stdout, stderr } = await spawnPromisified(execPath, [ - '--no-warnings', - '--experimental-loader', - `data:text/javascript,export ${function globalPreload() { - if (this != null) { - throw new Error('hook function must not be bound to ESMLoader instance'); - } - }}`, - fixtures.path('empty.js'), - ]); - - assert.strictEqual(stderr, ''); - assert.strictEqual(stdout, ''); - assert.strictEqual(code, 0); - assert.strictEqual(signal, null); - }); -}); diff --git a/test/es-module/test-esm-loader-hooks.mjs b/test/es-module/test-esm-loader-hooks.mjs index c6cc2e9778eee6..7ff1fc520d90b5 100644 --- a/test/es-module/test-esm-loader-hooks.mjs +++ b/test/es-module/test-esm-loader-hooks.mjs @@ -431,7 +431,17 @@ describe('Loader hooks', { concurrency: true }, () => { fixtures.path('empty.js'), ]); - assert.strictEqual(stderr.match(/`globalPreload` is an experimental feature/g).length, 1); + assert.strictEqual(stderr.match(/`globalPreload` has been removed; use `initialize` instead/g).length, 1); + }); + + it('should not emit warning when initialize is supplied', async () => { + const { stderr } = await spawnPromisified(execPath, [ + '--experimental-loader', + 'data:text/javascript,export function globalPreload(){}export function initialize(){}', + fixtures.path('empty.js'), + ]); + + assert.doesNotMatch(stderr, /`globalPreload` has been removed; use `initialize` instead/); }); }); diff --git a/test/es-module/test-esm-loader-side-effect.mjs b/test/es-module/test-esm-loader-side-effect.mjs deleted file mode 100644 index f76b10700ddc8f..00000000000000 --- a/test/es-module/test-esm-loader-side-effect.mjs +++ /dev/null @@ -1,32 +0,0 @@ -// Flags: --experimental-loader ./test/fixtures/es-module-loaders/loader-side-effect.mjs --require ./test/fixtures/es-module-loaders/loader-side-effect-require-preload.js -import { allowGlobals, mustCall } from '../common/index.mjs'; -import assert from 'assert'; -import { fileURLToPath } from 'url'; -import { Worker, isMainThread, parentPort } from 'worker_threads'; - -/* global implicitGlobalProperty */ -assert.strictEqual(globalThis.implicitGlobalProperty, 42); -allowGlobals(implicitGlobalProperty); - -/* global implicitGlobalConst */ -assert.strictEqual(implicitGlobalConst, 42 * 42); -allowGlobals(implicitGlobalConst); - -/* global explicitGlobalProperty */ -assert.strictEqual(globalThis.explicitGlobalProperty, 42 * 42 * 42); -allowGlobals(explicitGlobalProperty); - -/* global preloadOrder */ -assert.deepStrictEqual(globalThis.preloadOrder, ['--require', 'loader']); -allowGlobals(preloadOrder); - -if (isMainThread) { - const worker = new Worker(fileURLToPath(import.meta.url)); - const promise = new Promise((resolve, reject) => { - worker.on('message', resolve); - worker.on('error', reject); - }); - promise.then(mustCall()); -} else { - parentPort.postMessage('worker done'); -} diff --git a/test/fixtures/es-module-loaders/loader-side-effect-require-preload.js b/test/fixtures/es-module-loaders/loader-side-effect-require-preload.js deleted file mode 100644 index 98820b9379748e..00000000000000 --- a/test/fixtures/es-module-loaders/loader-side-effect-require-preload.js +++ /dev/null @@ -1,6 +0,0 @@ -/** - * This file is combined with `loader-side-effect.mjs` via `--require`. Its - * purpose is to test execution order of the two kinds of preload code. - */ - -(globalThis.preloadOrder || (globalThis.preloadOrder = [])).push('--require'); diff --git a/test/fixtures/es-module-loaders/loader-side-effect.mjs b/test/fixtures/es-module-loaders/loader-side-effect.mjs deleted file mode 100644 index e91cdea0527881..00000000000000 --- a/test/fixtures/es-module-loaders/loader-side-effect.mjs +++ /dev/null @@ -1,32 +0,0 @@ -// Arrow function so it closes over the this-value of the preload scope. -const globalPreloadSrc = () => { - /* global getBuiltin */ - const assert = getBuiltin('assert'); - const vm = getBuiltin('vm'); - - assert.strictEqual(typeof require, 'undefined'); - assert.strictEqual(typeof module, 'undefined'); - assert.strictEqual(typeof exports, 'undefined'); - assert.strictEqual(typeof __filename, 'undefined'); - assert.strictEqual(typeof __dirname, 'undefined'); - - assert.strictEqual(this, globalThis); - (globalThis.preloadOrder || (globalThis.preloadOrder = [])).push('loader'); - - vm.runInThisContext(`\ -var implicitGlobalProperty = 42; -const implicitGlobalConst = 42 * 42; -`); - - assert.strictEqual(globalThis.implicitGlobalProperty, 42); - (implicitGlobalProperty).foo = 'bar'; // assert: not strict mode - - globalThis.explicitGlobalProperty = 42 * 42 * 42; -} - -export function globalPreload() { - return `\ - -(${globalPreloadSrc.toString()})(); -`; -}