node/lib/internal/modules/esm/loader.js
Joyee Cheung 472edc775d
src: disambiguate terms used to refer to builtins and addons
The term "native module" dates back to some of the oldest code
in the code base. Within the context of Node.js core it usually
refers to modules that are native to Node.js (e.g. fs, http),
but it can cause confusion for people who don't work on this
part of the code base, as "native module" can also refer to
native addons - which is even the case in some of the API
docs and error messages.

This patch tries to make the usage of these terms more consistent.
Now within the context of Node.js core:

- JavaScript scripts that are built-in to Node.js are now referred
  to as "built-in(s)". If they are available as modules,
  they can also be referred to as "built-in module(s)".
- Dynamically-linked shared objects that are loaded into
  the Node.js processes are referred to as "addons".

We will try to avoid using the term "native modules" because it could
be ambiguous.

Changes in this patch:

File names:
- node_native_module.h -> node_builtins.h,
- node_native_module.cc -> node_builtins.cc

C++ binding names:
- `native_module` -> `builtins`

`node::Environment`:
- `native_modules_without_cache` -> `builtins_without_cache`
- `native_modules_with_cache` -> `builtins_with_cache`
- `native_modules_in_snapshot` -> `builtins_in_cache`
- `native_module_require` -> `builtin_module_require`

