mirror of
https://github.com/nodejs/node.git
synced 2025-05-08 20:39:30 +00:00

The current API shape si not great because it's too limited and redundant with the use of `MessagePort`. PR-URL: https://github.com/nodejs/node/pull/49529 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Jacob Smith <jacob@frende.me>
590 lines
20 KiB
JavaScript
590 lines
20 KiB
JavaScript
'use strict';
|
|
|
|
// This is needed to avoid cycles in esm/resolve <-> cjs/loader
|
|
require('internal/modules/cjs/loader');
|
|
|
|
const {
|
|
FunctionPrototypeCall,
|
|
ObjectSetPrototypeOf,
|
|
SafeWeakMap,
|
|
} = primordials;
|
|
|
|
const {
|
|
ERR_REQUIRE_ESM,
|
|
ERR_UNKNOWN_MODULE_FORMAT,
|
|
} = require('internal/errors').codes;
|
|
const { getOptionValue } = require('internal/options');
|
|
const { pathToFileURL } = require('internal/url');
|
|
const { emitExperimentalWarning } = require('internal/util');
|
|
const {
|
|
getDefaultConditions,
|
|
} = require('internal/modules/esm/utils');
|
|
let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer;
|
|
|
|
function newResolveCache() {
|
|
const { ResolveCache } = require('internal/modules/esm/module_map');
|
|
return new ResolveCache();
|
|
}
|
|
|
|
function newLoadCache() {
|
|
const { LoadCache } = require('internal/modules/esm/module_map');
|
|
return new LoadCache();
|
|
}
|
|
|
|
function getTranslators() {
|
|
const { translators } = require('internal/modules/esm/translators');
|
|
return translators;
|
|
}
|
|
|
|
/**
|
|
* @type {HooksProxy}
|
|
* Multiple loader instances exist for various, specific reasons (see code comments at site).
|
|
* In order to maintain consistency, we use a single worker (sandbox), which must sit apart of an
|
|
* individual loader instance.
|
|
*/
|
|
let hooksProxy;
|
|
|
|
/**
|
|
* @typedef {Record<string, any>} ModuleExports
|
|
*/
|
|
|
|
/**
|
|
* @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat
|
|
*/
|
|
|
|
/**
|
|
* @typedef {ArrayBuffer|TypedArray|string} ModuleSource
|
|
*/
|
|
|
|
/**
|
|
* This class covers the base machinery of module loading. To add custom
|
|
* behavior you can pass a customizations object and this object will be
|
|
* used to do the loading/resolving/registration process.
|
|
*/
|
|
class ModuleLoader {
|
|
/**
|
|
* The conditions for resolving packages if `--conditions` is not used.
|
|
*/
|
|
#defaultConditions = getDefaultConditions();
|
|
|
|
/**
|
|
* Map of already-loaded CJS modules to use
|
|
*/
|
|
cjsCache = new SafeWeakMap();
|
|
|
|
/**
|
|
* The index for assigning unique URLs to anonymous module evaluation
|
|
*/
|
|
evalIndex = 0;
|
|
|
|
/**
|
|
* Registry of resolved specifiers
|
|
*/
|
|
#resolveCache = newResolveCache();
|
|
|
|
/**
|
|
* Registry of loaded modules, akin to `require.cache`
|
|
*/
|
|
loadCache = newLoadCache();
|
|
|
|
/**
|
|
* Methods which translate input code or other information into ES modules
|
|
*/
|
|
translators = getTranslators();
|
|
|
|
/**
|
|
* Truthy to allow the use of `import.meta.resolve`. This is needed
|
|
* currently because the `Hooks` class does not have `resolveSync`
|
|
* implemented and `import.meta.resolve` requires it.
|
|
*/
|
|
allowImportMetaResolve;
|
|
|
|
/**
|
|
* Customizations to pass requests to.
|
|
*
|
|
* Note that this value _MUST_ be set with `setCustomizations`
|
|
* because it needs to copy `customizations.allowImportMetaResolve`
|
|
* to this property and failure to do so will cause undefined
|
|
* behavior when invoking `import.meta.resolve`.
|
|
* @see {ModuleLoader.setCustomizations}
|
|
*/
|
|
#customizations;
|
|
|
|
constructor(customizations) {
|
|
if (getOptionValue('--experimental-network-imports')) {
|
|
emitExperimentalWarning('Network Imports');
|
|
}
|
|
this.setCustomizations(customizations);
|
|
}
|
|
|
|
/**
|
|
* Change the currently activate customizations for this module
|
|
* loader to be the provided `customizations`.
|
|
*
|
|
* If present, this class customizes its core functionality to the
|
|
* `customizations` object, including registration, loading, and resolving.
|
|
* There are some responsibilities that this class _always_ takes
|
|
* care of, like validating outputs, so that the customizations object
|
|
* does not have to do so.
|
|
*
|
|
* The customizations object has the shape:
|
|
*
|
|
* ```ts
|
|
* interface LoadResult {
|
|
* format: ModuleFormat;
|
|
* source: ModuleSource;
|
|
* }
|
|
*
|
|
* interface ResolveResult {
|
|
* format: string;
|
|
* url: URL['href'];
|
|
* }
|
|
*
|
|
* interface Customizations {
|
|
* allowImportMetaResolve: boolean;
|
|
* load(url: string, context: object): Promise<LoadResult>
|
|
* resolve(
|
|
* originalSpecifier:
|
|
* string, parentURL: string,
|
|
* importAssertions: Record<string, string>
|
|
* ): Promise<ResolveResult>
|
|
* resolveSync(
|
|
* originalSpecifier:
|
|
* string, parentURL: string,
|
|
* importAssertions: Record<string, string>
|
|
* ) ResolveResult;
|
|
* register(specifier: string, parentURL: string): any;
|
|
* forceLoadHooks(): void;
|
|
* }
|
|
* ```
|
|
*
|
|
* Note that this class _also_ implements the `Customizations`
|
|
* interface, as does `CustomizedModuleLoader` and `Hooks`.
|
|
*
|
|
* Calling this function alters how modules are loaded and should be
|
|
* invoked with care.
|
|
* @param {object} customizations
|
|
*/
|
|
setCustomizations(customizations) {
|
|
this.#customizations = customizations;
|
|
if (customizations) {
|
|
this.allowImportMetaResolve = customizations.allowImportMetaResolve;
|
|
} else {
|
|
this.allowImportMetaResolve = true;
|
|
}
|
|
}
|
|
|
|
async eval(
|
|
source,
|
|
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href,
|
|
) {
|
|
const evalInstance = (url) => {
|
|
const { ModuleWrap } = internalBinding('module_wrap');
|
|
const { setCallbackForWrap } = require('internal/modules/esm/utils');
|
|
const module = new ModuleWrap(url, undefined, source, 0, 0);
|
|
setCallbackForWrap(module, {
|
|
initializeImportMeta: (meta, wrap) => this.importMetaInitialize(meta, { url }),
|
|
importModuleDynamically: (specifier, { url }, importAssertions) => {
|
|
return this.import(specifier, url, importAssertions);
|
|
},
|
|
});
|
|
|
|
return module;
|
|
};
|
|
const ModuleJob = require('internal/modules/esm/module_job');
|
|
const job = new ModuleJob(
|
|
this, url, undefined, evalInstance, false, false);
|
|
this.loadCache.set(url, undefined, job);
|
|
const { module } = await job.run();
|
|
|
|
return {
|
|
__proto__: null,
|
|
namespace: module.getNamespace(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get a (possibly still pending) module job from the cache,
|
|
* or create one and return its Promise.
|
|
* @param {string} specifier The string after `from` in an `import` statement,
|
|
* or the first parameter of an `import()`
|
|
* expression
|
|
* @param {string | undefined} parentURL The URL of the module importing this
|
|
* one, unless this is the Node.js entry
|
|
* point.
|
|
* @param {Record<string, string>} importAssertions Validations for the
|
|
* module import.
|
|
* @returns {Promise<ModuleJob>} The (possibly pending) module job
|
|
*/
|
|
async getModuleJob(specifier, parentURL, importAssertions) {
|
|
const resolveResult = await this.resolve(specifier, parentURL, importAssertions);
|
|
return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions);
|
|
}
|
|
|
|
getModuleJobSync(specifier, parentURL, importAssertions) {
|
|
const resolveResult = this.resolveSync(specifier, parentURL, importAssertions);
|
|
return this.getJobFromResolveResult(resolveResult, parentURL, importAssertions, true);
|
|
}
|
|
|
|
getJobFromResolveResult(resolveResult, parentURL, importAssertions, sync) {
|
|
const { url, format } = resolveResult;
|
|
const resolvedImportAssertions = resolveResult.importAssertions ?? importAssertions;
|
|
let job = this.loadCache.get(url, resolvedImportAssertions.type);
|
|
|
|
// CommonJS will set functions for lazy job evaluation.
|
|
if (typeof job === 'function') {
|
|
this.loadCache.set(url, undefined, job = job());
|
|
}
|
|
|
|
if (job === undefined) {
|
|
job = this.#createModuleJob(url, resolvedImportAssertions, parentURL, format, sync);
|
|
}
|
|
|
|
return job;
|
|
}
|
|
|
|
/**
|
|
* Create and cache an object representing a loaded module.
|
|
* @param {string} url The absolute URL that was resolved for this module
|
|
* @param {Record<string, string>} importAssertions Validations for the
|
|
* module import.
|
|
* @param {string} [parentURL] The absolute URL of the module importing this
|
|
* one, unless this is the Node.js entry point
|
|
* @param {string} [format] The format hint possibly returned by the
|
|
* `resolve` hook
|
|
* @returns {Promise<ModuleJob>} The (possibly pending) module job
|
|
*/
|
|
#createModuleJob(url, importAssertions, parentURL, format, sync) {
|
|
const callTranslator = ({ format: finalFormat, responseURL, source }, isMain) => {
|
|
const translator = getTranslators().get(finalFormat);
|
|
|
|
if (!translator) {
|
|
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL);
|
|
}
|
|
|
|
return FunctionPrototypeCall(translator, this, responseURL, source, isMain);
|
|
};
|
|
const context = { format, importAssertions };
|
|
const moduleProvider = sync ?
|
|
(url, isMain) => callTranslator(this.loadSync(url, context), isMain) :
|
|
async (url, isMain) => callTranslator(await this.load(url, context), isMain);
|
|
|
|
const inspectBrk = (
|
|
parentURL === undefined &&
|
|
getOptionValue('--inspect-brk')
|
|
);
|
|
|
|
if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) {
|
|
process.send({ 'watch:import': [url] });
|
|
}
|
|
|
|
const ModuleJob = require('internal/modules/esm/module_job');
|
|
const job = new ModuleJob(
|
|
this,
|
|
url,
|
|
importAssertions,
|
|
moduleProvider,
|
|
parentURL === undefined,
|
|
inspectBrk,
|
|
sync,
|
|
);
|
|
|
|
this.loadCache.set(url, importAssertions.type, job);
|
|
|
|
return job;
|
|
}
|
|
|
|
/**
|
|
* This method is usually called indirectly as part of the loading processes.
|
|
* Use directly with caution.
|
|
* @param {string} specifier The first parameter of an `import()` expression.
|
|
* @param {string} parentURL Path of the parent importing the module.
|
|
* @param {Record<string, string>} importAssertions Validations for the
|
|
* module import.
|
|
* @returns {Promise<ModuleExports>}
|
|
*/
|
|
async import(specifier, parentURL, importAssertions) {
|
|
const moduleJob = await this.getModuleJob(specifier, parentURL, importAssertions);
|
|
const { module } = await moduleJob.run();
|
|
return module.getNamespace();
|
|
}
|
|
|
|
/**
|
|
* @see {@link CustomizedModuleLoader.register}
|
|
*/
|
|
register(specifier, parentURL, data, transferList) {
|
|
if (!this.#customizations) {
|
|
// `CustomizedModuleLoader` is defined at the bottom of this file and
|
|
// available well before this line is ever invoked. This is here in
|
|
// order to preserve the git diff instead of moving the class.
|
|
// eslint-disable-next-line no-use-before-define
|
|
this.setCustomizations(new CustomizedModuleLoader());
|
|
}
|
|
return this.#customizations.register(specifier, parentURL, data, transferList);
|
|
}
|
|
|
|
/**
|
|
* Resolve the location of the module.
|
|
* @param {string} originalSpecifier The specified URL path of the module to
|
|
* be resolved.
|
|
* @param {string} [parentURL] The URL path of the module's parent.
|
|
* @param {ImportAssertions} importAssertions Assertions from the import
|
|
* statement or expression.
|
|
* @returns {{ format: string, url: URL['href'] }}
|
|
*/
|
|
resolve(originalSpecifier, parentURL, importAssertions) {
|
|
if (this.#customizations) {
|
|
return this.#customizations.resolve(originalSpecifier, parentURL, importAssertions);
|
|
}
|
|
const requestKey = this.#resolveCache.serializeKey(originalSpecifier, importAssertions);
|
|
const cachedResult = this.#resolveCache.get(requestKey, parentURL);
|
|
if (cachedResult != null) {
|
|
return cachedResult;
|
|
}
|
|
const result = this.defaultResolve(originalSpecifier, parentURL, importAssertions);
|
|
this.#resolveCache.set(requestKey, parentURL, result);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Just like `resolve` except synchronous. This is here specifically to support
|
|
* `import.meta.resolve` which must happen synchronously.
|
|
*/
|
|
resolveSync(originalSpecifier, parentURL, importAssertions) {
|
|
if (this.#customizations) {
|
|
return this.#customizations.resolveSync(originalSpecifier, parentURL, importAssertions);
|
|
}
|
|
return this.defaultResolve(originalSpecifier, parentURL, importAssertions);
|
|
}
|
|
|
|
/**
|
|
* Our `defaultResolve` is synchronous and can be used in both
|
|
* `resolve` and `resolveSync`. This function is here just to avoid
|
|
* repeating the same code block twice in those functions.
|
|
*/
|
|
defaultResolve(originalSpecifier, parentURL, importAssertions) {
|
|
defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve;
|
|
|
|
const context = {
|
|
__proto__: null,
|
|
conditions: this.#defaultConditions,
|
|
importAssertions,
|
|
parentURL,
|
|
};
|
|
|
|
return defaultResolve(originalSpecifier, context);
|
|
}
|
|
|
|
/**
|
|
* Provide source that is understood by one of Node's translators.
|
|
* @param {URL['href']} url The URL/path of the module to be loaded
|
|
* @param {object} [context] Metadata about the module
|
|
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
|
|
*/
|
|
async load(url, context) {
|
|
defaultLoad ??= require('internal/modules/esm/load').defaultLoad;
|
|
const result = this.#customizations ?
|
|
await this.#customizations.load(url, context) :
|
|
await defaultLoad(url, context);
|
|
this.validateLoadResult(url, result?.format);
|
|
return result;
|
|
}
|
|
|
|
loadSync(url, context) {
|
|
defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync;
|
|
|
|
let result = this.#customizations ?
|
|
this.#customizations.loadSync(url, context) :
|
|
defaultLoadSync(url, context);
|
|
let format = result?.format;
|
|
if (format === 'module') {
|
|
throw new ERR_REQUIRE_ESM(url, true);
|
|
}
|
|
if (format === 'commonjs') {
|
|
format = 'require-commonjs';
|
|
result = { __proto__: result, format };
|
|
}
|
|
this.validateLoadResult(url, format);
|
|
return result;
|
|
}
|
|
|
|
validateLoadResult(url, format) {
|
|
if (format == null) {
|
|
require('internal/modules/esm/load').throwUnknownModuleFormat(url, format);
|
|
}
|
|
}
|
|
|
|
importMetaInitialize(meta, context) {
|
|
if (this.#customizations) {
|
|
return this.#customizations.importMetaInitialize(meta, context, this);
|
|
}
|
|
importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta;
|
|
meta = importMetaInitializer(meta, context, this);
|
|
return meta;
|
|
}
|
|
|
|
/**
|
|
* No-op when no hooks have been supplied.
|
|
*/
|
|
forceLoadHooks() {
|
|
this.#customizations?.forceLoadHooks();
|
|
}
|
|
}
|
|
ObjectSetPrototypeOf(ModuleLoader.prototype, null);
|
|
|
|
class CustomizedModuleLoader {
|
|
|
|
allowImportMetaResolve = true;
|
|
|
|
/**
|
|
* Instantiate a module loader that uses user-provided custom loader hooks.
|
|
*/
|
|
constructor() {
|
|
getHooksProxy();
|
|
}
|
|
|
|
/**
|
|
* Register some loader specifier.
|
|
* @param {string} originalSpecifier The specified URL path of the loader to
|
|
* be registered.
|
|
* @param {string} parentURL The parent URL from where the loader will be
|
|
* registered if using it package name as specifier
|
|
* @param {any} [data] Arbitrary data to be passed from the custom loader
|
|
* (user-land) to the worker.
|
|
* @param {any[]} [transferList] Objects in `data` that are changing ownership
|
|
* @returns {{ format: string, url: URL['href'] }}
|
|
*/
|
|
register(originalSpecifier, parentURL, data, transferList) {
|
|
return hooksProxy.makeSyncRequest('register', transferList, originalSpecifier, parentURL, data);
|
|
}
|
|
|
|
/**
|
|
* Resolve the location of the module.
|
|
* @param {string} originalSpecifier The specified URL path of the module to
|
|
* be resolved.
|
|
* @param {string} [parentURL] The URL path of the module's parent.
|
|
* @param {ImportAssertions} importAssertions Assertions from the import
|
|
* statement or expression.
|
|
* @returns {{ format: string, url: URL['href'] }}
|
|
*/
|
|
resolve(originalSpecifier, parentURL, importAssertions) {
|
|
return hooksProxy.makeAsyncRequest('resolve', undefined, originalSpecifier, parentURL, importAssertions);
|
|
}
|
|
|
|
resolveSync(originalSpecifier, parentURL, importAssertions) {
|
|
// This happens only as a result of `import.meta.resolve` calls, which must be sync per spec.
|
|
return hooksProxy.makeSyncRequest('resolve', undefined, originalSpecifier, parentURL, importAssertions);
|
|
}
|
|
|
|
/**
|
|
* Provide source that is understood by one of Node's translators.
|
|
* @param {URL['href']} url The URL/path of the module to be loaded
|
|
* @param {object} [context] Metadata about the module
|
|
* @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>}
|
|
*/
|
|
load(url, context) {
|
|
return hooksProxy.makeAsyncRequest('load', undefined, url, context);
|
|
}
|
|
loadSync(url, context) {
|
|
return hooksProxy.makeSyncRequest('load', undefined, url, context);
|
|
}
|
|
|
|
importMetaInitialize(meta, context, loader) {
|
|
hooksProxy.importMetaInitialize(meta, context, loader);
|
|
}
|
|
|
|
forceLoadHooks() {
|
|
hooksProxy.waitForWorker();
|
|
}
|
|
}
|
|
|
|
let emittedExperimentalWarning = false;
|
|
/**
|
|
* A loader instance is used as the main entry point for loading ES modules. Currently, this is a singleton; there is
|
|
* only one used for loading the main module and everything in its dependency graph, though separate instances of this
|
|
* class might be instantiated as part of bootstrap for other purposes.
|
|
* @param {boolean} useCustomLoadersIfPresent If the user has provided loaders via the --loader flag, use them.
|
|
* @returns {ModuleLoader}
|
|
*/
|
|
function createModuleLoader(useCustomLoadersIfPresent = true) {
|
|
let customizations = null;
|
|
if (useCustomLoadersIfPresent &&
|
|
// Don't spawn a new worker if we're already in a worker thread created by instantiating CustomizedModuleLoader;
|
|
// doing so would cause an infinite loop.
|
|
!require('internal/modules/esm/utils').isLoaderWorker()) {
|
|
const userLoaderPaths = getOptionValue('--experimental-loader');
|
|
if (userLoaderPaths.length > 0) {
|
|
if (!emittedExperimentalWarning) {
|
|
emitExperimentalWarning('Custom ESM Loaders');
|
|
emittedExperimentalWarning = true;
|
|
}
|
|
customizations = new CustomizedModuleLoader();
|
|
}
|
|
}
|
|
|
|
return new ModuleLoader(customizations);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get the HooksProxy instance. If it is not defined, then create a new one.
|
|
* @returns {HooksProxy}
|
|
*/
|
|
function getHooksProxy() {
|
|
if (!hooksProxy) {
|
|
const { HooksProxy } = require('internal/modules/esm/hooks');
|
|
hooksProxy = new HooksProxy();
|
|
}
|
|
|
|
return hooksProxy;
|
|
}
|
|
|
|
/**
|
|
* Register a single loader programmatically.
|
|
* @param {string} specifier
|
|
* @param {string} [parentURL] Base to use when resolving `specifier`; optional if
|
|
* `specifier` is absolute. Same as `options.parentUrl`, just inline
|
|
* @param {object} [options] Additional options to apply, described below.
|
|
* @param {string} [options.parentURL] Base to use when resolving `specifier`
|
|
* @param {any} [options.data] Arbitrary data passed to the loader's `initialize` hook
|
|
* @param {any[]} [options.transferList] Objects in `data` that are changing ownership
|
|
* @returns {void} We want to reserve the return value for potential future extension of the API.
|
|
* @example
|
|
* ```js
|
|
* register('./myLoader.js');
|
|
* register('ts-node/esm', { parentURL: import.meta.url });
|
|
* register('./myLoader.js', { parentURL: import.meta.url });
|
|
* register('ts-node/esm', import.meta.url);
|
|
* register('./myLoader.js', import.meta.url);
|
|
* register(new URL('./myLoader.js', import.meta.url));
|
|
* register('./myLoader.js', {
|
|
* parentURL: import.meta.url,
|
|
* data: { banana: 'tasty' },
|
|
* });
|
|
* register('./myLoader.js', {
|
|
* parentURL: import.meta.url,
|
|
* data: someArrayBuffer,
|
|
* transferList: [someArrayBuffer],
|
|
* });
|
|
* ```
|
|
*/
|
|
function register(specifier, parentURL = undefined, options) {
|
|
const moduleLoader = require('internal/process/esm_loader').esmLoader;
|
|
if (parentURL != null && typeof parentURL === 'object') {
|
|
options = parentURL;
|
|
parentURL = options.parentURL;
|
|
}
|
|
moduleLoader.register(
|
|
`${specifier}`,
|
|
parentURL ?? 'data:',
|
|
options?.data,
|
|
options?.transferList,
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
createModuleLoader,
|
|
getHooksProxy,
|
|
register,
|
|
};
|