1'use strict'; 2 3const { 4 ArrayPrototypePush, 5 ArrayPrototypePushApply, 6 FunctionPrototypeCall, 7 Int32Array, 8 ObjectAssign, 9 ObjectDefineProperty, 10 ObjectSetPrototypeOf, 11 Promise, 12 ReflectSet, 13 SafeSet, 14 StringPrototypeSlice, 15 StringPrototypeStartsWith, 16 StringPrototypeToUpperCase, 17 globalThis, 18} = primordials; 19 20const { 21 Atomics: { 22 load: AtomicsLoad, 23 wait: AtomicsWait, 24 waitAsync: AtomicsWaitAsync, 25 }, 26 SharedArrayBuffer, 27} = globalThis; 28 29const { 30 ERR_INTERNAL_ASSERTION, 31 ERR_INVALID_ARG_TYPE, 32 ERR_INVALID_ARG_VALUE, 33 ERR_INVALID_RETURN_PROPERTY_VALUE, 34 ERR_INVALID_RETURN_VALUE, 35 ERR_LOADER_CHAIN_INCOMPLETE, 36 ERR_METHOD_NOT_IMPLEMENTED, 37 ERR_UNKNOWN_BUILTIN_MODULE, 38 ERR_WORKER_UNSERIALIZABLE_ERROR, 39} = require('internal/errors').codes; 40const { URL } = require('internal/url'); 41const { canParse: URLCanParse } = internalBinding('url'); 42const { receiveMessageOnPort } = require('worker_threads'); 43const { 44 isAnyArrayBuffer, 45 isArrayBufferView, 46} = require('internal/util/types'); 47const { 48 validateObject, 49 validateString, 50} = require('internal/validators'); 51const { 52 emitExperimentalWarning, 53 kEmptyObject, 54} = require('internal/util'); 55 56const { 57 defaultResolve, 58 throwIfInvalidParentURL, 59} = require('internal/modules/esm/resolve'); 60const { 61 getDefaultConditions, 62 loaderWorkerId, 63} = require('internal/modules/esm/utils'); 64const { deserializeError } = require('internal/error_serdes'); 65const { 66 SHARED_MEMORY_BYTE_LENGTH, 67 WORKER_TO_MAIN_THREAD_NOTIFICATION, 68} = require('internal/modules/esm/shared_constants'); 69let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { 70 debug = fn; 71}); 72let importMetaInitializer; 73 74let importAssertionAlreadyWarned = false; 75 76function emitImportAssertionWarning() { 77 if (!importAssertionAlreadyWarned) { 78 importAssertionAlreadyWarned = true; 79 process.emitWarning('Use `importAttributes` instead of `importAssertions`', 'ExperimentalWarning'); 80 } 81} 82 83function defineImportAssertionAlias(context) { 84 return ObjectDefineProperty(context, 'importAssertions', { 85 __proto__: null, 86 configurable: true, 87 get() { 88 emitImportAssertionWarning(); 89 return this.importAttributes; 90 }, 91 set(value) { 92 emitImportAssertionWarning(); 93 return ReflectSet(this, 'importAttributes', value); 94 }, 95 }); 96} 97 98/** 99 * @typedef {object} ExportedHooks 100 * @property {Function} initialize Customizations setup hook. 101 * @property {Function} globalPreload Global preload hook. 102 * @property {Function} resolve Resolve hook. 103 * @property {Function} load Load hook. 104 */ 105 106/** 107 * @typedef {object} KeyedHook 108 * @property {Function} fn The hook function. 109 * @property {URL['href']} url The URL of the module. 110 */ 111 112// [2] `validate...()`s throw the wrong error 113 114class Hooks { 115 #chains = { 116 /** 117 * Prior to ESM loading. These are called once before any modules are started. 118 * @private 119 * @property {KeyedHook[]} globalPreload Last-in-first-out list of preload hooks. 120 */ 121 globalPreload: [], 122 123 /** 124 * Phase 1 of 2 in ESM loading. 125 * The output of the `resolve` chain of hooks is passed into the `load` chain of hooks. 126 * @private 127 * @property {KeyedHook[]} resolve Last-in-first-out collection of resolve hooks. 128 */ 129 resolve: [ 130 { 131 fn: defaultResolve, 132 url: 'node:internal/modules/esm/resolve', 133 }, 134 ], 135 136 /** 137 * Phase 2 of 2 in ESM loading. 138 * @private 139 * @property {KeyedHook[]} load Last-in-first-out collection of loader hooks. 140 */ 141 load: [ 142 { 143 fn: require('internal/modules/esm/load').defaultLoad, 144 url: 'node:internal/modules/esm/load', 145 }, 146 ], 147 }; 148 149 // Cache URLs we've already validated to avoid repeated validation 150 #validatedUrls = new SafeSet(); 151 152 allowImportMetaResolve = false; 153 154 /** 155 * Import and register custom/user-defined module loader hook(s). 156 * @param {string} urlOrSpecifier 157 * @param {string} parentURL 158 * @param {any} [data] Arbitrary data to be passed from the custom 159 * loader (user-land) to the worker. 160 */ 161 async register(urlOrSpecifier, parentURL, data) { 162 const moduleLoader = require('internal/process/esm_loader').esmLoader; 163 const keyedExports = await moduleLoader.import( 164 urlOrSpecifier, 165 parentURL, 166 kEmptyObject, 167 ); 168 await this.addCustomLoader(urlOrSpecifier, keyedExports, data); 169 } 170 171 /** 172 * Collect custom/user-defined module loader hook(s). 173 * After all hooks have been collected, the global preload hook(s) must be initialized. 174 * @param {string} url Custom loader specifier 175 * @param {Record<string, unknown>} exports 176 * @param {any} [data] Arbitrary data to be passed from the custom loader (user-land) 177 * to the worker. 178 * @returns {any | Promise<any>} User data, ignored unless it's a promise, in which case it will be awaited. 179 */ 180 addCustomLoader(url, exports, data) { 181 const { 182 globalPreload, 183 initialize, 184 resolve, 185 load, 186 } = pluckHooks(exports); 187 188 if (globalPreload && !initialize) { 189 emitExperimentalWarning( 190 '`globalPreload` is planned for removal in favor of `initialize`. `globalPreload`', 191 ); 192 ArrayPrototypePush(this.#chains.globalPreload, { __proto__: null, fn: globalPreload, url }); 193 } 194 if (resolve) { 195 const next = this.#chains.resolve[this.#chains.resolve.length - 1]; 196 ArrayPrototypePush(this.#chains.resolve, { __proto__: null, fn: resolve, url, next }); 197 } 198 if (load) { 199 const next = this.#chains.load[this.#chains.load.length - 1]; 200 ArrayPrototypePush(this.#chains.load, { __proto__: null, fn: load, url, next }); 201 } 202 return initialize?.(data); 203 } 204 205 /** 206 * Initialize `globalPreload` hooks. 207 */ 208 initializeGlobalPreload() { 209 const preloadScripts = []; 210 for (let i = this.#chains.globalPreload.length - 1; i >= 0; i--) { 211 const { MessageChannel } = require('internal/worker/io'); 212 const channel = new MessageChannel(); 213 const { 214 port1: insidePreload, 215 port2: insideLoader, 216 } = channel; 217 218 insidePreload.unref(); 219 insideLoader.unref(); 220 221 const { 222 fn: preload, 223 url: specifier, 224 } = this.#chains.globalPreload[i]; 225 226 const preloaded = preload({ 227 port: insideLoader, 228 }); 229 230 if (preloaded == null) { continue; } 231 232 if (typeof preloaded !== 'string') { // [2] 233 throw new ERR_INVALID_RETURN_VALUE( 234 'a string', 235 `${specifier} globalPreload`, 236 preload, 237 ); 238 } 239 240 ArrayPrototypePush(preloadScripts, { 241 code: preloaded, 242 port: insidePreload, 243 }); 244 } 245 return preloadScripts; 246 } 247 248 /** 249 * Resolve the location of the module. 250 * 251 * Internally, this behaves like a backwards iterator, wherein the stack of 252 * hooks starts at the top and each call to `nextResolve()` moves down 1 step 253 * until it reaches the bottom or short-circuits. 254 * @param {string} originalSpecifier The specified URL path of the module to 255 * be resolved. 256 * @param {string} [parentURL] The URL path of the module's parent. 257 * @param {ImportAttributes} [importAttributes] Attributes from the import 258 * statement or expression. 259 * @returns {Promise<{ format: string, url: URL['href'] }>} 260 */ 261 async resolve( 262 originalSpecifier, 263 parentURL, 264 importAttributes = { __proto__: null }, 265 ) { 266 throwIfInvalidParentURL(parentURL); 267 268 const chain = this.#chains.resolve; 269 const context = { 270 conditions: getDefaultConditions(), 271 importAttributes, 272 parentURL, 273 }; 274 const meta = { 275 chainFinished: null, 276 context, 277 hookErrIdentifier: '', 278 hookName: 'resolve', 279 shortCircuited: false, 280 }; 281 282 const validateArgs = (hookErrIdentifier, suppliedSpecifier, ctx) => { 283 validateString( 284 suppliedSpecifier, 285 `${hookErrIdentifier} specifier`, 286 ); // non-strings can be coerced to a URL string 287 288 if (ctx) { validateObject(ctx, `${hookErrIdentifier} context`); } 289 }; 290 const validateOutput = (hookErrIdentifier, output) => { 291 if (typeof output !== 'object' || output === null) { // [2] 292 throw new ERR_INVALID_RETURN_VALUE( 293 'an object', 294 hookErrIdentifier, 295 output, 296 ); 297 } 298 }; 299 300 const nextResolve = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput }); 301 302 const resolution = await nextResolve(originalSpecifier, context); 303 const { hookErrIdentifier } = meta; // Retrieve the value after all settled 304 305 validateOutput(hookErrIdentifier, resolution); 306 307 if (resolution?.shortCircuit === true) { meta.shortCircuited = true; } 308 309 if (!meta.chainFinished && !meta.shortCircuited) { 310 throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier); 311 } 312 313 let resolvedImportAttributes; 314 const { 315 format, 316 url, 317 } = resolution; 318 319 if (typeof url !== 'string') { 320 // non-strings can be coerced to a URL string 321 // validateString() throws a less-specific error 322 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 323 'a URL string', 324 hookErrIdentifier, 325 'url', 326 url, 327 ); 328 } 329 330 // Avoid expensive URL instantiation for known-good URLs 331 if (!this.#validatedUrls.has(url)) { 332 // No need to convert to string, since the type is already validated 333 if (!URLCanParse(url)) { 334 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 335 'a URL string', 336 hookErrIdentifier, 337 'url', 338 url, 339 ); 340 } 341 342 this.#validatedUrls.add(url); 343 } 344 345 if (!('importAttributes' in resolution) && ('importAssertions' in resolution)) { 346 emitImportAssertionWarning(); 347 resolvedImportAttributes = resolution.importAssertions; 348 } else { 349 resolvedImportAttributes = resolution.importAttributes; 350 } 351 352 if ( 353 resolvedImportAttributes != null && 354 typeof resolvedImportAttributes !== 'object' 355 ) { 356 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 357 'an object', 358 hookErrIdentifier, 359 'importAttributes', 360 resolvedImportAttributes, 361 ); 362 } 363 364 if ( 365 format != null && 366 typeof format !== 'string' // [2] 367 ) { 368 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 369 'a string', 370 hookErrIdentifier, 371 'format', 372 format, 373 ); 374 } 375 376 return { 377 __proto__: null, 378 format, 379 importAttributes: resolvedImportAttributes, 380 url, 381 }; 382 } 383 384 resolveSync(_originalSpecifier, _parentURL, _importAttributes) { 385 throw new ERR_METHOD_NOT_IMPLEMENTED('resolveSync()'); 386 } 387 388 /** 389 * Provide source that is understood by one of Node's translators. 390 * 391 * Internally, this behaves like a backwards iterator, wherein the stack of 392 * hooks starts at the top and each call to `nextLoad()` moves down 1 step 393 * until it reaches the bottom or short-circuits. 394 * @param {URL['href']} url The URL/path of the module to be loaded 395 * @param {object} context Metadata about the module 396 * @returns {Promise<{ format: ModuleFormat, source: ModuleSource }>} 397 */ 398 async load(url, context = {}) { 399 const chain = this.#chains.load; 400 const meta = { 401 chainFinished: null, 402 context, 403 hookErrIdentifier: '', 404 hookName: 'load', 405 shortCircuited: false, 406 }; 407 408 const validateArgs = (hookErrIdentifier, nextUrl, ctx) => { 409 if (typeof nextUrl !== 'string') { 410 // Non-strings can be coerced to a URL string 411 // validateString() throws a less-specific error 412 throw new ERR_INVALID_ARG_TYPE( 413 `${hookErrIdentifier} url`, 414 'a URL string', 415 nextUrl, 416 ); 417 } 418 419 // Avoid expensive URL instantiation for known-good URLs 420 if (!this.#validatedUrls.has(nextUrl)) { 421 // No need to convert to string, since the type is already validated 422 if (!URLCanParse(nextUrl)) { 423 throw new ERR_INVALID_ARG_VALUE( 424 `${hookErrIdentifier} url`, 425 nextUrl, 426 'should be a URL string', 427 ); 428 } 429 430 this.#validatedUrls.add(nextUrl); 431 } 432 433 if (ctx) { validateObject(ctx, `${hookErrIdentifier} context`); } 434 }; 435 const validateOutput = (hookErrIdentifier, output) => { 436 if (typeof output !== 'object' || output === null) { // [2] 437 throw new ERR_INVALID_RETURN_VALUE( 438 'an object', 439 hookErrIdentifier, 440 output, 441 ); 442 } 443 }; 444 445 const nextLoad = nextHookFactory(chain[chain.length - 1], meta, { validateArgs, validateOutput }); 446 447 const loaded = await nextLoad(url, defineImportAssertionAlias(context)); 448 const { hookErrIdentifier } = meta; // Retrieve the value after all settled 449 450 validateOutput(hookErrIdentifier, loaded); 451 452 if (loaded?.shortCircuit === true) { meta.shortCircuited = true; } 453 454 if (!meta.chainFinished && !meta.shortCircuited) { 455 throw new ERR_LOADER_CHAIN_INCOMPLETE(hookErrIdentifier); 456 } 457 458 const { 459 format, 460 source, 461 } = loaded; 462 let responseURL = loaded.responseURL; 463 464 if (responseURL === undefined) { 465 responseURL = url; 466 } 467 468 let responseURLObj; 469 if (typeof responseURL === 'string') { 470 try { 471 responseURLObj = new URL(responseURL); 472 } catch { 473 // responseURLObj not defined will throw in next branch. 474 } 475 } 476 477 if (responseURLObj?.href !== responseURL) { 478 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 479 'undefined or a fully resolved URL string', 480 hookErrIdentifier, 481 'responseURL', 482 responseURL, 483 ); 484 } 485 486 if (format == null) { 487 require('internal/modules/esm/load').throwUnknownModuleFormat(url, format); 488 } 489 490 if (typeof format !== 'string') { // [2] 491 throw new ERR_INVALID_RETURN_PROPERTY_VALUE( 492 'a string', 493 hookErrIdentifier, 494 'format', 495 format, 496 ); 497 } 498 499 if ( 500 source != null && 501 typeof source !== 'string' && 502 !isAnyArrayBuffer(source) && 503 !isArrayBufferView(source) 504 ) { 505 throw ERR_INVALID_RETURN_PROPERTY_VALUE( 506 'a string, an ArrayBuffer, or a TypedArray', 507 hookErrIdentifier, 508 'source', 509 source, 510 ); 511 } 512 513 return { 514 __proto__: null, 515 format, 516 responseURL, 517 source, 518 }; 519 } 520 521 forceLoadHooks() { 522 // No-op 523 } 524 525 importMetaInitialize(meta, context, loader) { 526 importMetaInitializer ??= require('internal/modules/esm/initialize_import_meta').initializeImportMeta; 527 meta = importMetaInitializer(meta, context, loader); 528 return meta; 529 } 530} 531ObjectSetPrototypeOf(Hooks.prototype, null); 532 533/** 534 * There may be multiple instances of Hooks/HooksProxy, but there is only 1 Internal worker, so 535 * there is only 1 MessageChannel. 536 */ 537let MessageChannel; 538class HooksProxy { 539 /** 540 * Shared memory. Always use Atomics method to read or write to it. 541 * @type {Int32Array} 542 */ 543 #lock; 544 /** 545 * The InternalWorker instance, which lets us communicate with the loader thread. 546 */ 547 #worker; 548 549 /** 550 * The last notification ID received from the worker. This is used to detect 551 * if the worker has already sent a notification before putting the main 552 * thread to sleep, to avoid a race condition. 553 * @type {number} 554 */ 555 #workerNotificationLastId = 0; 556 557 /** 558 * Track how many async responses the main thread should expect. 559 * @type {number} 560 */ 561 #numberOfPendingAsyncResponses = 0; 562 563 #isReady = false; 564 565 constructor() { 566 const { InternalWorker } = require('internal/worker'); 567 MessageChannel ??= require('internal/worker/io').MessageChannel; 568 569 const lock = new SharedArrayBuffer(SHARED_MEMORY_BYTE_LENGTH); 570 this.#lock = new Int32Array(lock); 571 572 this.#worker = new InternalWorker(loaderWorkerId, { 573 stderr: false, 574 stdin: false, 575 stdout: false, 576 trackUnmanagedFds: false, 577 workerData: { 578 lock, 579 }, 580 }); 581 this.#worker.unref(); // ! Allows the process to eventually exit. 582 this.#worker.on('exit', process.exit); 583 } 584 585 waitForWorker() { 586 if (!this.#isReady) { 587 const { kIsOnline } = require('internal/worker'); 588 if (!this.#worker[kIsOnline]) { 589 debug('wait for signal from worker'); 590 AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, 0); 591 const response = this.#worker.receiveMessageSync(); 592 if (response == null || response.message.status === 'exit') { return; } 593 const { preloadScripts } = this.#unwrapMessage(response); 594 this.#executePreloadScripts(preloadScripts); 595 } 596 597 this.#isReady = true; 598 } 599 } 600 601 /** 602 * Invoke a remote method asynchronously. 603 * @param {string} method Method to invoke 604 * @param {any[]} [transferList] Objects in `args` to be transferred 605 * @param {any[]} args Arguments to pass to `method` 606 * @returns {Promise<any>} 607 */ 608 async makeAsyncRequest(method, transferList, ...args) { 609 this.waitForWorker(); 610 611 MessageChannel ??= require('internal/worker/io').MessageChannel; 612 const asyncCommChannel = new MessageChannel(); 613 614 // Pass work to the worker. 615 debug('post async message to worker', { method, args, transferList }); 616 const finalTransferList = [asyncCommChannel.port2]; 617 if (transferList) { 618 ArrayPrototypePushApply(finalTransferList, transferList); 619 } 620 this.#worker.postMessage({ 621 __proto__: null, 622 method, args, 623 port: asyncCommChannel.port2, 624 }, finalTransferList); 625 626 if (this.#numberOfPendingAsyncResponses++ === 0) { 627 // On the next lines, the main thread will await a response from the worker thread that might 628 // come AFTER the last task in the event loop has run its course and there would be nothing 629 // left keeping the thread alive (and once the main thread dies, the whole process stops). 630 // However we want to keep the process alive until the worker thread responds (or until the 631 // event loop of the worker thread is also empty), so we ref the worker until we get all the 632 // responses back. 633 this.#worker.ref(); 634 } 635 636 let response; 637 do { 638 debug('wait for async response from worker', { method, args }); 639 await AtomicsWaitAsync(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, this.#workerNotificationLastId).value; 640 this.#workerNotificationLastId = AtomicsLoad(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION); 641 642 response = receiveMessageOnPort(asyncCommChannel.port1); 643 } while (response == null); 644 debug('got async response from worker', { method, args }, this.#lock); 645 646 if (--this.#numberOfPendingAsyncResponses === 0) { 647 // We got all the responses from the worker, its job is done (until next time). 648 this.#worker.unref(); 649 } 650 651 const body = this.#unwrapMessage(response); 652 asyncCommChannel.port1.close(); 653 return body; 654 } 655 656 /** 657 * Invoke a remote method synchronously. 658 * @param {string} method Method to invoke 659 * @param {any[]} [transferList] Objects in `args` to be transferred 660 * @param {any[]} args Arguments to pass to `method` 661 * @returns {any} 662 */ 663 makeSyncRequest(method, transferList, ...args) { 664 this.waitForWorker(); 665 666 // Pass work to the worker. 667 debug('post sync message to worker', { method, args, transferList }); 668 this.#worker.postMessage({ __proto__: null, method, args }, transferList); 669 670 let response; 671 do { 672 debug('wait for sync response from worker', { method, args }); 673 // Sleep until worker responds. 674 AtomicsWait(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION, this.#workerNotificationLastId); 675 this.#workerNotificationLastId = AtomicsLoad(this.#lock, WORKER_TO_MAIN_THREAD_NOTIFICATION); 676 677 response = this.#worker.receiveMessageSync(); 678 } while (response == null); 679 debug('got sync response from worker', { method, args }); 680 if (response.message.status === 'never-settle') { 681 process.exit(13); 682 } else if (response.message.status === 'exit') { 683 process.exit(response.message.body); 684 } 685 return this.#unwrapMessage(response); 686 } 687 688 #unwrapMessage(response) { 689 if (response.message.status === 'never-settle') { 690 return new Promise(() => {}); 691 } 692 const { status, body } = response.message; 693 if (status === 'error') { 694 if (body == null || typeof body !== 'object') { throw body; } 695 if (body.serializationFailed || body.serialized == null) { 696 throw ERR_WORKER_UNSERIALIZABLE_ERROR(); 697 } 698 699 // eslint-disable-next-line no-restricted-syntax 700 throw deserializeError(body.serialized); 701 } else { 702 return body; 703 } 704 } 705 706 #importMetaInitializer = require('internal/modules/esm/initialize_import_meta').initializeImportMeta; 707 708 importMetaInitialize(meta, context, loader) { 709 this.#importMetaInitializer(meta, context, loader); 710 } 711 712 #executePreloadScripts(preloadScripts) { 713 for (let i = 0; i < preloadScripts.length; i++) { 714 const { code, port } = preloadScripts[i]; 715 const { compileFunction } = require('vm'); 716 const preloadInit = compileFunction( 717 code, 718 ['getBuiltin', 'port', 'setImportMetaCallback'], 719 { 720 filename: '<preload>', 721 }, 722 ); 723 let finished = false; 724 let replacedImportMetaInitializer = false; 725 let next = this.#importMetaInitializer; 726 const { BuiltinModule } = require('internal/bootstrap/realm'); 727 // Calls the compiled preload source text gotten from the hook 728 // Since the parameters are named we use positional parameters 729 // see compileFunction above to cross reference the names 730 try { 731 FunctionPrototypeCall( 732 preloadInit, 733 globalThis, 734 // Param getBuiltin 735 (builtinName) => { 736 if (StringPrototypeStartsWith(builtinName, 'node:')) { 737 builtinName = StringPrototypeSlice(builtinName, 5); 738 } else if (!BuiltinModule.canBeRequiredWithoutScheme(builtinName)) { 739 throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); 740 } 741 if (BuiltinModule.canBeRequiredByUsers(builtinName)) { 742 return require(builtinName); 743 } 744 throw new ERR_UNKNOWN_BUILTIN_MODULE(builtinName); 745 }, 746 // Param port 747 port, 748 // setImportMetaCallback 749 (fn) => { 750 if (finished || typeof fn !== 'function') { 751 throw new ERR_INVALID_ARG_TYPE('fn', fn); 752 } 753 replacedImportMetaInitializer = true; 754 const parent = next; 755 next = (meta, context) => { 756 return fn(meta, context, parent); 757 }; 758 }, 759 ); 760 } finally { 761 finished = true; 762 if (replacedImportMetaInitializer) { 763 this.#importMetaInitializer = next; 764 } 765 } 766 } 767 } 768} 769ObjectSetPrototypeOf(HooksProxy.prototype, null); 770 771/** 772 * A utility function to pluck the hooks from a user-defined loader. 773 * @param {import('./loader.js).ModuleExports} exports 774 * @returns {ExportedHooks} 775 */ 776function pluckHooks({ 777 globalPreload, 778 initialize, 779 resolve, 780 load, 781}) { 782 const acceptedHooks = { __proto__: null }; 783 784 if (globalPreload) { 785 acceptedHooks.globalPreload = globalPreload; 786 } 787 if (resolve) { 788 acceptedHooks.resolve = resolve; 789 } 790 if (load) { 791 acceptedHooks.load = load; 792 } 793 794 if (initialize) { 795 acceptedHooks.initialize = initialize; 796 } 797 798 return acceptedHooks; 799} 800 801 802/** 803 * A utility function to iterate through a hook chain, track advancement in the 804 * chain, and generate and supply the `next<HookName>` argument to the custom 805 * hook. 806 * @param {Hook} current The (currently) first hook in the chain (this shifts 807 * on every call). 808 * @param {object} meta Properties that change as the current hook advances 809 * along the chain. 810 * @param {boolean} meta.chainFinished Whether the end of the chain has been 811 * reached AND invoked. 812 * @param {string} meta.hookErrIdentifier A user-facing identifier to help 813 * pinpoint where an error occurred. Ex "file:///foo.mjs 'resolve'". 814 * @param {string} meta.hookName The kind of hook the chain is (ex 'resolve') 815 * @param {boolean} meta.shortCircuited Whether a hook signaled a short-circuit. 816 * @param {(hookErrIdentifier, hookArgs) => void} validate A wrapper function 817 * containing all validation of a custom loader hook's intermediary output. Any 818 * validation within MUST throw. 819 * @returns {function next<HookName>(...hookArgs)} The next hook in the chain. 820 */ 821function nextHookFactory(current, meta, { validateArgs, validateOutput }) { 822 // First, prepare the current 823 const { hookName } = meta; 824 const { 825 fn: hook, 826 url: hookFilePath, 827 next, 828 } = current; 829 830 // ex 'nextResolve' 831 const nextHookName = `next${ 832 StringPrototypeToUpperCase(hookName[0]) + 833 StringPrototypeSlice(hookName, 1) 834 }`; 835 836 let nextNextHook; 837 if (next) { 838 nextNextHook = nextHookFactory(next, meta, { validateArgs, validateOutput }); 839 } else { 840 // eslint-disable-next-line func-name-matching 841 nextNextHook = function chainAdvancedTooFar() { 842 throw new ERR_INTERNAL_ASSERTION( 843 `ESM custom loader '${hookName}' advanced beyond the end of the chain.`, 844 ); 845 }; 846 } 847 848 return ObjectDefineProperty( 849 async (arg0 = undefined, context) => { 850 // Update only when hook is invoked to avoid fingering the wrong filePath 851 meta.hookErrIdentifier = `${hookFilePath} '${hookName}'`; 852 853 validateArgs(`${meta.hookErrIdentifier} hook's ${nextHookName}()`, arg0, context); 854 855 const outputErrIdentifier = `${hookFilePath} '${hookName}' hook's ${nextHookName}()`; 856 857 // Set when next<HookName> is actually called, not just generated. 858 if (!next) { meta.chainFinished = true; } 859 860 if (context) { // `context` has already been validated, so no fancy check needed. 861 ObjectAssign(meta.context, context); 862 } 863 864 const output = await hook(arg0, meta.context, nextNextHook); 865 validateOutput(outputErrIdentifier, output); 866 867 if (output?.shortCircuit === true) { meta.shortCircuited = true; } 868 869 return output; 870 }, 871 'name', 872 { __proto__: null, value: nextHookName }, 873 ); 874} 875 876 877exports.Hooks = Hooks; 878exports.HooksProxy = HooksProxy; 879