Skip to content

Commit

Permalink
esm: remove globalPreload hook (superseded by initialize)
Browse files Browse the repository at this point in the history
PR-URL: nodejs#49144
Reviewed-By: Geoffrey Booth <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
  • Loading branch information
JakobJingleheimer authored and alexfernandez committed Nov 1, 2023
1 parent ff715fe commit 7d3c468
Show file tree
Hide file tree
Showing 9 changed files with 31 additions and 429 deletions.
78 changes: 3 additions & 75 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ import('node:fs').then((esmFS) => {
<!-- YAML
added: v8.8.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/49144
description: Removed `globalPreload`.
- version: v20.6.0
pr-url: https://github.com/nodejs/node/pull/48842
description: Added `initialize` hook to replace `globalPreload`.
Expand Down Expand Up @@ -674,79 +677,6 @@ export async function load(url, context, nextLoad) {
In a more advanced scenario, this can also be used to transform an unsupported
source to a supported one (see [Examples](#examples) below).
#### `globalPreload()`
<!-- YAML
changes:
- version:
- v18.6.0
- v16.17.0
pr-url: https://github.com/nodejs/node/pull/42623
description: Add support for chaining globalPreload hooks.
-->
> 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() + '/<preload>');
// [...]
`;
}
```
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
Expand Down Expand Up @@ -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
Expand Down
138 changes: 12 additions & 126 deletions lib/internal/modules/esm/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@
const {
ArrayPrototypePush,
ArrayPrototypePushApply,
FunctionPrototypeCall,
Int32Array,
ObjectAssign,
ObjectDefineProperty,
ObjectSetPrototypeOf,
Promise,
SafeSet,
StringPrototypeSlice,
StringPrototypeStartsWith,
StringPrototypeToUpperCase,
globalThis,
} = primordials;
Expand All @@ -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');
Expand All @@ -49,7 +46,6 @@ const {
validateString,
} = require('internal/validators');
const {
emitExperimentalWarning,
kEmptyObject,
} = require('internal/util');

Expand All @@ -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.
*/
Expand All @@ -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.
Expand Down Expand Up @@ -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<string, unknown>} exports
* @param {any} [data] Arbitrary data to be passed from the custom loader (user-land)
Expand All @@ -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 });
Expand All @@ -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.
*
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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: '<preload>',
},
);
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
Expand All @@ -750,9 +633,6 @@ function pluckHooks({
}) {
const acceptedHooks = { __proto__: null };

if (globalPreload) {
acceptedHooks.globalPreload = globalPreload;
}
if (resolve) {
acceptedHooks.resolve = resolve;
}
Expand All @@ -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;
Expand Down
4 changes: 1 addition & 3 deletions lib/internal/modules/esm/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,7 @@ async function initializeHooks() {
);
}

const preloadScripts = hooks.initializeGlobalPreload();

return { __proto__: null, hooks, preloadScripts };
return hooks;
}

module.exports = {
Expand Down
9 changes: 4 additions & 5 deletions lib/internal/modules/esm/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

{
Expand All @@ -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
Expand All @@ -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.
Expand Down
Loading

0 comments on commit 7d3c468

Please sign in to comment.