'use strict'; // This is needed to avoid cycles in esm/resolve <-> cjs/loader require('internal/modules/cjs/loader'); const { Array, ArrayIsArray, ArrayPrototypeJoin, ArrayPrototypePush, FunctionPrototypeBind, FunctionPrototypeCall, ObjectAssign, ObjectCreate, ObjectDefineProperty, ObjectSetPrototypeOf, PromiseAll, ReflectApply, 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} 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` 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(...hookArgs)} The next hook in the chain. */ function nextHookFactory(chain, meta, validate) { // 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, validate); } 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 (...args) => { // Update only when hook is invoked to avoid fingering the wrong filePath meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`; validate(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, args); // Set when next is actually called, not just generated. if (generatedHookIndex === 0) { meta.chainFinished = true; } ArrayPrototypePush(args, nextNextHook); const output = await ReflectApply(hook, undefined, args); 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', ); // Use .bind() to avoid giving access to the Loader instance when called. if (globalPreload) { acceptedHooks.globalPreloader = FunctionPrototypeBind(globalPreload, null); } if (resolve) { acceptedHooks.resolver = FunctionPrototypeBind(resolve, null); } if (load) { acceptedHooks.loader = FunctionPrototypeBind(load, null); } 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()). */ async 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: FunctionPrototypeBind(globalPreloader), // [1] url, }, ); } if (resolver) { ArrayPrototypePush( this.#resolvers, { fn: FunctionPrototypeBind(resolver), // [1] url, }, ); } if (loader) { ArrayPrototypePush( this.#loaders, { fn: FunctionPrototypeBind(loader), // [1] url, }, ); } } // [1] ensure hook function is not bound to ESMLoader instance 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} importAssertions Validations for the * module import. * @returns {Promise} 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} 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} 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} importAssertions Validations for the * module import. * @returns {Promise} * 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, hookErrIdentifier: '', hookIndex: chain.length - 1, hookName: 'load', shortCircuited: false, }; const validate = (hookErrIdentifier, { 0: nextUrl, 1: 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', ); } } validateObject(ctx, `${hookErrIdentifier} context`); }; const nextLoad = nextHookFactory(chain, meta, validate); const loaded = await nextLoad(url, context); const { hookErrIdentifier } = meta; // Retrieve the value after all settled if (typeof loaded !== 'object') { // [2] throw new ERR_INVALID_RETURN_VALUE( 'an object', 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: '', } ); const { NativeModule } = 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 (NativeModule.canBeRequiredByUsers(builtinName) && NativeModule.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 meta = { chainFinished: null, hookErrIdentifier: '', hookIndex: chain.length - 1, hookName: 'resolve', shortCircuited: false, }; const context = { conditions: DEFAULT_CONDITIONS, importAssertions, parentURL, }; const validate = (hookErrIdentifier, { 0: suppliedSpecifier, 1: ctx }) => { validateString( suppliedSpecifier, `${hookErrIdentifier} specifier`, ); // non-strings can be coerced to a url string validateObject(ctx, `${hookErrIdentifier} context`); }; const nextResolve = nextHookFactory(chain, meta, validate); const resolution = await nextResolve(originalSpecifier, context); const { hookErrIdentifier } = meta; // Retrieve the value after all settled if (typeof resolution !== 'object') { // [2] throw new ERR_INVALID_RETURN_VALUE( 'an object', 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;