`node::EnvSerializeInfo`:
- `native_modules` -> `builtins

`node::native_module::NativeModuleLoader`:
- `native_module` namespace -> `builtins` namespace
- `NativeModuleLoader` -> `BuiltinLoader`
- `NativeModuleRecordMap` -> `BuiltinSourceMap`
- `NativeModuleCacheMap` -> `BuiltinCodeCacheMap`
- `ModuleIds` -> `BuiltinIds`
- `ModuleCategories` -> `BuiltinCategories`
- `LoadBuiltinModuleSource` -> `LoadBuiltinSource`

`loader.js`:
- `NativeModule` -> `BuiltinModule` (the `NativeModule` name used in
  `process.moduleLoadList` is kept for compatibility)

And other clarifications in the documentation and comments.

PR-URL: https://github.com/nodejs/node/pull/44135
Fixes: https://github.com/nodejs/node/issues/44036
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Michael Dawson <midawson@redhat.com>
Reviewed-By: Richard Lau <rlau@redhat.com>
Reviewed-By: Jiawen Geng <technicalcute@gmail.com>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Mohammed Keyvanzadeh <mohammadkeyvanzade94@gmail.com>
Reviewed-By: Tobias Nießen <tniessen@tnie.de>
Reviewed-By: Jan Krems <jan.krems@gmail.com>
2022-08-09 01:36:49 +08:00

902 lines
25 KiB
JavaScript

'use strict';
// This is needed to avoid cycles in esm/resolve <-> cjs/loader
require('internal/modules/cjs/loader');
const {
Array,
ArrayIsArray,
ArrayPrototypeJoin,
ArrayPrototypePush,
FunctionPrototypeCall,
ObjectAssign,
ObjectCreate,
ObjectDefineProperty,
ObjectSetPrototypeOf,
PromiseAll,
RegExpPrototypeExec,
SafeArrayIterator,
SafeWeakMap,
StringPrototypeSlice,
StringPrototypeToUpperCase,
globalThis,
} = primordials;
const { MessageChannel } = require('internal/worker/io');
const {
ERR_LOADER_CHAIN_INCOMPLETE,
ERR_INTERNAL_ASSERTION,
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_RETURN_PROPERTY_VALUE,
ERR_INVALID_RETURN_VALUE,
ERR_UNKNOWN_MODULE_FORMAT,
} = require('internal/errors').codes;
const { pathToFileURL, isURLInstance, URL } = require('internal/url');
const { emitExperimentalWarning } = require('internal/util');
const {
isAnyArrayBuffer,
isArrayBufferView,
} = require('internal/util/types');
const {
validateObject,
validateString,
} = require('internal/validators');
const ModuleMap = require('internal/modules/esm/module_map');
const ModuleJob = require('internal/modules/esm/module_job');
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');
const { getOptionValue } = require('internal/options');
/**
* @typedef {object} ExportedHooks
* @property {Function} globalPreload
* @property {Function} resolve
* @property {Function} load
*/
/**
* @typedef {Record<string, any>} ModuleExports
*/
/**
* @typedef {object} KeyedExports
* @property {ModuleExports} exports
* @property {URL['href']} url
*/
/**
* @typedef {object} KeyedHook
* @property {Function} fn
* @property {URL['href']} url
*/
/**
* @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat
*/
/**
* @typedef {ArrayBuffer|TypedArray|string} ModuleSource
*/
// [2] `validate...()`s throw the wrong error
let emittedSpecifierResolutionWarning = false;
/**
* A utility function to iterate through a hook chain, track advancement in the
* chain, and generate and supply the `next<HookName>` argument to the custom
* hook.
* @param {KeyedHook[]} chain The whole hook chain.
* @param {object} meta Properties that change as the current hook advances
* along the chain.
* @param {boolean} meta.chainFinished Whether the end of the chain has been
* reached AND invoked.
* @param {string} meta.hookErrIdentifier A user-facing identifier to help
* pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'".
* @param {number} meta.hookIndex A non-negative integer tracking the current
* position in the hook chain.
* @param {string} meta.hookName The kind of hook the chain is (ex 'resolve')
* @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit.
* @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function
* containing all validation of a custom loader hook's intermediary output. Any
* validation within MUST throw.
* @returns {function next<HookName>(...hookArgs)} The next hook in the chain.
*/
function nextHookFactory(chain, meta, { validateArgs, validateOutput }) {
// First, prepare the current
const { hookName } = meta;
const {
fn: hook,
url: hookFilePath,
} = chain[meta.hookIndex];
// ex 'nextResolve'
const nextHookName = `next${
StringPrototypeToUpperCase(hookName[0]) +
StringPrototypeSlice(hookName, 1)
}`;
// When hookIndex is 0, it's reached the default, which does not call next()
// so feed it a noop that blows up if called, so the problem is obvious.
const generatedHookIndex = meta.hookIndex;
let nextNextHook;
if (meta.hookIndex > 0) {
// Now, prepare the next: decrement the pointer so the next call to the
// factory generates the next link in the chain.
meta.hookIndex--;
nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput });
} else {
// eslint-disable-next-line func-name-matching
nextNextHook = function chainAdvancedTooFar() {
throw new ERR_INTERNAL_ASSERTION(
`ESM custom loader '${hookName}' advanced beyond the end of the chain.`
);
};
}
return ObjectDefineProperty(
async (arg0 = undefined, context) => {
// Update only when hook is invoked to avoid fingering the wrong filePath
meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`;
validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context);
const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`;
// Set when next<HookName> is actually called, not just generated.
if (generatedHookIndex === 0) { meta.chainFinished = true; }
if (context) { // `context` has already been validated, so no fancy check needed.
ObjectAssign(meta.context, context);
}
const output = await hook(arg0, meta.context, nextNextHook);
validateOutput(outputErrIdentifier, output);
if (output?.shortCircuit === true) { meta.shortCircuited = true; }
return output;
},
'name',
{ __proto__: null, value: nextHookName },
);
}
/**
* An ESMLoader 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.
*/
class ESMLoader {
/**
* Prior to ESM loading. These are called once before any modules are started.
* @private
* @property {KeyedHook[]} globalPreloaders Last-in-first-out
* list of preload hooks.
*/
#globalPreloaders = [];
/**
* Phase 2 of 2 in ESM loading.
* @private
* @property {KeyedHook[]} loaders Last-in-first-out
* collection of loader hooks.
*/
#loaders = [
{
fn: defaultLoad,
url: 'node:internal/modules/esm/load',
},
];
/**
* Phase 1 of 2 in ESM loading.
* @private
* @property {KeyedHook[]} resolvers Last-in-first-out
* collection of resolver hooks.
*/
#resolvers = [
{
fn: defaultResolve,
url: 'node:internal/modules/esm/resolve',
},
];
#importMetaInitializer = initializeImportMeta;
/**
* 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 loaded modules, akin to `require.cache`
*/
moduleMap = new ModuleMap();
/**
* Methods which translate input code or other information into ES modules
*/
translators = translators;
constructor() {
if (getOptionValue('--experimental-loader').length > 0) {
emitExperimentalWarning('Custom ESM Loaders');
}
if (getOptionValue('--experimental-network-imports')) {
emitExperimentalWarning('Network Imports');
}
if (
!emittedSpecifierResolutionWarning &&
getOptionValue('--experimental-specifier-resolution') === 'node'
) {
process.emitWarning(
'The Node.js specifier resolution flag is experimental. It could change or be removed at any time.',
'ExperimentalWarning'
);
emittedSpecifierResolutionWarning = true;
}
}
/**
*
* @param {ModuleExports} exports
* @returns {ExportedHooks}
*/
static pluckHooks({
globalPreload,
resolve,
load,
// obsolete hooks:
dynamicInstantiate,
getFormat,
getGlobalPreloadCode,
getSource,
transformSource,
}) {
const obsoleteHooks = [];
const acceptedHooks = ObjectCreate(null);
if (getGlobalPreloadCode) {
globalPreload ??= getGlobalPreloadCode;
process.emitWarning(
'Loader hook "getGlobalPreloadCode" has been renamed to "globalPreload"'
);
}
if (dynamicInstantiate) ArrayPrototypePush(
obsoleteHooks,
'dynamicInstantiate'
);
if (getFormat) ArrayPrototypePush(
obsoleteHooks,
'getFormat',
);
if (getSource) ArrayPrototypePush(
obsoleteHooks,
'getSource',
);
if (transformSource) ArrayPrototypePush(
obsoleteHooks,
'transformSource',
);
if (obsoleteHooks.length) process.emitWarning(
`Obsolete loader hook(s) supplied and will be ignored: ${
ArrayPrototypeJoin(obsoleteHooks, ', ')
}`,
'DeprecationWarning',
);
if (globalPreload) {
acceptedHooks.globalPreloader = globalPreload;
}
if (resolve) {
acceptedHooks.resolver = resolve;
}
if (load) {
acceptedHooks.loader = load;
}
return acceptedHooks;
}
/**
* Collect custom/user-defined hook(s). After all hooks have been collected,
* calls global preload hook(s).
* @param {KeyedExports} customLoaders
* A list of exports from user-defined loaders (as returned by
* ESMLoader.import()).
*/
addCustomLoaders(
customLoaders = [],
) {
for (let i = 0; i < customLoaders.length; i++) {
const {
exports,
url,
} = customLoaders[i];
const {
globalPreloader,
resolver,
loader,
} = ESMLoader.pluckHooks(exports);
if (globalPreloader) {
ArrayPrototypePush(
this.#globalPreloaders,
{
fn: globalPreloader,
url,
},
);
}
if (resolver) {
ArrayPrototypePush(
this.#resolvers,
{
fn: resolver,
url,
},
);
}
if (loader) {
ArrayPrototypePush(
this.#loaders,
{
fn: loader,
url,
},
);
}
}
this.preload();
}
async eval(
source,
url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href
) {
const evalInstance = (url) => {
const { ModuleWrap, callbackMap } = internalBinding('module_wrap');
const module = new ModuleWrap(url, undefined, source, 0, 0);
callbackMap.set(module, {
importModuleDynamically: (specifier, { url }, importAssertions) => {
return this.import(specifier, url, importAssertions);
}
});
return module;
};
const job = new ModuleJob(
this, url, undefined, evalInstance, false, false);
this.moduleMap.set(url, undefined, job);
const { module } = await job.run();
return {
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) {
let importAssertionsForResolve;
// By default, `this.#loaders` contains just the Node default load hook
if (this.#loaders.length !== 1) {
// We can skip cloning if there are no user-provided loaders because
// the Node.js default resolve hook does not use import assertions.
importAssertionsForResolve = ObjectAssign(
ObjectCreate(null),
importAssertions,
);
}
const { format, url } =
await this.resolve(specifier, parentURL, importAssertionsForResolve);
let job = this.moduleMap.get(url, importAssertions.type);
// CommonJS will set functions for lazy job evaluation.
if (typeof job === 'function') {
this.moduleMap.set(url, undefined, job = job());
}
if (job === undefined) {
job = this.#createModuleJob(url, importAssertions, parentURL, format);
}
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) {
const moduleProvider = async (url, isMain) => {
const {
format: finalFormat,
responseURL,
source,
} = await this.load(url, {
format,
importAssertions,
});
const translator = translators.get(finalFormat);
if (!translator) {
throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL);
}
return FunctionPrototypeCall(translator, this, responseURL, source, isMain);
};
const inspectBrk = (
parentURL === undefined &&
getOptionValue('--inspect-brk')
);
const job = new ModuleJob(
this,
url,
importAssertions,
moduleProvider,
parentURL === undefined,
inspectBrk
);
this.moduleMap.set(url, importAssertions.type, job);
return job;
}
/**
* This method is usually called indirectly as part of the loading processes.
* Internally, it is used directly to add loaders. Use directly with caution.
*
* This method must NOT be renamed: it functions as a dynamic import on a
* loader module.
*
* @param {string | string[]} specifiers Path(s) to the module.
* @param {string} parentURL Path of the parent importing the module.
* @param {Record<string, string>} importAssertions Validations for the
* module import.
* @returns {Promise<ExportedHooks | KeyedExports[]>}
* A collection of module export(s) or a list of collections of module
* export(s).
*/
async import(specifiers, parentURL, importAssertions) {
// For loaders, `import` is passed multiple things to process, it returns a
// list pairing the url and exports collected. This is especially useful for
// error messaging, to identity from where an export came. But, in most
// cases, only a single url is being "imported" (ex `import()`), so there is
// only 1 possible url from which the exports were collected and it is
// already known to the caller. Nesting that in a list would only ever
// create redundant work for the caller, so it is later popped off the
// internal list.
const wasArr = ArrayIsArray(specifiers);
if (!wasArr) { specifiers = [specifiers]; }
const count = specifiers.length;
const jobs = new Array(count);
for (let i = 0; i < count; i++) {
jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions)
.then((job) => job.run())
.then(({ module }) => module.getNamespace());
}
const namespaces = await PromiseAll(new SafeArrayIterator(jobs));
if (!wasArr) { return namespaces[0]; } // We can skip the pairing below
for (let i = 0; i < count; i++) {
const namespace = ObjectCreate(null);
namespace.url = specifiers[i];
namespace.exports = namespaces[i];
namespaces[i] = namespace;
}
return namespaces;
}
/**
* Provide source that is understood by one of Node's translators.
*
* Internally, this behaves like a backwards iterator, wherein the stack of
* hooks starts at the top and each call to `nextLoad()` moves down 1 step
* until it reaches the bottom or short-circuits.
*
* @param {URL['href']} url The URL/path of the module to be loaded
* @param {object} context Metadata about the module
* @returns {{ format: ModuleFormat, source: ModuleSource }}
*/
async load(url, context = {}) {
const chain = this.#loaders;
const meta = {
chainFinished: null,
context,
hookErrIdentifier: '',
hookIndex: chain.length - 1,
hookName: 'load',
shortCircuited: false,
};
const validateArgs = (hookErrIdentifier, nextUrl, ctx) => {
if (typeof nextUrl !== 'string') {
// non-strings can be coerced to a url string
// validateString() throws a less-specific error
throw new ERR_INVALID_ARG_TYPE(
`${hookErrIdentifier} url`,
'a url string',
nextUrl,
);
}
// Try to avoid expensive URL instantiation for known-good urls
if (!this.moduleMap.has(nextUrl)) {
try {
new URL(nextUrl);
} catch {
throw new ERR_INVALID_ARG_VALUE(
`${hookErrIdentifier} url`,
nextUrl,
'should be a url string',
);
}
}
if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
};
const validateOutput = (hookErrIdentifier, output) => {
if (typeof output !== 'object' || output === null) { // [2]
throw new ERR_INVALID_RETURN_VALUE(
'an object',
hookErrIdentifier,
output,
);
}
};
const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput });
const loaded = await nextLoad(url, context);
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
validateOutput(hookErrIdentifier, loaded);
if (loaded?.shortCircuit === true) { meta.shortCircuited = true; }
if (!meta.chainFinished && !meta.shortCircuited) {
throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
}
const {
format,
source,
} = loaded;
let responseURL = loaded.responseURL;
if (responseURL === undefined) {
responseURL = url;
}
let responseURLObj;
if (typeof responseURL === 'string') {
try {
responseURLObj = new URL(responseURL);
} catch {
// responseURLObj not defined will throw in next branch.
}
}
if (responseURLObj?.href !== responseURL) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'undefined or a fully resolved URL string',
hookErrIdentifier,
'responseURL',
responseURL,
);
}
if (format == null) {
const dataUrl = RegExpPrototypeExec(
/^data:([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/,
url,
);
throw new ERR_UNKNOWN_MODULE_FORMAT(
dataUrl ? dataUrl[1] : format,
url);
}
if (typeof format !== 'string') { // [2]
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string',
hookErrIdentifier,
'format',
format,
);
}
if (
source != null &&
typeof source !== 'string' &&
!isAnyArrayBuffer(source) &&
!isArrayBufferView(source)
) {
throw ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string, an ArrayBuffer, or a TypedArray',
hookErrIdentifier,
'source',
source
);
}
return {
format,
responseURL,
source,
};
}
preload() {
for (let i = this.#globalPreloaders.length - 1; i >= 0; i--) {
const channel = new MessageChannel();
const {
port1: insidePreload,
port2: insideLoader,
} = channel;
insidePreload.unref();
insideLoader.unref();
const {
fn: preloader,
url: specifier,
} = this.#globalPreloaders[i];
const preload = preloader({
port: insideLoader,
});
if (preload == null) { return; }
const hookErrIdentifier = `${specifier} globalPreload`;
if (typeof preload !== 'string') { // [2]
throw new ERR_INVALID_RETURN_VALUE(
'a string',
hookErrIdentifier,
preload,
);
}
const { compileFunction } = require('vm');
const preloadInit = compileFunction(
preload,
['getBuiltin', 'port', 'setImportMetaCallback'],
{
filename: '<preload>',
}
);
const { BuiltinModule } = 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 {
// 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 (BuiltinModule.canBeRequiredByUsers(builtinName) &&
BuiltinModule.canBeRequiredWithoutScheme(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) {
this.#importMetaInitializer = next;
}
}
}
}
importMetaInitialize(meta, context) {
this.#importMetaInitializer(meta, context);
}
/**
* Resolve the location of the module.
*
* Internally, this behaves like a backwards iterator, wherein the stack of
* hooks starts at the top and each call to `nextResolve()` moves down 1 step
* until it reaches the bottom or short-circuits.
*
* @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'] }}
*/
async resolve(
originalSpecifier,
parentURL,
importAssertions = ObjectCreate(null)
) {
const isMain = parentURL === undefined;
if (
!isMain &&
typeof parentURL !== 'string' &&
!isURLInstance(parentURL)
) {
throw new ERR_INVALID_ARG_TYPE(
'parentURL',
['string', 'URL'],
parentURL,
);
}
const chain = this.#resolvers;
const context = {
conditions: DEFAULT_CONDITIONS,
importAssertions,
parentURL,
};
const meta = {
chainFinished: null,
context,
hookErrIdentifier: '',
hookIndex: chain.length - 1,
hookName: 'resolve',
shortCircuited: false,
};
const validateArgs = (hookErrIdentifier, suppliedSpecifier, ctx) => {
validateString(
suppliedSpecifier,
`${hookErrIdentifier} specifier`,
); // non-strings can be coerced to a url string
if (ctx) validateObject(ctx, `${hookErrIdentifier} context`);
};
const validateOutput = (hookErrIdentifier, output) => {
if (typeof output !== 'object' || output === null) { // [2]
throw new ERR_INVALID_RETURN_VALUE(
'an object',
hookErrIdentifier,
output,
);
}
};
const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput });
const resolution = await nextResolve(originalSpecifier, context);
const { hookErrIdentifier } = meta; // Retrieve the value after all settled
validateOutput(hookErrIdentifier, resolution);
if (resolution?.shortCircuit === true) { meta.shortCircuited = true; }
if (!meta.chainFinished && !meta.shortCircuited) {
throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier);
}
const {
format,
url,
} = resolution;
if (
format != null &&
typeof format !== 'string' // [2]
) {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a string',
hookErrIdentifier,
'format',
format,
);
}
if (typeof url !== 'string') {
// non-strings can be coerced to a url string
// validateString() throws a less-specific error
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a url string',
hookErrIdentifier,
'url',
url,
);
}
// Try to avoid expensive URL instantiation for known-good urls
if (!this.moduleMap.has(url)) {
try {
new URL(url);
} catch {
throw new ERR_INVALID_RETURN_PROPERTY_VALUE(
'a url string',
hookErrIdentifier,
'url',
url,
);
}
}
return {
format,
url,
};
}
}
ObjectSetPrototypeOf(ESMLoader.prototype, null);
exports.ESMLoader = ESMLoader;