mirror of https://github.com/nodejs/node.git
module: add hook for global preload code
PR-URL: https://github.com/nodejs/node/pull/32068 Reviewed-By: Bradley Farias <bradley.meck@gmail.com> Reviewed-By: Geoffrey Booth <webmaster@geoffreybooth.com>pull/32078/head
parent
ecfb7b0988
commit
07a1fb953e
|
@ -1188,6 +1188,39 @@ export async function transformSource(source,
|
|||
}
|
||||
```
|
||||
|
||||
#### <code>getGlobalPreloadCode</code> hook
|
||||
|
||||
> Note: The loaders API is being redesigned. This hook may disappear or its
|
||||
> signature may change. Do not rely on the API described below.
|
||||
|
||||
Sometimes it can be necessary to run some code inside of the same global scope
|
||||
that the application will run in. This hook allows to return a string that will
|
||||
be ran as 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 will have to construct
|
||||
its own `require` using `module.createRequire()`.
|
||||
|
||||
```js
|
||||
/**
|
||||
* @returns {string} Code to run before application startup
|
||||
*/
|
||||
export function getGlobalPreloadCode() {
|
||||
return `\
|
||||
globalThis.someInjectedProperty = 42;
|
||||
console.log('I just set some globals!');
|
||||
|
||||
const { createRequire } = getBuiltin('module');
|
||||
|
||||
const require = createRequire(process.cwd + '/<preload>');
|
||||
// [...]
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
#### <code>dynamicInstantiate</code> hook
|
||||
|
||||
> Note: The loaders API is being redesigned. This hook may disappear or its
|
||||
|
|
|
@ -7,6 +7,7 @@ const {
|
|||
} = primordials;
|
||||
|
||||
const {
|
||||
ERR_INVALID_ARG_VALUE,
|
||||
ERR_INVALID_RETURN_PROPERTY,
|
||||
ERR_INVALID_RETURN_PROPERTY_VALUE,
|
||||
ERR_INVALID_RETURN_VALUE,
|
||||
|
@ -47,6 +48,14 @@ class Loader {
|
|||
// Map of already-loaded CJS modules to use
|
||||
this.cjsCache = new SafeMap();
|
||||
|
||||
// This hook is called before the first root module is imported. It's a
|
||||
// function that returns a piece of code that runs as a sloppy-mode script.
|
||||
// The script may evaluate to a function that can be called with a
|
||||
// `getBuiltin` helper that can be used to retrieve builtins.
|
||||
// If the hook returns `null` instead of a source string, it opts out of
|
||||
// running any preload code.
|
||||
// The preload code runs as soon as the hook module has finished evaluating.
|
||||
this._getGlobalPreloadCode = null;
|
||||
// The resolver has the signature
|
||||
// (specifier : string, parentURL : string, defaultResolve)
|
||||
// -> Promise<{ url : string }>
|
||||
|
@ -168,7 +177,16 @@ class Loader {
|
|||
return module.getNamespace();
|
||||
}
|
||||
|
||||
hook({ resolve, dynamicInstantiate, getFormat, getSource, transformSource }) {
|
||||
hook(hooks) {
|
||||
const {
|
||||
resolve,
|
||||
dynamicInstantiate,
|
||||
getFormat,
|
||||
getSource,
|
||||
transformSource,
|
||||
getGlobalPreloadCode,
|
||||
} = hooks;
|
||||
|
||||
// Use .bind() to avoid giving access to the Loader instance when called.
|
||||
if (resolve !== undefined)
|
||||
this._resolve = FunctionPrototypeBind(resolve, null);
|
||||
|
@ -185,6 +203,37 @@ class Loader {
|
|||
if (transformSource !== undefined) {
|
||||
this._transformSource = FunctionPrototypeBind(transformSource, null);
|
||||
}
|
||||
if (getGlobalPreloadCode !== undefined) {
|
||||
this._getGlobalPreloadCode =
|
||||
FunctionPrototypeBind(getGlobalPreloadCode, null);
|
||||
}
|
||||
}
|
||||
|
||||
runGlobalPreloadCode() {
|
||||
if (!this._getGlobalPreloadCode) {
|
||||
return;
|
||||
}
|
||||
const preloadCode = this._getGlobalPreloadCode();
|
||||
if (preloadCode === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof preloadCode !== 'string') {
|
||||
throw new ERR_INVALID_RETURN_VALUE(
|
||||
'string', 'loader getGlobalPreloadCode', preloadCode);
|
||||
}
|
||||
const { compileFunction } = require('vm');
|
||||
const preloadInit = compileFunction(preloadCode, ['getBuiltin'], {
|
||||
filename: '<preload>',
|
||||
});
|
||||
const { NativeModule } = require('internal/bootstrap/loaders');
|
||||
|
||||
preloadInit.call(globalThis, (builtinName) => {
|
||||
if (NativeModule.canBeRequiredByUsers(builtinName)) {
|
||||
return require(builtinName);
|
||||
}
|
||||
throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName);
|
||||
});
|
||||
}
|
||||
|
||||
async getModuleJob(specifier, parentURL) {
|
||||
|
|
|
@ -69,6 +69,7 @@ async function initializeLoader() {
|
|||
await ESMLoader.import(userLoader, pathToFileURL(cwd).href);
|
||||
ESMLoader = new Loader();
|
||||
ESMLoader.hook(hooks);
|
||||
ESMLoader.runGlobalPreloadCode();
|
||||
return exports.ESMLoader = ESMLoader;
|
||||
})();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
// 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');
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
/**
|
||||
* 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');
|
|
@ -0,0 +1,32 @@
|
|||
// Arrow function so it closes over the this-value of the preload scope.
|
||||
const globalPreload = () => {
|
||||
/* 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 getGlobalPreloadCode() {
|
||||
return `\
|
||||
<!-- assert: inside of script goal -->
|
||||
(${globalPreload.toString()})();
|
||||
`;
|
||||
}
|
Loading…
Reference in New Issue