Skip to content

Commit 062995c

Browse files
fix(runtime): rethrow deferred module errors without strict error handling
A hot update that introduces deferred imports now records evaluation errors itself when output.strictModuleErrorHandling is off, since the running bootstrap keeps none.
1 parent dd2cf74 commit 062995c

6 files changed

Lines changed: 63 additions & 4 deletions

File tree

‎lib/runtime/MakeDeferredNamespaceObjectRuntime.js‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,10 @@ function getOptimizedDeferredModule(
226226
* Renders the setup a deferred namespace helper needs when a hot update delivers
227227
* it, since the running bootstrap may not mark modules as evaluating.
228228
* @param {RuntimeTemplate} runtimeTemplate the runtime template
229+
* @param {boolean} recordErrors the bootstrap may not keep evaluation errors
229230
* @returns {string} setup code, run once per runtime
230231
*/
231-
function renderHotDeferredSetup(runtimeTemplate) {
232+
function renderHotDeferredSetup(runtimeTemplate, recordErrors) {
232233
const cache = RuntimeGlobals.deferredModuleNamespaceCache;
233234
return Template.asString([
234235
// The namespace cache doubles as the flag that the interceptor is installed.
@@ -242,7 +243,11 @@ function renderHotDeferredSetup(runtimeTemplate) {
242243
"if (factory) options.factory = function(module) {",
243244
Template.indent([
244245
"module.evaluating = true;",
245-
"try { return factory.apply(this, arguments); } finally { module.evaluating = false; }"
246+
"try { return factory.apply(this, arguments); }",
247+
...(recordErrors
248+
? ["catch (e) { module.error = e; throw e; }"]
249+
: []),
250+
"finally { module.evaluating = false; }"
246251
]),
247252
"};"
248253
]
@@ -287,8 +292,11 @@ class MakeOptimizedDeferredNamespaceObjectRuntimeModule extends HelperRuntimeMod
287292
const moduleCache = hot
288293
? RuntimeGlobals.moduleCache
289294
: "__webpack_module_cache__";
295+
// Without strict error handling the running bootstrap keeps no error to rethrow.
296+
const recordErrors =
297+
hot && !runtimeTemplate.outputOptions.strictModuleErrorHandling;
290298
return Template.asString([
291-
...(hot ? [renderHotDeferredSetup(runtimeTemplate)] : []),
299+
...(hot ? [renderHotDeferredSetup(runtimeTemplate, recordErrors)] : []),
292300
// Note: must be a function (not arrow), because this is used in body!
293301
// `asyncDeps` keeps a fixed positional slot even without async runtime
294302
// so the trailing `syncDeps` (defer-cycle closure) always lines up.
@@ -305,6 +313,11 @@ class MakeOptimizedDeferredNamespaceObjectRuntimeModule extends HelperRuntimeMod
305313
// exports. `syncDeps` carries the closure that catches it first.
306314
`${cst} cachedModule = ${moduleCache}[moduleId];`,
307315
'if (cachedModule !== undefined && (cachedModule.evaluating || cachedModule.evaluatingAsync)) throw new TypeError("Cannot access a deferred module namespace while the module is being evaluated");',
316+
...(recordErrors
317+
? [
318+
"if (cachedModule !== undefined && cachedModule.error !== undefined) throw cachedModule.error;"
319+
]
320+
: []),
308321
"if (syncDeps) for (var i = 0; i < syncDeps.length; i++) {",
309322
Template.indent([
310323
`${cst} depModule = ${moduleCache}[syncDeps[i]];`,
@@ -393,7 +406,10 @@ class MakeDeferredNamespaceObjectRuntimeModule extends HelperRuntimeModule {
393406
const namespaceCache = hot
394407
? RuntimeGlobals.deferredModuleNamespaceCache
395408
: "__webpack_module_deferred_namespace_cache__";
396-
return `${hot ? `${renderHotDeferredSetup(runtimeTemplate)}\n` : ""}${fn} = ${runtimeTemplate.basicFunction(
409+
// Without strict error handling the running bootstrap keeps no error to rethrow.
410+
const recordErrors =
411+
hot && !runtimeTemplate.outputOptions.strictModuleErrorHandling;
412+
return `${hot ? `${renderHotDeferredSetup(runtimeTemplate, recordErrors)}\n` : ""}${fn} = ${runtimeTemplate.basicFunction(
397413
"moduleId, mode, exportNames",
398414
[
399415
// WHY: the TC39 import-defer spec makes a deferred namespace distinct
@@ -440,6 +456,11 @@ class MakeDeferredNamespaceObjectRuntimeModule extends HelperRuntimeModule {
440456
// partial exports.
441457
`${cst} evaluatingModule = ${moduleCache}[moduleId];`,
442458
'if (evaluatingModule !== undefined && (evaluatingModule.evaluating || evaluatingModule.evaluatingAsync)) throw new TypeError("Cannot access a deferred module namespace while the module is being evaluated");',
459+
...(recordErrors
460+
? [
461+
"if (evaluatingModule !== undefined && evaluatingModule.error !== undefined) throw evaluatingModule.error;"
462+
]
463+
: []),
443464
`ns = ${RuntimeGlobals.require}(moduleId);`,
444465
hasAsync
445466
? `if (${RuntimeGlobals.asyncModuleExportSymbol} in ns) ns = ns[${RuntimeGlobals.asyncModuleExportSymbol}];`
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const getError = () => undefined;
2+
export const getNamespace = () => undefined;
3+
---
4+
import defer * as broken from "./error";
5+
export const getError = () => broken.value;
6+
export const getNamespace = () => broken;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { evaluations } from "./state";
2+
evaluations.push("error");
3+
throw new Error("deferred failure");
4+
export const value = 0;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { getError, getNamespace } from "./consumer";
2+
import { evaluations } from "./state";
3+
import update from "../../update.esm.js";
4+
5+
import.meta.webpackHot.accept("./consumer");
6+
7+
it("should rethrow a deferred module's error without strict error handling", (done) => {
8+
expect(getError()).toBe(undefined);
9+
NEXT(
10+
update(done, true, () => {
11+
expect(getError).toThrow("deferred failure");
12+
expect(getError).toThrow("deferred failure");
13+
const namespace = getNamespace();
14+
expect(() => namespace.value).toThrow("deferred failure");
15+
expect(evaluations).toEqual(["error"]);
16+
done();
17+
})
18+
);
19+
});
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const evaluations = [];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"use strict";
2+
3+
/** @type {import("../../../../").Configuration} */
4+
module.exports = {
5+
experiments: { deferImport: true },
6+
optimization: { concatenateModules: false },
7+
output: { strictModuleErrorHandling: false }
8+
};

0 commit comments

Comments
 (0)