diff --git a/lib/ContextModule.js b/lib/ContextModule.js index 4b0dd1d87..adf003cda 100644 --- a/lib/ContextModule.js +++ b/lib/ContextModule.js @@ -490,6 +490,7 @@ class ContextModule extends Module { const fakeMap = Object.create(null); for (const module of sortedModules) { const exportsType = module.getExportsType( + moduleGraph, this.options.namespaceObject === "strict" ); const id = chunkGraph.getModuleId(module); diff --git a/lib/ExportsInfo.js b/lib/ExportsInfo.js index 4923c6611..4d79dccbb 100644 --- a/lib/ExportsInfo.js +++ b/lib/ExportsInfo.js @@ -425,9 +425,6 @@ class ExportsInfo { return true; } } - if (this._sideEffectsOnlyInfo.getUsed(runtime) !== UsageState.Unused) { - return true; - } for (const exportInfo of this._exports.values()) { if (exportInfo.getUsed(runtime) !== UsageState.Unused) { return true; @@ -641,7 +638,10 @@ class ExportsInfo { getUsedName(name, runtime) { if (Array.isArray(name)) { // TODO improve this - if (name.length === 0) return name; + if (name.length === 0) { + if (!this.isUsed(runtime)) return false; + return name; + } let info = this.getReadOnlyExportInfo(name[0]); const x = info.getUsedName(name[0], runtime); if (x === false) return false; diff --git a/lib/Module.js b/lib/Module.js index 9f11a5a9a..27219d8b5 100644 --- a/lib/Module.js +++ b/lib/Module.js @@ -71,7 +71,7 @@ const makeSerializable = require("./util/makeSerializable"); * @property {string=} exportsArgument * @property {boolean=} strict * @property {string=} moduleConcatenationBailout - * @property {("default" | "namespace" | "flagged")=} exportsType + * @property {("default" | "namespace" | "flagged" | "dynamic")=} exportsType * @property {(false | "redirect" | "redirect-warn")=} defaultObject * @property {boolean=} strictHarmonyModule * @property {boolean=} async @@ -263,6 +263,10 @@ class Module extends DependenciesBlock { ).getUsedExports(this, undefined); } + /** + * @deprecated + * @returns {(string | OptimizationBailoutFunction)[]} list + */ get optimizationBailout() { return ModuleGraph.getModuleGraphForModule( this, @@ -372,6 +376,7 @@ class Module extends DependenciesBlock { } /** + * @param {ModuleGraph} moduleGraph the module graph * @param {boolean} strict the importing module is strict * @returns {"namespace" | "default-only" | "default-with-named" | "dynamic"} export type * "namespace": Exports is already a namespace object. namespace = exports. @@ -379,7 +384,7 @@ class Module extends DependenciesBlock { * "default-only": Provide a namespace object with only default export. namespace = { default: exports } * "default-with-named": Provide a namespace object with named and default export. namespace = { ...exports, default: exports } */ - getExportsType(strict) { + getExportsType(moduleGraph, strict) { switch (this.buildMeta && this.buildMeta.exportsType) { case "flagged": return strict ? "default-only" : "namespace"; @@ -393,6 +398,44 @@ class Module extends DependenciesBlock { default: return "default-only"; } + case "dynamic": { + if (strict) return "default-only"; + // Try to figure out value of __esModule by following reexports + const handleDefault = () => { + switch (this.buildMeta.defaultObject) { + case "redirect": + case "redirect-warn": + return "default-with-named"; + default: + return "default-only"; + } + }; + const exportInfo = moduleGraph.getExportInfo(this, "__esModule"); + if (exportInfo.provided === false) { + return handleDefault(); + } + const target = exportInfo.getTarget(moduleGraph); + if ( + !target || + !target.export || + target.export.length !== 1 || + target.export[0] !== "__esModule" + ) { + return "dynamic"; + } + switch ( + target.module.buildMeta && + target.module.buildMeta.exportsType + ) { + case "flagged": + case "namespace": + return "namespace"; + case "default": + return handleDefault(); + default: + return "dynamic"; + } + } default: return strict ? "default-only" : "dynamic"; } diff --git a/lib/RuntimeGlobals.js b/lib/RuntimeGlobals.js index 919227046..565934a5d 100644 --- a/lib/RuntimeGlobals.js +++ b/lib/RuntimeGlobals.js @@ -26,7 +26,7 @@ exports.exports = "__webpack_exports__"; exports.thisAsExports = "top-level-this-exports"; /** - * top-level this need to be the exports object + * runtime need to return the exports of the last entry module */ exports.returnExportsFromRuntime = "return-exports-from-runtime"; diff --git a/lib/RuntimeTemplate.js b/lib/RuntimeTemplate.js index 593943ea2..b36be96aa 100644 --- a/lib/RuntimeTemplate.js +++ b/lib/RuntimeTemplate.js @@ -357,7 +357,7 @@ class RuntimeTemplate { request, weak }); - const exportsType = module.getExportsType(strict); + const exportsType = module.getExportsType(chunkGraph.moduleGraph, strict); switch (exportsType) { case "namespace": return this.moduleRaw({ @@ -461,7 +461,7 @@ class RuntimeTemplate { request, weak }); - const exportsType = module.getExportsType(strict); + const exportsType = module.getExportsType(chunkGraph.moduleGraph, strict); let fakeType = 0; switch (exportsType) { case "namespace": @@ -590,6 +590,7 @@ class RuntimeTemplate { const optDeclaration = update ? "" : "var "; const exportsType = module.getExportsType( + chunkGraph.moduleGraph, originModule.buildMeta.strictHarmonyModule ); runtimeRequirements.add(RuntimeGlobals.require); @@ -646,6 +647,7 @@ class RuntimeTemplate { exportName = exportName ? [exportName] : []; } const exportsType = module.getExportsType( + moduleGraph, originModule.buildMeta.strictHarmonyModule ); diff --git a/lib/config/defaults.js b/lib/config/defaults.js index 21f9832d9..aafe5560f 100644 --- a/lib/config/defaults.js +++ b/lib/config/defaults.js @@ -680,7 +680,13 @@ const applyOptimizationDefaults = ( apply: compiler => { // Lazy load the Terser plugin const TerserPlugin = require("terser-webpack-plugin"); - new TerserPlugin().apply(compiler); + new TerserPlugin({ + terserOptions: { + compress: { + passes: 2 + } + } + }).apply(compiler); } } ]); diff --git a/lib/dependencies/CommonJsDependencyHelpers.js b/lib/dependencies/CommonJsDependencyHelpers.js new file mode 100644 index 000000000..e39555902 --- /dev/null +++ b/lib/dependencies/CommonJsDependencyHelpers.js @@ -0,0 +1,49 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const RuntimeGlobals = require("../RuntimeGlobals"); + +exports.handleDependencyBase = (depBase, module, runtimeRequirements) => { + let base = undefined; + let type; + switch (depBase) { + case "exports": + runtimeRequirements.add(RuntimeGlobals.exports); + base = module.exportsArgument; + type = "expression"; + break; + case "module.exports": + runtimeRequirements.add(RuntimeGlobals.module); + base = `${module.moduleArgument}.exports`; + type = "expression"; + break; + case "this": + runtimeRequirements.add(RuntimeGlobals.thisAsExports); + base = "this"; + type = "expression"; + break; + case "Object.defineProperty(exports)": + runtimeRequirements.add(RuntimeGlobals.exports); + base = module.exportsArgument; + type = "Object.defineProperty"; + break; + case "Object.defineProperty(module.exports)": + runtimeRequirements.add(RuntimeGlobals.module); + base = `${module.moduleArgument}.exports`; + type = "Object.defineProperty"; + break; + case "Object.defineProperty(this)": + runtimeRequirements.add(RuntimeGlobals.thisAsExports); + base = "this"; + type = "Object.defineProperty"; + break; + default: + throw new Error(`Unsupported base ${depBase}`); + } + + return [type, base]; +}; diff --git a/lib/dependencies/CommonJsExportRequireDependency.js b/lib/dependencies/CommonJsExportRequireDependency.js new file mode 100644 index 000000000..06d2c83e2 --- /dev/null +++ b/lib/dependencies/CommonJsExportRequireDependency.js @@ -0,0 +1,356 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const Dependency = require("../Dependency"); +const { UsageState } = require("../ExportsInfo"); +const Template = require("../Template"); +const { equals } = require("../util/ArrayHelpers"); +const makeSerializable = require("../util/makeSerializable"); +const propertyAccess = require("../util/propertyAccess"); +const { handleDependencyBase } = require("./CommonJsDependencyHelpers"); +const ModuleDependency = require("./ModuleDependency"); +const processExportInfo = require("./processExportInfo"); + +/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ +/** @typedef {import("../Dependency")} Dependency */ +/** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */ +/** @typedef {import("../Dependency").ReferencedExport} ReferencedExport */ +/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */ +/** @typedef {import("../Module")} Module */ +/** @typedef {import("../ModuleGraph")} ModuleGraph */ +/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ + +const idsSymbol = Symbol("CommonJsExportRequireDependency.ids"); + +const EMPTY_OBJECT = {}; + +class CommonJsExportRequireDependency extends ModuleDependency { + constructor(range, valueRange, base, names, request, ids, resultUsed) { + super(request); + this.range = range; + this.valueRange = valueRange; + this.base = base; + this.names = names; + this.ids = ids; + this.resultUsed = resultUsed; + this.asiSafe = false; + } + + get type() { + return "cjs export require"; + } + + /** + * @param {ModuleGraph} moduleGraph the module graph + * @returns {string[]} the imported id + */ + getIds(moduleGraph) { + return moduleGraph.getMeta(this)[idsSymbol] || this.ids; + } + + /** + * @param {ModuleGraph} moduleGraph the module graph + * @param {string[]} ids the imported ids + * @returns {void} + */ + setIds(moduleGraph, ids) { + moduleGraph.getMeta(this)[idsSymbol] = ids; + } + + /** + * Returns list of exports referenced by this dependency + * @param {ModuleGraph} moduleGraph module graph + * @param {RuntimeSpec} runtime the runtime for which the module is analysed + * @returns {(string[] | ReferencedExport)[]} referenced exports + */ + getReferencedExports(moduleGraph, runtime) { + const ids = this.getIds(moduleGraph); + const getFullResult = () => { + if (ids.length === 0) { + return Dependency.EXPORTS_OBJECT_REFERENCED; + } else { + return [ + { + name: ids, + canMangle: false + } + ]; + } + }; + if (this.resultUsed) return getFullResult(); + let exportsInfo = moduleGraph.getExportsInfo( + moduleGraph.getParentModule(this) + ); + for (const name of this.names) { + const exportInfo = exportsInfo.getReadOnlyExportInfo(name); + const used = exportInfo.getUsed(runtime); + if (used === UsageState.Unused) return Dependency.NO_EXPORTS_REFERENCED; + if (used !== UsageState.OnlyPropertiesUsed) return getFullResult(); + exportsInfo = exportInfo.exportsInfo; + if (!exportsInfo) return getFullResult(); + } + if (exportsInfo.otherExportsInfo.getUsed(runtime) !== UsageState.Unused) { + return getFullResult(); + } + /** @type {string[][]} */ + const referencedExports = []; + for (const exportInfo of exportsInfo.orderedExports) { + processExportInfo( + runtime, + referencedExports, + ids.concat(exportInfo.name), + exportInfo, + false + ); + } + return referencedExports.map(name => ({ + name, + canMangle: false + })); + } + + /** + * Returns the exported names + * @param {ModuleGraph} moduleGraph module graph + * @returns {ExportsSpec | undefined} export names + */ + getExports(moduleGraph) { + const ids = this.getIds(moduleGraph); + if (this.names.length === 1) { + const name = this.names[0]; + const from = moduleGraph.getModule(this); + return { + exports: [ + { + name, + from, + export: ids.length === 0 ? null : ids, + // we can't mangle names that are in an empty object + // because one could access the prototype property + // when export isn't set yet + canMangle: !(name in EMPTY_OBJECT) && false + } + ], + dependencies: [from] + }; + } else if (this.names.length > 0) { + const name = this.names[0]; + return { + exports: [ + { + name, + // we can't mangle names that are in an empty object + // because one could access the prototype property + // when export isn't set yet + canMangle: !(name in EMPTY_OBJECT) && false + } + ], + dependencies: undefined + }; + } else { + const from = moduleGraph.getModule(this); + const reexportInfo = this.getStarReexports(moduleGraph, undefined, from); + if (reexportInfo) { + return { + exports: Array.from(reexportInfo.exports, name => { + return { + name, + from, + export: ids.concat(name), + canMangle: !(name in EMPTY_OBJECT) && false + }; + }), + // TODO handle deep reexports + dependencies: [from] + }; + } else { + return { + exports: true, + from: ids.length === 0 ? from : undefined, + canMangle: false, + dependencies: [from] + }; + } + } + } + + /** + * @param {ModuleGraph} moduleGraph the module graph + * @param {RuntimeSpec} runtime the runtime + * @param {Module} importedModule the imported module (optional) + * @returns {{exports?: Set, checked?: Set}} information + */ + getStarReexports( + moduleGraph, + runtime, + importedModule = moduleGraph.getModule(this) + ) { + let importedExportsInfo = moduleGraph.getExportsInfo(importedModule); + const ids = this.getIds(moduleGraph); + if (ids.length > 0) + importedExportsInfo = importedExportsInfo.getNestedExportsInfo(ids); + let exportsInfo = moduleGraph.getExportsInfo( + moduleGraph.getParentModule(this) + ); + if (this.names.length > 0) + exportsInfo = exportsInfo.getNestedExportsInfo(this.names); + + const noExtraExports = + importedExportsInfo && + importedExportsInfo.otherExportsInfo.provided === false; + const noExtraImports = + exportsInfo && + exportsInfo.otherExportsInfo.getUsed(runtime) === UsageState.Unused; + + if (!noExtraExports && !noExtraImports) { + return; + } + + const isNamespaceImport = + importedModule.getExportsType(moduleGraph, false) === "namespace"; + + /** @type {Set} */ + const exports = new Set(); + /** @type {Set} */ + const checked = new Set(); + + if (noExtraImports) { + for (const exportInfo of exportsInfo.orderedExports) { + const name = exportInfo.name; + if (exportInfo.getUsed(runtime) === UsageState.Unused) continue; + if (name === "__esModule" && isNamespaceImport) { + exports.add(name); + } else if (importedExportsInfo) { + const importedExportInfo = importedExportsInfo.getReadOnlyExportInfo( + name + ); + if (importedExportInfo.provided === false) continue; + exports.add(name); + if (importedExportInfo.provided === true) continue; + checked.add(name); + } else { + exports.add(name); + checked.add(name); + } + } + } else if (noExtraExports) { + for (const importedExportInfo of importedExportsInfo.orderedExports) { + const name = importedExportInfo.name; + if (importedExportInfo.provided === false) continue; + if (exportsInfo) { + const exportInfo = exportsInfo.getReadOnlyExportInfo(name); + if (exportInfo.getUsed(runtime) === UsageState.Unused) continue; + } + exports.add(name); + if (importedExportInfo.provided === true) continue; + checked.add(name); + } + if (isNamespaceImport) { + exports.add("__esModule"); + checked.delete("__esModule"); + } + } + + return { exports, checked }; + } + + serialize(context) { + const { write } = context; + write(this.range); + write(this.valueRange); + write(this.base); + write(this.names); + write(this.ids); + write(this.resultUsed); + super.serialize(context); + } + + deserialize(context) { + const { read } = context; + this.range = read(); + this.valueRange = read(); + this.base = read(); + this.names = read(); + this.ids = read(); + this.resultUsed = read(); + super.deserialize(context); + } +} + +makeSerializable( + CommonJsExportRequireDependency, + "webpack/lib/dependencies/CommonJsExportRequireDependency" +); + +CommonJsExportRequireDependency.Template = class CommonJsExportRequireDependencyTemplate extends ModuleDependency.Template { + /** + * @param {Dependency} dependency the dependency for which the template should be applied + * @param {ReplaceSource} source the current replace source which can be modified + * @param {DependencyTemplateContext} templateContext the context object + * @returns {void} + */ + apply( + dependency, + source, + { + module, + runtimeTemplate, + chunkGraph, + moduleGraph, + runtimeRequirements, + runtime + } + ) { + const dep = /** @type {CommonJsExportRequireDependency} */ (dependency); + const used = moduleGraph + .getExportsInfo(module) + .getUsedName(dep.names, runtime); + + const [type, base] = handleDependencyBase( + dep.base, + module, + runtimeRequirements + ); + + const importedModule = moduleGraph.getModule(dep); + let requireExpr = runtimeTemplate.moduleExports({ + module: importedModule, + chunkGraph, + request: dep.request, + weak: dep.weak, + runtimeRequirements + }); + const ids = dep.getIds(moduleGraph); + const usedImported = moduleGraph + .getExportsInfo(importedModule) + .getUsedName(ids, runtime); + if (usedImported) { + const comment = equals(usedImported, ids) + ? "" + : Template.toNormalComment(propertyAccess(ids)) + " "; + requireExpr += `${comment}${propertyAccess(usedImported)}`; + } + + switch (type) { + case "expression": + source.replace( + dep.range[0], + dep.range[1] - 1, + used + ? `${base}${propertyAccess(used)} = ${requireExpr}` + : `/* unused reexport */ ${requireExpr}` + ); + return; + case "Object.defineProperty": + throw new Error("TODO"); + default: + throw new Error("Unexpected type"); + } + } +}; + +module.exports = CommonJsExportRequireDependency; diff --git a/lib/dependencies/CommonJsExportsDependency.js b/lib/dependencies/CommonJsExportsDependency.js index 8476fd9ca..23b203760 100644 --- a/lib/dependencies/CommonJsExportsDependency.js +++ b/lib/dependencies/CommonJsExportsDependency.js @@ -6,9 +6,9 @@ "use strict"; const InitFragment = require("../InitFragment"); -const RuntimeGlobals = require("../RuntimeGlobals"); const makeSerializable = require("../util/makeSerializable"); const propertyAccess = require("../util/propertyAccess"); +const { handleDependencyBase } = require("./CommonJsDependencyHelpers"); const NullDependency = require("./NullDependency"); /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ @@ -94,42 +94,11 @@ CommonJsExportsDependency.Template = class CommonJsExportsDependencyTemplate ext .getExportsInfo(module) .getUsedName(dep.names, runtime); - let base = undefined; - let type; - switch (dep.base) { - case "exports": - runtimeRequirements.add(RuntimeGlobals.exports); - base = module.exportsArgument; - type = "expression"; - break; - case "module.exports": - runtimeRequirements.add(RuntimeGlobals.module); - base = `${module.moduleArgument}.exports`; - type = "expression"; - break; - case "this": - runtimeRequirements.add(RuntimeGlobals.thisAsExports); - base = "this"; - type = "expression"; - break; - case "Object.defineProperty(exports)": - runtimeRequirements.add(RuntimeGlobals.exports); - base = module.exportsArgument; - type = "Object.defineProperty"; - break; - case "Object.defineProperty(module.exports)": - runtimeRequirements.add(RuntimeGlobals.module); - base = `${module.moduleArgument}.exports`; - type = "Object.defineProperty"; - break; - case "Object.defineProperty(this)": - runtimeRequirements.add(RuntimeGlobals.thisAsExports); - base = "this"; - type = "Object.defineProperty"; - break; - default: - throw new Error(`Unsupported base ${dep.base}`); - } + const [type, base] = handleDependencyBase( + dep.base, + module, + runtimeRequirements + ); switch (type) { case "expression": diff --git a/lib/dependencies/CommonJsExportsParserPlugin.js b/lib/dependencies/CommonJsExportsParserPlugin.js index 745b7e034..fd7a30a5e 100644 --- a/lib/dependencies/CommonJsExportsParserPlugin.js +++ b/lib/dependencies/CommonJsExportsParserPlugin.js @@ -6,19 +6,21 @@ "use strict"; const RuntimeGlobals = require("../RuntimeGlobals"); +const formatLocation = require("../formatLocation"); const { evaluateToString } = require("../javascript/JavascriptParserHelpers"); +const propertyAccess = require("../util/propertyAccess"); +const CommonJsExportRequireDependency = require("./CommonJsExportRequireDependency"); const CommonJsExportsDependency = require("./CommonJsExportsDependency"); const CommonJsSelfReferenceDependency = require("./CommonJsSelfReferenceDependency"); const DynamicExports = require("./DynamicExports"); const HarmonyExports = require("./HarmonyExports"); const ModuleDecoratorDependency = require("./ModuleDecoratorDependency"); +/** @typedef {import("estree").Expression} ExpressionNode */ /** @typedef {import("../NormalModule")} NormalModule */ +/** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */ /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */ -/** @type {WeakMap} */ -const moduleExportsState = new WeakMap(); - const getValueOfPropertyDescription = expr => { if (expr.type !== "ObjectExpression") return; for (const property of expr.properties) { @@ -49,14 +51,43 @@ const isFalsyLiteral = expr => { return false; }; -class CommonJsExportsParserPlugin { - static bailout(module) { - const value = moduleExportsState.get(module); - moduleExportsState.set(module, false); - if (value === true) { - module.buildMeta.exportsType = undefined; - module.buildMeta.defaultObject = false; +/** + * @param {JavascriptParser} parser the parser + * @param {ExpressionNode} expr expression + * @returns {{ argument: BasicEvaluatedExpression, ids: string[] } | undefined} parsed call + */ +const parseRequireCall = (parser, expr) => { + const ids = []; + while (expr.type === "MemberExpression") { + if (expr.object.type === "Super") return; + if (!expr.property) return; + const prop = expr.property; + if (expr.computed) { + if (prop.type !== "Literal") return; + ids.push(`${prop.value}`); + } else { + if (prop.type !== "Identifier") return; + ids.push(prop.name); } + expr = expr.object; + } + if (expr.type !== "CallExpression" || expr.arguments.length !== 1) return; + const callee = expr.callee; + if ( + callee.type !== "Identifier" || + parser.getVariableInfo(callee.name) !== "require" + ) { + return; + } + const arg = expr.arguments[0]; + if (arg.type === "SpreadElement") return; + const argValue = parser.evaluateExpression(arg); + return { argument: argValue, ids: ids.reverse() }; +}; + +class CommonJsExportsParserPlugin { + constructor(moduleGraph) { + this.moduleGraph = moduleGraph; } /** @@ -66,18 +97,24 @@ class CommonJsExportsParserPlugin { const enableStructuredExports = () => { DynamicExports.enable(parser.state); }; - const checkNamespace = (members, valueExpr) => { + const checkNamespace = (topLevel, members, valueExpr) => { if (!DynamicExports.isEnabled(parser.state)) return; if (members.length > 0 && members[0] === "__esModule") { - if (isTruthyLiteral(valueExpr)) { + if (isTruthyLiteral(valueExpr) && topLevel) { DynamicExports.setFlagged(parser.state); } else { - DynamicExports.bailout(parser.state); + DynamicExports.setDynamic(parser.state); } } }; - const bailout = () => { + const bailout = reason => { DynamicExports.bailout(parser.state); + if (reason) bailoutHint(reason); + }; + const bailoutHint = reason => { + this.moduleGraph + .getOptimizationBailout(parser.state.module) + .push(`CommonJS bailout: ${reason}`); }; // metadata // @@ -89,60 +126,74 @@ class CommonJsExportsParserPlugin { .tap("CommonJsPlugin", evaluateToString("object")); // exporting // + const handleAssignExport = (expr, base, members) => { + if (HarmonyExports.isEnabled(parser.state)) return; + // Handle reexporting + const requireCall = parseRequireCall(parser, expr.right); + if ( + requireCall && + requireCall.argument.isString() && + (members.length === 0 || members[0] !== "__esModule") + ) { + enableStructuredExports(); + // It's possible to reexport __esModule, so we must convert to a dynamic module + if (members.length === 0) DynamicExports.setDynamic(parser.state); + const dep = new CommonJsExportRequireDependency( + expr.range, + null, + base, + members, + requireCall.argument.string, + requireCall.ids, + !parser.isStatementLevelExpression(expr) + ); + dep.loc = expr.loc; + dep.optional = !!parser.scope.inTry; + parser.state.module.addDependency(dep); + return true; + } + if (members.length === 0) return; + enableStructuredExports(); + const remainingMembers = members; + checkNamespace( + parser.statementPath.length === 1 && + parser.isStatementLevelExpression(expr), + remainingMembers, + expr.right + ); + const dep = new CommonJsExportsDependency( + expr.left.range, + null, + base, + remainingMembers + ); + dep.loc = expr.loc; + parser.state.module.addDependency(dep); + parser.walkExpression(expr.right); + return true; + }; parser.hooks.assignMemberChain .for("exports") .tap("CommonJsExportsParserPlugin", (expr, members) => { - if (HarmonyExports.isEnabled(parser.state)) return; - enableStructuredExports(); - checkNamespace(members, expr.right); - const dep = new CommonJsExportsDependency( - expr.left.range, - null, - "exports", - members - ); - dep.loc = expr.loc; - parser.state.module.addDependency(dep); - return true; + return handleAssignExport(expr, "exports", members); }); parser.hooks.assignMemberChain .for("this") .tap("CommonJsExportsParserPlugin", (expr, members) => { - if (HarmonyExports.isEnabled(parser.state)) return; if (!parser.scope.topLevelScope) return; - enableStructuredExports(); - checkNamespace(members, expr.right); - const dep = new CommonJsExportsDependency( - expr.left.range, - null, - "this", - members - ); - dep.loc = expr.loc; - parser.state.module.addDependency(dep); - return true; + return handleAssignExport(expr, "this", members); }); parser.hooks.assignMemberChain .for("module") .tap("CommonJsExportsParserPlugin", (expr, members) => { - if (HarmonyExports.isEnabled(parser.state)) return; - if (members[0] !== "exports" || members.length <= 1) return; - enableStructuredExports(); - checkNamespace(members, expr.right); - const dep = new CommonJsExportsDependency( - expr.left.range, - null, - "module.exports", - members.slice(1) - ); - dep.loc = expr.loc; - parser.state.module.addDependency(dep); - return true; + if (members[0] !== "exports") return; + return handleAssignExport(expr, "module.exports", members.slice(1)); }); parser.hooks.call .for("Object.defineProperty") .tap("CommonJsExportsParserPlugin", expression => { const expr = /** @type {import("estree").CallExpression} */ (expression); + if (!parser.isStatementLevelExpression(expr)) return; if (expr.arguments.length !== 3) return; if (expr.arguments[0].type === "SpreadElement") return; if (expr.arguments[1].type === "SpreadElement") return; @@ -162,7 +213,11 @@ class CommonJsExportsParserPlugin { if (typeof property !== "string") return; enableStructuredExports(); const descArg = expr.arguments[2]; - checkNamespace([property], getValueOfPropertyDescription(descArg)); + checkNamespace( + parser.statementPath.length === 1, + [property], + getValueOfPropertyDescription(descArg) + ); const dep = new CommonJsExportsDependency( expr.range, expr.arguments[2].range, @@ -177,44 +232,87 @@ class CommonJsExportsParserPlugin { }); // Self reference // + const handleAccessExport = (expr, base, members, call = undefined) => { + if (HarmonyExports.isEnabled(parser.state)) return; + if (members.length === 0) { + bailout(`${base} is used directly at ${formatLocation(expr.loc)}`); + } + if (call && members.length === 1) { + bailoutHint( + `${base}${propertyAccess( + members + )}(...) prevents optimization as ${base} is passed as call context as ${formatLocation( + expr.loc + )}` + ); + } + const dep = new CommonJsSelfReferenceDependency( + expr.range, + base, + members, + call + ); + dep.loc = expr.loc; + parser.state.module.addDependency(dep); + if (call) { + parser.walkExpressions(call.arguments); + } + return true; + }; + parser.hooks.callMemberChain + .for("exports") + .tap("CommonJsExportsParserPlugin", (expr, members) => { + return handleAccessExport(expr.callee, "exports", members, expr); + }); + parser.hooks.expressionMemberChain + .for("exports") + .tap("CommonJsExportsParserPlugin", (expr, members) => { + return handleAccessExport(expr, "exports", members); + }); parser.hooks.expression .for("exports") .tap("CommonJsExportsParserPlugin", expr => { - if (HarmonyExports.isEnabled(parser.state)) return; - bailout(); - const dep = new CommonJsSelfReferenceDependency( - expr.range, - "exports", - [] + return handleAccessExport(expr, "exports", []); + }); + parser.hooks.callMemberChain + .for("module") + .tap("CommonJsExportsParserPlugin", (expr, members) => { + if (members[0] !== "exports") return; + return handleAccessExport( + expr.callee, + "module.exports", + members.slice(1), + expr ); - dep.loc = expr.loc; - parser.state.module.addDependency(dep); - return true; + }); + parser.hooks.expressionMemberChain + .for("module") + .tap("CommonJsExportsParserPlugin", (expr, members) => { + if (members[0] !== "exports") return; + return handleAccessExport(expr, "module.exports", members.slice(1)); }); parser.hooks.expression .for("module.exports") .tap("CommonJsExportsParserPlugin", expr => { - if (HarmonyExports.isEnabled(parser.state)) return; - bailout(); - const dep = new CommonJsSelfReferenceDependency( - expr.range, - "module.exports", - [] - ); - dep.loc = expr.loc; - parser.state.module.addDependency(dep); - return true; + return handleAccessExport(expr, "module.exports", []); + }); + parser.hooks.callMemberChain + .for("this") + .tap("CommonJsExportsParserPlugin", (expr, members) => { + if (!parser.scope.topLevelScope) return; + return handleAccessExport(expr.callee, "this", members, expr); + }); + parser.hooks.expressionMemberChain + .for("this") + .tap("CommonJsExportsParserPlugin", (expr, members) => { + if (!parser.scope.topLevelScope) return; + return handleAccessExport(expr, "this", members); }); parser.hooks.expression .for("this") .tap("CommonJsExportsParserPlugin", expr => { - if (HarmonyExports.isEnabled(parser.state)) return; if (!parser.scope.topLevelScope) return; - bailout(); - const dep = new CommonJsSelfReferenceDependency(expr.range, "this", []); - dep.loc = expr.loc; - parser.state.module.addDependency(dep); - return true; + return handleAccessExport(expr, "this", []); }); // Bailouts // diff --git a/lib/dependencies/CommonJsFullRequireDependency.js b/lib/dependencies/CommonJsFullRequireDependency.js index fd1da8c6a..31f74ce8e 100644 --- a/lib/dependencies/CommonJsFullRequireDependency.js +++ b/lib/dependencies/CommonJsFullRequireDependency.js @@ -5,7 +5,10 @@ "use strict"; +const Template = require("../Template"); +const { equals } = require("../util/ArrayHelpers"); const makeSerializable = require("../util/makeSerializable"); +const propertyAccess = require("../util/propertyAccess"); const ModuleDependency = require("./ModuleDependency"); /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ @@ -40,7 +43,7 @@ class CommonJsFullRequireDependency extends ModuleDependency { const importedModule = moduleGraph.getModule(this); if ( !importedModule || - importedModule.getExportsType(false) !== "namespace" + importedModule.getExportsType(moduleGraph, false) !== "namespace" ) { return [this.names.slice(0, -1)]; } @@ -96,29 +99,24 @@ CommonJsFullRequireDependency.Template = class CommonJsFullRequireDependencyTemp const dep = /** @type {CommonJsFullRequireDependency} */ (dependency); if (!dep.range) return; const importedModule = moduleGraph.getModule(dep); - const exports = runtimeTemplate.moduleExports({ + let requireExpr = runtimeTemplate.moduleExports({ module: importedModule, chunkGraph, request: dep.request, weak: dep.weak, runtimeRequirements }); - const exportExpr = runtimeTemplate.exportFromImport({ - moduleGraph, - module: importedModule, - request: dep.request, - exportName: dep.names, - originModule: module, - asiSafe: dep.asiSafe, - isCall: dep.call, - callContext: undefined, - defaultInterop: false, - importVar: exports, - initFragments, - runtime, - runtimeRequirements - }); - source.replace(dep.range[0], dep.range[1] - 1, exportExpr); + const ids = dep.names; + const usedImported = moduleGraph + .getExportsInfo(importedModule) + .getUsedName(ids, runtime); + if (usedImported) { + const comment = equals(usedImported, ids) + ? "" + : Template.toNormalComment(propertyAccess(ids)) + " "; + requireExpr += `${comment}${propertyAccess(usedImported)}`; + } + source.replace(dep.range[0], dep.range[1] - 1, requireExpr); } }; diff --git a/lib/dependencies/CommonJsPlugin.js b/lib/dependencies/CommonJsPlugin.js index 099406753..1dfd09b75 100644 --- a/lib/dependencies/CommonJsPlugin.js +++ b/lib/dependencies/CommonJsPlugin.js @@ -28,6 +28,7 @@ const { evaluateToIdentifier, toConstantDependency } = require("../javascript/JavascriptParserHelpers"); +const CommonJsExportRequireDependency = require("./CommonJsExportRequireDependency"); class CommonJsPlugin { constructor(options) { @@ -99,6 +100,15 @@ class CommonJsPlugin { new CommonJsExportsDependency.Template() ); + compilation.dependencyFactories.set( + CommonJsExportRequireDependency, + normalModuleFactory + ); + compilation.dependencyTemplates.set( + CommonJsExportRequireDependency, + new CommonJsExportRequireDependency.Template() + ); + const selfFactory = new SelfModuleFactory(compilation.moduleGraph); compilation.dependencyFactories.set( @@ -203,7 +213,9 @@ class CommonJsPlugin { ); new CommonJsImportsParserPlugin(options).apply(parser); - new CommonJsExportsParserPlugin().apply(parser); + new CommonJsExportsParserPlugin(compilation.moduleGraph).apply( + parser + ); }; normalModuleFactory.hooks.parser diff --git a/lib/dependencies/CommonJsSelfReferenceDependency.js b/lib/dependencies/CommonJsSelfReferenceDependency.js index 6815fe92c..f9b31a92d 100644 --- a/lib/dependencies/CommonJsSelfReferenceDependency.js +++ b/lib/dependencies/CommonJsSelfReferenceDependency.js @@ -5,8 +5,8 @@ "use strict"; -const { UsageState } = require("../ExportsInfo"); const RuntimeGlobals = require("../RuntimeGlobals"); +const { equals } = require("../util/ArrayHelpers"); const makeSerializable = require("../util/makeSerializable"); const propertyAccess = require("../util/propertyAccess"); const NullDependency = require("./NullDependency"); @@ -20,11 +20,12 @@ const NullDependency = require("./NullDependency"); /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ class CommonJsSelfReferenceDependency extends NullDependency { - constructor(range, base, names) { + constructor(range, base, names, call) { super(); this.range = range; this.base = base; this.names = names; + this.call = call; } get type() { @@ -49,7 +50,7 @@ class CommonJsSelfReferenceDependency extends NullDependency { * @returns {(string[] | ReferencedExport)[]} referenced exports */ getReferencedExports(moduleGraph, runtime) { - return [this.names]; + return [this.call ? this.names.slice(0, -1) : this.names]; } serialize(context) { @@ -57,6 +58,7 @@ class CommonJsSelfReferenceDependency extends NullDependency { write(this.range); write(this.base); write(this.names); + write(this.call); super.serialize(context); } @@ -65,6 +67,7 @@ class CommonJsSelfReferenceDependency extends NullDependency { this.range = read(); this.base = read(); this.names = read(); + this.call = read(); super.deserialize(context); } } @@ -90,13 +93,6 @@ CommonJsSelfReferenceDependency.Template = class CommonJsSelfReferenceDependency let used; if (dep.names.length === 0) { used = dep.names; - } else if (module.buildMeta && module.buildMeta.exportsType === "default") { - const defaultInfo = moduleGraph.getExportInfo(module, "default"); - if (defaultInfo.getUsed(runtime) === UsageState.Used) { - used = dep.names; - } else { - used = defaultInfo.exportsInfo.getUsedName(dep.names, runtime); - } } else { used = moduleGraph.getExportsInfo(module).getUsedName(dep.names, runtime); } @@ -124,7 +120,7 @@ CommonJsSelfReferenceDependency.Template = class CommonJsSelfReferenceDependency throw new Error(`Unsupported base ${dep.base}`); } - if (base === dep.base && used.join() === dep.names.join()) { + if (base === dep.base && equals(used, dep.names)) { // Nothing has to be changed // We don't use a replacement for compat reasons // for plugins that update `module._source` which they @@ -135,7 +131,7 @@ CommonJsSelfReferenceDependency.Template = class CommonJsSelfReferenceDependency source.replace( dep.range[0], dep.range[1] - 1, - `/* self exports access */ ${base}${propertyAccess(used)}` + `${base}${propertyAccess(used)}` ); } }; diff --git a/lib/dependencies/DynamicExports.js b/lib/dependencies/DynamicExports.js index 1ac1007f1..7b3a827c1 100644 --- a/lib/dependencies/DynamicExports.js +++ b/lib/dependencies/DynamicExports.js @@ -44,7 +44,19 @@ exports.enable = parserState => { exports.setFlagged = parserState => { const value = parserStateExportsState.get(parserState); if (value !== true) return; - parserState.module.buildMeta.exportsType = "flagged"; + const buildMeta = parserState.module.buildMeta; + if (buildMeta.exportsType === "dynamic") return; + buildMeta.exportsType = "flagged"; +}; + +/** + * @param {ParserState} parserState parser state + * @returns {void} + */ +exports.setDynamic = parserState => { + const value = parserStateExportsState.get(parserState); + if (value !== true) return; + parserState.module.buildMeta.exportsType = "dynamic"; }; /** diff --git a/lib/dependencies/HarmonyCompatibilityDependency.js b/lib/dependencies/HarmonyCompatibilityDependency.js index ebe2454a2..93f8b729f 100644 --- a/lib/dependencies/HarmonyCompatibilityDependency.js +++ b/lib/dependencies/HarmonyCompatibilityDependency.js @@ -5,6 +5,7 @@ "use strict"; +const { UsageState } = require("../ExportsInfo"); const InitFragment = require("../InitFragment"); const RuntimeGlobals = require("../RuntimeGlobals"); const makeSerializable = require("../util/makeSerializable"); @@ -45,9 +46,11 @@ HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate runtime } ) { - // TODO avoid getUsedExports - const usedExports = moduleGraph.getUsedExports(module, runtime); - if (usedExports === true || usedExports === null) { + const exportsInfo = moduleGraph.getExportsInfo(module); + if ( + exportsInfo.getReadOnlyExportInfo("__esModule").getUsed(runtime) !== + UsageState.Unused + ) { const content = runtimeTemplate.defineEsModuleFlagStatement({ exportsArgument: module.exportsArgument, runtimeRequirements @@ -63,17 +66,15 @@ HarmonyCompatibilityDependency.Template = class HarmonyExportDependencyTemplate } if (moduleGraph.isAsync(module)) { runtimeRequirements.add(RuntimeGlobals.module); - if (usedExports !== false) - runtimeRequirements.add(RuntimeGlobals.exports); + const used = exportsInfo.isUsed(runtime); + if (used) runtimeRequirements.add(RuntimeGlobals.exports); initFragments.push( new InitFragment( `${module.moduleArgument}.exports = (async () => {\n`, InitFragment.STAGE_ASYNC_BOUNDARY, 0, undefined, - usedExports !== false - ? `\nreturn ${module.exportsArgument};\n})();` - : "\n})();" + used ? `\nreturn ${module.exportsArgument};\n})();` : "\n})();" ) ); } diff --git a/lib/dependencies/HarmonyExportImportedSpecifierDependency.js b/lib/dependencies/HarmonyExportImportedSpecifierDependency.js index c76ada4d8..3fa3ded0c 100644 --- a/lib/dependencies/HarmonyExportImportedSpecifierDependency.js +++ b/lib/dependencies/HarmonyExportImportedSpecifierDependency.js @@ -15,6 +15,7 @@ const makeSerializable = require("../util/makeSerializable"); const propertyAccess = require("../util/propertyAccess"); const HarmonyExportInitFragment = require("./HarmonyExportInitFragment"); const HarmonyImportDependency = require("./HarmonyImportDependency"); +const processExportInfo = require("./processExportInfo"); /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */ /** @typedef {import("../ChunkGraph")} ChunkGraph */ @@ -37,54 +38,6 @@ const HarmonyImportDependency = require("./HarmonyImportDependency"); const idsSymbol = Symbol("HarmonyExportImportedSpecifierDependency.ids"); -/** - * @param {RuntimeSpec} runtime the runtime - * @param {string[][]} referencedExports list of referenced exports, will be added to - * @param {string[]} prefix export prefix - * @param {ExportInfo=} exportInfo the export info - * @param {boolean} defaultPointsToSelf when true, using default will reference itself - * @param {Set} alreadyVisited already visited export info (to handle circular reexports) - */ -const processExportInfo = ( - runtime, - referencedExports, - prefix, - exportInfo, - defaultPointsToSelf = false, - alreadyVisited = new Set() -) => { - if (!exportInfo) { - referencedExports.push(prefix); - return; - } - if (alreadyVisited.has(exportInfo)) return; - alreadyVisited.add(exportInfo); - const used = exportInfo.getUsed(runtime); - if (used === UsageState.Unused) return; - if ( - used !== UsageState.OnlyPropertiesUsed || - !exportInfo.exportsInfo || - exportInfo.exportsInfo.otherExportsInfo.getUsed(runtime) !== - UsageState.Unused - ) { - referencedExports.push(prefix); - return; - } - const exportsInfo = exportInfo.exportsInfo; - for (const exportInfo of exportsInfo.orderedExports) { - processExportInfo( - runtime, - referencedExports, - defaultPointsToSelf && exportInfo.name === "default" - ? prefix - : prefix.concat(exportInfo.name), - exportInfo, - false, - alreadyVisited - ); - } -}; - class NormalReexportItem { /** * @param {string} name export name @@ -229,6 +182,7 @@ class HarmonyExportImportedSpecifierDependency extends HarmonyImportDependency { } const importedExportsType = importedModule.getExportsType( + moduleGraph, parentModule.buildMeta.strictHarmonyModule ); @@ -403,7 +357,7 @@ class HarmonyExportImportedSpecifierDependency extends HarmonyImportDependency { if (exportInfo.getUsed(runtime) === UsageState.Unused) continue; exports.add(name); if (importedExportInfo.provided === true) continue; - checked.add(exportInfo.name); + checked.add(name); } } diff --git a/lib/dependencies/HarmonyImportDependency.js b/lib/dependencies/HarmonyImportDependency.js index 9df244c5a..3f141bfed 100644 --- a/lib/dependencies/HarmonyImportDependency.js +++ b/lib/dependencies/HarmonyImportDependency.js @@ -104,6 +104,7 @@ class HarmonyImportDependency extends ModuleDependency { const parentModule = moduleGraph.getParentModule(this); const exportsType = importedModule.getExportsType( + moduleGraph, parentModule.buildMeta.strictHarmonyModule ); switch (exportsType) { @@ -199,16 +200,18 @@ class HarmonyImportDependency extends ModuleDependency { */ updateHash(hash, context) { const { chunkGraph } = context; + const { moduleGraph } = chunkGraph; super.updateHash(hash, context); - const importedModule = chunkGraph.moduleGraph.getModule(this); + const importedModule = moduleGraph.getModule(this); if (importedModule) { - const parentModule = chunkGraph.moduleGraph.getParentModule(this); + const parentModule = moduleGraph.getParentModule(this); hash.update( importedModule.getExportsType( + moduleGraph, parentModule.buildMeta && parentModule.buildMeta.strictHarmonyModule ) ); - if (chunkGraph.moduleGraph.isAsync(importedModule)) hash.update("async"); + if (moduleGraph.isAsync(importedModule)) hash.update("async"); } hash.update(`${this.sourceOrder}`); } diff --git a/lib/dependencies/HarmonyImportSpecifierDependency.js b/lib/dependencies/HarmonyImportSpecifierDependency.js index 03c2a48c4..f2123e1bb 100644 --- a/lib/dependencies/HarmonyImportSpecifierDependency.js +++ b/lib/dependencies/HarmonyImportSpecifierDependency.js @@ -113,7 +113,10 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency { const selfModule = moduleGraph.getParentModule(this); const importedModule = moduleGraph.getModule(this); switch ( - importedModule.getExportsType(selfModule.buildMeta.strictHarmonyModule) + importedModule.getExportsType( + moduleGraph, + selfModule.buildMeta.strictHarmonyModule + ) ) { case "default-only": case "default-with-named": diff --git a/lib/dependencies/processExportInfo.js b/lib/dependencies/processExportInfo.js new file mode 100644 index 000000000..435c4ac98 --- /dev/null +++ b/lib/dependencies/processExportInfo.js @@ -0,0 +1,65 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Tobias Koppers @sokra +*/ + +"use strict"; + +const { UsageState } = require("../ExportsInfo"); + +/** @typedef {import("../ExportsInfo").ExportInfo} ExportInfo */ +/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ + +/** + * @param {RuntimeSpec} runtime the runtime + * @param {string[][]} referencedExports list of referenced exports, will be added to + * @param {string[]} prefix export prefix + * @param {ExportInfo=} exportInfo the export info + * @param {boolean} defaultPointsToSelf when true, using default will reference itself + * @param {Set} alreadyVisited already visited export info (to handle circular reexports) + */ +const processExportInfo = ( + runtime, + referencedExports, + prefix, + exportInfo, + defaultPointsToSelf = false, + alreadyVisited = new Set() +) => { + if (!exportInfo) { + referencedExports.push(prefix); + return; + } + const used = exportInfo.getUsed(runtime); + if (used === UsageState.Unused) return; + if (alreadyVisited.has(exportInfo)) { + referencedExports.push(prefix); + return; + } + alreadyVisited.add(exportInfo); + if ( + used !== UsageState.OnlyPropertiesUsed || + !exportInfo.exportsInfo || + exportInfo.exportsInfo.otherExportsInfo.getUsed(runtime) !== + UsageState.Unused + ) { + alreadyVisited.delete(exportInfo); + referencedExports.push(prefix); + return; + } + const exportsInfo = exportInfo.exportsInfo; + for (const exportInfo of exportsInfo.orderedExports) { + processExportInfo( + runtime, + referencedExports, + defaultPointsToSelf && exportInfo.name === "default" + ? prefix + : prefix.concat(exportInfo.name), + exportInfo, + false, + alreadyVisited + ); + } + alreadyVisited.delete(exportInfo); +}; +module.exports = processExportInfo; diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js index 25b44842d..d0beeffca 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -17,6 +17,7 @@ const BasicEvaluatedExpression = require("./BasicEvaluatedExpression"); /** @typedef {import("estree").ArrayExpression} ArrayExpressionNode */ /** @typedef {import("estree").BinaryExpression} BinaryExpressionNode */ /** @typedef {import("estree").BlockStatement} BlockStatementNode */ +/** @typedef {import("estree").SequenceExpression} SequenceExpressionNode */ /** @typedef {import("estree").CallExpression} CallExpressionNode */ /** @typedef {import("estree").ClassDeclaration} ClassDeclarationNode */ /** @typedef {import("estree").ClassExpression} ClassExpressionNode */ @@ -235,7 +236,7 @@ class JavascriptParser extends Parser { /** @type {HookMap>} */ call: new HookMap(() => new SyncBailHook(["expression"])), /** Something like "a.b()" */ - /** @type {HookMap>} */ + /** @type {HookMap>} */ callMemberChain: new HookMap( () => new SyncBailHook(["expression", "members"]) ), @@ -294,6 +295,7 @@ class JavascriptParser extends Parser { this.state = undefined; this.comments = undefined; this.semicolons = undefined; + /** @type {(StatementNode|ExpressionNode)[]} */ this.statementPath = undefined; this.prevStatement = undefined; this.currentTagData = undefined; @@ -983,7 +985,7 @@ class JavascriptParser extends Parser { .setSideEffects(param.couldHaveSideEffects()) .setRange(expr.range); }); - ["substr", "substring"].forEach(fn => { + ["substr", "substring", "slice"].forEach(fn => { this.hooks.evaluateCallExpressionMember .for(fn) .tap("JavascriptParser", (expr, param) => { @@ -2242,8 +2244,29 @@ class JavascriptParser extends Parser { this.scope.topLevelScope = wasTopLevel; } + /** + * @param {SequenceExpressionNode} expression the sequence + */ walkSequenceExpression(expression) { - if (expression.expressions) this.walkExpressions(expression.expressions); + if (!expression.expressions) return; + // We treat sequence expressions like statements when they are one statement level + // This has some benefits for optimizations that only work on statement level + const currentStatement = this.statementPath[this.statementPath.length - 1]; + if ( + currentStatement === expression || + (currentStatement.type === "ExpressionStatement" && + currentStatement.expression === expression) + ) { + const old = this.statementPath.pop(); + for (const expr of expression.expressions) { + this.statementPath.push(expr); + this.walkExpression(expr); + this.statementPath.pop(); + } + this.statementPath.push(old); + } else { + this.walkExpressions(expression.expressions); + } } walkUpdateExpression(expression) { @@ -2325,8 +2348,8 @@ class JavascriptParser extends Parser { }); return; } - this.walkExpression(expression.right); if (expression.left.type.endsWith("Pattern")) { + this.walkExpression(expression.right); this.enterPattern(expression.left, (name, decl) => { if (!this.callHooksForName(this.hooks.assign, name, expression)) { this.defineVariable(name); @@ -2349,8 +2372,10 @@ class JavascriptParser extends Parser { return; } } + this.walkExpression(expression.right); this.walkExpression(expression.left); } else { + this.walkExpression(expression.right); this.walkExpression(expression.left); } } @@ -2565,14 +2590,20 @@ class JavascriptParser extends Parser { if (exprInfo) { switch (exprInfo.type) { case "expression": { + const result1 = this.callHooksForInfo( + this.hooks.expression, + exprInfo.name, + expression + ); + if (result1 === true) return; const members = exprInfo.getMembers(); - const result = this.callHooksForInfo( + const result2 = this.callHooksForInfo( this.hooks.expressionMemberChain, exprInfo.rootInfo, expression, members ); - if (result === true) return; + if (result2 === true) return; this.walkMemberExpressionWithExpressionName( expression, exprInfo.name, @@ -2616,12 +2647,6 @@ class JavascriptParser extends Parser { members, onUnhandled ) { - const result = this.callHooksForInfo( - this.hooks.expression, - name, - expression - ); - if (result === true) return; if (expression.object.type === "MemberExpression") { // optimize the case where expression.object is a MemberExpression too. // we can keep info here when calling walkMemberExpression directly @@ -2629,6 +2654,12 @@ class JavascriptParser extends Parser { expression.property.name || `${expression.property.value}`; name = name.slice(0, -property.length - 1); members.pop(); + const result = this.callHooksForInfo( + this.hooks.expression, + name, + expression.object + ); + if (result === true) return; this.walkMemberExpressionWithExpressionName( expression.object, name, @@ -3170,6 +3201,15 @@ class JavascriptParser extends Parser { ); } + isStatementLevelExpression(expr) { + const currentStatement = this.statementPath[this.statementPath.length - 1]; + return ( + expr === currentStatement || + (currentStatement.type === "ExpressionStatement" && + currentStatement.expression === expr) + ); + } + getTagData(name, tag) { const info = this.scope.definitions.get(name); if (info instanceof VariableInfo) { diff --git a/lib/optimize/ConcatenatedModule.js b/lib/optimize/ConcatenatedModule.js index 617b76e91..6bc82d851 100644 --- a/lib/optimize/ConcatenatedModule.js +++ b/lib/optimize/ConcatenatedModule.js @@ -256,7 +256,10 @@ const getExternalImport = ( asiSafe ) => { let exprStart = info.name; - const exportsType = importedModule.getExportsType(strictHarmonyModule); + const exportsType = importedModule.getExportsType( + moduleGraph, + strictHarmonyModule + ); if (exportName.length === 0) { switch (exportsType) { case "default-only": @@ -1197,6 +1200,7 @@ class ConcatenatedModule extends Module { if ( info.module.buildMeta.exportsType === "default" || info.module.buildMeta.exportsType === "flagged" || + info.module.buildMeta.exportsType === "dynamic" || !info.module.buildMeta.exportsType ) { const externalNameInterop = this.findNewName( @@ -1208,7 +1212,10 @@ class ConcatenatedModule extends Module { allUsedNames.add(externalNameInterop); info.interopNamespaceObjectName = externalNameInterop; } - if (!info.module.buildMeta.exportsType) { + if ( + info.module.buildMeta.exportsType === "dynamic" || + !info.module.buildMeta.exportsType + ) { const externalNameInterop = this.findNewName( "default", allUsedNames, @@ -1436,6 +1443,7 @@ class ConcatenatedModule extends Module { ); } else if ( info.module.buildMeta.exportsType === "flagged" || + info.module.buildMeta.exportsType === "dynamic" || !info.module.buildMeta.exportsType ) { runtimeRequirements.add(RuntimeGlobals.createFakeNamespaceObject); diff --git a/lib/optimize/MangleExportsPlugin.js b/lib/optimize/MangleExportsPlugin.js index bd349825a..2d1d811e4 100644 --- a/lib/optimize/MangleExportsPlugin.js +++ b/lib/optimize/MangleExportsPlugin.js @@ -51,17 +51,23 @@ const mangleExportsInfo = (deterministic, exportsInfo, canBeArray) => { /** @type {ExportInfo[]} */ const mangleableExports = []; const empty = canBeArray ? ARRAY : OBJECT; - // Don't rename 1-2 char exports or exports that can't be mangled for (const exportInfo of exportsInfo.ownedExports) { const name = exportInfo.name; if (!exportInfo.hasUsedName()) { if ( + // Can the export be mangled? exportInfo.canMangle !== true || + // Never rename 1 char exports (name.length === 1 && /^[a-zA-Z0-9_$]/.test(name)) || + // Don't rename 2 char exports in deterministic mode (deterministic && name.length === 2 && /^[a-zA-Z_$][a-zA-Z0-9_$]|^[1-9][0-9]/.test(name)) || - (exportInfo.provided !== true && exportInfo.name in empty) + // Don't rename exports that are not provided and in prototype chain of JSON + (exportInfo.provided !== true && exportInfo.name in empty) || + // Don't rename exports that are neither provided nor used + (exportInfo.provided === false && + exportInfo.getUsed(undefined) === UsageState.Unused) ) { exportInfo.setUsedName(name); usedNames.add(name); diff --git a/lib/util/internalSerializables.js b/lib/util/internalSerializables.js index 910984ab0..d69782d22 100644 --- a/lib/util/internalSerializables.js +++ b/lib/util/internalSerializables.js @@ -47,6 +47,8 @@ module.exports = { require("../dependencies/CachedConstDependency"), "dependencies/CommonJsRequireContextDependency": () => require("../dependencies/CommonJsRequireContextDependency"), + "dependencies/CommonJsExportRequireDependency": () => + require("../dependencies/CommonJsExportRequireDependency"), "dependencies/CommonJsExportsDependency": () => require("../dependencies/CommonJsExportsDependency"), "dependencies/CommonJsFullRequireDependency": () => diff --git a/test/__snapshots__/StatsTestCases.test.js.snap b/test/__snapshots__/StatsTestCases.test.js.snap index 0b63caba4..4fc6253a5 100644 --- a/test/__snapshots__/StatsTestCases.test.js.snap +++ b/test/__snapshots__/StatsTestCases.test.js.snap @@ -521,10 +521,10 @@ exports[`StatsTestCases should print correct stats for chunks-development 1`] = Time: X ms Built at: 1970-04-20 12:42:42 PublicPath: (none) -asset b_js.bundle.js 901 bytes [emitted] -asset bundle.js 9.75 KiB [emitted] (name: main) +asset b_js.bundle.js 968 bytes [emitted] +asset bundle.js 9.82 KiB [emitted] (name: main) asset c_js.bundle.js 1.1 KiB [emitted] -asset d_js-e_js.bundle.js 1.25 KiB [emitted] +asset d_js-e_js.bundle.js 1.38 KiB [emitted] Entrypoint main = bundle.js chunk b_js.bundle.js 22 bytes <{main}> [rendered] > ./b ./index.js 2:0-16 @@ -600,6 +600,19 @@ Entrypoint main = main.js ./index.js 1 bytes [built]" `; +exports[`StatsTestCases should print correct stats for common-libs 1`] = ` +"Hash: cb2d8154f4c30528e43b +Time: X ms +Built at: 1970-04-20 12:42:42 +asset react.js 3.12 KiB [emitted] [minimized] (name: react) +asset react.js.LICENSE.txt 295 bytes [emitted] +Entrypoint react = react.js +./react.js 69 bytes [built] +../../../node_modules/react/index.js 190 bytes [built] +../../../node_modules/react/cjs/react.production.min.js 6.52 KiB [built] +../../../node_modules/object-assign/index.js 2.06 KiB [built]" +`; + exports[`StatsTestCases should print correct stats for commons-chunk-min-size-0 1`] = ` "Hash: b96c5a233eb1688dd315 Time: X ms @@ -2114,18 +2127,22 @@ chunk {996} (runtime: main) 996.js 22 bytes <{179}> [rendered] ModuleConcatenation bailout: Module is not an ECMAScript module [847] ./a.js 22 bytes {179} [depth 1] [built] [used exports unknown] + CommonJS bailout: module.exports is used directly at 1:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module [996] ./b.js 22 bytes {996} [depth 1] [built] [used exports unknown] + CommonJS bailout: module.exports is used directly at 1:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module [460] ./c.js 54 bytes {460} [depth 1] [built] [used exports unknown] ModuleConcatenation bailout: Module is not an ECMAScript module [767] ./d.js 22 bytes {524} [depth 2] [built] [used exports unknown] + CommonJS bailout: module.exports is used directly at 1:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module [390] ./e.js 22 bytes {524} [depth 2] [built] [used exports unknown] + CommonJS bailout: module.exports is used directly at 1:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module webpack/runtime/ensure chunk 326 bytes {179} [runtime] [no exports] @@ -2342,6 +2359,7 @@ chunk {179} (runtime: main) main.js (main) 73 bytes (javascript) 4.88 KiB (runti > ./index main [847] ./a.js 22 bytes {179} [depth 1] [built] [used exports unknown] + CommonJS bailout: module.exports is used directly at 1:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module cjs self exports reference [847] ./a.js 1:0-14 cjs require ./a [10] ./index.js 1:0-14 @@ -2380,12 +2398,14 @@ chunk {524} (runtime: main) 524.js 44 bytes <{460}> [rendered] > [460] ./c.js 1:0-52 [767] ./d.js 22 bytes {524} [depth 2] [built] [used exports unknown] + CommonJS bailout: module.exports is used directly at 1:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module require.ensure item ./d [460] ./c.js 1:0-52 cjs self exports reference [767] ./d.js 1:0-14 X ms [10] -> X ms [460] -> X ms (resolving: X ms, restoring: X ms, integration: X ms, building: X ms, storing: X ms) [390] ./e.js 22 bytes {524} [depth 2] [built] [used exports unknown] + CommonJS bailout: module.exports is used directly at 1:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module require.ensure item ./e [460] ./c.js 1:0-52 cjs self exports reference [390] ./e.js 1:0-14 @@ -2394,6 +2414,7 @@ chunk {996} (runtime: main) 996.js 22 bytes <{179}> [rendered] > ./b [10] ./index.js 2:0-16 [996] ./b.js 22 bytes {996} [depth 1] [built] [used exports unknown] + CommonJS bailout: module.exports is used directly at 1:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module cjs self exports reference [996] ./b.js 1:0-14 amd require ./b [10] ./index.js 2:0-16 @@ -2944,6 +2965,7 @@ Entrypoint entry = entry.js ModuleConcatenation bailout: Cannot concat with ./ref-from-cjs.js: Module ./ref-from-cjs.js is referenced from these modules with unsupported syntax: ./cjs.js (referenced with cjs require) ./entry.js 32 bytes [built] ./cjs.js 59 bytes [built] + CommonJS bailout: module.exports is used directly at 3:0-14 ModuleConcatenation bailout: Module is not an ECMAScript module ./ref-from-cjs.js 45 bytes [built] ./eval.js 35 bytes [built] @@ -3065,9 +3087,9 @@ Entrypoint main = main.js `; exports[`StatsTestCases should print correct stats for side-effects-optimization 1`] = ` -"Hash: d26335b5c063fdfc2a88c8cd6c316db9be4953d2 +"Hash: dfef98e0ef4320bb5e7c18a1362fdec91250a8dc Child - Hash: d26335b5c063fdfc2a88 + Hash: dfef98e0ef4320bb5e7c Time: X ms Built at: 1970-04-20 12:42:42 asset main.js 207 bytes [emitted] [minimized] (name: main) @@ -3088,7 +3110,7 @@ Child ModuleConcatenation bailout: Module is not an ECMAScript module + 4 hidden modules Child - Hash: c8cd6c316db9be4953d2 + Hash: 18a1362fdec91250a8dc Time: X ms Built at: 1970-04-20 12:42:42 asset main.no-side.js 1.24 KiB [emitted] [minimized] (name: main) @@ -4269,7 +4291,7 @@ Child global: `; exports[`StatsTestCases should print correct stats for tree-shaking 1`] = ` -"Hash: e0f1716012b18c82ad97 +"Hash: 9b742a666c2c28e22fdf Time: X ms Built at: 1970-04-20 12:42:42 asset bundle.js 7.09 KiB [emitted] (name: main) diff --git a/types.d.ts b/types.d.ts index 0c24d61c8..1706ef959 100644 --- a/types.d.ts +++ b/types.d.ts @@ -3478,7 +3478,7 @@ declare abstract class JavascriptParser extends Parser { topLevelAwait: SyncBailHook<[Expression], boolean | void>; call: HookMap>; callMemberChain: HookMap< - SyncBailHook<[Expression, string[]], boolean | void> + SyncBailHook<[CallExpression, string[]], boolean | void> >; memberChainOfCallMemberChain: HookMap< SyncBailHook< @@ -3513,9 +3513,56 @@ declare abstract class JavascriptParser extends Parser { state: Record & ParserStateBase; comments: any; semicolons: any; - statementEndPos: any; - lastStatementEndPos: any; - statementStartPos: any; + statementPath: ( + | UnaryExpression + | ThisExpression + | ArrayExpression + | ObjectExpression + | FunctionExpression + | ArrowFunctionExpression + | YieldExpression + | SimpleLiteral + | RegExpLiteral + | UpdateExpression + | BinaryExpression + | AssignmentExpression + | LogicalExpression + | MemberExpression + | ConditionalExpression + | SimpleCallExpression + | NewExpression + | SequenceExpression + | TemplateLiteral + | TaggedTemplateExpression + | ClassExpression + | MetaProperty + | Identifier + | AwaitExpression + | ImportExpression + | ChainExpression + | ExpressionStatement + | BlockStatement + | EmptyStatement + | DebuggerStatement + | WithStatement + | ReturnStatement + | LabeledStatement + | BreakStatement + | ContinueStatement + | IfStatement + | SwitchStatement + | ThrowStatement + | TryStatement + | WhileStatement + | DoWhileStatement + | ForStatement + | ForInStatement + | ForOfStatement + | FunctionDeclaration + | VariableDeclaration + | ClassDeclaration + )[]; + prevStatement: any; currentTagData: any; initializeEvaluating(): void; getRenameIdentifier(expr?: any): string; @@ -3584,7 +3631,7 @@ declare abstract class JavascriptParser extends Parser { walkObjectExpression(expression?: any): void; walkFunctionExpression(expression?: any): void; walkArrowFunctionExpression(expression?: any): void; - walkSequenceExpression(expression?: any): void; + walkSequenceExpression(expression: SequenceExpression): void; walkUpdateExpression(expression?: any): void; walkUnaryExpression(expression?: any): void; walkLeftRightExpression(expression?: any): void; @@ -3664,6 +3711,7 @@ declare abstract class JavascriptParser extends Parser { evaluate(source?: any): BasicEvaluatedExpression; getComments(range?: any): any; isAsiPosition(pos?: any): any; + isStatementLevelExpression(expr?: any): boolean; getTagData(name?: any, tag?: any): any; tagVariable(name?: any, tag?: any, data?: any): void; defineVariable(name?: any): void; @@ -3759,7 +3807,7 @@ declare interface KnownBuildMeta { exportsArgument?: string; strict?: boolean; moduleConcatenationBailout?: string; - exportsType?: "namespace" | "default" | "flagged"; + exportsType?: "namespace" | "dynamic" | "default" | "flagged"; defaultObject?: false | "redirect" | "redirect-warn"; strictHarmonyModule?: boolean; async?: boolean; @@ -4176,6 +4224,7 @@ declare class Module extends DependenciesBlock { readonly exportsArgument: string; readonly moduleArgument: string; getExportsType( + moduleGraph: ModuleGraph, strict: boolean ): "namespace" | "default-only" | "default-with-named" | "dynamic"; addPresentationalDependency(presentationalDependency: Dependency): void;