GeezerVison is showing github.com — module: handle null source from async loader hooks in sync hooks · nodejs/node@be1b84f
File treeExpand file treeCollapse file tree Open diff view settings fixtures/module-hooks/sync-and-async
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/cjs/loader.js +4-4Lines changed: 4 additions & 4 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -177,6 +177,7 @@ const { | 177 | 177 | registerHooks, | 178 | 178 | resolveHooks, | 179 | 179 | resolveWithHooks, | | 180 | + validateLoadStrict, | 180 | 181 | } = require('internal/modules/customization_hooks'); | 181 | 182 | const { stripTypeScriptModuleTypes } = require('internal/modules/typescript'); | 182 | 183 | const packageJsonReader = require('internal/modules/package_json_reader'); | @@ -1175,7 +1176,7 @@ function loadBuiltinWithHooks(id, url, format) { | 1175 | 1176 | url ??= `node:${id}`; | 1176 | 1177 | // TODO(joyeecheung): do we really want to invoke the load hook for the builtins? | 1177 | 1178 | const loadResult = loadWithHooks(url, format || 'builtin', /* importAttributes */ undefined, | 1178 | | - getCjsConditionsArray(), getDefaultLoad(url, id)); | | 1179 | + getCjsConditionsArray(), getDefaultLoad(url, id), validateLoadStrict); | 1179 | 1180 | if (loadResult.format && loadResult.format !== 'builtin') { | 1180 | 1181 | return undefined; // Format has been overridden, return undefined for the caller to continue loading. | 1181 | 1182 | } | @@ -1791,10 +1792,9 @@ function loadSource(mod, filename, formatFromNode) { | 1791 | 1792 | mod[kURL] = convertCJSFilenameToURL(filename); | 1792 | 1793 | } | 1793 | 1794 |
| | 1795 | + const defaultLoad = getDefaultLoad(mod[kURL], filename); | 1794 | 1796 | const loadResult = loadWithHooks(mod[kURL], mod[kFormat], /* importAttributes */ undefined, | 1795 | | - getCjsConditionsArray(), | 1796 | | - getDefaultLoad(mod[kURL], filename)); | 1797 | | - | | 1797 | + getCjsConditionsArray(), defaultLoad, validateLoadStrict); | 1798 | 1798 | // Reset the module properties with load hook results. | 1799 | 1799 | if (loadResult.format !== undefined) { | 1800 | 1800 | mod[kFormat] = loadResult.format; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/customization_hooks.js +34-10Lines changed: 34 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -262,29 +262,56 @@ function validateResolve(specifier, context, result) { | 262 | 262 | */ | 263 | 263 |
| 264 | 264 | /** | 265 | | - * Validate the result returned by a chain of resolve hook. | | 265 | + * Validate the result returned by a chain of load hook. | 266 | 266 | * @param {string} url URL passed into the hooks. | 267 | 267 | * @param {ModuleLoadContext} context Context passed into the hooks. | 268 | 268 | * @param {ModuleLoadResult} result Result produced by load hooks. | 269 | 269 | * @returns {ModuleLoadResult} | 270 | 270 | */ | 271 | | -function validateLoad(url, context, result) { | | 271 | +function validateLoadStrict(url, context, result) { | | 272 | + validateSourceStrict(url, context, result); | | 273 | + validateFormat(url, context, result); | | 274 | + return result; | | 275 | +} | | 276 | + | | 277 | +function validateLoadSloppy(url, context, result) { | | 278 | + validateSourcePermissive(url, context, result); | | 279 | + validateFormat(url, context, result); | | 280 | + return result; | | 281 | +} | | 282 | + | | 283 | +function validateSourceStrict(url, context, result) { | 272 | 284 | const { source, format } = result; | 273 | 285 | // To align with module.register(), the load hooks are still invoked for | 274 | 286 | // the builtins even though the default load step only provides null as source, | 275 | 287 | // and any source content for builtins provided by the user hooks are ignored. | 276 | 288 | if (!StringPrototypeStartsWith(url, 'node:') && | 277 | 289 | typeof result.source !== 'string' && | 278 | 290 | !isAnyArrayBuffer(source) && | 279 | | - !isArrayBufferView(source)) { | | 291 | + !isArrayBufferView(source) && | | 292 | + format !== 'addon') { | 280 | 293 | throw new ERR_INVALID_RETURN_PROPERTY_VALUE( | 281 | 294 | 'a string, an ArrayBuffer, or a TypedArray', | 282 | 295 | 'load', | 283 | 296 | 'source', | 284 | 297 | source, | 285 | 298 | ); | 286 | 299 | } | | 300 | +} | 287 | 301 |
| | 302 | +function validateSourcePermissive(url, context, result) { | | 303 | + const { source, format } = result; | | 304 | + if (format === 'commonjs' && source == null) { | | 305 | + // Accommodate the quirk in defaultLoad used by asynchronous loader hooks | | 306 | + // which sets source to null for commonjs. | | 307 | + // See: https://github.com/nodejs/node/issues/57327#issuecomment-2701382020 | | 308 | + return; | | 309 | + } | | 310 | + validateSourceStrict(url, context, result); | | 311 | +} | | 312 | + | | 313 | +function validateFormat(url, context, result) { | | 314 | + const { format } = result; | 288 | 315 | if (typeof format !== 'string' && format !== undefined) { | 289 | 316 | throw new ERR_INVALID_RETURN_PROPERTY_VALUE( | 290 | 317 | 'a string', | @@ -293,12 +320,6 @@ function validateLoad(url, context, result) { | 293 | 320 | format, | 294 | 321 | ); | 295 | 322 | } | 296 | | - | 297 | | - return { | 298 | | - __proto__: null, | 299 | | - format, | 300 | | - source, | 301 | | - }; | 302 | 323 | } | 303 | 324 |
| 304 | 325 | class ModuleResolveContext { | @@ -338,9 +359,10 @@ let decoder; | 338 | 359 | * @param {ImportAttributes|undefined} importAttributes | 339 | 360 | * @param {string[]} conditions | 340 | 361 | * @param {(url: string, context: ModuleLoadContext) => ModuleLoadResult} defaultLoad | | 362 | + * @param {(url: string, context: ModuleLoadContext, result: ModuleLoadResult) => ModuleLoadResult} validateLoad | 341 | 363 | * @returns {ModuleLoadResult} | 342 | 364 | */ | 343 | | -function loadWithHooks(url, originalFormat, importAttributes, conditions, defaultLoad) { | | 365 | +function loadWithHooks(url, originalFormat, importAttributes, conditions, defaultLoad, validateLoad) { | 344 | 366 | debug('loadWithHooks', url, originalFormat); | 345 | 367 | const context = new ModuleLoadContext(originalFormat, importAttributes, conditions); | 346 | 368 | if (loadHooks.length === 0) { | @@ -403,4 +425,6 @@ module.exports = { | 403 | 425 | registerHooks, | 404 | 426 | resolveHooks, | 405 | 427 | resolveWithHooks, | | 428 | + validateLoadStrict, | | 429 | + validateLoadSloppy, | 406 | 430 | }; |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/hooks.js +1-1Lines changed: 1 addition & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -61,7 +61,7 @@ const { | 61 | 61 | SHARED_MEMORY_BYTE_LENGTH, | 62 | 62 | WORKER_TO_MAIN_THREAD_NOTIFICATION, | 63 | 63 | } = require('internal/modules/esm/shared_constants'); | 64 | | -let debug = require('internal/util/debuglog').debuglog('esm', (fn) => { | | 64 | +let debug = require('internal/util/debuglog').debuglog('async_loader_worker', (fn) => { | 65 | 65 | debug = fn; | 66 | 66 | }); | 67 | 67 | let importMetaInitializer; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/load.js +17-5Lines changed: 17 additions & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -141,22 +141,34 @@ function defaultLoadSync(url, context = kEmptyObject) { | 141 | 141 |
| 142 | 142 | throwIfUnsupportedURLScheme(urlInstance, false); | 143 | 143 |
| | 144 | + let shouldBeReloadedByCJSLoader = false; | 144 | 145 | if (urlInstance.protocol === 'node:') { | 145 | 146 | source = null; | 146 | | - } else if (source == null) { | 147 | | - ({ responseURL, source } = getSourceSync(urlInstance, context)); | 148 | | - context.source = source; | 149 | | - } | | 147 | + format ??= 'builtin'; | | 148 | + } else if (format === 'addon') { | | 149 | + // Skip loading addon file content. It must be loaded with dlopen from file system. | | 150 | + source = null; | | 151 | + } else { | | 152 | + if (source == null) { | | 153 | + ({ responseURL, source } = getSourceSync(urlInstance, context)); | | 154 | + context = { __proto__: context, source }; | | 155 | + } | 150 | 156 |
| 151 | | - format ??= defaultGetFormat(urlInstance, context); | | 157 | + // Now that we have the source for the module, run `defaultGetFormat` to detect its format. | | 158 | + format ??= defaultGetFormat(urlInstance, context); | 152 | 159 |
| | 160 | + // For backward compatibility reasons, we need to let go through Module._load | | 161 | + // again. | | 162 | + shouldBeReloadedByCJSLoader = (format === 'commonjs'); | | 163 | + } | 153 | 164 | validateAttributes(url, format, importAttributes); | 154 | 165 |
| 155 | 166 | return { | 156 | 167 | __proto__: null, | 157 | 168 | format, | 158 | 169 | responseURL, | 159 | 170 | source, | | 171 | + shouldBeReloadedByCJSLoader, | 160 | 172 | }; | 161 | 173 | } | 162 | 174 |
| |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/modules/esm/loader.js +35-26Lines changed: 35 additions & 26 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -55,8 +55,9 @@ const { | 55 | 55 | resolveWithHooks, | 56 | 56 | loadHooks, | 57 | 57 | loadWithHooks, | | 58 | + validateLoadSloppy, | 58 | 59 | } = require('internal/modules/customization_hooks'); | 59 | | -let defaultResolve, defaultLoad, defaultLoadSync, importMetaInitializer; | | 60 | +let defaultResolve, defaultLoadSync, importMetaInitializer; | 60 | 61 |
| 61 | 62 | const { tracingChannel } = require('diagnostics_channel'); | 62 | 63 | const onImport = tracingChannel('module.import'); | @@ -146,6 +147,10 @@ let hooksProxy; | 146 | 147 | * @typedef {ArrayBuffer|TypedArray|string} ModuleSource | 147 | 148 | */ | 148 | 149 |
| | 150 | +/** | | 151 | + * @typedef {{ format: ModuleFormat, source: ModuleSource, translatorKey: string }} TranslateContext | | 152 | + */ | | 153 | + | 149 | 154 | /** | 150 | 155 | * This class covers the base machinery of module loading. To add custom | 151 | 156 | * behavior you can pass a customizations object and this object will be | @@ -503,18 +508,19 @@ class ModuleLoader { | 503 | 508 |
| 504 | 509 | const loadResult = this.#loadSync(url, { format, importAttributes }); | 505 | 510 |
| | 511 | + const formatFromLoad = loadResult.format; | 506 | 512 | // Use the synchronous commonjs translator which can deal with cycles. | 507 | | - const finalFormat = | 508 | | - loadResult.format === 'commonjs' || | 509 | | - loadResult.format === 'commonjs-typescript' ? 'commonjs-sync' : loadResult.format; | | 513 | + const translatorKey = (formatFromLoad === 'commonjs' || formatFromLoad === 'commonjs-typescript') ? | | 514 | + 'commonjs-sync' : formatFromLoad; | 510 | 515 |
| 511 | | - if (finalFormat === 'wasm') { | | 516 | + if (translatorKey === 'wasm') { | 512 | 517 | assert.fail('WASM is currently unsupported by require(esm)'); | 513 | 518 | } | 514 | 519 |
| 515 | 520 | const { source } = loadResult; | 516 | 521 | const isMain = (parentURL === undefined); | 517 | | - const wrap = this.#translate(url, finalFormat, source, parentURL); | | 522 | + const translateContext = { format: formatFromLoad, source, translatorKey, __proto__: null }; | | 523 | + const wrap = this.#translate(url, translateContext, parentURL); | 518 | 524 | assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`); | 519 | 525 |
| 520 | 526 | if (process.env.WATCH_REPORT_DEPENDENCIES && process.send) { | @@ -523,7 +529,7 @@ class ModuleLoader { | 523 | 529 |
| 524 | 530 | const cjsModule = wrap[imported_cjs_symbol]; | 525 | 531 | if (cjsModule) { | 526 | | - assert(finalFormat === 'commonjs-sync'); | | 532 | + assert(translatorKey === 'commonjs-sync'); | 527 | 533 | // Check if the ESM initiating import CJS is being required by the same CJS module. | 528 | 534 | if (cjsModule?.[kIsExecuting]) { | 529 | 535 | const parentFilename = urlToFilename(parentURL); | @@ -547,22 +553,22 @@ class ModuleLoader { | 547 | 553 | * Translate a loaded module source into a ModuleWrap. This is run synchronously, | 548 | 554 | * but the translator may return the ModuleWrap in a Promise. | 549 | 555 | * @param {string} url URL of the module to be translated. | 550 | | - * @param {string} format Format of the module to be translated. This is used to find | 551 | | - * matching translators. | 552 | | - * @param {ModuleSource} source Source of the module to be translated. | 553 | | - * @param {string|undefined} parentURL URL of the parent module. Undefined if it's the entry point. | | 556 | + * @param {TranslateContext} translateContext Context for the translator | | 557 | + * @param {string|undefined} parentURL URL of the module initiating the module loading for the first time. | | 558 | + * Undefined if it's the entry point. | 554 | 559 | * @returns {ModuleWrap} | 555 | 560 | */ | 556 | | - #translate(url, format, source, parentURL) { | | 561 | + #translate(url, translateContext, parentURL) { | | 562 | + const { translatorKey, format } = translateContext; | 557 | 563 | this.validateLoadResult(url, format); | 558 | | - const translator = getTranslators().get(format); | | 564 | + const translator = getTranslators().get(translatorKey); | 559 | 565 |
| 560 | 566 | if (!translator) { | 561 | | - throw new ERR_UNKNOWN_MODULE_FORMAT(format, url); | | 567 | + throw new ERR_UNKNOWN_MODULE_FORMAT(translatorKey, url); | 562 | 568 | } | 563 | 569 |
| 564 | | - const result = FunctionPrototypeCall(translator, this, url, source, parentURL === undefined); | 565 | | - assert(result instanceof ModuleWrap); | | 570 | + const result = FunctionPrototypeCall(translator, this, url, translateContext, parentURL); | | 571 | + assert(result instanceof ModuleWrap, `The ${format} module returned is not a ModuleWrap`); | 566 | 572 | return result; | 567 | 573 | } | 568 | 574 |
| @@ -575,7 +581,8 @@ class ModuleLoader { | 575 | 581 | * @returns {ModuleWrap} | 576 | 582 | */ | 577 | 583 | loadAndTranslateForRequireInImportedCJS(url, loadContext, parentURL) { | 578 | | - const { format: formatFromLoad, source } = this.#loadSync(url, loadContext); | | 584 | + const loadResult = this.#loadSync(url, loadContext); | | 585 | + const formatFromLoad = loadResult.format; | 579 | 586 |
| 580 | 587 | if (formatFromLoad === 'wasm') { // require(wasm) is not supported. | 581 | 588 | throw new ERR_UNKNOWN_MODULE_FORMAT(formatFromLoad, url); | @@ -587,15 +594,16 @@ class ModuleLoader { | 587 | 594 | } | 588 | 595 | } | 589 | 596 |
| 590 | | - let finalFormat = formatFromLoad; | | 597 | + let translatorKey = formatFromLoad; | 591 | 598 | if (formatFromLoad === 'commonjs') { | 592 | | - finalFormat = 'require-commonjs'; | | 599 | + translatorKey = 'require-commonjs'; | 593 | 600 | } | 594 | 601 | if (formatFromLoad === 'commonjs-typescript') { | 595 | | - finalFormat = 'require-commonjs-typescript'; | | 602 | + translatorKey = 'require-commonjs-typescript'; | 596 | 603 | } | 597 | 604 |
| 598 | | - const wrap = this.#translate(url, finalFormat, source, parentURL); | | 605 | + const translateContext = { ...loadResult, translatorKey, __proto__: null }; | | 606 | + const wrap = this.#translate(url, translateContext, parentURL); | 599 | 607 | assert(wrap instanceof ModuleWrap, `Translator used for require(${url}) should not be async`); | 600 | 608 | return wrap; | 601 | 609 | } | @@ -610,8 +618,9 @@ class ModuleLoader { | 610 | 618 | */ | 611 | 619 | loadAndTranslate(url, loadContext, parentURL) { | 612 | 620 | const maybePromise = this.load(url, loadContext); | 613 | | - const afterLoad = ({ format, source }) => { | 614 | | - return this.#translate(url, format, source, parentURL); | | 621 | + const afterLoad = (loadResult) => { | | 622 | + const translateContext = { ...loadResult, translatorKey: loadResult.format, __proto__: null }; | | 623 | + return this.#translate(url, translateContext, parentURL); | 615 | 624 | }; | 616 | 625 | if (isPromise(maybePromise)) { | 617 | 626 | return maybePromise.then(afterLoad); | @@ -837,8 +846,8 @@ class ModuleLoader { | 837 | 846 | return this.#customizations.load(url, context); | 838 | 847 | } | 839 | 848 |
| 840 | | - defaultLoad ??= require('internal/modules/esm/load').defaultLoad; | 841 | | - return defaultLoad(url, context); | | 849 | + defaultLoadSync ??= require('internal/modules/esm/load').defaultLoadSync; | | 850 | + return defaultLoadSync(url, context); | 842 | 851 | } | 843 | 852 |
| 844 | 853 | /** | @@ -873,7 +882,7 @@ class ModuleLoader { | 873 | 882 | // TODO(joyeecheung): construct the ModuleLoadContext in the loaders directly instead | 874 | 883 | // of converting them from plain objects in the hooks. | 875 | 884 | return loadWithHooks(url, context.format, context.importAttributes, this.#defaultConditions, | 876 | | - this.#loadAndMaybeBlockOnLoaderThread.bind(this)); | | 885 | + this.#loadAndMaybeBlockOnLoaderThread.bind(this), validateLoadSloppy); | 877 | 886 | } | 878 | 887 | return this.#loadAndMaybeBlockOnLoaderThread(url, context); | 879 | 888 | } | |
|