feat: implement tc39 Defer Module Evaluation (experiment)

* feat: implement tc39 defer import proposal

Co-authored-by: Nicolo Ribaudo <hello@nicr.dev>

* fix: keys of DeferredNamespaceObject

* refactor: MakeDeferredNamespaceObjectRuntimeModule

---------

Co-authored-by: Nicolo Ribaudo <hello@nicr.dev>
Co-authored-by: Hai <haijie0619@gmail.com>
This commit is contained in:
Jack Works 2025-07-02 20:02:03 +08:00 committed by GitHub
parent d4f275dcc2
commit cf1dc2f131
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 2327 additions and 89 deletions

View File

@ -805,6 +805,10 @@ export type CssParserNamedExports = boolean;
* Enable/disable `url()`/`image-set()`/`src()`/`image()` functions handling.
*/
export type CssParserUrl = boolean;
/**
* Options for defer import.
*/
export type DeferImportExperimentOptions = boolean;
/**
* A Function returning a Promise resolving to a normalized entry.
*/
@ -4002,6 +4006,10 @@ export interface ExperimentsExtra {
* Enable css support.
*/
css?: boolean;
/**
* Enable experimental tc39 proposal https://github.com/tc39/proposal-defer-import-eval. This allows to defer execution of a module until it's first use.
*/
deferImport?: boolean;
/**
* Compile entrypoints and import()s only when they are accessed.
*/
@ -4019,6 +4027,10 @@ export interface ExperimentsNormalizedExtra {
* Enable css support.
*/
css?: boolean;
/**
* Enable experimental tc39 proposal https://github.com/tc39/proposal-defer-import-eval. This allows to defer execution of a module until it's first use.
*/
deferImport?: boolean;
/**
* Compile entrypoints and import()s only when they are accessed.
*/

View File

@ -15,13 +15,14 @@ const {
/** @typedef {import("./optimize/ConcatenatedModule").ModuleInfo} ModuleInfo */
const MODULE_REFERENCE_REGEXP =
/^__WEBPACK_MODULE_REFERENCE__(\d+)_([\da-f]+|ns)(_call)?(_directImport)?(?:_asiSafe(\d))?__$/;
/^__WEBPACK_MODULE_REFERENCE__(\d+)_([\da-f]+|ns)(_call)?(_directImport)?(_deferredImport)?(?:_asiSafe(\d))?__$/;
/**
* @typedef {object} ModuleReferenceOptions
* @property {string[]} ids the properties/exports of the module
* @property {boolean} call true, when this referenced export is called
* @property {boolean} directImport true, when this referenced export is directly imported (not via property access)
* @property {boolean} deferredImport true, when this referenced export is deferred
* @property {boolean | undefined} asiSafe if the position is ASI safe or unknown
*/
@ -90,11 +91,18 @@ class ConcatenationScope {
*/
createModuleReference(
module,
{ ids = undefined, call = false, directImport = false, asiSafe = false }
{
ids = undefined,
call = false,
directImport = false,
deferredImport = false,
asiSafe = false
}
) {
const info = /** @type {ModuleInfo} */ (this._modulesMap.get(module));
const callFlag = call ? "_call" : "";
const directImportFlag = directImport ? "_directImport" : "";
const deferredImportFlag = deferredImport ? "_deferredImport" : "";
const asiSafeFlag = asiSafe
? "_asiSafe1"
: asiSafe === false
@ -104,7 +112,7 @@ class ConcatenationScope {
? Buffer.from(JSON.stringify(ids), "utf-8").toString("hex")
: "ns";
// a "._" is appended to allow "delete ...", which would cause a SyntaxError in strict mode
return `__WEBPACK_MODULE_REFERENCE__${info.index}_${exportData}${callFlag}${directImportFlag}${asiSafeFlag}__._`;
return `__WEBPACK_MODULE_REFERENCE__${info.index}_${exportData}${callFlag}${directImportFlag}${deferredImportFlag}${asiSafeFlag}__._`;
}
/**
@ -123,7 +131,7 @@ class ConcatenationScope {
const match = MODULE_REFERENCE_REGEXP.exec(name);
if (!match) return null;
const index = Number(match[1]);
const asiSafe = match[5];
const asiSafe = match[6];
return {
index,
ids:
@ -132,6 +140,7 @@ class ConcatenationScope {
: JSON.parse(Buffer.from(match[2], "hex").toString("utf-8")),
call: Boolean(match[3]),
directImport: Boolean(match[4]),
deferredImport: Boolean(match[5]),
asiSafe: asiSafe ? asiSafe === "1" : undefined
};
}

View File

@ -101,7 +101,10 @@ class Dependency {
/** @type {boolean} */
this.weak = false;
// TODO check if this can be moved into ModuleDependency
/** @type {boolean} */
/** @type {boolean | undefined} */
this.defer = false;
// TODO check if this can be moved into ModuleDependency
/** @type {boolean | undefined} */
this.optional = false;
this._locSL = 0;
this._locSC = 0;
@ -309,6 +312,7 @@ class Dependency {
write(this._locEC);
write(this._locI);
write(this._locN);
write(this.defer);
}
/**
@ -323,6 +327,7 @@ class Dependency {
this._locEC = read();
this._locI = read();
this._locN = read();
this.defer = read();
}
}

View File

@ -832,7 +832,7 @@ class ExportsInfo {
/** @typedef {Map<string, RuntimeUsageStateType>} UsedInRuntime */
/** @typedef {{ module: Module, export: string[] }} TargetItemWithoutConnection */
/** @typedef {{ module: Module, export: string[], deferred: boolean }} TargetItemWithoutConnection */
/** @typedef {{ module: Module, connection: ModuleGraphConnection, export: string[] | undefined }} TargetItemWithConnection */
@ -1341,7 +1341,10 @@ class ExportInfo {
/** @type {TargetItemWithoutConnection} */
let target = {
module: rawTarget.connection.module,
export: rawTarget.export
export: rawTarget.export,
deferred: Boolean(
rawTarget.connection.dependency && rawTarget.connection.dependency.defer
)
};
for (;;) {
if (validTargetModuleFilter(target.module)) return target;
@ -1361,7 +1364,8 @@ class ExportInfo {
module: newTarget.module,
export: newTarget.export
? newTarget.export.concat(target.export.slice(1))
: target.export.slice(1)
: target.export.slice(1),
deferred: newTarget.deferred
};
}
}

View File

@ -46,6 +46,7 @@ const makeSerializable = require("./util/makeSerializable");
/** @typedef {import("./util/fs").InputFileSystem} InputFileSystem */
/** @typedef {import("./util/identifier").AssociatedObjectForCache} AssociatedObjectForCache */
/** @typedef {import("./util/runtime").RuntimeSpec} RuntimeSpec */
/** @typedef {"namespace" | "default-only" | "default-with-named" | "dynamic"} ExportsType */
/**
* @template T
@ -527,7 +528,7 @@ class Module extends DependenciesBlock {
/**
* @param {ModuleGraph} moduleGraph the module graph
* @param {boolean | undefined} strict the importing module is strict
* @returns {"namespace" | "default-only" | "default-with-named" | "dynamic"} export type
* @returns {ExportsType} export type
* "namespace": Exports is already a namespace object. namespace = exports.
* "dynamic": Check at runtime if __esModule is set. When set: namespace = { ...exports, default: exports }. When not set: namespace = { default: exports }.
* "default-only": Provide a namespace object with only default export. namespace = { default: exports }

View File

@ -752,6 +752,25 @@ class ModuleGraph {
return mgm.async;
}
/**
* @param {Module} module the module
* @returns {boolean} true, if the module is used as a deferred module at least once
*/
isDeferred(module) {
if (this.isAsync(module)) return false;
const connections = this.getIncomingConnections(module);
for (const connection of connections) {
if (
!connection.dependency ||
connection.dependency instanceof
require("./dependencies/CommonJsSelfReferenceDependency")
)
continue;
if (connection.dependency.defer) return true;
}
return false;
}
/**
* @param {Module} module the module
* @returns {void}

View File

@ -116,6 +116,16 @@ module.exports.definePropertyGetters = "__webpack_require__.d";
*/
module.exports.makeNamespaceObject = "__webpack_require__.r";
/**
* make a deferred namespace object
*/
module.exports.makeDeferredNamespaceObject = "__webpack_require__.z";
/**
* the internal symbol that makeDeferredNamespaceObject is using.
*/
module.exports.makeDeferredNamespaceObjectSymbol = "__webpack_require__.zS";
/**
* create a fake namespace object
*/
@ -385,3 +395,13 @@ module.exports.relativeUrl = "__webpack_require__.U";
* ) => void
*/
module.exports.asyncModule = "__webpack_require__.a";
/**
* The internal symbol that asyncModule is using.
*/
module.exports.asyncModuleExportSymbol = "__webpack_require__.aE";
/**
* The internal symbol that asyncModule is using.
*/
module.exports.asyncModuleDoneSymbol = "__webpack_require__.aD";

View File

@ -25,6 +25,7 @@ const GetTrustedTypesPolicyRuntimeModule = require("./runtime/GetTrustedTypesPol
const GlobalRuntimeModule = require("./runtime/GlobalRuntimeModule");
const HasOwnPropertyRuntimeModule = require("./runtime/HasOwnPropertyRuntimeModule");
const LoadScriptRuntimeModule = require("./runtime/LoadScriptRuntimeModule");
const MakeDeferredNamespaceObjectRuntime = require("./runtime/MakeDeferredNamespaceObjectRuntime");
const MakeNamespaceObjectRuntimeModule = require("./runtime/MakeNamespaceObjectRuntimeModule");
const NonceRuntimeModule = require("./runtime/NonceRuntimeModule");
const OnChunksLoadedRuntimeModule = require("./runtime/OnChunksLoadedRuntimeModule");
@ -79,7 +80,8 @@ const GLOBALS_ON_REQUIRE = [
RuntimeGlobals.initializeSharing,
RuntimeGlobals.loadScript,
RuntimeGlobals.systemContext,
RuntimeGlobals.onChunksLoaded
RuntimeGlobals.onChunksLoaded,
RuntimeGlobals.makeDeferredNamespaceObject
];
const MODULE_DEPENDENCIES = {
@ -97,6 +99,13 @@ const TREE_DEPENDENCIES = {
RuntimeGlobals.makeNamespaceObject,
RuntimeGlobals.require
],
[RuntimeGlobals.makeDeferredNamespaceObject]: [
RuntimeGlobals.definePropertyGetters,
RuntimeGlobals.makeNamespaceObject,
RuntimeGlobals.createFakeNamespaceObject,
RuntimeGlobals.hasOwnProperty,
RuntimeGlobals.require
],
[RuntimeGlobals.initializeSharing]: [RuntimeGlobals.shareScopeMap],
[RuntimeGlobals.shareScopeMap]: [RuntimeGlobals.hasOwnProperty]
};
@ -184,6 +193,17 @@ class RuntimePlugin {
);
return true;
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.makeDeferredNamespaceObject)
.tap("RuntimePlugin", (chunk, runtimeRequirement) => {
compilation.addRuntimeModule(
chunk,
new MakeDeferredNamespaceObjectRuntime(
runtimeRequirement.has(RuntimeGlobals.asyncModule)
)
);
return true;
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.hasOwnProperty)
.tap(PLUGIN_NAME, chunk => {
@ -246,7 +266,11 @@ class RuntimePlugin {
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.asyncModule)
.tap(PLUGIN_NAME, chunk => {
compilation.addRuntimeModule(chunk, new AsyncModuleRuntimeModule());
const experiments = compilation.options.experiments;
compilation.addRuntimeModule(
chunk,
new AsyncModuleRuntimeModule(experiments.deferImport)
);
return true;
});
compilation.hooks.runtimeRequirementInTree

View File

@ -8,6 +8,10 @@
const InitFragment = require("./InitFragment");
const RuntimeGlobals = require("./RuntimeGlobals");
const Template = require("./Template");
const {
getMakeDeferredNamespaceModeFromExportsType,
getOptimizedDeferredModule
} = require("./runtime/MakeDeferredNamespaceObjectRuntime");
const { equals } = require("./util/ArrayHelpers");
const compileBooleanMatcher = require("./util/compileBooleanMatcher");
const propertyAccess = require("./util/propertyAccess");
@ -779,22 +783,26 @@ class RuntimeTemplate {
* @param {object} options options object
* @param {boolean=} options.update whether a new variable should be created or the existing one updated
* @param {Module} options.module the module
* @param {ModuleGraph} options.moduleGraph the module graph
* @param {ChunkGraph} options.chunkGraph the chunk graph
* @param {string} options.request the request that should be printed as comment
* @param {string} options.importVar name of the import variable
* @param {Module} options.originModule module in which the statement is emitted
* @param {boolean=} options.weak true, if this is a weak dependency
* @param {RuntimeRequirements} options.runtimeRequirements if set, will be filled with runtime requirements
* @param {boolean=} options.defer if set, the module will be deferred
* @returns {[string, string]} the import statement and the compat statement
*/
importStatement({
update,
module,
moduleGraph,
chunkGraph,
request,
importVar,
originModule,
weak,
defer,
runtimeRequirements
}) {
if (!module) {
@ -805,6 +813,29 @@ class RuntimeTemplate {
""
];
}
/** @type {Set<Module>} */
const innerAsyncDependencies = new Set();
defer = defer && (module.buildMeta ? !module.buildMeta.async : true);
if (this.compilation.options.experiments.deferImport && defer) {
const seen = new Set();
(function gatherInnerAsyncDependencies(mod) {
if (!moduleGraph.isAsync(mod) || seen.has(mod)) return;
seen.add(mod);
if (mod.buildMeta && mod.buildMeta.async) {
innerAsyncDependencies.add(mod);
} else {
for (const dep of mod.dependencies) {
const module = moduleGraph.getModule(dep);
if (module) {
gatherInnerAsyncDependencies(module);
}
}
}
})(module);
}
if (chunkGraph.getModuleId(module) === null) {
if (weak) {
// only weak referenced modules don't get an id
@ -840,9 +871,19 @@ class RuntimeTemplate {
(originModule.buildMeta).strictHarmonyModule
);
runtimeRequirements.add(RuntimeGlobals.require);
const importContent = `/* harmony import */ ${optDeclaration}${importVar} = ${RuntimeGlobals.require}(${moduleId});\n`;
let importContent;
if (defer) {
importContent = `/* deferred harmony import */ ${optDeclaration}${importVar} = ${getOptimizedDeferredModule(
this,
exportsType,
moduleId,
Array.from(innerAsyncDependencies, mod => chunkGraph.getModuleId(mod))
)};\n`;
} else {
importContent = `/* harmony import */ ${optDeclaration}${importVar} = ${RuntimeGlobals.require}(${moduleId});\n`;
}
if (exportsType === "dynamic") {
if (exportsType === "dynamic" && !defer) {
runtimeRequirements.add(RuntimeGlobals.compatGetDefaultExport);
return [
importContent,
@ -856,6 +897,7 @@ class RuntimeTemplate {
* @template GenerateContext
* @param {object} options options
* @param {ModuleGraph} options.moduleGraph the module graph
* @param {ChunkGraph} options.chunkGraph the chunk graph
* @param {Module} options.module the module
* @param {string} options.request the request
* @param {string | string[]} options.exportName the export name
@ -868,10 +910,12 @@ class RuntimeTemplate {
* @param {InitFragment<GenerateContext>[]} options.initFragments init fragments will be added here
* @param {RuntimeSpec} options.runtime runtime for which this code will be generated
* @param {RuntimeRequirements} options.runtimeRequirements if set, will be filled with runtime requirements
* @param {boolean=} options.defer if true, the module will be deferred.
* @returns {string} expression
*/
exportFromImport({
moduleGraph,
chunkGraph,
module,
request,
exportName,
@ -883,13 +927,16 @@ class RuntimeTemplate {
importVar,
initFragments,
runtime,
runtimeRequirements
runtimeRequirements,
defer
}) {
if (!module) {
return this.missingModule({
request
});
}
defer = defer && (module.buildMeta ? !module.buildMeta.async : true);
if (!Array.isArray(exportName)) {
exportName = exportName ? [exportName] : [];
}
@ -900,7 +947,21 @@ class RuntimeTemplate {
);
if (defaultInterop) {
// when the defaultInterop is used (when a ESM imports a CJS module),
if (exportName.length > 0 && exportName[0] === "default") {
if (defer && exportsType !== "namespace") {
const access = `${importVar}.a${propertyAccess(exportName, 1)}`;
if (isCall || asiSafe === undefined) {
return access;
}
return asiSafe ? `(${access})` : `;(${access})`;
}
// accessing the .default property is same thing as `require()` the module.
// For example:
// import mod from "cjs"; mod.default.x;
// is translated to
// var mod = require("cjs"); mod.x;
switch (exportsType) {
case "dynamic":
if (isCall) {
@ -918,7 +979,11 @@ class RuntimeTemplate {
break;
}
} else if (exportName.length > 0) {
// the property used is not .default.
// For example:
// import * as ns from "cjs"; cjs.prop;
if (exportsType === "default-only") {
// in the strictest case, it is a runtime error (e.g. NodeJS behavior of CJS-ESM interop).
return `/* non-default import from non-esm module */undefined${propertyAccess(
exportName,
1
@ -929,10 +994,17 @@ class RuntimeTemplate {
) {
return "/* __esModule */true";
}
} else if (defer) {
// now exportName.length is 0
// fall through to the end of this function, create the namespace there.
} else if (
exportsType === "default-only" ||
exportsType === "default-with-named"
) {
// now exportName.length is 0, which means the namespace object is used in an unknown way
// for example:
// import * as ns from "cjs"; console.log(ns);
// we will need to createFakeNamespaceObject that simulates ES Module namespace object
runtimeRequirements.add(RuntimeGlobals.createFakeNamespaceObject);
initFragments.push(
new InitFragment(
@ -952,6 +1024,8 @@ class RuntimeTemplate {
if (exportName.length > 0) {
const exportsInfo = moduleGraph.getExportsInfo(module);
// in some case the exported item is renamed (get this by getUsedName). for example,
// x.default might be emitted as x.Z (default is renamed to Z)
const used = exportsInfo.getUsedName(exportName, runtime);
if (!used) {
const comment = Template.toNormalComment(
@ -962,7 +1036,9 @@ class RuntimeTemplate {
const comment = equals(used, exportName)
? ""
: `${Template.toNormalComment(propertyAccess(exportName))} `;
const access = `${importVar}${comment}${propertyAccess(used)}`;
const access = `${importVar}${
defer ? ".a" : ""
}${comment}${propertyAccess(used)}`;
if (isCall && callContext === false) {
return asiSafe
? `(0,${access})`
@ -972,6 +1048,30 @@ class RuntimeTemplate {
}
return access;
}
if (defer) {
initFragments.push(
new InitFragment(
`var ${importVar}_deferred_namespace_cache;\n`,
InitFragment.STAGE_CONSTANTS,
-1,
`${importVar}_deferred_namespace_cache`
)
);
runtimeRequirements.add(RuntimeGlobals.makeDeferredNamespaceObject);
const id = chunkGraph.getModuleId(module);
const type = getMakeDeferredNamespaceModeFromExportsType(exportsType);
const init = `${
RuntimeGlobals.makeDeferredNamespaceObject
}(${JSON.stringify(id)}, ${type})`;
return `/*#__PURE__*/ ${
asiSafe ? "" : asiSafe === false ? ";" : "Object"
}(${importVar}_deferred_namespace_cache || (${importVar}_deferred_namespace_cache = ${init}))`;
}
// if we hit here, the importVar is either
// - already a ES module namespace object
// - or imported by a way that does not need interop.
return importVar;
}

View File

@ -404,7 +404,8 @@ class WebpackOptionsApply extends OptionsApply {
new CompatibilityPlugin().apply(compiler);
new HarmonyModulesPlugin({
topLevelAwait: options.experiments.topLevelAwait
topLevelAwait: options.experiments.topLevelAwait,
deferImport: options.experiments.deferImport
}).apply(compiler);
if (options.amd !== false) {
const AMDPlugin = require("./dependencies/AMDPlugin");

View File

@ -5,6 +5,7 @@
"use strict";
const WebpackError = require("../WebpackError");
const { getImportAttributes } = require("../javascript/JavascriptParser");
const InnerGraph = require("../optimize/InnerGraph");
const ConstDependency = require("./ConstDependency");
@ -14,7 +15,8 @@ const HarmonyExportImportedSpecifierDependency = require("./HarmonyExportImporte
const HarmonyExportSpecifierDependency = require("./HarmonyExportSpecifierDependency");
const { ExportPresenceModes } = require("./HarmonyImportDependency");
const {
harmonySpecifierTag
harmonySpecifierTag,
getImportMode
} = require("./HarmonyImportDependencyParserPlugin");
const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
@ -31,8 +33,9 @@ const PLUGIN_NAME = "HarmonyExportDependencyParserPlugin";
module.exports = class HarmonyExportDependencyParserPlugin {
/**
* @param {import("../../declarations/WebpackOptions").JavascriptParserOptions} options options
* @param {boolean=} deferImport defer import enabled
*/
constructor(options) {
constructor(options, deferImport) {
this.exportPresenceMode =
options.reexportExportsPresence !== undefined
? ExportPresenceModes.fromUserOption(options.reexportExportsPresence)
@ -41,6 +44,7 @@ module.exports = class HarmonyExportDependencyParserPlugin {
: options.strictExportPresence
? ExportPresenceModes.ERROR
: ExportPresenceModes.AUTO;
this.deferImport = deferImport;
}
/**
@ -73,10 +77,24 @@ module.exports = class HarmonyExportDependencyParserPlugin {
clearDep.loc = /** @type {DependencyLocation} */ (statement.loc);
clearDep.loc.index = -1;
parser.state.module.addPresentationalDependency(clearDep);
const { defer } = getImportMode(
parser,
statement,
this.deferImport,
true
);
if (defer) {
const error = new WebpackError(
"Deferred re-export (`export defer * as namespace from '...'`) is not a part of the Import Defer proposal.\nUse the following code instead:\n import defer * as namespace from '...';\n export { namespace };"
);
error.loc = statement.loc || undefined;
parser.state.current.addError(error);
}
const sideEffectDep = new HarmonyImportSideEffectDependency(
/** @type {string} */ (source),
parser.state.lastHarmonyImportOrder,
getImportAttributes(statement)
getImportAttributes(statement),
defer
);
sideEffectDep.loc = Object.create(
/** @type {DependencyLocation} */ (statement.loc)
@ -156,7 +174,8 @@ module.exports = class HarmonyExportDependencyParserPlugin {
null,
exportPresenceMode,
null,
settings.attributes
settings.attributes,
settings.defer
)
: new HarmonyExportSpecifierDependency(id, name);
dep.loc = Object.create(
@ -187,6 +206,12 @@ module.exports = class HarmonyExportDependencyParserPlugin {
parser.state.harmonyStarExports || new HarmonyStarExportsList();
}
const attributes = getImportAttributes(statement);
const { defer } = getImportMode(
parser,
statement,
this.deferImport,
false
);
const dep = new HarmonyExportImportedSpecifierDependency(
/** @type {string} */ (source),
parser.state.lastHarmonyImportOrder,
@ -196,7 +221,8 @@ module.exports = class HarmonyExportDependencyParserPlugin {
harmonyStarExports && harmonyStarExports.slice(),
exportPresenceMode,
harmonyStarExports,
attributes
attributes,
defer
);
if (harmonyStarExports) {
harmonyStarExports.push(dep);

View File

@ -27,7 +27,7 @@ class HarmonyExportExpressionDependency extends NullDependency {
* @param {Range} range range
* @param {Range} rangeStatement range statement
* @param {string} prefix prefix
* @param {string | { range: Range, prefix: string, suffix: string }=} declarationId declaration id
* @param {string | { id?: string | undefined, range: Range, prefix: string, suffix: string }=} declarationId declaration id
*/
constructor(range, rangeStatement, prefix, declarationId) {
super();

View File

@ -12,6 +12,9 @@ const HarmonyLinkingError = require("../HarmonyLinkingError");
const InitFragment = require("../InitFragment");
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const {
getMakeDeferredNamespaceModeFromExportsType
} = require("../runtime/MakeDeferredNamespaceObjectRuntime");
const { countIterable } = require("../util/IterableHelpers");
const { first, combine } = require("../util/SetHelpers");
const makeSerializable = require("../util/makeSerializable");
@ -374,6 +377,7 @@ class HarmonyExportImportedSpecifierDependency extends HarmonyImportDependency {
* @param {ExportPresenceMode} exportPresenceMode mode of checking export names
* @param {HarmonyStarExportsList | null} allStarExports all star exports in the module
* @param {ImportAttributes=} attributes import attributes
* @param {boolean=} deferEvaluation defer evaluation
*/
constructor(
request,
@ -384,7 +388,8 @@ class HarmonyExportImportedSpecifierDependency extends HarmonyImportDependency {
otherStarExports,
exportPresenceMode,
allStarExports,
attributes
attributes,
deferEvaluation
) {
super(request, sourceOrder, attributes);
@ -394,6 +399,7 @@ class HarmonyExportImportedSpecifierDependency extends HarmonyImportDependency {
this.otherStarExports = otherStarExports;
this.exportPresenceMode = exportPresenceMode;
this.allStarExports = allStarExports;
this.defer = deferEvaluation;
}
/**
@ -1011,6 +1017,7 @@ HarmonyExportImportedSpecifierDependency.Template = class HarmonyExportImportedS
mode,
templateContext.module,
moduleGraph,
templateContext.chunkGraph,
runtime,
templateContext.runtimeTemplate,
templateContext.runtimeRequirements
@ -1024,6 +1031,7 @@ HarmonyExportImportedSpecifierDependency.Template = class HarmonyExportImportedS
* @param {ExportMode} mode the export mode
* @param {Module} module the current module
* @param {ModuleGraph} moduleGraph the module graph
* @param {ChunkGraph} chunkGraph the chunk graph
* @param {RuntimeSpec} runtime the runtime
* @param {RuntimeTemplate} runtimeTemplate the runtime template
* @param {RuntimeRequirements} runtimeRequirements runtime requirements
@ -1035,6 +1043,7 @@ HarmonyExportImportedSpecifierDependency.Template = class HarmonyExportImportedS
mode,
module,
moduleGraph,
chunkGraph,
runtime,
runtimeTemplate,
runtimeRequirements
@ -1042,6 +1051,29 @@ HarmonyExportImportedSpecifierDependency.Template = class HarmonyExportImportedS
const importedModule = /** @type {Module} */ (moduleGraph.getModule(dep));
const importVar = dep.getImportVar(moduleGraph);
if (
(mode.type === "reexport-namespace-object" ||
mode.type === "reexport-fake-namespace-object") &&
dep.defer &&
!moduleGraph.isAsync(importedModule)
) {
initFragments.push(
...this.getReexportDeferredNamespaceObjectFragments(
importedModule,
chunkGraph,
moduleGraph
.getExportsInfo(module)
.getUsedName(mode.name ? mode.name : [], runtime),
importVar,
importedModule.getExportsType(
moduleGraph,
module.buildMeta && module.buildMeta.strictHarmonyModule
),
runtimeRequirements
)
);
return;
}
switch (mode.type) {
case "missing":
case "empty-star":
@ -1311,6 +1343,46 @@ HarmonyExportImportedSpecifierDependency.Template = class HarmonyExportImportedS
];
}
/**
* @param {Module} module module
* @param {ChunkGraph} chunkGraph chunkGraph
* @param {string | false | string[]} key key
* @param {string} name name
* @param {import("../Module").ExportsType} exportsType exportsType
* @param {Set<string>} runtimeRequirements runtimeRequirements
* @returns {InitFragment<GenerateContext>[]} fragments
*/
getReexportDeferredNamespaceObjectFragments(
module,
chunkGraph,
key,
name,
exportsType,
runtimeRequirements
) {
runtimeRequirements.add(RuntimeGlobals.exports);
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
runtimeRequirements.add(RuntimeGlobals.makeDeferredNamespaceObject);
const map = new Map();
const moduleId = JSON.stringify(chunkGraph.getModuleId(module));
const mode = getMakeDeferredNamespaceModeFromExportsType(exportsType);
map.set(
key,
`/* reexport deferred namespace object */ ${name}_deferred_namespace_cache || (${name}_deferred_namespace_cache = ${RuntimeGlobals.makeDeferredNamespaceObject}(${moduleId}, ${mode}))`
);
return [
new InitFragment(
`var ${name}_deferred_namespace_cache;\n`,
InitFragment.STAGE_CONSTANTS,
-1,
`${name}_deferred_namespace_cache`
),
new HarmonyExportInitFragment(module.exportsArgument, map)
];
}
/**
* @param {Module} module module
* @param {string} key key

View File

@ -120,11 +120,13 @@ class HarmonyImportDependency extends ModuleDependency {
return runtimeTemplate.importStatement({
update,
module: /** @type {Module} */ (moduleGraph.getModule(this)),
moduleGraph,
chunkGraph,
importVar: this.getImportVar(moduleGraph),
request: this.request,
originModule: module,
runtimeRequirements
runtimeRequirements,
defer: this.defer
});
}

View File

@ -5,7 +5,10 @@
"use strict";
const CommentCompilationWarning = require("../CommentCompilationWarning");
const HotModuleReplacementPlugin = require("../HotModuleReplacementPlugin");
const UnsupportedFeatureWarning = require("../UnsupportedFeatureWarning");
const WebpackError = require("../WebpackError");
const { getImportAttributes } = require("../javascript/JavascriptParser");
const InnerGraph = require("../optimize/InnerGraph");
const ConstDependency = require("./ConstDependency");
@ -49,6 +52,7 @@ const harmonySpecifierTag = Symbol("harmony import");
* @property {string} name
* @property {boolean} await
* @property {ImportAttributes=} attributes
* @property {boolean | undefined} defer
*/
const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin";
@ -56,8 +60,9 @@ const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin";
module.exports = class HarmonyImportDependencyParserPlugin {
/**
* @param {JavascriptParserOptions} options options
* @param {boolean | undefined} deferImport defer import enabled
*/
constructor(options) {
constructor(options, deferImport) {
this.exportPresenceMode =
options.importExportsPresence !== undefined
? ExportPresenceModes.fromUserOption(options.importExportsPresence)
@ -67,6 +72,7 @@ module.exports = class HarmonyImportDependencyParserPlugin {
? ExportPresenceModes.ERROR
: ExportPresenceModes.AUTO;
this.strictThisContextOnImports = options.strictThisContextOnImports;
this.deferImport = deferImport;
}
/**
@ -119,10 +125,28 @@ module.exports = class HarmonyImportDependencyParserPlugin {
parser.state.module.addPresentationalDependency(clearDep);
parser.unsetAsiPosition(/** @type {Range} */ (statement.range)[1]);
const attributes = getImportAttributes(statement);
const { defer } = getImportMode(
parser,
statement,
this.deferImport,
true
);
if (
defer &&
(statement.specifiers.length !== 1 ||
statement.specifiers[0].type !== "ImportNamespaceSpecifier")
) {
const error = new WebpackError(
"Deferred import can only be used with `import * as namespace from '...'` syntax."
);
error.loc = statement.loc || undefined;
parser.state.current.addError(error);
}
const sideEffectDep = new HarmonyImportSideEffectDependency(
/** @type {string} */ (source),
parser.state.lastHarmonyImportOrder,
attributes
attributes,
defer
);
sideEffectDep.loc = /** @type {DependencyLocation} */ (statement.loc);
parser.state.module.addDependency(sideEffectDep);
@ -132,12 +156,19 @@ module.exports = class HarmonyImportDependencyParserPlugin {
PLUGIN_NAME,
(statement, source, id, name) => {
const ids = id === null ? [] : [id];
const { defer } = getImportMode(
parser,
statement,
this.deferImport,
false
);
parser.tagVariable(name, harmonySpecifierTag, {
name,
source,
ids,
sourceOrder: parser.state.lastHarmonyImportOrder,
attributes: getImportAttributes(statement)
attributes: getImportAttributes(statement),
defer
});
return true;
}
@ -194,7 +225,8 @@ module.exports = class HarmonyImportDependencyParserPlugin {
(expr.range),
exportPresenceMode,
settings.attributes,
[]
[],
settings.defer
);
dep.referencedPropertiesInDestructuring =
parser.destructuringAssignmentPropertiesFor(expr);
@ -241,7 +273,8 @@ module.exports = class HarmonyImportDependencyParserPlugin {
(expr.range),
exportPresenceMode,
settings.attributes,
ranges
ranges,
settings.defer
);
dep.referencedPropertiesInDestructuring =
parser.destructuringAssignmentPropertiesFor(expr);
@ -290,7 +323,8 @@ module.exports = class HarmonyImportDependencyParserPlugin {
/** @type {Range} */ (expr.range),
exportPresenceMode,
settings.attributes,
ranges
ranges,
settings.defer
);
dep.directImport = members.length === 0;
dep.call = true;
@ -357,4 +391,57 @@ module.exports = class HarmonyImportDependencyParserPlugin {
}
};
/**
* @param {JavascriptParser} parser parser
* @param {ExportNamedDeclaration | ExportAllDeclaration | ImportDeclaration} node node
* @param {boolean | undefined} deferImportEnabled defer import enabled
* @param {boolean} reportSyntaxError report syntax error
* @returns {{defer: boolean}} import attributes
*/
function getImportMode(parser, node, deferImportEnabled, reportSyntaxError) {
const result = { defer: false };
if ("phase" in node && node.phase === "defer") {
if (deferImportEnabled) {
result.defer = true;
} else if (reportSyntaxError) {
const error = new WebpackError(
"Deferred import syntax (`import defer * as namespace from '...'`) cannot be used unless experimental.deferImport is true."
);
error.loc = node.loc || undefined;
parser.state.current.addError(error);
}
}
if (!node.range) {
return result;
}
const { options, errors } = parser.parseCommentOptions(node.range);
if (errors) {
for (const e of errors) {
const { comment } = e;
if (!comment.loc) continue;
parser.state.module.addWarning(
new CommentCompilationWarning(
`Compilation error while processing magic comment(-s): /*${comment.value}*/: ${e.message}`,
comment.loc
)
);
}
}
if (!options) return result;
if (options.webpackDefer) {
if (typeof options.webpackDefer === "boolean") {
result.defer = options.webpackDefer;
} else if (node.loc) {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
"webpackDefer magic comment expected a boolean value.",
node.loc
)
);
}
}
return result;
}
module.exports.harmonySpecifierTag = harmonySpecifierTag;
module.exports.getImportMode = getImportMode;

View File

@ -16,6 +16,7 @@ const HarmonyImportDependency = require("./HarmonyImportDependency");
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */
/** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */
/** @typedef {import("../WebpackError")} WebpackError */
/** @typedef {import("../javascript/JavascriptParser").ImportAttributes} ImportAttributes */
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
@ -25,9 +26,11 @@ class HarmonyImportSideEffectDependency extends HarmonyImportDependency {
* @param {string} request the request string
* @param {number} sourceOrder source order
* @param {ImportAttributes=} attributes import attributes
* @param {boolean=} deferred deferred
*/
constructor(request, sourceOrder, attributes) {
constructor(request, sourceOrder, attributes, deferred) {
super(request, sourceOrder, attributes);
this.defer = deferred;
}
get type() {

View File

@ -51,6 +51,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
* @param {ExportPresenceMode} exportPresenceMode export presence mode
* @param {ImportAttributes | undefined} attributes import attributes
* @param {Range[] | undefined} idRanges ranges for members of ids; the two arrays are right-aligned
* @param {boolean=} deferred deferred
*/
constructor(
request,
@ -60,7 +61,8 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
range,
exportPresenceMode,
attributes,
idRanges // TODO webpack 6 make this non-optional. It must always be set to properly trim ids.
idRanges, // TODO webpack 6 make this non-optional. It must always be set to properly trim ids.
deferred
) {
super(request, sourceOrder, attributes);
this.ids = ids;
@ -77,6 +79,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
this.usedByExports = undefined;
/** @type {Set<DestructuringAssignmentProperty> | undefined} */
this.referencedPropertiesInDestructuring = undefined;
this.defer = deferred;
}
// TODO webpack 6 remove
@ -420,13 +423,15 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
exportExpr = concatenationScope.createModuleReference(
connection.module,
{
asiSafe: dep.asiSafe
asiSafe: dep.asiSafe,
deferredImport: dep.defer
}
);
} else if (dep.namespaceObjectAsContext && ids.length === 1) {
exportExpr =
concatenationScope.createModuleReference(connection.module, {
asiSafe: dep.asiSafe
asiSafe: dep.asiSafe,
deferredImport: dep.defer
}) + propertyAccess(ids);
} else {
exportExpr = concatenationScope.createModuleReference(
@ -435,7 +440,8 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
ids,
call: dep.call,
directImport: dep.directImport,
asiSafe: dep.asiSafe
asiSafe: dep.asiSafe,
deferredImport: dep.defer
}
);
}
@ -448,6 +454,7 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
exportExpr = runtimeTemplate.exportFromImport({
moduleGraph,
module: /** @type {Module} */ (moduleGraph.getModule(dep)),
chunkGraph: templateContext.chunkGraph,
request: dep.request,
exportName: ids,
originModule: module,
@ -458,7 +465,8 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
importVar: dep.getImportVar(moduleGraph),
initFragments,
runtime,
runtimeRequirements
runtimeRequirements,
defer: dep.defer
});
}
return exportExpr;

View File

@ -28,11 +28,14 @@ const HarmonyTopLevelThisParserPlugin = require("./HarmonyTopLevelThisParserPlug
/** @typedef {import("../../declarations/WebpackOptions").JavascriptParserOptions} JavascriptParserOptions */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../javascript/JavascriptParser")} Parser */
/**
* @typedef HarmonyModulesPluginOptions
* @property {boolean=} topLevelAwait
* @property {boolean=} deferImport
*/
const PLUGIN_NAME = "HarmonyModulesPlugin";
/** @typedef {{ topLevelAwait?: boolean }} HarmonyModulesPluginOptions */
class HarmonyModulesPlugin {
/**
* @param {HarmonyModulesPluginOptions} options options
@ -131,8 +134,14 @@ class HarmonyModulesPlugin {
return;
new HarmonyDetectionParserPlugin(this.options).apply(parser);
new HarmonyImportDependencyParserPlugin(parserOptions).apply(parser);
new HarmonyExportDependencyParserPlugin(parserOptions).apply(parser);
new HarmonyImportDependencyParserPlugin(
parserOptions,
this.options.deferImport
).apply(parser);
new HarmonyExportDependencyParserPlugin(
parserOptions,
this.options.deferImport
).apply(parser);
new HarmonyTopLevelThisParserPlugin().apply(parser);
};

View File

@ -93,6 +93,28 @@ class ImportParserPlugin {
}
}
let phase = expr.phase;
if (!phase && importOptions && importOptions.webpackDefer !== undefined) {
if (typeof importOptions.webpackDefer !== "boolean") {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
`\`webpackDefer\` expected a boolean, but received: ${importOptions.webpackDefer}.`,
/** @type {DependencyLocation} */ (expr.loc)
)
);
} else if (importOptions.webpackDefer) {
phase = "defer";
}
}
if (phase === "defer") {
parser.state.module.addWarning(
new UnsupportedFeatureWarning(
"import.defer() is not implemented yet.",
/** @type {DependencyLocation} */ (expr.loc)
)
);
}
if (importOptions) {
if (importOptions.webpackIgnore !== undefined) {
if (typeof importOptions.webpackIgnore !== "boolean") {

View File

@ -1162,6 +1162,16 @@ class JavascriptModulesPlugin {
buf.push("");
}
if (runtimeRequirements.has(RuntimeGlobals.makeDeferredNamespaceObject)) {
// in order to optimize of DeferredNamespaceObject, we remove all proxy handlers after the module initialize
// (see MakeDeferredNamespaceObjectRuntimeModule)
// This requires all deferred imports to a module can get the module export object before the module
// is evaluated.
buf.push("// The deferred module cache");
buf.push("var __webpack_module_deferred_exports__ = {};");
buf.push("");
}
if (useRequire) {
buf.push("// The require function");
buf.push(`function ${RuntimeGlobals.require}(moduleId) {`);
@ -1431,6 +1441,9 @@ class JavascriptModulesPlugin {
const needModuleLoaded = runtimeRequirements.has(
RuntimeGlobals.moduleLoaded
);
const needModuleDefer = runtimeRequirements.has(
RuntimeGlobals.makeDeferredNamespaceObject
);
const content = Template.asString([
"// Check if module is in cache",
"var cachedModule = __webpack_module_cache__[moduleId];",
@ -1447,7 +1460,9 @@ class JavascriptModulesPlugin {
Template.indent([
needModuleId ? "id: moduleId," : "// no module.id needed",
needModuleLoaded ? "loaded: false," : "// no module.loaded needed",
"exports: {}"
needModuleDefer
? "exports: __webpack_module_deferred_exports__[moduleId] || {}"
: "exports: {}"
]),
"};",
"",
@ -1456,7 +1471,13 @@ class JavascriptModulesPlugin {
"// Execute the module function",
"var threw = true;",
"try {",
Template.indent([moduleExecution, "threw = false;"]),
Template.indent([
moduleExecution,
"threw = false;",
...(needModuleDefer
? ["delete __webpack_module_deferred_exports__[moduleId];"]
: [])
]),
"} finally {",
Template.indent([
"if(threw) delete __webpack_module_cache__[moduleId];"
@ -1467,14 +1488,27 @@ class JavascriptModulesPlugin {
? Template.asString([
"// Execute the module function",
"try {",
Template.indent(moduleExecution),
Template.indent(
needModuleDefer
? [
moduleExecution,
"delete __webpack_module_deferred_exports__[moduleId];"
]
: moduleExecution
),
"} catch(e) {",
Template.indent(["module.error = e;", "throw e;"]),
"}"
])
: Template.asString([
"// Execute the module function",
moduleExecution
moduleExecution,
...(needModuleDefer
? [
"// delete __webpack_module_deferred_exports__[module];",
"// skipped because strictModuleErrorHandling is not enabled."
]
: [])
]),
needModuleLoaded
? Template.asString([

View File

@ -83,6 +83,7 @@ const BasicEvaluatedExpression = require("./BasicEvaluatedExpression");
/** @typedef {import("estree").Statement} Statement */
/** @typedef {import("estree").ExportDefaultDeclaration} ExportDefaultDeclaration */
/** @typedef {import("estree").Super} Super */
/** @typedef {import("estree").ImportSpecifier} ImportSpecifier */
/** @typedef {import("estree").TaggedTemplateExpression} TaggedTemplateExpression */
/** @typedef {import("estree").TemplateLiteral} TemplateLiteral */
/** @typedef {import("estree").AssignmentProperty} AssignmentProperty */
@ -102,10 +103,10 @@ const BasicEvaluatedExpression = require("./BasicEvaluatedExpression");
// TODO remove cast when @types/estree has been updated to import assertions
/** @typedef {import("estree").BaseNode & { type: "ImportAttribute", key: Identifier | Literal, value: Literal }} ImportAttribute */
/** @typedef {import("estree").ImportDeclaration & { attributes?: Array<ImportAttribute> }} ImportDeclaration */
/** @typedef {import("estree").ImportDeclaration & { attributes?: Array<ImportAttribute>, phase?: "defer" }} ImportDeclaration */
/** @typedef {import("estree").ExportNamedDeclaration & { attributes?: Array<ImportAttribute> }} ExportNamedDeclaration */
/** @typedef {import("estree").ExportAllDeclaration & { attributes?: Array<ImportAttribute> }} ExportAllDeclaration */
/** @typedef {import("estree").ImportExpression & { options?: Expression | null }} ImportExpression */
/** @typedef {import("estree").ImportExpression & { options?: Expression | null, phase?: "defer" }} ImportExpression */
/** @typedef {ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration} ModuleDeclaration */
/** @type {string[]} */
@ -179,8 +180,147 @@ const importAssertions = Parser =>
}
};
// follow https://www.npmjs.com/acorn-import-defer if possible
/** @type {(BaseParser: typeof AcornParser) => typeof AcornParser} */
const importDefer = Parser =>
class extends Parser {
/**
* @this {InstanceType<AcornParser>}
* @param {ImportDeclaration} node import declaration
* @returns {ImportDeclaration} import declaration
*/
parseImport(node) {
this._defer = false;
// eslint-disable-next-line no-warning-comments
// @ts-ignore
const result = super.parseImport(node);
if (this._defer) {
node.phase = "defer";
}
return result;
}
/**
* @this {InstanceType<AcornParser>}
* @returns {ImportSpecifier[]} import specifiers
*/
parseImportSpecifiers() {
// eslint-disable-next-line no-warning-comments
// @ts-ignore
if (!this.isContextual("defer")) return super.parseImportSpecifiers();
const deferId = this.parseIdent();
if (this.isContextual("from") || this.type === tokTypes.comma) {
const defaultSpecifier = this.startNodeAt(
deferId.start,
deferId.loc.start
);
defaultSpecifier.local = deferId;
this.checkLValSimple(deferId, /* BIND_LEXICAL */ 2);
const nodes = [
this.finishNode(defaultSpecifier, "ImportDefaultSpecifier")
];
if (this.eat(tokTypes.comma)) {
if (this.type !== tokTypes.star && this.type !== tokTypes.braceL) {
this.unexpected();
}
// eslint-disable-next-line no-warning-comments
// @ts-ignore
nodes.push(...super.parseImportSpecifiers());
}
return nodes;
}
this._defer = true;
if (this.type !== tokTypes.star) {
this.raiseRecoverable(
deferId.start,
"'import defer' can only be used with namespace imports."
);
}
// eslint-disable-next-line no-warning-comments
// @ts-ignore
return super.parseImportSpecifiers();
}
/**
* @this {InstanceType<AcornParser>}
* @param {boolean} forNew forNew
* @returns {ImportExpression} import expression
*/
parseExprImport(forNew) {
// eslint-disable-next-line no-warning-comments
// @ts-ignore
const node = super.parseExprImport(forNew);
if (node.type === "MetaProperty" && node.property.name === "defer") {
if (this.type === tokTypes.parenL) {
const dynImport = this.parseDynamicImport(
this.startNodeAt(node.start, node.loc.start)
);
dynImport.phase = "defer";
return dynImport;
}
this.raiseRecoverable(
node.start,
"'import.defer' can only be used in a dynamic import."
);
}
return node;
}
/**
* @this {InstanceType<AcornParser>}
* @param {MetaProperty} node node
* @returns {MetaProperty} import.meta
*/
parseImportMeta(node) {
this.next();
const containsEsc = this.containsEsc;
node.property = this.parseIdent(true);
const { name } = node.property;
if (name !== "meta" && name !== "defer") {
this.raiseRecoverable(
// eslint-disable-next-line no-warning-comments
// @ts-ignore
node.property.start,
"The only valid meta property for import is 'import.meta'"
);
}
if (containsEsc) {
this.raiseRecoverable(
// eslint-disable-next-line no-warning-comments
// @ts-ignore
node.start,
`'import.${name}' must not contain escaped characters`
);
}
if (
name === "meta" &&
this.options.sourceType !== "module" &&
!this.options.allowImportExportEverywhere
) {
this.raiseRecoverable(
// eslint-disable-next-line no-warning-comments
// @ts-ignore
node.start,
"Cannot use 'import.meta' outside a module"
);
}
return this.finishNode(node, "MetaProperty");
}
};
// Syntax: https://developer.mozilla.org/en/SpiderMonkey/Parser_API
const parser = AcornParser.extend(importAssertions);
const parser = AcornParser.extend(importAssertions, importDefer);
/** @typedef {Record<string, string> & { _isLegacyAssert?: boolean }} ImportAttributes */

View File

@ -22,7 +22,12 @@ const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const { DEFAULTS } = require("../config/defaults");
const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
const JavascriptParser = require("../javascript/JavascriptParser");
const {
getMakeDeferredNamespaceModeFromExportsType,
getOptimizedDeferredModule
} = require("../runtime/MakeDeferredNamespaceObjectRuntime");
const { equals } = require("../util/ArrayHelpers");
const LazySet = require("../util/LazySet");
const { concatComparators } = require("../util/comparators");
@ -154,12 +159,12 @@ if (!ReferencerClass.prototype.PropertyDefinition) {
* @property {Map<string, string> | undefined} rawExportMap
* @property {string=} namespaceExportSymbol
* @property {string | undefined} namespaceObjectName
* @property {boolean} interopNamespaceObjectUsed
* @property {string | undefined} interopNamespaceObjectName
* @property {boolean} interopNamespaceObject2Used
* @property {string | undefined} interopNamespaceObject2Name
* @property {boolean} interopDefaultAccessUsed
* @property {string | undefined} interopDefaultAccessName
* @property {boolean} interopNamespaceObjectUsed "default-with-named" namespace
* @property {string | undefined} interopNamespaceObjectName "default-with-named" namespace
* @property {boolean} interopNamespaceObject2Used "default-only" namespace
* @property {string | undefined} interopNamespaceObject2Name "default-only" namespace
* @property {boolean} interopDefaultAccessUsed runtime namespace object that detects "__esModule"
* @property {string | undefined} interopDefaultAccessName runtime namespace object that detects "__esModule"
*/
/**
@ -168,13 +173,17 @@ if (!ReferencerClass.prototype.PropertyDefinition) {
* @property {Module} module
* @property {RuntimeSpec | boolean} runtimeCondition
* @property {number} index
* @property {string | undefined} name
* @property {boolean} interopNamespaceObjectUsed
* @property {string | undefined} interopNamespaceObjectName
* @property {boolean} interopNamespaceObject2Used
* @property {string | undefined} interopNamespaceObject2Name
* @property {boolean} interopDefaultAccessUsed
* @property {string | undefined} interopDefaultAccessName
* @property {string | undefined} name module.exports / harmony namespace object
* @property {string | undefined} deferredName deferred module.exports / harmony namespace object
* @property {boolean} deferred the module is deferred at least once
* @property {boolean} deferredNamespaceObjectUsed deferred namespace object that being used in a not-analyzable way so it must be materialized
* @property {string | undefined} deferredNamespaceObjectName deferred namespace object that being used in a not-analyzable way so it must be materialized
* @property {boolean} interopNamespaceObjectUsed "default-with-named" namespace
* @property {string | undefined} interopNamespaceObjectName "default-with-named" namespace
* @property {boolean} interopNamespaceObject2Used "default-only" namespace
* @property {string | undefined} interopNamespaceObject2Name "default-only" namespace
* @property {boolean} interopDefaultAccessUsed runtime namespace object that detects "__esModule"
* @property {string | undefined} interopDefaultAccessName runtime namespace object that detects "__esModule"
*/
/**
@ -219,6 +228,14 @@ const compareNumbers = (a, b) => {
};
const bySourceOrder = createComparator("sourceOrder", compareNumbers);
const byRangeStart = createComparator("rangeStart", compareNumbers);
const moveDeferToLast = (
/** @type {{ defer?: boolean }} */ a,
/** @type {{ defer?: boolean }} */ b
) => {
if (a.defer === b.defer) return 0;
if (a.defer) return 1;
return -1;
};
/**
* @param {Iterable<string>} iterable iterable object
@ -257,6 +274,7 @@ const joinIterableWithComma = iterable => {
* @param {RuntimeTemplate} runtimeTemplate the runtime template
* @param {Set<ConcatenatedModuleInfo>} neededNamespaceObjects modules for which a namespace object should be generated
* @param {boolean} asCall asCall
* @param {boolean} depDeferred the dependency is deferred
* @param {boolean | undefined} strictHarmonyModule strictHarmonyModule
* @param {boolean | undefined} asiSafe asiSafe
* @param {Set<ExportInfo>} alreadyVisited alreadyVisited
@ -272,6 +290,7 @@ const getFinalBinding = (
runtimeTemplate,
neededNamespaceObjects,
asCall,
depDeferred,
strictHarmonyModule,
asiSafe,
alreadyVisited = new Set()
@ -280,21 +299,36 @@ const getFinalBinding = (
moduleGraph,
strictHarmonyModule
);
const deferred =
depDeferred &&
info.type === "external" &&
info.deferred &&
!moduleGraph.isAsync(info.module);
if (exportName.length === 0) {
switch (exportsType) {
case "default-only":
info.interopNamespaceObject2Used = true;
if (deferred) info.deferredNamespaceObjectUsed = true;
else info.interopNamespaceObject2Used = true;
return {
info,
rawName: /** @type {string} */ (info.interopNamespaceObject2Name),
rawName: /** @type {string} */ (
deferred
? info.deferredNamespaceObjectName
: info.interopNamespaceObject2Name
),
ids: exportName,
exportName
};
case "default-with-named":
info.interopNamespaceObjectUsed = true;
if (deferred) info.deferredNamespaceObjectUsed = true;
else info.interopNamespaceObjectUsed = true;
return {
info,
rawName: /** @type {string} */ (info.interopNamespaceObjectName),
rawName: /** @type {string} */ (
deferred
? info.deferredNamespaceObjectName
: info.interopNamespaceObjectName
),
ids: exportName,
exportName
};
@ -348,6 +382,14 @@ const getFinalBinding = (
switch (exportName[0]) {
case "default": {
exportName = exportName.slice(1);
if (deferred) {
return {
info,
rawName: `${info.deferredName}.a`,
ids: exportName,
exportName
};
}
info.interopDefaultAccessUsed = true;
const defaultExport = asCall
? `${info.interopDefaultAccessName}()`
@ -389,6 +431,15 @@ const getFinalBinding = (
exportName
};
case "external":
if (deferred) {
info.deferredNamespaceObjectUsed = true;
return {
info,
rawName: /** @type {string} */ (info.deferredNamespaceObjectName),
ids: exportName,
exportName
};
}
return {
info,
rawName:
@ -480,6 +531,7 @@ const getFinalBinding = (
runtimeTemplate,
neededNamespaceObjects,
asCall,
reexport.deferred,
/** @type {BuildMeta} */
(info.module.buildMeta).strictHarmonyModule,
asiSafe,
@ -519,7 +571,15 @@ const getFinalBinding = (
const comment = equals(used, exportName)
? ""
: Template.toNormalComment(`${exportName.join(".")}`);
return { info, rawName: info.name + comment, ids: used, exportName };
return {
info,
rawName:
(deferred ? info.deferredName : info.name) +
(deferred ? ".a" : "") +
comment,
ids: used,
exportName
};
}
}
};
@ -534,6 +594,7 @@ const getFinalBinding = (
* @param {RuntimeTemplate} runtimeTemplate the runtime template
* @param {Set<ConcatenatedModuleInfo>} neededNamespaceObjects modules for which a namespace object should be generated
* @param {boolean} asCall asCall
* @param {boolean} depDeferred the dependency is deferred
* @param {boolean | undefined} callContext callContext
* @param {boolean | undefined} strictHarmonyModule strictHarmonyModule
* @param {boolean | undefined} asiSafe asiSafe
@ -549,6 +610,7 @@ const getFinalName = (
runtimeTemplate,
neededNamespaceObjects,
asCall,
depDeferred,
callContext,
strictHarmonyModule,
asiSafe
@ -563,6 +625,7 @@ const getFinalName = (
runtimeTemplate,
neededNamespaceObjects,
asCall,
depDeferred,
strictHarmonyModule,
asiSafe
);
@ -881,6 +944,8 @@ class ConcatenatedModule extends Module {
const list = [];
/** @type {Map<Module, RuntimeSpec | true>} */
const existingEntries = new Map();
const deferEnabled =
this.compilation && this.compilation.options.experiments.deferImport;
/**
* @param {Module} module a module
@ -895,7 +960,7 @@ class ConcatenatedModule extends Module {
connections.push(c);
}
/**
* @type {Array<{ connection: ModuleGraphConnection, sourceOrder: number, rangeStart: number }>}
* @type {Array<{ connection: ModuleGraphConnection, sourceOrder: number, rangeStart: number, defer?: boolean }>}
*/
const references = connections
.filter(connection => {
@ -915,7 +980,8 @@ class ConcatenatedModule extends Module {
return {
connection,
sourceOrder: dep.sourceOrder,
rangeStart: dep.range && dep.range[0]
rangeStart: dep.range && dep.range[0],
defer: dep.defer
};
});
/**
@ -930,11 +996,16 @@ class ConcatenatedModule extends Module {
* a.a(); // first range
* b.b(); // second range
*
* If the import is deferred, we always move it to the last.
* If there is no reexport, we have the same source.
* If there is reexport, but module has side effects, this will lead to reexport module only.
* If there is side-effects-free reexport, we can get simple deterministic result with range start comparison.
*/
references.sort(concatComparators(bySourceOrder, byRangeStart));
if (deferEnabled) {
// do not combine those two sorts. defer is not the same as source or range which has a comparable number, defer is only moving them.
references.sort(moveDeferToLast);
}
/** @type {Map<Module, { connection: ModuleGraphConnection, runtimeCondition: RuntimeSpec | true }>} */
const referencesMap = new Map();
for (const { connection } of references) {
@ -1209,6 +1280,7 @@ class ConcatenatedModule extends Module {
runtimeTemplate,
neededNamespaceObjects,
false,
match.deferredImport,
/** @type {BuildMeta} */
(info.module.buildMeta).strictHarmonyModule,
true
@ -1348,6 +1420,28 @@ class ConcatenatedModule extends Module {
allUsedNames.add(externalName);
info.name = externalName;
topLevelDeclarations.add(externalName);
if (info.deferred) {
const externalName = findNewName(
"deferred",
allUsedNames,
namespaceObjectUsedNames,
info.module.readableIdentifier(requestShortener)
);
allUsedNames.add(externalName);
info.deferredName = externalName;
topLevelDeclarations.add(externalName);
const externalNameInterop = findNewName(
"deferredNamespaceObject",
allUsedNames,
namespaceObjectUsedNames,
info.module.readableIdentifier(requestShortener)
);
allUsedNames.add(externalNameInterop);
info.deferredNamespaceObjectName = externalNameInterop;
topLevelDeclarations.add(externalNameInterop);
}
break;
}
}
@ -1365,7 +1459,8 @@ class ConcatenatedModule extends Module {
}
if (
buildMeta.exportsType === "default" &&
buildMeta.defaultObject !== "redirect"
buildMeta.defaultObject !== "redirect" &&
info.interopNamespaceObject2Used
) {
const externalNameInterop = findNewName(
"namespaceObject2",
@ -1411,6 +1506,7 @@ class ConcatenatedModule extends Module {
runtimeTemplate,
neededNamespaceObjects,
match.call,
match.deferredImport,
!match.directImport,
/** @type {BuildMeta} */
(info.module.buildMeta).strictHarmonyModule,
@ -1463,6 +1559,7 @@ class ConcatenatedModule extends Module {
neededNamespaceObjects,
false,
false,
false,
strictHarmonyModule,
true
);
@ -1564,6 +1661,7 @@ class ConcatenatedModule extends Module {
runtimeTemplate,
neededNamespaceObjects,
false,
false,
undefined,
/** @type {BuildMeta} */
(info.module.buildMeta).strictHarmonyModule,
@ -1605,7 +1703,10 @@ ${defineGetters}`
}
}
/** @type {InitFragment<ChunkRenderContext>[]} */
const chunkInitFragments = [];
const deferEnabled =
this.compilation && this.compilation.options.experiments.deferImport;
// evaluate modules in order
for (const rawInfo of modulesWithInfo) {
@ -1617,6 +1718,31 @@ ${defineGetters}`
result.add(
`\n;// ${info.module.readableIdentifier(requestShortener)}\n`
);
// If a module is deferred in other places, but used as non-deferred here,
// the module itself will be emitted as mod_deferred (in the case "external"),
// we need to emit an extra import declaration to evaluate it in order.
if (deferEnabled) {
for (const dep of info.module.dependencies) {
if (
!dep.defer &&
dep instanceof HarmonyImportSideEffectDependency
) {
const referredModule = moduleGraph.getModule(dep);
if (!referredModule) continue;
if (moduleGraph.isDeferred(referredModule)) {
const deferredModuleInfo = /** @type {ExternalModuleInfo} */ (
modulesWithInfo.find(
i => i.type === "external" && i.module === referredModule
)
);
if (!deferredModuleInfo) continue;
result.add(
`\n// non-deferred import to a deferred module (${referredModule.readableIdentifier(requestShortener)})\nvar ${deferredModuleInfo.name} = ${deferredModuleInfo.deferredName}.a;`
);
}
}
}
}
result.add(/** @type {ReplaceSource} */ (info.source));
if (info.chunkInitFragments) {
for (const f of info.chunkInitFragments) chunkInitFragments.push(f);
@ -1649,11 +1775,23 @@ ${defineGetters}`
isConditional = true;
result.add(`if (${condition}) {\n`);
}
result.add(
`var ${info.name} = ${RuntimeGlobals.require}(${JSON.stringify(
chunkGraph.getModuleId(info.module)
)});`
);
const moduleId = JSON.stringify(chunkGraph.getModuleId(info.module));
if (info.deferred) {
const loader = getOptimizedDeferredModule(
runtimeTemplate,
info.module.getExportsType(
moduleGraph,
this.rootModule.buildMeta &&
this.rootModule.buildMeta.strictHarmonyModule
),
moduleId,
// an async module will opt-out of the concat module optimization.
[]
);
result.add(`var ${info.deferredName} = ${loader};`);
} else {
result.add(`var ${info.name} = __webpack_require__(${moduleId});`);
}
name = info.name;
break;
}
@ -1661,6 +1799,18 @@ ${defineGetters}`
// @ts-expect-error never is expected here
throw new Error(`Unsupported concatenation entry type ${info.type}`);
}
if (info.type === "external" && info.deferredNamespaceObjectUsed) {
runtimeRequirements.add(RuntimeGlobals.makeDeferredNamespaceObject);
result.add(
`\nvar ${info.deferredNamespaceObjectName} = /*#__PURE__*/${
RuntimeGlobals.makeDeferredNamespaceObject
}(${JSON.stringify(
chunkGraph.getModuleId(info.module)
)}, ${getMakeDeferredNamespaceModeFromExportsType(
info.module.getExportsType(moduleGraph, strictHarmonyModule)
)});`
);
}
if (info.interopNamespaceObjectUsed) {
runtimeRequirements.add(RuntimeGlobals.createFakeNamespaceObject);
result.add(
@ -1841,12 +1991,16 @@ ${defineGetters}`
runtimeCondition: info.runtimeCondition,
index,
name: undefined,
deferredName: undefined,
interopNamespaceObjectUsed: false,
interopNamespaceObjectName: undefined,
interopNamespaceObject2Used: false,
interopNamespaceObject2Name: undefined,
interopDefaultAccessUsed: false,
interopDefaultAccessName: undefined
interopDefaultAccessName: undefined,
deferred: moduleGraph.isDeferred(info.module),
deferredNamespaceObjectName: undefined,
deferredNamespaceObjectUsed: false
};
break;
default:

View File

@ -223,6 +223,11 @@ class ModuleConcatenationPlugin {
canBeInner = false;
}
if (moduleGraph.isDeferred(module)) {
setInnerBailoutReason(module, `Module is deferred`);
canBeInner = false;
}
if (canBeRoot) relevantModules.push(module);
if (canBeInner) possibleInners.add(module);
}

View File

@ -11,8 +11,12 @@ const HelperRuntimeModule = require("./HelperRuntimeModule");
/** @typedef {import("../Compilation")} Compilation */
class AsyncModuleRuntimeModule extends HelperRuntimeModule {
constructor() {
/**
* @param {boolean=} deferInterop if defer import is used.
*/
constructor(deferInterop = false) {
super("async module");
this._deferInterop = deferInterop;
}
/**
@ -22,10 +26,20 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
const compilation = /** @type {Compilation} */ (this.compilation);
const { runtimeTemplate } = compilation;
const fn = RuntimeGlobals.asyncModule;
const defer = this._deferInterop;
return Template.asString([
'var webpackQueues = typeof Symbol === "function" ? Symbol("webpack queues") : "__webpack_queues__";',
`var webpackExports = typeof Symbol === "function" ? Symbol("webpack exports") : "${RuntimeGlobals.exports}";`,
'var webpackError = typeof Symbol === "function" ? Symbol("webpack error") : "__webpack_error__";',
'var hasSymbol = typeof Symbol === "function";',
'var webpackQueues = hasSymbol ? Symbol("webpack queues") : "__webpack_queues__";',
`var webpackExports = ${
defer ? `${RuntimeGlobals.asyncModuleExportSymbol}= ` : ""
}hasSymbol ? Symbol("webpack exports") : "${RuntimeGlobals.exports}";`,
'var webpackError = hasSymbol ? Symbol("webpack error") : "__webpack_error__";',
defer
? `var webpackDone = ${RuntimeGlobals.asyncModuleDoneSymbol} = hasSymbol ? Symbol("webpack done") : "__webpack_done__";`
: "",
defer
? `var webpackDefer = ${RuntimeGlobals.makeDeferredNamespaceObjectSymbol} = hasSymbol ? Symbol("webpack defer") : "__webpack_defer__";`
: "",
`var resolveQueue = ${runtimeTemplate.basicFunction("queue", [
"if(queue && queue.d < 1) {",
Template.indent([
@ -45,6 +59,39 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
`deps.map(${runtimeTemplate.basicFunction("dep", [
'if(dep !== null && typeof dep === "object") {',
Template.indent([
defer
? Template.asString([
"if(!dep[webpackQueues] && dep[webpackDefer]) {",
Template.indent([
"var asyncDeps = dep[webpackDefer];",
`var hasUnresolvedAsyncSubgraph = asyncDeps.some(${runtimeTemplate.basicFunction(
"id",
[
"var cache = __webpack_module_cache__[id];",
"return !cache || cache[webpackDone] === false;"
]
)});`,
"if (hasUnresolvedAsyncSubgraph) {",
Template.indent([
"var d = dep;",
"dep = {",
Template.indent([
"then(callback) {",
Template.indent([
"Promise.all(asyncDeps.map(__webpack_require__))",
`.then(${runtimeTemplate.returningFunction(
"callback(d)"
)});`
]),
"}"
]),
"};"
]),
"} else return dep;"
]),
"}"
])
: "",
"if(dep[webpackQueues]) return dep;",
"if(dep.then) {",
Template.indent([
@ -58,6 +105,7 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
"resolveQueue(queue);"
])});`,
"var obj = {};",
defer ? "obj[webpackDefer] = false;" : "",
`obj[webpackQueues] = ${runtimeTemplate.expressionFunction(
"fn(queue)",
"fn"
@ -92,11 +140,12 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
"fn"
)};`,
"module.exports = promise;",
`body(${runtimeTemplate.basicFunction("deps", [
`var handle = ${runtimeTemplate.basicFunction("deps", [
"currentDeps = wrapDeps(deps);",
"var fn;",
`var getResult = ${runtimeTemplate.returningFunction(
`currentDeps.map(${runtimeTemplate.basicFunction("d", [
defer ? "if(d[webpackDefer]) return d;" : "",
"if(d[webpackError]) throw d[webpackError];",
"return d[webpackExports];"
])})`
@ -114,16 +163,22 @@ class AsyncModuleRuntimeModule extends HelperRuntimeModule {
"q"
)};`,
`currentDeps.map(${runtimeTemplate.expressionFunction(
"dep[webpackQueues](fnQueue)",
`${
defer ? "dep[webpackDefer]||" : ""
}dep[webpackQueues](fnQueue)`,
"dep"
)});`
]
)});`,
"return fn.r ? promise : getResult();"
])}, ${runtimeTemplate.expressionFunction(
"(err ? reject(promise[webpackError] = err) : outerResolve(exports)), resolveQueue(queue)",
])}`,
`var done = ${runtimeTemplate.expressionFunction(
`(err ? reject(promise[webpackError] = err) : outerResolve(exports)), resolveQueue(queue)${
defer ? ", promise[webpackDone] = true" : ""
}`,
"err"
)});`,
)}`,
"body(handle, done);",
"queue && queue.d < 0 && (queue.d = 0);"
])};`
]);

View File

@ -0,0 +1,214 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const RuntimeGlobals = require("../RuntimeGlobals");
const Template = require("../Template");
const HelperRuntimeModule = require("./HelperRuntimeModule");
/**
* @param {import("../Module").ExportsType} exportsType exports type
* @returns {string} mode
*/
function getMakeDeferredNamespaceModeFromExportsType(exportsType) {
if (exportsType === "namespace") return `/* ${exportsType} */ 0`;
if (exportsType === "default-only") return `/* ${exportsType} */ 1`;
if (exportsType === "default-with-named") return `/* ${exportsType} */ 2`;
if (exportsType === "dynamic") return `/* ${exportsType} */ 3`;
return "";
}
/**
* @param {import("../ModuleTemplate").RuntimeTemplate} _runtimeTemplate runtimeTemplate
* @param {import("../Module").ExportsType} exportsType exportsType
* @param {string} moduleId moduleId
* @param {(import("../ChunkGraph").ModuleId | null)[]} asyncDepsIds asyncDepsIds
* @returns {string} function
*/
function getOptimizedDeferredModule(
_runtimeTemplate,
exportsType,
moduleId,
asyncDepsIds
) {
const isAsync = asyncDepsIds && asyncDepsIds.length;
const init = `${RuntimeGlobals.require}(${moduleId})${
isAsync ? `[${RuntimeGlobals.asyncModuleExportSymbol}]` : ""
}`;
const props = [
`/* ${exportsType} */ get a() {`,
// 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.
exportsType === "namespace" || exportsType === "dynamic"
? Template.indent([
`var exports = ${init};`,
`${
exportsType === "dynamic" ? "if (exports.__esModule) " : ""
}Object.defineProperty(this, "a", { value: exports });`,
"return exports;"
])
: Template.indent([`return ${init};`]),
isAsync ? "}," : "}",
isAsync
? `[${
RuntimeGlobals.makeDeferredNamespaceObjectSymbol
}]: ${JSON.stringify(asyncDepsIds.filter(x => x !== null))}`
: ""
];
return Template.asString(["{", Template.indent(props), "}"]);
}
const strictModuleCache = [
"if (cachedModule && cachedModule.error === undefined) {",
Template.indent([
"var exports = cachedModule.exports;",
"if (mode == 0) return exports;",
`if (mode == 1) return ${RuntimeGlobals.createFakeNamespaceObject}(exports);`,
`if (mode == 2) return ${RuntimeGlobals.createFakeNamespaceObject}(exports, 2);`,
`if (mode == 3) return ${RuntimeGlobals.createFakeNamespaceObject}(exports, 6);` // 2 | 4
]),
"}"
];
const nonStrictModuleCache = [
"// optimization not applied when output.strictModuleErrorHandling is off"
];
class MakeDeferredNamespaceObjectRuntimeModule extends HelperRuntimeModule {
/**
* @param {boolean} hasAsyncRuntime if async module is used.
*/
constructor(hasAsyncRuntime) {
super("make deferred namespace object");
this.hasAsyncRuntime = hasAsyncRuntime;
}
/**
* @returns {string | null} runtime code
*/
generate() {
if (!this.compilation) return null;
const { runtimeTemplate } = this.compilation;
const fn = RuntimeGlobals.makeDeferredNamespaceObject;
const hasAsync = this.hasAsyncRuntime;
const strictError =
this.compilation.options.output.strictModuleErrorHandling;
const init = runtimeTemplate.supportsOptionalChaining()
? "init?.();"
: "if (init) init();";
return `${fn} = ${runtimeTemplate.basicFunction("moduleId, mode", [
"// mode: 0 => namespace (esm)",
"// mode: 1 => default-only (esm strict cjs)",
"// mode: 2 => default-with-named (esm-cjs compat)",
"// mode: 3 => dynamic (if exports has __esModule, then esm, otherwise default-with-named)",
"",
"var cachedModule = __webpack_module_cache__[moduleId];",
...(strictError ? strictModuleCache : nonStrictModuleCache),
"",
`var init = ${runtimeTemplate.basicFunction("", [
`ns = ${RuntimeGlobals.require}(moduleId);`,
hasAsync
? `if (${RuntimeGlobals.asyncModuleExportSymbol} in ns) ns = ns[${RuntimeGlobals.asyncModuleExportSymbol}];`
: "",
"init = null;",
"if (mode == 0 || mode == 3 && ns.__esModule && typeof ns === 'object') {",
Template.indent([
"delete handler.defineProperty;",
"delete handler.deleteProperty;",
"delete handler.set;",
"delete handler.get;",
"delete handler.has;",
"delete handler.ownKeys;",
"delete handler.getOwnPropertyDescriptor;"
]),
"} else if (mode == 1) {",
Template.indent([
`ns = ${RuntimeGlobals.createFakeNamespaceObject}(ns);`
]),
"} else if (mode == 2) {",
Template.indent([
`ns = ${RuntimeGlobals.createFakeNamespaceObject}(ns, 2);`
]),
"} else if (mode == 3) {",
Template.indent([
`ns = ${RuntimeGlobals.createFakeNamespaceObject}(ns, 6);`
]),
"}"
])};`,
"",
`var ns = ${
strictError ? "" : "cachedModule && cachedModule.exports || "
}__webpack_module_deferred_exports__[moduleId] || (__webpack_module_deferred_exports__[moduleId] = { __proto__: null });`,
"var handler = {",
Template.indent([
"__proto__: null,",
`get: ${runtimeTemplate.basicFunction("_, name", [
"switch (name) {",
Template.indent([
'case "__esModule": return true;',
'case Symbol.toStringTag: return "Deferred Module";',
'case "then": return undefined;'
]),
"}",
init,
`return ns[name];`
])},`,
`has: ${runtimeTemplate.basicFunction("_, name", [
"switch (name) {",
Template.indent(
[
'case "__esModule":',
"case Symbol.toStringTag:",
hasAsync
? `case ${RuntimeGlobals.makeDeferredNamespaceObjectSymbol}:`
: "",
Template.indent("return true;"),
'case "then":',
Template.indent("return false;")
].filter(Boolean)
),
"}",
init,
`return name in ns;`
])},`,
`ownKeys: ${runtimeTemplate.basicFunction("", [
init,
`var keys = Reflect.ownKeys(ns).filter(${runtimeTemplate.expressionFunction('x !== "then"', "x")}).concat([Symbol.toStringTag]);`,
"return keys;"
])},`,
`getOwnPropertyDescriptor: ${runtimeTemplate.basicFunction("_, name", [
"switch (name) {",
Template.indent([
'case "__esModule": return { value: true, configurable: !!mode };',
'case Symbol.toStringTag: return { value: "Deferred Module", configurable: !!mode };',
'case "then": return undefined;'
]),
"}",
init,
"var desc = Reflect.getOwnPropertyDescriptor(ns, name);",
`if (mode == 2 && name == "default" && !desc) {`,
Template.indent("desc = { value: ns, configurable: true };"),
"}",
"return desc;"
])},`,
`defineProperty: ${runtimeTemplate.basicFunction("_, name", [
init,
// Note: This behavior does not match the spec one, but since webpack does not do it either
// for a normal Module Namespace object (in MakeNamespaceObjectRuntimeModule), let's keep it simple.
"return false;"
])},`,
`deleteProperty: ${runtimeTemplate.returningFunction("false")},`,
`set: ${runtimeTemplate.returningFunction("false")},`
]),
"}",
// we don't fully emulate ES Module semantics in this Proxy to align with normal webpack esm namespace object.
"return new Proxy(ns, handler);"
])};`;
}
}
module.exports = MakeDeferredNamespaceObjectRuntimeModule;
module.exports.getMakeDeferredNamespaceModeFromExportsType =
getMakeDeferredNamespaceModeFromExportsType;
module.exports.getOptimizedDeferredModule = getOptimizedDeferredModule;

View File

@ -105,6 +105,7 @@ class AsyncWebAssemblyJavascriptGenerator extends Generator {
return runtimeTemplate.importStatement({
update: false,
module: importedModule,
moduleGraph,
chunkGraph,
request,
originModule: module,
@ -131,6 +132,7 @@ class AsyncWebAssemblyJavascriptGenerator extends Generator {
)}: ${runtimeTemplate.exportFromImport({
moduleGraph,
module: importedModule,
chunkGraph,
request,
exportName: dep.name,
originModule: module,

View File

@ -97,6 +97,7 @@ class WebAssemblyJavascriptGenerator extends Generator {
initParams.push(
runtimeTemplate.exportFromImport({
moduleGraph,
chunkGraph,
module: importedModule,
request: dep.request,
importVar: importData.importVar,
@ -129,6 +130,7 @@ class WebAssemblyJavascriptGenerator extends Generator {
`${exportProp} = ${runtimeTemplate.exportFromImport({
moduleGraph,
module: /** @type {Module} */ (moduleGraph.getModule(dep)),
chunkGraph,
request: dep.request,
importVar: importData.importVar,
originModule: module,
@ -158,6 +160,7 @@ class WebAssemblyJavascriptGenerator extends Generator {
([module, { importVar, request, reexports }]) => {
const importStatement = runtimeTemplate.importStatement({
module,
moduleGraph,
chunkGraph,
request,
importVar,

File diff suppressed because one or more lines are too long

View File

@ -563,6 +563,11 @@
"description": "Enable/disable `url()`/`image-set()`/`src()`/`image()` functions handling.",
"type": "boolean"
},
"DeferImportExperimentOptions": {
"description": "Options for defer import.",
"type": "boolean",
"required": ["asyncModule"]
},
"Dependencies": {
"description": "References to other configurations to depend on.",
"type": "array",
@ -989,6 +994,10 @@
"description": "Enable css support.",
"type": "boolean"
},
"deferImport": {
"description": "Enable experimental tc39 proposal https://github.com/tc39/proposal-defer-import-eval. This allows to defer execution of a module until it's first use.",
"type": "boolean"
},
"futureDefaults": {
"description": "Apply defaults of next major version.",
"type": "boolean"
@ -1091,6 +1100,10 @@
"description": "Enable css support.",
"type": "boolean"
},
"deferImport": {
"description": "Enable experimental tc39 proposal https://github.com/tc39/proposal-defer-import-eval. This allows to defer execution of a module until it's first use.",
"type": "boolean"
},
"futureDefaults": {
"description": "Apply defaults of next major version.",
"type": "boolean"

View File

@ -649,6 +649,19 @@ Object {
"multiple": false,
"simpleType": "boolean",
},
"experiments-defer-import": Object {
"configs": Array [
Object {
"description": "Enable experimental tc39 proposal https://github.com/tc39/proposal-defer-import-eval. This allows to defer execution of a module until it's first use.",
"multiple": false,
"path": "experiments.deferImport",
"type": "boolean",
},
],
"description": "Enable experimental tc39 proposal https://github.com/tc39/proposal-defer-import-eval. This allows to defer execution of a module until it's first use.",
"multiple": false,
"simpleType": "boolean",
},
"experiments-future-defaults": Object {
"configs": Array [
Object {

View File

@ -0,0 +1,4 @@
export {};
__configCases__deferImport__proposal.push("START async-mod-dep.js");
__configCases__deferImport__proposal.push("END async-mod-dep.js");

View File

@ -0,0 +1,8 @@
import "./async-mod-dep.js";
__configCases__deferImport__proposal.push("START async-mod.js");
await 0;
export let x = 2;
__configCases__deferImport__proposal.push("END async-mod.js");

View File

@ -0,0 +1,5 @@
export {};
__configCases__deferImport__proposal.push("START deep-async-dep.js");
await 0;
__configCases__deferImport__proposal.push("END deep-async-dep.js");

View File

@ -0,0 +1,7 @@
import "./deep-async-dep.js";
__configCases__deferImport__proposal.push("START deep-async.js");
export let x = 3;
__configCases__deferImport__proposal.push("END deep-async.js");

View File

@ -0,0 +1,9 @@
import * as fullSync from /* webpackDefer: true */ "./full-sync.js";
import * as asyncMod from /* webpackDefer: true */ "./async-mod.js";
import * as deepAsync from /* webpackDefer: true */ "./deep-async.js";
__configCases__deferImport__proposal.push("START entry.js");
export default { fullSync, asyncMod, deepAsync };
__configCases__deferImport__proposal.push("END entry.js");

View File

@ -0,0 +1,4 @@
export {};
__configCases__deferImport__proposal.push("START full-sync-dep.js");
__configCases__deferImport__proposal.push("END full-sync-dep.js");

View File

@ -0,0 +1,7 @@
import "./full-sync-dep.js";
__configCases__deferImport__proposal.push("START full-sync.js");
export let x = 1;
__configCases__deferImport__proposal.push("END full-sync.js");

View File

@ -0,0 +1,46 @@
it("should compile", async () => {
const logs = global.__configCases__deferImport__proposal = [];
// change to other way if webpack in the future rejects require a TLA esm.
let mod = require("./entry.js");
expect(mod).toBeInstanceOf(Promise);
expect(logs).toEqual([
"START async-mod-dep.js",
"END async-mod-dep.js",
"START async-mod.js",
"START deep-async-dep.js"
]);
logs.length = 0;
let { default: namespaces } = await mod;
expect(logs).toEqual([
"END async-mod.js",
"END deep-async-dep.js",
"START entry.js",
"END entry.js"
]);
logs.length = 0;
let fullSyncX = namespaces.fullSync.x;
expect(fullSyncX).toBe(1);
expect(logs).toEqual([
"START full-sync-dep.js",
"END full-sync-dep.js",
"START full-sync.js",
"END full-sync.js"
]);
logs.length = 0;
let asyncModX = namespaces.asyncMod.x;
expect(asyncModX).toBe(2);
expect(logs).toEqual([]);
let deepAsyncX = namespaces.deepAsync.x;
expect(deepAsyncX).toBe(3);
expect(logs).toEqual([
"START deep-async.js",
"END deep-async.js"
]);
});

View File

@ -0,0 +1,14 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
mode: "none",
experiments: {
topLevelAwait: true,
deferImport: true
}
};

View File

@ -0,0 +1,7 @@
module.exports = [
[/used with `import \* as namespace from '...'`/],
[/used with `import \* as namespace from '...'`/],
[/used with `import \* as namespace from '...'`/],
[/is not a part of the Import Defer proposal/],
[/is not a part of the Import Defer proposal/]
];

View File

@ -0,0 +1,11 @@
import { f } from /* webpackDefer: true */ "./mod.js"; // error
import f2 from /* webpackDefer: true */ "./mod.js"; // error
import * as f3 from /* webpackDefer: true */ "./mod.js";
import f4, { f as f5 } from /* webpackDefer: true */ "./mod.js"; // error
export * as f4 from /* webpackDefer: true */ "./mod.js"; // error
export { f as f5 } from /* webpackDefer: true */ "./mod.js"; // error
export default [f, f2, f3, f4, f5];
export { f3 }

View File

@ -0,0 +1,2 @@
export function f() {}
export default function f2() {}

View File

@ -0,0 +1,13 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
experiments: {
topLevelAwait: true,
deferImport: true
}
};

View File

@ -0,0 +1,5 @@
import { data, setData } from "./side-effect-counter";
if (data !== undefined)
throw new Error("No module should be executed before this one.");
setData("0.js");

View File

@ -0,0 +1,3 @@
import "./0.js";
import * as ns from "./deferred.js";
export const x = ns;

View File

@ -0,0 +1,5 @@
import { data, setData } from "./side-effect-counter";
if (data !== "0.js") throw new Error("Expected 0.js to be executed first.");
setData("deferred");
export {};

View File

@ -0,0 +1,6 @@
import * as ns from /* webpackDefer: true */ "./deferred.js";
import "./1.js";
if (Math.random() > 1) {
console.log(ns);
}

View File

@ -0,0 +1,3 @@
it("execution order should be correct.", () => {
return import("./entry.js");
});

View File

@ -0,0 +1,4 @@
export let data;
export function setData(d) {
data = d;
}

View File

@ -0,0 +1,13 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
optimization: {},
experiments: {
deferImport: true
}
};

View File

@ -0,0 +1,22 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
entry: ["../defer-runtime/all.js"],
optimization: {},
module: {
rules: [
{
test: /index\.js/,
type: "javascript/esm"
}
]
},
experiments: {
deferImport: true
}
};

View File

@ -0,0 +1,14 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
entry: ["../defer-runtime/all.js"],
optimization: {},
experiments: {
deferImport: true
}
};

View File

@ -0,0 +1,16 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
entry: ["../defer-runtime/all-native-syntax.js"],
optimization: {
concatenateModules: false
},
experiments: {
deferImport: true
}
};

View File

@ -0,0 +1,24 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
entry: ["../defer-runtime/all.js"],
optimization: {
concatenateModules: false
},
module: {
rules: [
{
test: /index\.js/,
type: "javascript/esm"
}
]
},
experiments: {
deferImport: true
}
};

View File

@ -0,0 +1,207 @@
import defer * as dynamic_default from "./commonjs/dynamic_default.cjs";
import defer * as dynamic_default_ns from "./commonjs/dynamic_default_ns.cjs";
import defer * as dynamic_named from "./commonjs/dynamic_named.cjs";
import defer * as dynamic_named_ns from "./commonjs/dynamic_named_ns.cjs";
import defer * as dynamic_both from "./commonjs/dynamic-both.cjs";
import defer * as dynamic_both_ns from "./commonjs/dynamic_both_ns.cjs";
import defer * as flagged_default from "./commonjs/flagged_default.js";
import defer * as flagged_default_ns from "./commonjs/flagged_default_ns.js";
import defer * as flagged_named from "./commonjs/flagged_named.js";
import defer * as flagged_named_ns from "./commonjs/flagged_named_ns.js";
import defer * as flagged_both from "./commonjs/flagged_both.js";
import defer * as flagged_both_ns from "./commonjs/flagged_both_ns.js";
import defer * as esm_default from "./esm/esm_default.mjs";
import defer * as esm_default_ns from "./esm/esm_default_ns.mjs";
import defer * as esm_named from "./esm/esm_named.mjs";
import defer * as esm_named_ns from "./esm/esm_named_ns.mjs";
import defer * as esm_both from "./esm/esm_both.mjs";
import defer * as esm_both_ns from "./esm/esm_both_ns.mjs";
import { reexport_ns, reexport_cjs_ns } from "./esm/reexport.mjs";
import {
assertTouched as a,
assertUntouched as b,
reset as c
} from "./side-effect-counter.js";
const [assertTouched, assertUntouched, reset] = [a, b, c];
it("should defer the module until first use", () => {
reset();
dynamic_default.default;
assertTouched();
expect(dynamic_default.default()).toBe("func");
assertTouched();
reset();
dynamic_named.f;
assertTouched();
expect(dynamic_named.f()).toBe("func");
expect(new dynamic_named.T().x).toBe(1);
assertTouched();
reset();
dynamic_both.default;
assertTouched();
expect(dynamic_both.default()).toBe("func");
expect(dynamic_both.default.x).toBe(1);
expect(new dynamic_both.default.T().x).toBe(1);
assertTouched();
// then flagged, without namespace
reset();
flagged_default.default;
assertTouched();
expect(flagged_default.default()).toBe("func");
assertTouched();
reset();
flagged_named.f;
assertTouched();
expect(flagged_named.f()).toBe("func");
expect(new flagged_named.T().x).toBe(1);
assertTouched();
reset();
flagged_both.default;
assertTouched();
expect(flagged_both.default()).toBe("func");
expect(flagged_both.x).toBe(1);
expect(new flagged_both.T().x).toBe(1);
assertTouched();
// then esm, without namespace
reset();
esm_default.default;
assertTouched();
expect(esm_default.default()).toBe("func");
assertTouched();
reset();
esm_named.f;
assertTouched();
expect(esm_named.f()).toBe("func");
expect(new esm_named.T().x).toBe(1);
assertTouched();
reset();
esm_both.default;
assertTouched();
expect(esm_both.default()).toBe("func");
expect(esm_both.x).toBe(1);
expect(new esm_both.T().x).toBe(1);
assertTouched();
// then dynamic with namespace
reset();
assertIsNamespaceObject(dynamic_default_ns);
assertUntouched();
Reflect.get(dynamic_default_ns, "default");
assertTouched();
expect(Reflect.get(dynamic_default_ns, "default")()).toBe("func");
assertTouched();
reset();
assertIsNamespaceObject(dynamic_named_ns);
assertUntouched();
Reflect.get(dynamic_named_ns, "f");
assertTouched();
expect(Reflect.get(dynamic_named_ns, "f")()).toBe("func");
expect(new dynamic_named_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(dynamic_both_ns);
assertUntouched();
Reflect.get(dynamic_both_ns, "default");
assertTouched();
expect(Reflect.get(dynamic_both_ns, "default")()).toBe("func");
expect(Reflect.get(dynamic_both_ns, "x")).toBe(1);
expect(new dynamic_both_ns.T().x).toBe(1);
assertTouched();
// then flagged with namespace
reset();
assertIsNamespaceObject(flagged_default_ns);
assertUntouched();
Reflect.get(flagged_default_ns, "default");
assertTouched();
expect(Reflect.get(flagged_default_ns, "default")()).toBe("func");
assertTouched();
reset();
assertIsNamespaceObject(flagged_named_ns);
assertUntouched();
Reflect.get(flagged_named_ns, "f");
assertTouched();
expect(Reflect.get(flagged_named_ns, "f")()).toBe("func");
expect(new flagged_named_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(flagged_both_ns);
assertUntouched();
Reflect.get(flagged_both_ns, "default");
assertTouched();
expect(Reflect.get(flagged_both_ns, "default")()).toBe("func");
expect(Reflect.get(flagged_both_ns, "x")).toBe(1);
expect(new flagged_both_ns.T().x).toBe(1);
assertTouched();
// then esm with namespace
reset();
assertIsNamespaceObject(esm_default_ns);
assertUntouched();
Reflect.get(esm_default_ns, "default");
assertTouched();
expect(Reflect.get(esm_default_ns, "default")()).toBe("func");
assertTouched();
reset();
assertIsNamespaceObject(esm_named_ns);
assertUntouched();
Reflect.get(esm_named_ns, "f");
assertTouched();
expect(Reflect.get(esm_named_ns, "f")()).toBe("func");
expect(new esm_named_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(esm_both_ns);
assertUntouched();
Reflect.get(esm_both_ns, "default");
assertTouched();
expect(Reflect.get(esm_both_ns, "default")()).toBe("func");
expect(Reflect.get(esm_both_ns, "x")).toBe(1);
expect(new esm_both_ns.T().x).toBe(1);
assertTouched();
// then reexported with namespace
reset();
assertIsNamespaceObject(reexport_ns);
assertUntouched();
Reflect.get(reexport_ns, "f");
assertTouched();
expect(Reflect.get(reexport_ns, "f")()).toBe("func");
expect(new reexport_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(reexport_cjs_ns);
assertUntouched();
Reflect.get(reexport_cjs_ns, "default");
Reflect.get(reexport_cjs_ns, "default").f;
assertTouched();
expect(Reflect.get(reexport_cjs_ns, "default").f()).toBe("func");
expect(new reexport_cjs_ns.T().x).toBe(1);
assertTouched();
});
function assertIsNamespaceObject(ns) {
if (typeof ns !== "object" || !ns)
throw new TypeError("namespace is not an object.");
if (!ns[Symbol.toStringTag])
throw new Error(
"namespace object does not have a Symbol.toStringTag property."
);
}

View File

@ -0,0 +1,214 @@
import * as dynamic_default from /* webpackDefer: true */ "./commonjs/dynamic_default.cjs";
import * as dynamic_default_ns from /* webpackDefer: true */ "./commonjs/dynamic_default_ns.cjs";
import * as dynamic_named from /* webpackDefer: true */ "./commonjs/dynamic_named.cjs";
import * as dynamic_named_ns from /* webpackDefer: true */ "./commonjs/dynamic_named_ns.cjs";
import * as dynamic_both from /* webpackDefer: true */ "./commonjs/dynamic-both.cjs";
import * as dynamic_both_ns from /* webpackDefer: true */ "./commonjs/dynamic_both_ns.cjs";
import * as flagged_default from /* webpackDefer: true */ "./commonjs/flagged_default.js";
import * as flagged_default_ns from /* webpackDefer: true */ "./commonjs/flagged_default_ns.js";
import * as flagged_named from /* webpackDefer: true */ "./commonjs/flagged_named.js";
import * as flagged_named_ns from /* webpackDefer: true */ "./commonjs/flagged_named_ns.js";
import * as flagged_both from /* webpackDefer: true */ "./commonjs/flagged_both.js";
import * as flagged_both_ns from /* webpackDefer: true */ "./commonjs/flagged_both_ns.js";
import * as esm_default from /* webpackDefer: true */ "./esm/esm_default.mjs";
import * as esm_default_ns from /* webpackDefer: true */ "./esm/esm_default_ns.mjs";
import * as esm_named from /* webpackDefer: true */ "./esm/esm_named.mjs";
import * as esm_named_ns from /* webpackDefer: true */ "./esm/esm_named_ns.mjs";
import * as esm_both from /* webpackDefer: true */ "./esm/esm_both.mjs";
import * as esm_both_ns from /* webpackDefer: true */ "./esm/esm_both_ns.mjs";
import * as never from /* webpackDefer: true */ "./esm/esm_both_ns.mjs";
import { reexport_ns, reexport_cjs_ns } from "./esm/reexport.mjs";
import {
assertTouched as a,
assertUntouched as b,
reset as c
} from "./side-effect-counter.js";
const [assertTouched, assertUntouched, reset] = [a, b, c];
it("should defer the module until first use", () => {
assertUntouched();
dynamic_default.default;
assertTouched();
expect(dynamic_default.default()).toBe("func");
assertTouched();
reset();
dynamic_named.f;
assertTouched();
expect(dynamic_named.f()).toBe("func");
expect(new dynamic_named.T().x).toBe(1);
assertTouched();
reset();
dynamic_both.default;
assertTouched();
expect(dynamic_both.default()).toBe("func");
expect(dynamic_both.default.x).toBe(1);
expect(new dynamic_both.default.T().x).toBe(1);
assertTouched();
// then flagged, without namespace
reset();
flagged_default.default;
assertTouched();
expect(flagged_default.default()).toBe("func");
assertTouched();
reset();
flagged_named.f;
assertTouched();
expect(flagged_named.f()).toBe("func");
expect(new flagged_named.T().x).toBe(1);
assertTouched();
reset();
flagged_both.default;
assertTouched();
expect(flagged_both.default()).toBe("func");
expect(flagged_both.x).toBe(1);
expect(new flagged_both.T().x).toBe(1);
assertTouched();
// then esm, without namespace
reset();
esm_default.default;
assertTouched();
expect(esm_default.default()).toBe("func");
assertTouched();
reset();
esm_named.f;
assertTouched();
expect(esm_named.f()).toBe("func");
expect(new esm_named.T().x).toBe(1);
assertTouched();
reset();
esm_both.default;
assertTouched();
expect(esm_both.default()).toBe("func");
expect(esm_both.x).toBe(1);
expect(new esm_both.T().x).toBe(1);
assertTouched();
// then dynamic with namespace
reset();
assertIsNamespaceObject(dynamic_default_ns);
assertUntouched();
Reflect.get(dynamic_default_ns, "default");
assertTouched();
expect(Reflect.get(dynamic_default_ns, "default")()).toBe("func");
assertTouched();
reset();
assertIsNamespaceObject(dynamic_named_ns);
assertUntouched();
Reflect.get(dynamic_named_ns, "f");
assertTouched();
expect(Reflect.get(dynamic_named_ns, "f")()).toBe("func");
expect(new dynamic_named_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(dynamic_both_ns);
assertUntouched();
Reflect.get(dynamic_both_ns, "default");
assertTouched();
expect(Reflect.get(dynamic_both_ns, "default")()).toBe("func");
expect(Reflect.get(dynamic_both_ns, "x")).toBe(1);
expect(new dynamic_both_ns.T().x).toBe(1);
assertTouched();
// then flagged with namespace
reset();
assertIsNamespaceObject(flagged_default_ns);
assertUntouched();
Reflect.get(flagged_default_ns, "default");
assertTouched();
expect(Reflect.get(flagged_default_ns, "default")()).toBe("func");
assertTouched();
reset();
assertIsNamespaceObject(flagged_named_ns);
assertUntouched();
Reflect.get(flagged_named_ns, "f");
assertTouched();
expect(Reflect.get(flagged_named_ns, "f")()).toBe("func");
expect(new flagged_named_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(flagged_both_ns);
assertUntouched();
Reflect.get(flagged_both_ns, "default");
assertTouched();
expect(Reflect.get(flagged_both_ns, "default")()).toBe("func");
expect(Reflect.get(flagged_both_ns, "x")).toBe(1);
expect(new flagged_both_ns.T().x).toBe(1);
assertTouched();
// then esm with namespace
reset();
assertIsNamespaceObject(esm_default_ns);
assertUntouched();
Reflect.get(esm_default_ns, "default");
assertTouched();
expect(Reflect.get(esm_default_ns, "default")()).toBe("func");
assertTouched();
reset();
assertIsNamespaceObject(esm_named_ns);
assertUntouched();
Reflect.get(esm_named_ns, "f");
assertTouched();
expect(Reflect.get(esm_named_ns, "f")()).toBe("func");
expect(new esm_named_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(esm_both_ns);
assertUntouched();
Reflect.get(esm_both_ns, "default");
assertTouched();
expect(Reflect.get(esm_both_ns, "default")()).toBe("func");
expect(Reflect.get(esm_both_ns, "x")).toBe(1);
expect(new esm_both_ns.T().x).toBe(1);
assertTouched();
// then reexported with namespace
reset();
assertIsNamespaceObject(reexport_ns);
assertUntouched();
Reflect.get(reexport_ns, "f");
assertTouched();
expect(Reflect.get(reexport_ns, "f")()).toBe("func");
expect(new reexport_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(reexport_cjs_ns);
assertUntouched();
Reflect.get(reexport_cjs_ns, "default");
Reflect.get(reexport_cjs_ns, "default").f;
assertTouched();
expect(Reflect.get(reexport_cjs_ns, "default").f()).toBe("func");
expect(new reexport_cjs_ns.T().x).toBe(1);
assertTouched();
reset();
assertIsNamespaceObject(never);
expect((mod => mod.then)(never)).toBeUndefined();
});
function assertIsNamespaceObject(ns) {
if (typeof ns !== "object" || !ns)
throw new TypeError("namespace is not an object.");
if (!ns[Symbol.toStringTag])
throw new Error(
"namespace object does not have a Symbol.toStringTag property."
);
}

View File

@ -0,0 +1,13 @@
const { touch } = require("../side-effect-counter");
touch();
module.exports = function () {
return "func";
};
module.exports.x = 1;
module.exports.T = class T {
constructor() {
this.x = 1;
}
}

View File

@ -0,0 +1,13 @@
const { touch } = require("../side-effect-counter");
touch();
module.exports = function () {
return "func";
};
module.exports.x = 1;
module.exports.T = class T {
constructor() {
this.x = 1;
}
}

View File

@ -0,0 +1,7 @@
const { touch } = require("../side-effect-counter");
touch();
module.exports = function () {
return "func";
};

View File

@ -0,0 +1,7 @@
const { touch } = require("../side-effect-counter");
touch();
module.exports = function () {
return "func";
};

View File

@ -0,0 +1,12 @@
const { touch } = require("../side-effect-counter");
touch();
module.exports.f = function () {
return "func";
};
module.exports.T = class T {
constructor() {
this.x = 1;
}
}

View File

@ -0,0 +1,12 @@
const { touch } = require("../side-effect-counter");
touch();
module.exports.f = function () {
return "func";
};
module.exports.T = class {
constructor() {
this.x = 1;
}
}

View File

@ -0,0 +1,16 @@
const { touch } = require("../side-effect-counter");
module.exports.__esModule = true;
module.exports.default = f;
module.exports.x = 1;
module.exports.T = class T {
constructor() {
this.x = 1;
}
}
function f() {
return "func";
}
touch();

View File

@ -0,0 +1,16 @@
const { touch } = require("../side-effect-counter");
module.exports.__esModule = true;
module.exports.default = f;
module.exports.x = 1;
module.exports.T = class T {
constructor() {
this.x = 1;
}
}
function f() {
return "func";
}
touch();

View File

@ -0,0 +1,10 @@
const { touch } = require("../side-effect-counter");
module.exports.__esModule = true;
module.exports.default = f;
function f() {
return "func";
}
touch();

View File

@ -0,0 +1,10 @@
const { touch } = require("../side-effect-counter");
module.exports.__esModule = true;
module.exports.default = f;
function f() {
return "func";
}
touch();

View File

@ -0,0 +1,15 @@
const { touch } = require("../side-effect-counter");
module.exports.__esModule = true;
module.exports.f = f;
module.exports.T = class T {
constructor() {
this.x = 1;
}
}
function f() {
return "func";
}
touch();

View File

@ -0,0 +1,15 @@
const { touch } = require("../side-effect-counter");
module.exports.__esModule = true;
module.exports.f = f;
module.exports.T = class T {
constructor() {
this.x = 1;
}
}
function f() {
return "func";
}
touch();

View File

@ -0,0 +1,12 @@
import { touch } from "../side-effect-counter.js";
touch();
export default function () {
return "func";
}
export const x = 1;
export class T {
constructor() {
this.x = 1;
}
}

View File

@ -0,0 +1,12 @@
import { touch } from "../side-effect-counter.js";
touch();
export default function () {
return "func";
}
export const x = 1;
export class T {
constructor() {
this.x = 1;
}
}

View File

@ -0,0 +1,6 @@
import { touch } from "../side-effect-counter.js";
touch();
export default function () {
return "func";
}

View File

@ -0,0 +1,6 @@
import { touch } from "../side-effect-counter.js";
touch();
export default function () {
return "func";
}

View File

@ -0,0 +1,12 @@
import { touch } from "../side-effect-counter.js";
export function f() {
return "func";
}
export class T {
constructor() {
this.x = 1;
}
}
touch();

View File

@ -0,0 +1,13 @@
import { touch } from "../side-effect-counter.js";
export function f() {
return "func";
}
export class T {
constructor() {
this.x = 1;
}
}
touch();

View File

@ -0,0 +1 @@
throw new Error('this file should never be evaluated.')

View File

@ -0,0 +1,13 @@
const { touch } = require("../side-effect-counter");
function f() {
return "func";
}
touch();
module.exports.f = f;
module.exports.T = class T {
constructor() {
this.x = 1;
}
}

View File

@ -0,0 +1,13 @@
import { touch } from "../side-effect-counter.js";
export function f() {
return "func";
}
export class T {
constructor() {
this.x = 1;
}
}
touch();

View File

@ -0,0 +1,4 @@
import * as reexport_ns from /* webpackDefer: true */ "./reexport-deep.mjs";
import * as reexport_cjs_ns from /* webpackDefer: true */ "./reexport-deep.cjs";
export { reexport_ns, reexport_cjs_ns };

View File

@ -0,0 +1,14 @@
let count = 0;
export function touch() {
count++;
}
export function reset() {
count = 0;
}
export function assertTouched() {
if (count === 0) throw new Error("Side effect not triggered.");
if (count > 1) throw new Error("Side effect triggered more than expected.");
}
export function assertUntouched() {
if (count !== 0) throw new Error("Side effect triggered.");
}

View File

@ -0,0 +1,16 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
entry: ["./all.js"],
optimization: {
concatenateModules: false
},
experiments: {
deferImport: true
}
};

View File

@ -0,0 +1,3 @@
import * as deferred from /* webpackDefer: true */ './deferred.js';
import { order } from './order.js';
order.push([0, deferred][0]);

View File

@ -0,0 +1,3 @@
import './deferred.js';
import { order } from './order.js';
order.push(1);

View File

@ -0,0 +1,12 @@
import * as deferred from /* webpackDefer: true */ './deferred.1.js';
import { order } from './order.js';
order.push(['a', deferred][0]);
export function next() {
if (typeof avoidAnalyze(deferred).then !== 'undefined') {
throw new Error('deferred should be a deferred namespace object.');
}
}
function avoidAnalyze(params) {
return params;
}

View File

@ -0,0 +1,9 @@
import * as ns from './deferred.1.js';
import { order } from './order.js';
order.push(['b', ns][0]);
if (typeof avoidAnalyze(ns).then === 'undefined') {
throw new Error('ns should not be a deferred namespace object.');
}
function avoidAnalyze(params) {
return params;
}

View File

@ -0,0 +1,4 @@
import { order } from "./order";
order.push("deferred.1");
export function then() {}

View File

@ -0,0 +1,3 @@
import { order } from "./order";
order.push("deferred");

View File

@ -0,0 +1,15 @@
import { order } from "./order";
import './0.js';
import './1.js';
import './a.js';
import './b.js';
import { next } from "./a.js";
const expected = "0 deferred 1 a deferred.1 b";
if (order.join(' ') !== expected) {
throw new Error(
`Expected order to be "${expected}", but got "${order.join(' ')}"`
);
}
next();

View File

@ -0,0 +1,3 @@
it("execution order should be correct.", () => {
return import("./entry.js");
});

View File

@ -0,0 +1 @@
export const order = [];

View File

@ -0,0 +1,15 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
optimization: {
concatenateModules: true
},
experiments: {
deferImport: true
}
};

View File

@ -0,0 +1,6 @@
import { data, setData } from "./side-effect-counter";
if (data !== "entry")
throw new Error("Expected entry.js to be executed first.");
setData("deferred");
export {};

View File

@ -0,0 +1,11 @@
import * as ns from /* webpackDefer: true */ "./deferred.js";
import { data, setData } from "./side-effect-counter.js";
if (data !== undefined)
throw new Error("No side effect should be trigger before this.");
setData("entry");
await import("./sync-access.js");
if (Math.random() > 1) {
console.log(ns);
}

View File

@ -0,0 +1,3 @@
it("execution order should be correct.", () => {
return import("./entry.js");
});

View File

@ -0,0 +1,4 @@
export let data;
export function setData(d) {
data = d;
}

View File

@ -0,0 +1,6 @@
import * as ns from "./deferred.js";
import { data } from "./side-effect-counter.js";
if (data !== "deferred")
throw new Error("Expected deferred.js to be executed first.");
Object.entries(ns);

View File

@ -0,0 +1,14 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
},
optimization: {},
experiments: {
topLevelAwait: true,
deferImport: true
}
};

View File

@ -0,0 +1 @@
module.exports = [[/cannot be used unless experimental\.deferImport is true/]];

View File

@ -0,0 +1,2 @@
import defer * as f3 from "./mod.js";
export default f3.default;

View File

@ -0,0 +1,2 @@
export function f() {}
export default function f2() {}

View File

@ -0,0 +1,9 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
output: {
// TODO: not sure why CI set optionalChaining to true on Node 10 and fails the test
environment: {
optionalChaining: false
}
}
};

Some files were not shown because too many files have changed in this diff Show More