/
githubmirror
/
webpack
Обзор
Документация
Войти
/
githubmirror
/
webpack
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v5.109.1
lib/runtime/MakeDeferredNamespaceObjectRuntime.js
549 строк
22 KB
Alexander Akait
fix: test262 conformance for import defer and dynamic import() options (#21446)
18 июл 2026, 19:23
Не верифицирован
18 июл 2026, 19:23
a4d53f1
Код
Авторство
О чём код?
/* MIT License http://www.opensource.org/licenses/mit-license.php */ "use strict"; const RuntimeGlobals = require("../RuntimeGlobals"); const Template = require("../Template"); const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency"); const HelperRuntimeModule = require("./HelperRuntimeModule"); /** @typedef {import("../ChunkGraph")} ChunkGraph */ /** @typedef {import("../Module")} Module */ /** @typedef {import("../ModuleGraph")} ModuleGraph */ /** @typedef {import("../Module").RuntimeRequirements} RuntimeRequirements */ /** @typedef {import("../Module").ExportsType} ExportsType */ /** @typedef {import("../ChunkGraph").ModuleId} ModuleId */ /** * @typedef {object} DeferredCycleState * @property {Map<Module, Set<Module>>} sccOf strongly-connected component per module (shared object per component) * @property {Map<Module, Set<Module> | null>} peers memoized SCC peers (component minus the module) per deferred module */ /** @type {WeakMap<ModuleGraph, DeferredCycleState>} */ const deferredCycleCache = new WeakMap(); /** * Materializes the active harmony-import targets of a module (deferred edges * included, matching the spec's `RequestedModules` recursion). * @param {ModuleGraph} moduleGraph the module graph * @param {Module} module the module * @returns {Module[]} imported modules */ function harmonyImportTargets(moduleGraph, module) { const connections = moduleGraph.getOutgoingConnectionsByModule(module); if (!connections) return []; /** @type {Module[]} */ const targets = []; for (const [dep, moduleConnections] of connections) { if ( dep && moduleConnections.some( (c) => c.dependency instanceof HarmonyImportDependency && c.isTargetActive(undefined) ) ) { targets.push(dep); } } return targets; } /** * Assigns every module reachable from `seed` (through active harmony imports) * to its strongly-connected component via an iterative Tarjan pass, writing the * results into `sccOf`. Modules already assigned by an earlier pass are treated * as finished, so across all deferred imports every edge is visited once * (O(V + E) total) rather than re-walked per import. * @param {ModuleGraph} moduleGraph the module graph * @param {Module} seed the module to explore from * @param {Map<Module, Set<Module>>} sccOf module-to-component map to populate * @returns {void} */ function assignStronglyConnectedComponents(moduleGraph, seed, sccOf) { if (sccOf.has(seed)) return; /** @type {Map<Module, number>} */ const index = new Map(); /** @type {Map<Module, number>} */ const low = new Map(); /** @type {Set<Module>} */ const onStack = new Set(); /** @type {Module[]} */ const componentStack = []; /** @type {{ node: Module, targets: Module[], i: number }[]} */ const work = []; let counter = 0; /** * @param {Module} node node to open */ const open = (node) => { index.set(node, counter); low.set(node, counter); counter++; componentStack.push(node); onStack.add(node); work.push({ node, targets: harmonyImportTargets(moduleGraph, node), i: 0 }); }; open(seed); while (work.length > 0) { const frame = work[work.length - 1]; if (frame.i < frame.targets.length) { const next = frame.targets[frame.i++]; // Already in a finished component (from this or a prior pass): skip. if (sccOf.has(next)) continue; if (!index.has(next)) { open(next); } else if (onStack.has(next)) { const nodeLow = /** @type {number} */ (low.get(frame.node)); const nextIndex = /** @type {number} */ (index.get(next)); if (nextIndex < nodeLow) low.set(frame.node, nextIndex); } continue; } work.pop(); const node = frame.node; const nodeLow = /** @type {number} */ (low.get(node)); if (work.length > 0) { const parent = work[work.length - 1].node; if (nodeLow < /** @type {number} */ (low.get(parent))) { low.set(parent, nodeLow); } } if (nodeLow === index.get(node)) { /** @type {Set<Module>} */ const component = new Set(); let member; do { member = /** @type {Module} */ (componentStack.pop()); onStack.delete(member); component.add(member); } while (member !== node); for (const m of component) sccOf.set(m, component); } } } /** * Per the TC39 import-defer spec's `ReadyForSyncExecution`, forcing evaluation * of a deferred module must throw when any module in its transitive static * import closure is currently evaluating. Only a module in `module`'s own * strongly-connected component can be evaluating at that point (anything else it * imports is either already evaluated or not started), so we emit just the SCC * peers instead of the whole (potentially huge) forward closure. * @param {ModuleGraph} moduleGraph the module graph * @param {Module} module the deferred module * @returns {Set<Module> | null} the SCC peers when cyclic, otherwise null */ function getDeferredCycleModules(moduleGraph, module) { let state = deferredCycleCache.get(moduleGraph); if (state === undefined) { state = { sccOf: new Map(), peers: new Map() }; deferredCycleCache.set(moduleGraph, state); } const cached = state.peers.get(module); if (cached !== undefined) return cached; assignStronglyConnectedComponents(moduleGraph, module, state.sccOf); const component = state.sccOf.get(module); // A trivial component (size 1, no self-cycle peers) means no cycle; a direct // self-loop is already covered by the `evaluating` check on `module` itself. let result = null; if (component !== undefined && component.size > 1) { result = new Set(component); result.delete(module); } state.peers.set(module, result); return result; } /** * Maps a defer-cycle closure to the runtime module ids whose `evaluating` flag * the deferred namespace must check. `resolveId` maps a closure module to the * runtime id that carries its evaluation state (its own id, or the id of the * concatenated module that absorbed it); unresolved (`null`) members are * dropped. Returns `null` when there is nothing to check. * @param {Set<Module> | null} closure closure modules, or null when not cyclic * @param {(module: Module) => ModuleId | null} resolveId maps a module to its runtime id * @returns {ModuleId[] | null} deduplicated runtime ids, or null */ function getDeferredCycleModuleIds(closure, resolveId) { if (closure === null) return null; /** @type {Set<ModuleId>} */ const ids = new Set(); for (const module of closure) { const id = resolveId(module); if (id !== null) ids.add(id); } return ids.size > 0 ? [...ids] : null; } /** * @param {ExportsType} exportsType exports type * @returns {string} mode */ function getMakeDeferredNamespaceModeFromExportsType(exportsType) { // number is from createFakeNamespaceObject mode ^ 1 if (exportsType === "namespace") return `/* ${exportsType} */ 8`; if (exportsType === "default-only") return `/* ${exportsType} */ 0`; if (exportsType === "default-with-named") return `/* ${exportsType} */ 2`; if (exportsType === "dynamic") return `/* ${exportsType} */ 6`; throw new Error(`Unknown exports type: ${exportsType}`); } /** * @param {string} moduleId moduleId * @param {ExportsType} exportsType exportsType * @param {(ModuleId | null)[]} asyncDepsIds asyncDepsIds * @param {ModuleId[] | null} syncCycleDepsIds transitive static closure ids when the module is part of a defer cycle * @param {RuntimeRequirements} runtimeRequirements runtime requirements * @returns {string} call make optimized deferred namespace object */ function getOptimizedDeferredModule( moduleId, exportsType, asyncDepsIds, syncCycleDepsIds, runtimeRequirements ) { runtimeRequirements.add(RuntimeGlobals.makeOptimizedDeferredNamespaceObject); const mode = getMakeDeferredNamespaceModeFromExportsType(exportsType); const asyncDeps = asyncDepsIds.filter((x) => x !== null); const hasSync = syncCycleDepsIds !== null && syncCycleDepsIds.length > 0; // `syncDeps` is passed positionally after `asyncDeps`, so a `0` placeholder // keeps the slot when the module has cycle deps but no async deps. const args = [moduleId, mode]; if (asyncDeps.length > 0 || hasSync) { args.push(asyncDeps.length > 0 ? JSON.stringify(asyncDeps) : "0"); } if (hasSync) args.push(JSON.stringify(syncCycleDepsIds)); return `${RuntimeGlobals.makeOptimizedDeferredNamespaceObject}(${args.join( ", " )})`; } class MakeOptimizedDeferredNamespaceObjectRuntimeModule extends HelperRuntimeModule { /** * @param {boolean} hasAsyncRuntime if async module is used. */ constructor(hasAsyncRuntime) { super("make optimized deferred namespace object"); /** @type {boolean} */ this.hasAsyncRuntime = hasAsyncRuntime; } /** * Generates runtime code for this runtime module. * @returns {string | null} runtime code */ generate() { if (!this.compilation) return null; const { runtimeTemplate } = this.compilation; const cst = runtimeTemplate.renderConst(); const lt = runtimeTemplate.renderLet(); const fn = RuntimeGlobals.makeOptimizedDeferredNamespaceObject; const hasAsync = this.hasAsyncRuntime; return Template.asString([ // Note: must be a function (not arrow), because this is used in body! // `asyncDeps` keeps a fixed positional slot even without async runtime // so the trailing `syncDeps` (defer-cycle closure) always lines up. `${fn} = function(moduleId, mode, asyncDeps, syncDeps) {`, Template.indent([ `${cst} r = this;`, hasAsync ? `${cst} isAsync = asyncDeps && asyncDeps.length;` : "", `${cst} obj = {`, Template.indent([ "get a() {", Template.indent([ // Forcing evaluation of a module that is currently evaluating // (a cycle reached through a deferred import) must throw rather // than expose its partial exports. `syncDeps` (present only for // cyclic deferred modules) carries the transitive static closure, // so an evaluating dependency is caught before any evaluation. `${cst} cachedModule = __webpack_module_cache__[moduleId];`, 'if (cachedModule !== undefined && (cachedModule.evaluating || cachedModule.evaluatingAsync)) throw new TypeError("Cannot access a deferred module namespace while the module is being evaluated");', "if (syncDeps) for (var i = 0; i < syncDeps.length; i++) {", Template.indent([ `${cst} depModule = __webpack_module_cache__[syncDeps[i]];`, 'if (depModule !== undefined && (depModule.evaluating || depModule.evaluatingAsync)) throw new TypeError("Cannot access a deferred module namespace while a dependency is being evaluated");' ]), "}", `${lt} exports = r(moduleId);`, hasAsync ? `if(isAsync) exports = exports[${RuntimeGlobals.asyncModuleExportSymbol}];` : "", // if exportsType is "namespace" we can generate the most optimized code, // on the second access, we can avoid trigger the getter. // we can also do this if exportsType is "dynamic" and there is a "__esModule" property on it. 'if(mode & 8 || (mode & 4 && exports.__esModule)) Object.defineProperty(this, "a", { value: exports });', "return exports;" ]), "}" ]), "};", hasAsync ? `if(isAsync) obj[${RuntimeGlobals.deferredModuleAsyncTransitiveDependenciesSymbol}] = asyncDeps;` : "", "return obj;" ]), "};" ]); } } class MakeDeferredNamespaceObjectRuntimeModule extends HelperRuntimeModule { /** * @param {boolean} hasAsyncRuntime if async module is used. */ constructor(hasAsyncRuntime) { super("make deferred namespace object"); /** @type {boolean} */ this.hasAsyncRuntime = hasAsyncRuntime; } /** * Generates runtime code for this runtime module. * @returns {string | null} runtime code */ generate() { if (!this.compilation) return null; const { runtimeTemplate } = this.compilation; const cst = runtimeTemplate.renderConst(); const lt = runtimeTemplate.renderLet(); const fn = RuntimeGlobals.makeDeferredNamespaceObject; const hasAsync = this.hasAsyncRuntime; const init = `${runtimeTemplate.optionalChaining("init", "()")};`; return `${fn} = ${runtimeTemplate.basicFunction("moduleId, mode", [ // Per the TC39 import-defer spec, deferred namespaces are // distinct from their eager counterparts and the same module // referenced from multiple defer-import sites must yield the // same object. Cache the Proxy / fake namespace per-moduleId so // repeated calls (including across files) share identity. // // Bit 16 (`createFakeNamespaceObject`'s "return value when // it's Promise-like" flag added by // `RuntimeTemplate.moduleNamespacePromise` for dynamic // imports) is irrelevant for deferred namespaces — the value // passed into `createFakeNamespaceObject` here is always the // resolved module exports (after unwrapping the async-module // export symbol when present), never a Promise. Strip it // once so all downstream behavior, the cache key, and the // `createFakeNamespaceObject` call below see the same shape // mode. This keeps static defer (mode 8) and dynamic // `await import.defer` (mode 8 | 16) sharing the same // Deferred Module Namespace object, while still keying by // `(moduleId, mode)` so distinct exports-type shapes // (e.g. one importer treats a CJS module as // "default-with-named", another as "namespace") get // distinct cache entries. "mode &= ~16;", `${lt} byMode = __webpack_module_deferred_namespace_cache__[moduleId];`, "if (byMode && byMode[mode] !== undefined) return byMode[mode];", "if (!byMode) byMode = __webpack_module_deferred_namespace_cache__[moduleId] = {};", `${cst} cachedModule = __webpack_module_cache__[moduleId];`, "if (cachedModule && cachedModule.error === undefined && !(mode & 8)) {", Template.indent([ `${lt} exports = cachedModule.exports;`, hasAsync ? `if (${RuntimeGlobals.asyncModuleExportSymbol} in exports) exports = exports[${RuntimeGlobals.asyncModuleExportSymbol}];` : "", `return byMode[mode] = ${RuntimeGlobals.createFakeNamespaceObject}(exports, mode);` ]), "}", "", `${lt} init = ${runtimeTemplate.basicFunction("", [ // A deferred namespace that forces evaluation of a module that is // already evaluating (a cycle) must throw rather than expose its // partial exports. `${cst} evaluatingModule = __webpack_module_cache__[moduleId];`, 'if (evaluatingModule !== undefined && (evaluatingModule.evaluating || evaluatingModule.evaluatingAsync)) throw new TypeError("Cannot access a deferred module namespace while the module is being evaluated");', `ns = ${RuntimeGlobals.require}(moduleId);`, hasAsync ? `if (${RuntimeGlobals.asyncModuleExportSymbol} in ns) ns = ns[${RuntimeGlobals.asyncModuleExportSymbol}];` : "", "init = null;", "if (mode & 8 || mode & 4 && ns.__esModule && typeof ns === 'object') {", Template.indent([ // Drop only the read-side traps after init: with the // resolved namespace's own keys mirrored onto // `ns_target` below, the default `Reflect` behavior // returns the right values via the live-binding // getters, so we no longer need to intercept `get` / // `has` / `ownKeys` / `getOwnPropertyDescriptor`. // // The mutation traps (`set`, `deleteProperty`, // `defineProperty`) are kept because per the TC39 // import-defer spec, `[[Set]]` / `[[Delete]]` / // `[[DefineOwnProperty]]` on a Deferred Module // Namespace Exotic Object never succeed — and the // proxy target itself remains extensible // (architecturally we cannot freeze it up-front), // so without these traps `ns.notExported = "x"` // after evaluation would silently create a property // on the target instead of returning false. "delete handler.get;", "delete handler.has;", "delete handler.ownKeys;", "delete handler.getOwnPropertyDescriptor;" ]), "} else {", Template.indent([ `ns = ${RuntimeGlobals.createFakeNamespaceObject}(ns, mode);` ]), "}", // Mirror own properties from the resolved namespace onto the proxy // target so that proxy invariants hold for callers that structurally // introspect via `Object.keys` / `Object.getOwnPropertyNames` / // `Object.getOwnPropertyDescriptor`: when our trap reports a // non-configurable descriptor for a key, the target must also have // that key with a matching descriptor. // // `__esModule` and `Symbol.toStringTag` are intentionally skipped: // the proxy synthesizes "Deferred Module" / true regardless of what // the underlying namespace exposes (per the TC39 import-defer // proposal, the [[StringTag]] of a Deferred Module Namespace // Exotic Object is "Deferred Module"), and the target was already // pre-populated with those values below. `${cst} keys = Reflect.ownKeys(ns);`, "for (var i = 0; i < keys.length; i++) {", Template.indent([ `${cst} k = keys[i];`, 'if (k === "__esModule" || k === Symbol.toStringTag) continue;', `if (!${runtimeTemplate.objectHasOwn("ns_target", "k")}) {`, Template.indent([ "try { Object.defineProperty(ns_target, k, Reflect.getOwnPropertyDescriptor(ns, k)); } catch (_) {}" ]), "}" ]), "}" ])};`, "", // The proxy target is a fresh placeholder, separate from // `__webpack_module_deferred_exports__[moduleId]` (which is reused // by `__webpack_require__` as `module.exports` for deferred-loaded // modules and would conflict with our pre-populated synthetic // `__esModule` / `Symbol.toStringTag` non-configurable properties). // Using a dedicated target keeps the proxy invariant-compliant // without interfering with the module's own exports object. `${cst} ns_target = { __proto__: null };`, // Pre-populate the synthetic deferred-namespace properties with // fully non-configurable, non-writable, non-enumerable descriptors // (matching the TC39 import-defer spec for Module Namespace // Exotic Objects). The trap returns the same descriptors below. 'Object.defineProperty(ns_target, "__esModule", { value: true });', 'Object.defineProperty(ns_target, Symbol.toStringTag, { value: "Deferred Module" });', `${lt} ns = ns_target;`, `${cst} handler = {`, Template.indent([ "__proto__: null,", // Per the TC39 import-defer proposal, `IsSymbolLikeNamespaceKey` // returns true for any Symbol-keyed access (and for "then"); such // accesses go through `OrdinaryGetOwnProperty` and must not // trigger evaluation of the deferred module. The Symbol checks // below short-circuit to the pre-populated target without // running `init()`. `${runtimeTemplate.method("get", "_, name", [ "switch (name) {", Template.indent([ 'case "__esModule": return true;', 'case Symbol.toStringTag: return "Deferred Module";', 'case "then": return undefined;' ]), "}", 'if (typeof name === "symbol") return ns_target[name];', init, "return ns[name];" ])},`, `${runtimeTemplate.method("has", "_, name", [ "switch (name) {", Template.indent( [ 'case "__esModule":', "case Symbol.toStringTag:", hasAsync ? `case ${RuntimeGlobals.deferredModuleAsyncTransitiveDependenciesSymbol}:` : "", Template.indent("return true;"), 'case "then":', Template.indent("return false;") ].filter(Boolean) ), "}", 'if (typeof name === "symbol") return name in ns_target;', init, "return name in ns;" ])},`, `${runtimeTemplate.method("ownKeys", "", [ init, `${cst} filtered = Reflect.ownKeys(ns).filter(${runtimeTemplate.expressionFunction( 'x !== "then" && x !== Symbol.toStringTag', "x" )});`, `${cst} keys = ${ runtimeTemplate.supportsSpread() ? "[...filtered, Symbol.toStringTag]" : "filtered.concat([Symbol.toStringTag])" };`, "return keys;" ])},`, `${runtimeTemplate.method("getOwnPropertyDescriptor", "_, name", [ "switch (name) {", Template.indent([ // Match the descriptors actually defined on `ns_target` // (non-configurable, non-writable, non-enumerable) so the // proxy invariant holds for both the trap result and any // post-init forwarding via the deleted-handler path. 'case "__esModule": return { value: true, writable: false, enumerable: false, configurable: false };', 'case Symbol.toStringTag: return { value: "Deferred Module", writable: false, enumerable: false, configurable: false };', 'case "then": return undefined;' ]), "}", 'if (typeof name === "symbol") return Reflect.getOwnPropertyDescriptor(ns_target, name);', init, `${lt} desc = Reflect.getOwnPropertyDescriptor(ns, name);`, 'if (mode & 2 && name == "default" && !desc) {', Template.indent("desc = { value: ns, configurable: true };"), "}", "return desc;" ])},`, // `defineProperty` always rejects, but per the TC39 spec it // must still trigger evaluation for string keys (the spec // algorithm calls `[[GetOwnProperty]]` first, which forces // evaluation on a deferred namespace). Symbol keys go through // OrdinaryDefineOwnProperty and do not trigger eval. `${runtimeTemplate.method("defineProperty", "_, name", [ 'if (typeof name === "symbol" || name === "then") return false;', init, "return false;" ])},`, // `deleteProperty` rejects, but per the TC39 spec it must // still trigger evaluation for string keys (the spec // algorithm calls `GetModuleExportsList` for non-symbol-like // keys, forcing evaluation on a deferred namespace). `${runtimeTemplate.method("deleteProperty", "_, name", [ 'if (typeof name === "symbol" || name === "then") return false;', init, "return false;" ])},`, // `set` always returns false without triggering evaluation — // the spec [[Set]] algorithm for Module Namespaces is just // "return false" (no [[GetOwnProperty]], no eval). `set: ${runtimeTemplate.returningFunction("false")},` ]), "}", // we don't fully emulate ES Module semantics in this Proxy to align with normal webpack esm namespace object. "return byMode[mode] = new Proxy(ns_target, handler);" ])};`; } } module.exports.MakeDeferredNamespaceObjectRuntimeModule = MakeDeferredNamespaceObjectRuntimeModule; module.exports.MakeOptimizedDeferredNamespaceObjectRuntimeModule = MakeOptimizedDeferredNamespaceObjectRuntimeModule; module.exports.getDeferredCycleModuleIds = getDeferredCycleModuleIds; module.exports.getDeferredCycleModules = getDeferredCycleModules; module.exports.getMakeDeferredNamespaceModeFromExportsType = getMakeDeferredNamespaceModeFromExportsType; module.exports.getOptimizedDeferredModule = getOptimizedDeferredModule;