Merge pull request #10426 from webpack/bugfix/concat-circular

hoist exports to the top of a concatenated module
This commit is contained in:
Tobias Koppers 2020-02-21 12:49:59 +01:00 committed by GitHub
commit 22b6abdd4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 312 additions and 90 deletions

View File

@ -28,9 +28,36 @@ const joinIterableWithComma = iterable => {
return str;
};
const printExportsInfoToSource = (source, indent, exportsInfo) => {
let hasExports = false;
const printExportsInfoToSource = (
source,
indent,
exportsInfo,
alreadyPrinted = new Set()
) => {
const otherExportsInfo = exportsInfo.otherExportsInfo;
let alreadyPrintedExports = 0;
// determine exports to print
const printedExports = [];
for (const exportInfo of exportsInfo.orderedExports) {
if (!alreadyPrinted.has(exportInfo)) {
alreadyPrinted.add(exportInfo);
printedExports.push(exportInfo);
} else {
alreadyPrintedExports++;
}
}
let showOtherExports = false;
if (!alreadyPrinted.has(otherExportsInfo)) {
alreadyPrinted.add(otherExportsInfo);
showOtherExports = true;
} else {
alreadyPrintedExports++;
}
// print the exports
for (const exportInfo of printedExports) {
source.add(
Template.toComment(
`${indent}export ${JSON.stringify(exportInfo.name).slice(
@ -40,20 +67,39 @@ const printExportsInfoToSource = (source, indent, exportsInfo) => {
) + "\n"
);
if (exportInfo.exportsInfo) {
printExportsInfoToSource(source, indent + " ", exportInfo.exportsInfo);
printExportsInfoToSource(
source,
indent + " ",
exportInfo.exportsInfo,
alreadyPrinted
);
}
hasExports = true;
}
const otherExportsInfo = exportsInfo.otherExportsInfo;
if (otherExportsInfo.provided !== false || otherExportsInfo.used !== false) {
const title = hasExports ? "other exports" : "exports";
if (alreadyPrintedExports) {
source.add(
Template.toComment(
`${indent}${title} [${otherExportsInfo.getProvidedInfo()}] [${otherExportsInfo.getUsedInfo()}]`
`${indent}... (${alreadyPrintedExports} already listed exports)`
) + "\n"
);
}
if (showOtherExports) {
if (
otherExportsInfo.provided !== false ||
otherExportsInfo.used !== false
) {
const title =
printedExports.length > 0 || alreadyPrintedExports > 0
? "other exports"
: "exports";
source.add(
Template.toComment(
`${indent}${title} [${otherExportsInfo.getProvidedInfo()}] [${otherExportsInfo.getUsedInfo()}]`
) + "\n"
);
}
}
};
class ModuleInfoHeaderPlugin {

View File

@ -40,16 +40,23 @@ const EMPTY_MAP = new Map();
const EMPTY_SET = new Set();
/**
*
* @param {string[][]} referencedExports list of referenced exports, will be added to
* @param {string[]} prefix export prefix
* @param {ExportInfo} exportInfo the export info
* @param {Set<ExportInfo>} alreadyVisited already visited export info (to handle circular reexports)
*/
const processExportInfo = (referencedExports, prefix, exportInfo) => {
const processExportInfo = (
referencedExports,
prefix,
exportInfo,
alreadyVisited = new Set()
) => {
if (!exportInfo) {
referencedExports.push(prefix);
return;
}
if (alreadyVisited.has(exportInfo)) return;
alreadyVisited.add(exportInfo);
if (exportInfo.used === UsageState.Unused) return;
if (
exportInfo.used !== UsageState.OnlyPropertiesUsed ||
@ -64,7 +71,8 @@ const processExportInfo = (referencedExports, prefix, exportInfo) => {
processExportInfo(
referencedExports,
prefix.concat(exportInfo.name),
exportInfo
exportInfo,
alreadyVisited
);
}
};

View File

@ -32,7 +32,7 @@ const EMPTY_SET = new Set();
class HarmonyExportInitFragment extends InitFragment {
/**
* @param {string} exportsArgument the promises that should be awaited
* @param {string} exportsArgument the exports identifier
* @param {Map<string, string>} exportMap mapping from used name to exposed variable name
* @param {Set<string>} unusedExports list of unused export names
*/

View File

@ -5,7 +5,6 @@
"use strict";
const RuntimeGlobals = require("../RuntimeGlobals");
const makeSerializable = require("../util/makeSerializable");
const HarmonyExportInitFragment = require("./HarmonyExportInitFragment");
const NullDependency = require("./NullDependency");
@ -82,9 +81,6 @@ HarmonyExportSpecifierDependency.Template = class HarmonyExportSpecifierDependen
return;
}
runtimeRequirements.add(RuntimeGlobals.exports);
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
const map = new Map();
map.set(used, `/* binding */ ${dep.id}`);
initFragments.push(

View File

@ -19,7 +19,6 @@ const Template = require("../Template");
const HarmonyCompatibilityDependency = require("../dependencies/HarmonyCompatibilityDependency");
const HarmonyExportExpressionDependency = require("../dependencies/HarmonyExportExpressionDependency");
const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
const HarmonyExportInitFragment = require("../dependencies/HarmonyExportInitFragment");
const HarmonyExportSpecifierDependency = require("../dependencies/HarmonyExportSpecifierDependency");
const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
@ -150,6 +149,22 @@ const bySourceOrder = (a, b) => {
return 0;
};
const joinIterableWithComma = iterable => {
// This is more performant than Array.from().join(", ")
// as it doesn't create an array
let str = "";
let first = true;
for (const item of iterable) {
if (first) {
first = false;
} else {
str += ", ";
}
str += item;
}
return str;
};
const arrayEquals = (a, b) => {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
@ -157,6 +172,7 @@ const arrayEquals = (a, b) => {
}
return true;
};
/**
* @typedef {Object} ConcatenationEntry
* @property {"concatenated" | "external"} type
@ -186,23 +202,26 @@ const ensureNsObjSource = (
const nsObj = [];
const exportsInfo = moduleGraph.getExportsInfo(info.module);
for (const exportInfo of exportsInfo.orderedExports) {
const finalName = getFinalName(
moduleGraph,
info,
[exportInfo.name],
moduleToInfoMap,
requestShortener,
runtimeTemplate,
false,
undefined,
strictHarmonyModule,
true
);
nsObj.push(
`\n ${JSON.stringify(
exportInfo.getUsedName()
)}: ${runtimeTemplate.returningFunction(finalName)}`
);
const usedName = exportInfo.getUsedName();
if (usedName) {
const finalName = getFinalName(
moduleGraph,
info,
[exportInfo.name],
moduleToInfoMap,
requestShortener,
runtimeTemplate,
false,
undefined,
strictHarmonyModule,
true
);
nsObj.push(
`\n ${JSON.stringify(usedName)}: ${runtimeTemplate.returningFunction(
finalName
)}`
);
}
}
info.namespaceObjectSource = `var ${name} = {};\n${
RuntimeGlobals.makeNamespaceObject
@ -937,10 +956,20 @@ class ConcatenatedModule extends Module {
// Create mapping from module to info
const moduleToInfoMap = modulesWithInfoToMap(modulesWithInfo);
// Map with all root exposed used exports
/** @type {Map<string, function(RequestShortener): string>} */
const exportsMap = new Map();
// Set with all root exposed unused exports
/** @type {Set<string>} */
const unusedExports = new Set();
// Configure template decorators for dependencies
const innerDependencyTemplates = this._getInnerDependencyTemplates(
dependencyTemplates,
moduleToInfoMap
moduleToInfoMap,
exportsMap,
unusedExports
);
// Generate source code and analyse scopes
@ -1193,6 +1222,7 @@ class ConcatenatedModule extends Module {
moduleGraph.getExportsInfo(this).otherExportsInfo.used !==
UsageState.Unused
) {
result.add(`/* ESM COMPAT FLAG */\n`);
result.add(
runtimeTemplate.defineEsModuleFlagStatement({
exportsArgument: this.exportsArgument,
@ -1201,9 +1231,41 @@ class ConcatenatedModule extends Module {
);
}
// define exports
if (exportsMap.size > 0) {
runtimeRequirements.add(RuntimeGlobals.exports);
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
const definitions = [];
for (const [key, value] of exportsMap) {
definitions.push(
`\n ${JSON.stringify(key)}: ${runtimeTemplate.returningFunction(
value(requestShortener)
)}`
);
}
result.add(`\n// EXPORTS\n`);
result.add(
`${RuntimeGlobals.definePropertyGetters}(${
this.exportsArgument
}, {${definitions.join(",")}\n});\n`
);
}
// list unused exports
if (unusedExports.size > 0) {
result.add(
`\n// UNUSED EXPORTS: ${joinIterableWithComma(unusedExports)}\n`
);
}
// define required namespace objects (must be before evaluation modules)
for (const info of modulesWithInfo) {
if (info.type === "concatenated" && info.namespaceObjectSource) {
result.add(
`\n// NAMESPACE OBJECT: ${info.module.readableIdentifier(
requestShortener
)}\n`
);
result.add(info.namespaceObjectSource);
runtimeRequirements.add(RuntimeGlobals.makeNamespaceObject);
runtimeRequirements.add(RuntimeGlobals.definePropertyGetters);
@ -1444,12 +1506,18 @@ class ConcatenatedModule extends Module {
}
/**
*
* @param {DependencyTemplates} dependencyTemplates outer dependency templates
* @param {Map<Module, ModuleInfo>} moduleToInfoMap map for module info
* @param {Map<string, function(RequestShortener): string>} exportsMap mapping from used name to exposed variable name
* @param {Set<string>} unusedExports list of unused export names
* @returns {DependencyTemplates} inner dependency templates
*/
_getInnerDependencyTemplates(dependencyTemplates, moduleToInfoMap) {
_getInnerDependencyTemplates(
dependencyTemplates,
moduleToInfoMap,
exportsMap,
unusedExports
) {
const innerDependencyTemplates = dependencyTemplates.clone();
innerDependencyTemplates.set(
HarmonyImportSpecifierDependency,
@ -1469,14 +1537,20 @@ class ConcatenatedModule extends Module {
HarmonyExportSpecifierDependency,
new HarmonyExportSpecifierDependencyConcatenatedTemplate(
dependencyTemplates.get(HarmonyExportSpecifierDependency),
this.rootModule
this.rootModule,
moduleToInfoMap,
exportsMap,
unusedExports
)
);
innerDependencyTemplates.set(
HarmonyExportExpressionDependency,
new HarmonyExportExpressionDependencyConcatenatedTemplate(
dependencyTemplates.get(HarmonyExportExpressionDependency),
this.rootModule
this.rootModule,
moduleToInfoMap,
exportsMap,
unusedExports
)
);
innerDependencyTemplates.set(
@ -1484,7 +1558,9 @@ class ConcatenatedModule extends Module {
new HarmonyExportImportedSpecifierDependencyConcatenatedTemplate(
dependencyTemplates.get(HarmonyExportImportedSpecifierDependency),
this.rootModule,
moduleToInfoMap
moduleToInfoMap,
exportsMap,
unusedExports
)
);
innerDependencyTemplates.set(
@ -1664,10 +1740,19 @@ class HarmonyImportSideEffectDependencyConcatenatedTemplate extends DependencyTe
}
class HarmonyExportSpecifierDependencyConcatenatedTemplate extends DependencyTemplate {
constructor(originalTemplate, rootModule) {
constructor(
originalTemplate,
rootModule,
modulesMap,
exportsMap,
unusedExports
) {
super();
this.originalTemplate = originalTemplate;
this.rootModule = rootModule;
this.modulesMap = modulesMap;
this.exportsMap = exportsMap;
this.unusedExports = unusedExports;
}
/**
@ -1676,18 +1761,39 @@ class HarmonyExportSpecifierDependencyConcatenatedTemplate extends DependencyTem
* @param {DependencyTemplateContext} templateContext the context object
* @returns {void}
*/
apply(dependency, source, templateContext) {
if (templateContext.module === this.rootModule) {
this.originalTemplate.apply(dependency, source, templateContext);
apply(dependency, source, { module, moduleGraph }) {
const dep = /** @type {HarmonyExportSpecifierDependency} */ (dependency);
if (module === this.rootModule) {
const used = module.getUsedName(moduleGraph, dep.name);
if (used) {
const info = this.modulesMap.get(module);
if (!this.exportsMap.has(used)) {
this.exportsMap.set(
used,
() => `/* binding */ ${info.internalNames.get(dep.id)}`
);
}
} else {
this.unusedExports.add(dep.name || "namespace");
}
}
}
}
class HarmonyExportExpressionDependencyConcatenatedTemplate extends DependencyTemplate {
constructor(originalTemplate, rootModule) {
constructor(
originalTemplate,
rootModule,
modulesMap,
exportsMap,
unusedExports
) {
super();
this.originalTemplate = originalTemplate;
this.rootModule = rootModule;
this.modulesMap = modulesMap;
this.exportsMap = exportsMap;
this.unusedExports = unusedExports;
}
/**
@ -1706,11 +1812,18 @@ class HarmonyExportExpressionDependencyConcatenatedTemplate extends DependencyTe
if (module === this.rootModule) {
const used = module.getUsedName(moduleGraph, "default");
if (used) {
const map = new Map();
map.set(used, "__WEBPACK_MODULE_DEFAULT_EXPORT__");
initFragments.push(
new HarmonyExportInitFragment(module.exportsArgument, map)
);
const info = this.modulesMap.get(module);
if (!this.exportsMap.has(used)) {
this.exportsMap.set(
used,
() =>
`/* default */ ${info.internalNames.get(
"__WEBPACK_MODULE_DEFAULT_EXPORT__"
)}`
);
}
} else {
this.unusedExports.add("default");
}
}
@ -1737,11 +1850,19 @@ class HarmonyExportExpressionDependencyConcatenatedTemplate extends DependencyTe
}
class HarmonyExportImportedSpecifierDependencyConcatenatedTemplate extends DependencyTemplate {
constructor(originalTemplate, rootModule, modulesMap) {
constructor(
originalTemplate,
rootModule,
modulesMap,
exportsMap,
unusedExports
) {
super();
this.originalTemplate = originalTemplate;
this.rootModule = rootModule;
this.modulesMap = modulesMap;
this.exportsMap = exportsMap;
this.unusedExports = unusedExports;
}
/**
@ -1800,47 +1921,37 @@ class HarmonyExportImportedSpecifierDependencyConcatenatedTemplate extends Depen
* @returns {void}
*/
apply(dependency, source, templateContext) {
const { module, moduleGraph, initFragments } = templateContext;
const { module, moduleGraph, runtimeTemplate } = templateContext;
const dep = /** @type {HarmonyExportImportedSpecifierDependency} */ (dependency);
const importedModule = moduleGraph.getModule(dep);
const info = this.modulesMap.get(importedModule);
if (!info) {
this.originalTemplate.apply(dependency, source, templateContext);
return;
} else if (module === this.rootModule) {
const exportDefs = this.getExports(dep, templateContext);
for (const def of exportDefs) {
const used = module.getUsedName(moduleGraph, def.name);
if (!used) {
initFragments.push(
new HarmonyExportInitFragment(
this.rootModule.exportsArgument,
undefined,
new Set([def.name])
)
);
continue;
}
let finalName;
if (def.ids.length === 0) {
finalName = createModuleReference({
info,
strict: module.buildMeta.strictHarmonyModule,
asiSafe: true
});
if (used) {
if (!this.exportsMap.has(used)) {
this.exportsMap.set(used, requestShortener => {
const finalName = getFinalName(
moduleGraph,
info,
def.ids,
this.modulesMap,
requestShortener,
runtimeTemplate,
false,
false,
module.buildMeta.strictHarmonyModule,
true
);
return `/* reexport */ ${finalName}`;
});
}
} else {
finalName = createModuleReference({
info,
ids: def.ids,
strict: module.buildMeta.strictHarmonyModule,
asiSafe: true
});
this.unusedExports.add(def.name);
}
const map = new Map();
map.set(used, `/* concated reexport */ ${finalName}`);
initFragments.push(
new HarmonyExportInitFragment(this.rootModule.exportsArgument, map)
);
}
}
}

View File

@ -690,8 +690,8 @@ Child
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size
703-35b05b4f9ece3e5b7253.js 438 bytes [emitted] [immutable]
703-35b05b4f9ece3e5b7253.js.map 343 bytes [emitted] [dev]
703-35b05b4f9ece3e5b7253.js 460 bytes [emitted] [immutable]
703-35b05b4f9ece3e5b7253.js.map 344 bytes [emitted] [dev]
main-322147d992bb0a885b10.js 8.02 KiB [emitted] [immutable] [name: main]
main-322147d992bb0a885b10.js.map 7.14 KiB [emitted] [dev] [name: (main)]
Entrypoint main = main-322147d992bb0a885b10.js (main-322147d992bb0a885b10.js.map)
@ -703,8 +703,8 @@ Child
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size
703-35b05b4f9ece3e5b7253.js 438 bytes [emitted] [immutable]
703-35b05b4f9ece3e5b7253.js.map 343 bytes [emitted] [dev]
703-35b05b4f9ece3e5b7253.js 460 bytes [emitted] [immutable]
703-35b05b4f9ece3e5b7253.js.map 344 bytes [emitted] [dev]
main-322147d992bb0a885b10.js 8.02 KiB [emitted] [immutable] [name: main]
main-322147d992bb0a885b10.js.map 7.14 KiB [emitted] [dev] [name: (main)]
Entrypoint main = main-322147d992bb0a885b10.js (main-322147d992bb0a885b10.js.map)
@ -716,7 +716,7 @@ Child
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size
703-d460da0006247c80e6fd.js 1.49 KiB [emitted] [immutable]
703-d460da0006247c80e6fd.js 1.51 KiB [emitted] [immutable]
main-d716298143ad43b551dc.js 8.91 KiB [emitted] [immutable] [name: main]
Entrypoint main = main-d716298143ad43b551dc.js
./a/index.js 40 bytes [built]
@ -727,7 +727,7 @@ Child
Time: Xms
Built at: 1970-04-20 12:42:42
Asset Size
703-d460da0006247c80e6fd.js 1.49 KiB [emitted] [immutable]
703-d460da0006247c80e6fd.js 1.51 KiB [emitted] [immutable]
main-d716298143ad43b551dc.js 8.91 KiB [emitted] [immutable] [name: main]
Entrypoint main = main-d716298143ad43b551dc.js
./b/index.js 40 bytes [built]
@ -2635,7 +2635,7 @@ external \\"external\\" 42 bytes [built]
`;
exports[`StatsTestCases should print correct stats for scope-hoisting-multi 1`] = `
"Hash: 7cb360b116ad92cf28c75017658cecd20c95d17f
"Hash: 7cb360b116ad92cf28c7eecef53ff6803bb3e2c1
Child
Hash: 7cb360b116ad92cf28c7
Time: Xms
@ -2655,7 +2655,7 @@ Child
./common_lazy_shared.js 25 bytes [built]
+ 12 hidden modules
Child
Hash: 5017658cecd20c95d17f
Hash: eecef53ff6803bb3e2c1
Time: Xms
Built at: 1970-04-20 12:42:42
Entrypoint first = b-vendor.js b-first.js
@ -2678,7 +2678,7 @@ Child
ModuleConcatenation bailout: Cannot concat with ./common_lazy_shared.js (<- Module is referenced from different chunks by these modules: ./lazy_first.js, ./lazy_second.js, ./lazy_shared.js)
./common_lazy.js 25 bytes [built]
./common_lazy_shared.js 25 bytes [built]
+ 14 hidden modules"
+ 12 hidden modules"
`;
exports[`StatsTestCases should print correct stats for side-effects-issue-7428 1`] = `

View File

@ -0,0 +1 @@
import "./external";

View File

@ -0,0 +1,13 @@
import { a, b, c, default as d } from "./root";
expect(a()).toBe("a");
if (process.env.NODE_ENV === "production") {
// These two cases only work correctly when scope hoisted
expect(b()).toBe("b");
expect(Object(c).b()).toBe("b");
}
expect(() => d).toThrow();
export function test() {
expect(d).toBe(d);
}

View File

@ -0,0 +1,7 @@
it("should hoist exports in a concatenated module", () => {
return import("./root-ref").then(m => {
m.test();
});
});
if (Math.random() < 0) import("./external-ref");

View File

@ -0,0 +1,6 @@
export function b() {
return "b";
}
export function bb() {
return "bb";
}

View File

@ -0,0 +1 @@
export { test } from "./root";

View File

@ -0,0 +1,13 @@
export { test } from "./external";
import * as c from "./module";
export { c };
import * as cc from "./module";
export { cc };
export * from "./module";
export default "d";
export function a() {
return "a";
}
export function aa() {
return "aa";
}

View File

@ -0,0 +1,2 @@
import cts from "./cts";
export default cts.connectData(function() {});

View File

@ -0,0 +1,2 @@
import cts from "./cts";
export function b() {}

View File

@ -0,0 +1,3 @@
import cts from "./cts";
import a from "./a";
export function c() {}

View File

@ -0,0 +1,6 @@
import * as cts from "./cts";
export { cts as default };
export function connectData() {}
export function yyy() {}
export { b } from "./b";
export { c } from "./c";

View File

@ -0,0 +1,5 @@
it("should import these modules correctly", () => {
return import("./main");
});
if (Math.random() < 0) import("./b");

View File

@ -0,0 +1,2 @@
import cts from "./cts";
import a from "./a";