@@ -120,7 +120,19 @@ const { defaultLoadSync, throwUnknownModuleFormat } = require('internal/modules/ |
120 | 120 | */ |
121 | 121 |
|
122 | 122 | /** |
123 | | - * @typedef {{ format: ModuleFormat, source: ModuleSource, translatorKey: string }} TranslateContext |
| 123 | + * @typedef {{format: string, url: string, isResolvedBySyncHooks: boolean}} ResolveResult |
| 124 | + */ |
| 125 | + |
| 126 | +/** |
| 127 | + * @typedef {{ |
| 128 | + * url: string, |
| 129 | + * format: ModuleFormat, |
| 130 | + * source: ModuleSource, |
| 131 | + * responseURL?: string, |
| 132 | + * translatorKey: string, |
| 133 | + * isResolvedBySyncHooks: boolean, |
| 134 | + * isSourceLoadedSynchronously: boolean, |
| 135 | + * }} TranslateContext |
124 | 136 | */ |
125 | 137 |
|
126 | 138 | /** |
@@ -382,19 +394,25 @@ class ModuleLoader { |
382 | 394 | /** |
383 | 395 | * Load a module and translate it into a ModuleWrap for require(esm). |
384 | 396 | * This is run synchronously, and the translator always return a ModuleWrap synchronously. |
385 | | - * @param {string} url URL of the module to be translated. |
| 397 | + * @param {ResolveResult} resolveResult Result from the resolve step. |
386 | 398 | * @param {object} loadContext See {@link load} |
387 | 399 | * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point. |
388 | 400 | * @param {ModuleRequest} request Module request. |
389 | 401 | * @returns {ModuleWrap} |
390 | 402 | */ |
391 | | - loadAndTranslateForImportInRequiredESM(url, loadContext, parentURL, request) { |
| 403 | + loadAndTranslateForImportInRequiredESM(resolveResult, loadContext, parentURL, request) { |
| 404 | + const { url } = resolveResult; |
392 | 405 | const loadResult = this.#loadSync(url, loadContext); |
393 | 406 | // Use the synchronous commonjs translator which can deal with cycles. |
394 | 407 | const formatFromLoad = loadResult.format; |
395 | 408 | const translatorKey = (formatFromLoad === 'commonjs' || formatFromLoad === 'commonjs-typescript') ? |
396 | 409 | 'commonjs-sync' : formatFromLoad; |
397 | | - const translateContext = { ...loadResult, translatorKey, __proto__: null }; |
| 410 | + const translateContext = { |
| 411 | + ...resolveResult, |
| 412 | + ...loadResult, |
| 413 | + translatorKey, |
| 414 | + __proto__: null, |
| 415 | + }; |
398 | 416 | const wrap = this.#translate(url, translateContext, parentURL); |
399 | 417 | assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`); |
400 | 418 |
|
@@ -443,12 +461,13 @@ class ModuleLoader { |
443 | 461 | /** |
444 | 462 | * Load a module and translate it into a ModuleWrap for require() in imported CJS. |
445 | 463 | * This is run synchronously, and the translator always return a ModuleWrap synchronously. |
446 | | - * @param {string} url URL of the module to be translated. |
| 464 | + * @param {ResolveResult} resolveResult Result from the resolve step. |
447 | 465 | * @param {object} loadContext See {@link load} |
448 | 466 | * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point. |
449 | 467 | * @returns {ModuleWrap} |
450 | 468 | */ |
451 | | - loadAndTranslateForRequireInImportedCJS(url, loadContext, parentURL) { |
| 469 | + loadAndTranslateForRequireInImportedCJS(resolveResult, loadContext, parentURL) { |
| 470 | + const { url } = resolveResult; |
452 | 471 | const loadResult = this.#loadSync(url, loadContext); |
453 | 472 | const formatFromLoad = loadResult.format; |
454 | 473 |
|
@@ -470,7 +489,12 @@ class ModuleLoader { |
470 | 489 | translatorKey = 'require-commonjs-typescript'; |
471 | 490 | } |
472 | 491 |
|
473 | | - const translateContext = { ...loadResult, translatorKey, __proto__: null }; |
| 492 | + const translateContext = { |
| 493 | + ...resolveResult, |
| 494 | + ...loadResult, |
| 495 | + translatorKey, |
| 496 | + __proto__: null, |
| 497 | + }; |
474 | 498 | const wrap = this.#translate(url, translateContext, parentURL); |
475 | 499 | assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`); |
476 | 500 | return wrap; |
@@ -479,15 +503,21 @@ class ModuleLoader { |
479 | 503 | /** |
480 | 504 | * Load a module and translate it into a ModuleWrap for ordinary imported ESM. |
481 | 505 | * This may be run asynchronously if there are asynchronous module loader hooks registered. |
482 | | - * @param {string} url URL of the module to be translated. |
| 506 | + * @param {ResolveResult} resolveResult Result from the resolve step. |
483 | 507 | * @param {object} loadContext See {@link load} |
484 | 508 | * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point. |
485 | 509 | * @returns {Promise<ModuleWrap>|ModuleWrap} |
486 | 510 | */ |
487 | | - loadAndTranslate(url, loadContext, parentURL) { |
| 511 | + loadAndTranslate(resolveResult, loadContext, parentURL) { |
| 512 | + const { url } = resolveResult; |
488 | 513 | const maybePromise = this.load(url, loadContext); |
489 | 514 | const afterLoad = (loadResult) => { |
490 | | - const translateContext = { ...loadResult, translatorKey: loadResult.format, __proto__: null }; |
| 515 | + const translateContext = { |
| 516 | + ...resolveResult, |
| 517 | + ...loadResult, |
| 518 | + translatorKey: loadResult.format, |
| 519 | + __proto__: null, |
| 520 | + }; |
491 | 521 | return this.#translate(url, translateContext, parentURL); |
492 | 522 | }; |
493 | 523 | if (isPromise(maybePromise)) { |
@@ -503,7 +533,7 @@ class ModuleLoader { |
503 | 533 | * the module should be linked by the time this returns. Otherwise it may still have |
504 | 534 | * pending module requests. |
505 | 535 | * @param {string} parentURL See {@link getOrCreateModuleJob} |
506 | | - * @param {{format: string, url: string}} resolveResult |
| 536 | + * @param {ResolveResult} resolveResult |
507 | 537 | * @param {ModuleRequest} request Module request. |
508 | 538 | * @param {ModuleRequestType} requestType Type of the module request. |
509 | 539 | * @returns {ModuleJobBase} The (possibly pending) module job |
@@ -542,11 +572,11 @@ class ModuleLoader { |
542 | 572 |
|
543 | 573 | let moduleOrModulePromise; |
544 | 574 | if (requestType === kRequireInImportedCJS) { |
545 | | - moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(url, context, parentURL); |
| 575 | + moduleOrModulePromise = this.loadAndTranslateForRequireInImportedCJS(resolveResult, context, parentURL); |
546 | 576 | } else if (requestType === kImportInRequiredESM) { |
547 | | - moduleOrModulePromise = this.loadAndTranslateForImportInRequiredESM(url, context, parentURL, request); |
| 577 | + moduleOrModulePromise = this.loadAndTranslateForImportInRequiredESM(resolveResult, context, parentURL, request); |
548 | 578 | } else { |
549 | | - moduleOrModulePromise = this.loadAndTranslate(url, context, parentURL); |
| 579 | + moduleOrModulePromise = this.loadAndTranslate(resolveResult, context, parentURL); |
550 | 580 | } |
551 | 581 |
|
552 | 582 | if (requestType === kImportInRequiredESM || requestType === kRequireInImportedCJS || |
@@ -660,7 +690,7 @@ class ModuleLoader { |
660 | 690 | * @param {string} [parentURL] The URL of the module where the module request is initiated. |
661 | 691 | * It's undefined if it's from the root module. |
662 | 692 | * @param {ModuleRequest} request Module request. |
663 | | - * @returns {Promise<{format: string, url: string}>|{format: string, url: string}} |
| 693 | + * @returns {Promise<ResolveResult>|ResolveResult} |
664 | 694 | */ |
665 | 695 | #resolve(parentURL, request) { |
666 | 696 | if (this.isForAsyncLoaderHookWorker) { |
@@ -696,15 +726,18 @@ class ModuleLoader { |
696 | 726 | /** |
697 | 727 | * This is the default resolve step for module.registerHooks(), which incorporates asynchronous hooks |
698 | 728 | * from module.register() which are run in a blocking fashion for it to be synchronous. |
| 729 | + * @param {{isResolvedByDefaultResolve: boolean}} out Output object to track whether the default resolve was used |
| 730 | + * without polluting the user-visible resolve result. |
699 | 731 | * @param {string|URL} specifier See {@link resolveSync}. |
700 | 732 | * @param {{ parentURL?: string, importAttributes: ImportAttributes, conditions?: string[]}} context |
701 | 733 | * See {@link resolveSync}. |
702 | 734 | * @returns {{ format: string, url: string }} |
703 | 735 | */ |
704 | | - #resolveAndMaybeBlockOnLoaderThread(specifier, context) { |
| 736 | + #resolveAndMaybeBlockOnLoaderThread(out, specifier, context) { |
705 | 737 | if (this.#asyncLoaderHooks?.resolveSync) { |
706 | 738 | return this.#asyncLoaderHooks.resolveSync(specifier, context.parentURL, context.importAttributes); |
707 | 739 | } |
| 740 | + out.isResolvedByDefaultResolve = true; |
708 | 741 | return this.#cachedDefaultResolve(specifier, context); |
709 | 742 | } |
710 | 743 |
|
@@ -719,31 +752,45 @@ class ModuleLoader { |
719 | 752 | * @param {boolean} [shouldSkipSyncHooks] Whether to skip the synchronous hooks registered by module.registerHooks(). |
720 | 753 | * This is used to maintain compatibility for the re-invented require.resolve (in imported CJS customized |
721 | 754 | * by module.register()`) which invokes the CJS resolution separately from the hook chain. |
722 | | - * @returns {{ format: string, url: string }} |
| 755 | + * @returns {ResolveResult} |
723 | 756 | */ |
724 | 757 | resolveSync(parentURL, request, shouldSkipSyncHooks = false) { |
725 | 758 | const specifier = `${request.specifier}`; |
726 | 759 | const importAttributes = request.attributes ?? kEmptyObject; |
| 760 | + // Use an output parameter to track the state and avoid polluting the user-visible resolve results. |
| 761 | + const out = { isResolvedByDefaultResolve: false, __proto__: null }; |
727 | 762 |
|
| 763 | + let result; |
| 764 | + let isResolvedBySyncHooks = false; |
728 | 765 | if (!shouldSkipSyncHooks && syncResolveHooks.length) { |
729 | 766 | // Has module.registerHooks() hooks, chain the asynchronous hooks in the default step. |
730 | | - return resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions, |
731 | | - this.#resolveAndMaybeBlockOnLoaderThread.bind(this)); |
| 767 | + result = resolveWithSyncHooks(specifier, parentURL, importAttributes, this.#defaultConditions, |
| 768 | + this.#resolveAndMaybeBlockOnLoaderThread.bind(this, out)); |
| 769 | + // If the default step ran, sync hooks did not short-circuit the resolution. |
| 770 | + isResolvedBySyncHooks = !out.isResolvedByDefaultResolve; |
| 771 | + } else { |
| 772 | + const context = { |
| 773 | + ...request, |
| 774 | + conditions: this.#defaultConditions, |
| 775 | + parentURL, |
| 776 | + importAttributes, |
| 777 | + __proto__: null, |
| 778 | + }; |
| 779 | + result = this.#resolveAndMaybeBlockOnLoaderThread(out, specifier, context); |
732 | 780 | } |
733 | | - const context = { |
734 | | - ...request, |
735 | | - conditions: this.#defaultConditions, |
736 | | - parentURL, |
737 | | - importAttributes, |
738 | | - __proto__: null, |
739 | | - }; |
740 | | - return this.#resolveAndMaybeBlockOnLoaderThread(specifier, context); |
| 781 | + result.isResolvedBySyncHooks = isResolvedBySyncHooks; |
| 782 | + return result; |
741 | 783 | } |
742 | 784 |
|
743 | 785 | /** |
744 | 786 | * Provide source that is understood by one of Node's translators. Handles customization hooks, |
745 | 787 | * if any. |
746 | | - * @typedef { {format: ModuleFormat, source: ModuleSource }} LoadResult |
| 788 | + * @typedef {{ |
| 789 | + * format: ModuleFormat, |
| 790 | + * source: ModuleSource, |
| 791 | + * responseURL?: string, |
| 792 | + * isSourceLoadedSynchronously: boolean, |
| 793 | + * }} LoadResult |
747 | 794 | * @param {string} url The URL of the module to be loaded. |
748 | 795 | * @param {object} context Metadata about the module |
749 | 796 | * @returns {Promise<LoadResult> | LoadResult}} |
@@ -759,14 +806,19 @@ class ModuleLoader { |
759 | 806 | /** |
760 | 807 | * This is the default load step for module.registerHooks(), which incorporates asynchronous hooks |
761 | 808 | * from module.register() which are run in a blocking fashion for it to be synchronous. |
| 809 | + * @param {{isSourceLoadedSynchronously: boolean}} out |
| 810 | + * Output object to track whether the source was loaded synchronously without polluting |
| 811 | + * the user-visible load result. |
762 | 812 | * @param {string} url See {@link load} |
763 | 813 | * @param {object} context See {@link load} |
764 | 814 | * @returns {{ format: ModuleFormat, source: ModuleSource }} |
765 | 815 | */ |
766 | | - #loadAndMaybeBlockOnLoaderThread(url, context) { |
| 816 | + #loadAndMaybeBlockOnLoaderThread(out, url, context) { |
767 | 817 | if (this.#asyncLoaderHooks?.loadSync) { |
| 818 | + out.isSourceLoadedSynchronously = false; |
768 | 819 | return this.#asyncLoaderHooks.loadSync(url, context); |
769 | 820 | } |
| 821 | + out.isSourceLoadedSynchronously = true; |
770 | 822 | return defaultLoadSync(url, context); |
771 | 823 | } |
772 | 824 |
|
@@ -777,17 +829,32 @@ class ModuleLoader { |
777 | 829 | * This is here to support `require()` in imported CJS and `module.registerHooks()` hooks. |
778 | 830 | * @param {string} url See {@link load} |
779 | 831 | * @param {object} [context] See {@link load} |
780 | | - * @returns {{ format: ModuleFormat, source: ModuleSource }} |
| 832 | + * @returns {LoadResult} |
781 | 833 | */ |
782 | 834 | #loadSync(url, context) { |
| 835 | + // Use an output parameter to track the state and avoid polluting the user-visible resolve results. |
| 836 | + const out = { |
| 837 | + isSourceLoadedSynchronously: true, |
| 838 | + __proto__: null, |
| 839 | + }; |
| 840 | + let result; |
783 | 841 | if (syncLoadHooks.length) { |
784 | 842 | // Has module.registerHooks() hooks, chain the asynchronous hooks in the default step. |
785 | 843 | // TODO(joyeecheung): construct the ModuleLoadContext in the loaders directly instead |
786 | 844 | // of converting them from plain objects in the hooks. |
787 | | - return loadWithSyncHooks(url, context.format, context.importAttributes, this.#defaultConditions, |
788 | | - this.#loadAndMaybeBlockOnLoaderThread.bind(this), validateLoadSloppy); |
| 845 | + result = loadWithSyncHooks( |
| 846 | + url, |
| 847 | + context.format, |
| 848 | + context.importAttributes, |
| 849 | + this.#defaultConditions, |
| 850 | + this.#loadAndMaybeBlockOnLoaderThread.bind(this, out), |
| 851 | + validateLoadSloppy, |
| 852 | + ); |
| 853 | + } else { |
| 854 | + result = this.#loadAndMaybeBlockOnLoaderThread(out, url, context); |
789 | 855 | } |
790 | | - return this.#loadAndMaybeBlockOnLoaderThread(url, context); |
| 856 | + result.isSourceLoadedSynchronously = out.isSourceLoadedSynchronously; |
| 857 | + return result; |
791 | 858 | } |
792 | 859 |
|
793 | 860 | validateLoadResult(url, format) { |
|