1'use strict'; 2 3// This is needed to avoid cycles in esm/resolve <-> cjs/loader 4require('internal/modules/cjs/loader'); 5 6const { 7 Array, 8 ArrayIsArray, 9 ArrayPrototypeJoin, 10 ArrayPrototypePush, 11 FunctionPrototypeCall, 12 ObjectAssign, 13 ObjectCreate, 14 ObjectDefineProperty, 15 ObjectSetPrototypeOf, 16 RegExpPrototypeExec, 17 SafePromiseAllReturnArrayLike, 18 SafeWeakMap, 19 StringPrototypeSlice, 20 StringPrototypeToUpperCase, 21 globalThis, 22} = primordials; 23const { MessageChannel } = require('internal/worker/io'); 24 25const { 26 ERR_LOADER_CHAIN_INCOMPLETE, 27 ERR_INTERNAL_ASSERTION, 28 ERR_INVALID_ARG_TYPE, 29 ERR_INVALID_ARG_VALUE, 30 ERR_INVALID_RETURN_PROPERTY_VALUE, 31 ERR_INVALID_RETURN_VALUE, 32 ERR_UNKNOWN_MODULE_FORMAT, 33} = require('internal/errors').codes; 34const { pathToFileURL, isURL, URL } = require('internal/url'); 35const { emitExperimentalWarning } = require('internal/util'); 36const { 37 isAnyArrayBuffer, 38 isArrayBufferView, 39} = require('internal/util/types'); 40const { 41 validateObject, 42 validateString, 43} = require('internal/validators'); 44const ModuleMap = require('internal/modules/esm/module_map'); 45const ModuleJob = require('internal/modules/esm/module_job'); 46 47const { 48 defaultResolve, 49 DEFAULT_CONDITIONS, 50} = require('internal/modules/esm/resolve'); 51const { 52 initializeImportMeta, 53} = require('internal/modules/esm/initialize_import_meta'); 54const { defaultLoad } = require('internal/modules/esm/load'); 55const { translators } = require( 56 'internal/modules/esm/translators'); 57const { getOptionValue } = require('internal/options'); 58 59/** 60 * @typedef {object} ExportedHooks 61 * @property {Function} globalPreload Global preload hook. 62 * @property {Function} resolve Resolve hook. 63 * @property {Function} load Load hook. 64 */ 65 66/** 67 * @typedef {Record<string, any>} ModuleExports 68 */ 69 70/** 71 * @typedef {object} KeyedExports 72 * @property {ModuleExports} exports The contents of the module. 73 * @property {URL['href']} url The URL of the module. 74 */ 75 76/** 77 * @typedef {object} KeyedHook 78 * @property {Function} fn The hook function. 79 * @property {URL['href']} url The URL of the module. 80 */ 81 82/** 83 * @typedef {'builtin'|'commonjs'|'json'|'module'|'wasm'} ModuleFormat 84 */ 85 86/** 87 * @typedef {ArrayBuffer|TypedArray|string} ModuleSource 88 */ 89 90// [2] `validate...()`s throw the wrong error 91 92let emittedSpecifierResolutionWarning = false; 93 94/** 95 * A utility function to iterate through a hook chain, track advancement in the 96 * chain, and generate and supply the `next<HookName>` argument to the custom 97 * hook. 98 * @param {KeyedHook[]} chain The whole hook chain. 99 * @param {object} meta Properties that change as the current hook advances 100 * along the chain. 101 * @param {boolean} meta.chainFinished Whether the end of the chain has been 102 * reached AND invoked. 103 * @param {string} meta.hookErrIdentifier A user-facing identifier to help 104 * pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'". 105 * @param {number} meta.hookIndex A non-negative integer tracking the current 106 * position in the hook chain. 107 * @param {string} meta.hookName The kind of hook the chain is (ex 'resolve') 108 * @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit. 109 * @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function 110 * containing all validation of a custom loader hook's intermediary output. Any 111 * validation within MUST throw. 112 * @returns {function next<HookName>(...hookArgs)} The next hook in the chain. 113 */ 114function nextHookFactory(chain, meta, { validateArgs, validateOutput }) { 115 // First, prepare the current 116 const { hookName } = meta; 117 const { 118 fn: hook, 119 url: hookFilePath, 120 } = chain[meta.hookIndex]; 121 122 // ex 'nextResolve' 123 const nextHookName = `next${ 124 StringPrototypeToUpperCase(hookName[0]) + 125 StringPrototypeSlice(hookName, 1) 126 }`; 127 128 // When hookIndex is 0, it's reached the default, which does not call next() 129 // so feed it a noop that blows up if called, so the problem is obvious. 130 const generatedHookIndex = meta.hookIndex; 131 let nextNextHook; 132 if (meta.hookIndex > 0) { 133 // Now, prepare the next: decrement the pointer so the next call to the 134 // factory generates the next link in the chain. 135 meta.hookIndex--; 136 137 nextNextHook = nextHookFactory(chain, meta, { validateArgs, validateOutput }); 138 } else { 139 // eslint-disable-next-line func-name-matching 140 nextNextHook = function chainAdvancedTooFar() { 141 throw new ERR_INTERNAL_ASSERTION( 142 `ESM custom loader '${hookName}' advanced beyond the end of the chain.`, 143 ); 144 }; 145 } 146 147 return ObjectDefineProperty( 148 async (arg0 = undefined, context) => { 149 // Update only when hook is invoked to avoid fingering the wrong filePath 150 meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`; 151 152 validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context); 153 154 const outputErrIdentifier = `${chain[generatedHookIndex].url} '${hookName}' hook's ${nextHookName}()`; 155 156 // Set when next<HookName> is actually called, not just generated. 157 if (generatedHookIndex === 0) { meta.chainFinished = true; } 158 159 if (context) { // `context` has already been validated, so no fancy check needed. 160 ObjectAssign(meta.context, context); 161 } 162 163 const output = await hook(arg0, meta.context, nextNextHook); 164 165 validateOutput(outputErrIdentifier, output); 166 167 if (output?.shortCircuit === true) { meta.shortCircuited = true; } 168 return output; 169 170 }, 171 'name', 172 { __proto__: null, value: nextHookName }, 173 ); 174} 175 176/** 177 * An ESMLoader instance is used as the main entry point for loading ES modules. 178 * Currently, this is a singleton -- there is only one used for loading 179 * the main module and everything in its dependency graph. 180 */ 181class ESMLoader { 182 #hooks = { 183 /** 184 * Prior to ESM loading. These are called once before any modules are started. 185 * @private 186 * @property {KeyedHook[]} globalPreload Last-in-first-out list of preload hooks. 187 */ 188 globalPreload: [], 189 190 /** 191 * Phase 2 of 2 in ESM loading (phase 1 is below). 192 * @private 193 * @property {KeyedHook[]} load Last-in-first-out collection of loader hooks. 194 */ 195 load: [ 196 { 197 fn: defaultLoad, 198 url: 'node:internal/modules/esm/load', 199 }, 200 ], 201 202 /** 203 * Phase 1 of 2 in ESM loading. 204 * @private 205 * @property {KeyedHook[]} resolve Last-in-first-out collection of resolve hooks. 206 */ 207 resolve: [ 208 { 209 fn: defaultResolve, 210 url: 'node:internal/modules/esm/resolve', 211 }, 212 ], 213 }; 214 215 #importMetaInitializer = initializeImportMeta; 216 217 /** 218 * Map of already-loaded CJS modules to use 219 */ 220 cjsCache = new SafeWeakMap(); 221 222 /** 223 * The index for assigning unique URLs to anonymous module evaluation 224 */ 225 evalIndex = 0; 226 227 /** 228 * Registry of loaded modules, akin to `require.cache` 229 */ 230 moduleMap = new ModuleMap(); 231 232 /** 233 * Methods which translate input code or other information into ES modules 234 */ 235 translators = translators; 236 237 constructor() { 238 if (getOptionValue('--experimental-loader').length > 0) { 239 emitExperimentalWarning('Custom ESM Loaders'); 240 } 241 if (getOptionValue('--experimental-network-imports')) { 242 emitExperimentalWarning('Network Imports'); 243 } 244 if ( 245 !emittedSpecifierResolutionWarning && 246 getOptionValue('--experimental-specifier-resolution') === 'node' 247 ) { 248 process.emitWarning( 249 'The Node.js specifier resolution flag is experimental. It could change or be removed at any time.', 250 'ExperimentalWarning', 251 ); 252 emittedSpecifierResolutionWarning = true; 253 } 254 } 255 256 /** 257 * 258 * @param {ModuleExports} exports 259 * @returns {ExportedHooks} 260 */ 261 static pluckHooks({ 262 globalPreload, 263 resolve, 264 load, 265 // obsolete hooks: 266 dynamicInstantiate, 267 getFormat, 268 getGlobalPreloadCode, 269 getSource, 270 transformSource, 271 }) { 272 const obsoleteHooks = []; 273 const acceptedHooks = ObjectCreate(null); 274 275 if (getGlobalPreloadCode) { 276 globalPreload ??= getGlobalPreloadCode; 277 278 process.emitWarning( 279 'Loader hook "getGlobalPreloadCode" has been renamed to "globalPreload"', 280 ); 281 } 282 if (dynamicInstantiate) ArrayPrototypePush( 283 obsoleteHooks, 284 'dynamicInstantiate', 285 ); 286 if (getFormat) ArrayPrototypePush( 287 obsoleteHooks, 288 'getFormat', 289 ); 290 if (getSource) ArrayPrototypePush( 291 obsoleteHooks, 292 'getSource', 293 ); 294 if (transformSource) ArrayPrototypePush( 295 obsoleteHooks, 296 'transformSource', 297 ); 298 299 if (obsoleteHooks.length) process.emitWarning( 300 `Obsolete loader hook(s) supplied and will be ignored: ${ 301 ArrayPrototypeJoin(obsoleteHooks, ', ') 302 }`, 303 'DeprecationWarning', 304 ); 305 306 if (globalPreload) { 307 acceptedHooks.globalPreload = globalPreload; 308 } 309 if (resolve) { 310 acceptedHooks.resolve = resolve; 311 } 312 if (load) { 313 acceptedHooks.load = load; 314 } 315 316 return acceptedHooks; 317 } 318 319 /** 320 * Collect custom/user-defined hook(s). After all hooks have been collected, 321 * calls global preload hook(s). 322 * @param {KeyedExports} customLoaders 323 * A list of exports from user-defined loaders (as returned by 324 * ESMLoader.import()). 325 */ 326 addCustomLoaders( 327 customLoaders = [], 328 ) { 329 for (let i = 0; i < customLoaders.length; i++) { 330 const { 331 exports, 332 url, 333 } = customLoaders[i]; 334 const { 335 globalPreload, 336 resolve, 337 load, 338 } = ESMLoader.pluckHooks(exports); 339 340 if (globalPreload) { 341 ArrayPrototypePush( 342 this.#hooks.globalPreload, 343 { 344 fn: globalPreload, 345 url, 346 }, 347 ); 348 } 349 if (resolve) { 350 ArrayPrototypePush( 351 this.#hooks.resolve, 352 { 353 fn: resolve, 354 url, 355 }, 356 ); 357 } 358 if (load) { 359 ArrayPrototypePush( 360 this.#hooks.load, 361 { 362 fn: load, 363 url, 364 }, 365 ); 366 } 367 } 368 369 this.preload(); 370 } 371 372 async eval( 373 source, 374 url = pathToFileURL(`${process.cwd()}/[eval${++this.evalIndex}]`).href, 375 ) { 376 const evalInstance = (url) => { 377 const { ModuleWrap, callbackMap } = internalBinding('module_wrap'); 378 const module = new ModuleWrap(url, undefined, source, 0, 0); 379 callbackMap.set(module, { 380 importModuleDynamically: (specifier, { url }, importAssertions) => { 381 return this.import(specifier, url, importAssertions); 382 }, 383 }); 384 385 return module; 386 }; 387 const job = new ModuleJob( 388 this, url, undefined, evalInstance, false, false); 389 this.moduleMap.set(url, undefined, job); 390 const { module } = await job.run(); 391 392 return { 393 namespace: module.getNamespace(), 394 }; 395 } 396 397 /** 398 * Get a (possibly still pending) module job from the cache, 399 * or create one and return its Promise. 400 * @param {string} specifier The string after `from` in an `import` statement, 401 * or the first parameter of an `import()` 402 * expression 403 * @param {string | undefined} parentURL The URL of the module importing this 404 * one, unless this is the Node.js entry 405 * point. 406 * @param {Record<string, string>} importAssertions Validations for the 407 * module import. 408 * @returns {Promise<ModuleJob>} The (possibly pending) module job 409 */ 410 async getModuleJob(specifier, parentURL, importAssertions) { 411 let importAssertionsForResolve; 412 413 // By default, `this.#hooks.load` contains just the Node default load hook 414 if (this.#hooks.load.length !== 1) { 415 // We can skip cloning if there are no user-provided loaders because 416 // the Node.js default resolve hook does not use import assertions. 417 importAssertionsForResolve = { 418 __proto__: null, 419 ...importAssertions, 420 }; 421 } 422 423 const { format, url } = 424 await this.resolve(specifier, parentURL, importAssertionsForResolve); 425 426 let job = this.moduleMap.get(url, importAssertions.type); 427 428 // CommonJS will set functions for lazy job evaluation. 429 if (typeof job === 'function') { 430 this.moduleMap.set(url, undefined, job = job()); 431 } 432 433 if (job === undefined) { 434 job = this.#createModuleJob(url, importAssertions, parentURL, format); 435 } 436 437 return job; 438 } 439 440 /** 441 * Create and cache an object representing a loaded module. 442 * @param {string} url The absolute URL that was resolved for this module 443 * @param {Record<string, string>} importAssertions Validations for the 444 * module import. 445 * @param {string} [parentURL] The absolute URL of the module importing this 446 * one, unless this is the Node.js entry point 447 * @param {string} [format] The format hint possibly returned by the 448 * `resolve` hook 449 * @returns {Promise<ModuleJob>} The (possibly pending) module job 450 */ 451 #createModuleJob(url, importAssertions, parentURL, format) { 452 const moduleProvider = async (url, isMain) => { 453 const { 454 format: finalFormat, 455 responseURL, 456 source, 457 } = await this.load(url, { 458 format, 459 importAssertions, 460 }); 461 462 const translator = translators.get(finalFormat); 463 464 if (!translator) { 465 throw new ERR_UNKNOWN_MODULE_FORMAT(finalFormat, responseURL); 466 } 467 468 return FunctionPrototypeCall(translator, this, responseURL, source, isMain); 469 }; 470 471 const inspectBrk = ( 472 parentURL === undefined && 473 getOptionValue('--inspect-brk') 474 ); 475 476 if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { 477 process.send({ 'watch:import': [url] }); 478 } 479 480 const job = new ModuleJob( 481 this, 482 url, 483 importAssertions, 484 moduleProvider, 485 parentURL === undefined, 486 inspectBrk, 487 ); 488 489 this.moduleMap.set(url, importAssertions.type, job); 490 491 return job; 492 } 493 494 /** 495 * This method is usually called indirectly as part of the loading processes. 496 * Internally, it is used directly to add loaders. Use directly with caution. 497 * 498 * This method must NOT be renamed: it functions as a dynamic import on a 499 * loader module. 500 * @param {string | string[]} specifiers Path(s) to the module. 501 * @param {string} parentURL Path of the parent importing the module. 502 * @param {Record<string, string>} importAssertions Validations for the 503 * module import. 504 * @returns {Promise<ExportedHooks | KeyedExports[]>} 505 * A collection of module export(s) or a list of collections of module 506 * export(s). 507 */ 508 async import(specifiers, parentURL, importAssertions) { 509 // For loaders, `import` is passed multiple things to process, it returns a 510 // list pairing the url and exports collected. This is especially useful for 511 // error messaging, to identity from where an export came. But, in most 512 // cases, only a single url is being "imported" (ex `import()`), so there is 513 // only 1 possible url from which the exports were collected and it is 514 // already known to the caller. Nesting that in a list would only ever 515 // create redundant work for the caller, so it is later popped off the 516 // internal list. 517 const wasArr = ArrayIsArray(specifiers); 518 if (!wasArr) { specifiers = [specifiers]; } 519 520 const count = specifiers.length; 521 const jobs = new Array(count); 522 523 for (let i = 0; i < count; i++) { 524 jobs[i] = this.getModuleJob(specifiers[i], parentURL, importAssertions) 525 .then((job) => job.run()) 526 .then(({ module }) => module.getNamespace()); 527 } 528 529 const namespaces = await SafePromiseAllReturnArrayLike(jobs); 530 531 if (!wasArr) { return namespaces[0]; } // We can skip the pairing below 532 533 for (let i = 0; i < count; i++) { 534 namespaces[i] = { 535 __proto__: null, 536 url: specifiers[i], 537 exports: namespaces[i], 538 }; 539 } 540 541 return namespaces; 542 } 543 544 /** 545 * Provide source that is understood by one of Node's translators. 546 * 547 * Internally, this behaves like a backwards iterator, wherein the stack of 548 * hooks starts at the top and each call to `nextLoad()` moves down 1 step 549 * until it reaches the bottom or short-circuits. 550 * @param {URL['href']} url The URL/path of the module to be loaded 551 * @param {object} context Metadata about the module 552 * @returns {{ format: ModuleFormat, source: ModuleSource }} 553 */ 554 async load(url, context = {}) { 555 const chain = this.#hooks.load; 556 const meta = { 557 chainFinished: null, 558 context, 559 hookErrIdentifier: '', 560 hookIndex: chain.length - 1, 561 hookName: 'load', 562 shortCircuited: false, 563 }; 564 565 const validateArgs = (hookErrIdentifier, nextUrl, ctx) => { 566 if (typeof nextUrl !== 'string') { 567 // non-strings can be coerced to a url string 568 // validateString() throws a less-specific error 569 throw new ERR_INVALID_ARG_TYPE( 570 `${hookErrIdentifier} url`, 571 'a url string', 572 nextUrl, 573 ); 574 } 575 576 // Try to avoid expensive URL instantiation for known-good urls 577 if (!this.moduleMap.has(nextUrl)) { 578 try { 579 new URL(nextUrl); 580 } catch { 581 throw new ERR_INVALID_ARG_VALUE( 582 `${hookErrIdentifier} url`, 583 nextUrl, 584 'should be a url string', 585 ); 586 } 587 } 588 589 if (ctx) validateObject(ctx, `${hookErrIdentifier} context`); 590 }; 591 const validateOutput = (hookErrIdentifier, output) => { 592 if (typeof output !== 'object' || output === null) { // [2] 593 throw new ERR_INVALID_RETURN_VALUE( 594 'an object', 595 hookErrIdentifier, 596 output, 597 ); 598 } 599 }; 600 601 const nextLoad = nextHookFactory(chain, meta, { validateArgs, validateOutput }); 602 603 const loaded = await nextLoad(url, context); 604 const { hookErrIdentifier } = meta; // Retrieve the value after all settled 605 606 validateOutput(hookErrIdentifier, loaded); 607 608 if (loaded?.shortCircuit === true) { meta.shortCircuited = true; } 609 610 if (!meta.chainFinished && !meta.shortCircuited) { 611 throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier); 612 } 613 614 const { 615 format, 616 source, 617 } = loaded; 618 let responseURL = loaded.responseURL; 619 620 if (responseURL === undefined) { 621 responseURL = url; 622 } 623 624 let responseURLObj; 625 if (typeof responseURL === 'string') { 626 try { 627 responseURLObj = new URL(responseURL); 628 } catch { 629 // responseURLObj not defined will throw in next branch. 630 } 631 } 632 633 if (responseURLObj?.href !== responseURL) { 634 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 635 'undefined or a fully resolved URL string', 636 hookErrIdentifier, 637 'responseURL', 638 responseURL, 639 ); 640 } 641 642 if (format == null) { 643 const dataUrl = RegExpPrototypeExec( 644 /^data:([^/]+\/[^;,]+)(?:[^,]*?)(;base64)?,/, 645 url, 646 ); 647 648 throw new ERR_UNKNOWN_MODULE_FORMAT( 649 dataUrl ? dataUrl[1] : format, 650 url); 651 } 652 653 if (typeof format !== 'string') { // [2] 654 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 655 'a string', 656 hookErrIdentifier, 657 'format', 658 format, 659 ); 660 } 661 662 if ( 663 source != null && 664 typeof source !== 'string' && 665 !isAnyArrayBuffer(source) && 666 !isArrayBufferView(source) 667 ) { 668 throw ERR_INVALID_RETURN_PROPERTY_VALUE( 669 'a string, an ArrayBuffer, or a TypedArray', 670 hookErrIdentifier, 671 'source', 672 source, 673 ); 674 } 675 676 return { 677 __proto__: null, 678 format, 679 responseURL, 680 source, 681 }; 682 } 683 684 preload() { 685 for (let i = this.#hooks.globalPreload.length - 1; i >= 0; i--) { 686 const channel = new MessageChannel(); 687 const { 688 port1: insidePreload, 689 port2: insideLoader, 690 } = channel; 691 692 insidePreload.unref(); 693 insideLoader.unref(); 694 695 const { 696 fn: preload, 697 url: specifier, 698 } = this.#hooks.globalPreload[i]; 699 700 const preloaded = preload({ 701 port: insideLoader, 702 }); 703 704 if (preloaded == null) { return; } 705 706 const hookErrIdentifier = `${specifier} globalPreload`; 707 708 if (typeof preloaded !== 'string') { // [2] 709 throw new ERR_INVALID_RETURN_VALUE( 710 'a string', 711 hookErrIdentifier, 712 preload, 713 ); 714 } 715 const { compileFunction } = require('vm'); 716 const preloadInit = compileFunction( 717 preloaded, 718 ['getBuiltin', 'port', 'setImportMetaCallback'], 719 { 720 filename: '<preload>', 721 }, 722 ); 723 const { BuiltinModule } = require('internal/bootstrap/loaders'); 724 // We only allow replacing the importMetaInitializer during preload, 725 // after preload is finished, we disable the ability to replace it 726 // 727 // This exposes accidentally setting the initializer too late by 728 // throwing an error. 729 let finished = false; 730 let replacedImportMetaInitializer = false; 731 let next = this.#importMetaInitializer; 732 try { 733 // Calls the compiled preload source text gotten from the hook 734 // Since the parameters are named we use positional parameters 735 // see compileFunction above to cross reference the names 736 FunctionPrototypeCall( 737 preloadInit, 738 globalThis, 739 // Param getBuiltin 740 (builtinName) => { 741 if (BuiltinModule.canBeRequiredByUsers(builtinName) && 742 BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { 743 return require(builtinName); 744 } 745 throw new ERR_INVALID_ARG_VALUE('builtinName', builtinName); 746 }, 747 // Param port 748 insidePreload, 749 // Param setImportMetaCallback 750 (fn) => { 751 if (finished || typeof fn !== 'function') { 752 throw new ERR_INVALID_ARG_TYPE('fn', fn); 753 } 754 replacedImportMetaInitializer = true; 755 const parent = next; 756 next = (meta, context) => { 757 return fn(meta, context, parent); 758 }; 759 }); 760 } finally { 761 finished = true; 762 if (replacedImportMetaInitializer) { 763 this.#importMetaInitializer = next; 764 } 765 } 766 } 767 } 768 769 importMetaInitialize(meta, context) { 770 this.#importMetaInitializer(meta, context); 771 } 772 773 /** 774 * Resolve the location of the module. 775 * 776 * Internally, this behaves like a backwards iterator, wherein the stack of 777 * hooks starts at the top and each call to `nextResolve()` moves down 1 step 778 * until it reaches the bottom or short-circuits. 779 * @param {string} originalSpecifier The specified URL path of the module to 780 * be resolved. 781 * @param {string} [parentURL] The URL path of the module's parent. 782 * @param {ImportAssertions} importAssertions Assertions from the import 783 * statement or expression. 784 * @returns {{ format: string, url: URL['href'] }} 785 */ 786 async resolve(originalSpecifier, parentURL, importAssertions) { 787 const isMain = parentURL === undefined; 788 789 if ( 790 !isMain && 791 typeof parentURL !== 'string' && 792 !isURL(parentURL) 793 ) { 794 throw new ERR_INVALID_ARG_TYPE( 795 'parentURL', 796 ['string', 'URL'], 797 parentURL, 798 ); 799 } 800 const chain = this.#hooks.resolve; 801 const context = { 802 conditions: DEFAULT_CONDITIONS, 803 importAssertions, 804 parentURL, 805 }; 806 const meta = { 807 chainFinished: null, 808 context, 809 hookErrIdentifier: '', 810 hookIndex: chain.length - 1, 811 hookName: 'resolve', 812 shortCircuited: false, 813 }; 814 815 const validateArgs = (hookErrIdentifier, suppliedSpecifier, ctx) => { 816 validateString( 817 suppliedSpecifier, 818 `${hookErrIdentifier} specifier`, 819 ); // non-strings can be coerced to a url string 820 821 if (ctx) validateObject(ctx, `${hookErrIdentifier} context`); 822 }; 823 const validateOutput = (hookErrIdentifier, output) => { 824 if (typeof output !== 'object' || output === null) { // [2] 825 throw new ERR_INVALID_RETURN_VALUE( 826 'an object', 827 hookErrIdentifier, 828 output, 829 ); 830 } 831 }; 832 833 const nextResolve = nextHookFactory(chain, meta, { validateArgs, validateOutput }); 834 835 const resolution = await nextResolve(originalSpecifier, context); 836 const { hookErrIdentifier } = meta; // Retrieve the value after all settled 837 838 validateOutput(hookErrIdentifier, resolution); 839 840 if (resolution?.shortCircuit === true) { meta.shortCircuited = true; } 841 842 if (!meta.chainFinished && !meta.shortCircuited) { 843 throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier); 844 } 845 846 const { 847 format, 848 url, 849 } = resolution; 850 851 if ( 852 format != null && 853 typeof format !== 'string' // [2] 854 ) { 855 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 856 'a string', 857 hookErrIdentifier, 858 'format', 859 format, 860 ); 861 } 862 863 if (typeof url !== 'string') { 864 // non-strings can be coerced to a url string 865 // validateString() throws a less-specific error 866 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 867 'a url string', 868 hookErrIdentifier, 869 'url', 870 url, 871 ); 872 } 873 874 // Try to avoid expensive URL instantiation for known-good urls 875 if (!this.moduleMap.has(url)) { 876 try { 877 new URL(url); 878 } catch { 879 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 880 'a url string', 881 hookErrIdentifier, 882 'url', 883 url, 884 ); 885 } 886 } 887 888 return { 889 __proto__: null, 890 format, 891 url, 892 }; 893 } 894} 895 896ObjectSetPrototypeOf(ESMLoader.prototype, null); 897 898exports.ESMLoader = ESMLoader; 899