Skip to content

Commit

Permalink
Merge pull request #11802 from webpack/bugfix/11770
Browse files Browse the repository at this point in the history
generate code that executes depending on runtime
  • Loading branch information
sokra committed Oct 27, 2020
2 parents d09357b + e2706b8 commit 923be31
Show file tree
Hide file tree
Showing 54 changed files with 1,099 additions and 274 deletions.
4 changes: 2 additions & 2 deletions declarations/WebpackOptions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,9 +1467,9 @@ export interface Optimization {
*/
runtimeChunk?: OptimizationRuntimeChunk;
/**
* Skip over modules which are flagged to contain no side effects when exports are not used.
* Skip over modules which contain no side effects when exports are not used (false: disabled, 'flag': only use manually placed side effects flag, true: also analyse source code for side effects).
*/
sideEffects?: boolean;
sideEffects?: "flag" | boolean;
/**
* Optimize duplication and caching by splitting chunks by shared modules and cache group.
*/
Expand Down
19 changes: 19 additions & 0 deletions lib/ChunkGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,8 @@ class ChunkGraph {
this._chunks = new WeakMap();
/** @private @type {WeakMap<AsyncDependenciesBlock, ChunkGroup>} */
this._blockChunkGroups = new WeakMap();
/** @private @type {Map<string, string | number>} */
this._runtimeIds = new Map();
/** @type {ModuleGraph} */
this.moduleGraph = moduleGraph;

Expand Down Expand Up @@ -1144,6 +1146,23 @@ class ChunkGraph {
cgm.id = id;
}

/**
* @param {string} runtime runtime
* @returns {string | number} the id of the runtime
*/
getRuntimeId(runtime) {
return this._runtimeIds.get(runtime);
}

/**
* @param {string} runtime runtime
* @param {string | number} id the id of the runtime
* @returns {void}
*/
setRuntimeId(runtime, id) {
this._runtimeIds.set(runtime, id);
}

/**
* @param {Module} module the module
* @param {RuntimeSpec} runtime the runtime
Expand Down
29 changes: 25 additions & 4 deletions lib/Compilation.js
Original file line number Diff line number Diff line change
Expand Up @@ -1939,6 +1939,8 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
this.hooks.optimizeChunkIds.call(this.chunks);
this.hooks.afterOptimizeChunkIds.call(this.chunks);

this.assignRuntimeIds();

this.sortItemsWithChunkIds();

if (shouldRecord) {
Expand Down Expand Up @@ -2556,6 +2558,21 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
}
}

assignRuntimeIds() {
const { chunkGraph } = this;
const processEntrypoint = ep => {
const runtime = ep.options.runtime || ep.name;
const chunk = ep.getRuntimeChunk();
chunkGraph.setRuntimeId(runtime, chunk.id);
};
for (const ep of this.entrypoints.values()) {
processEntrypoint(ep);
}
for (const ep of this.asyncEntrypoints) {
processEntrypoint(ep);
}
}

sortItemsWithChunkIds() {
for (const chunkGroup of this.chunkGroups) {
chunkGroup.sortItems();
Expand Down Expand Up @@ -2592,15 +2609,16 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o

createModuleHashes() {
let statModulesHashed = 0;
const chunkGraph = this.chunkGraph;
const { chunkGraph, runtimeTemplate } = this;
const { hashFunction, hashDigest, hashDigestLength } = this.outputOptions;
for (const module of this.modules) {
for (const runtime of chunkGraph.getModuleRuntimes(module)) {
statModulesHashed++;
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash, {
chunkGraph,
runtime
runtime,
runtimeTemplate
});
const moduleHashDigest = /** @type {string} */ (moduleHash.digest(
hashDigest
Expand All @@ -2623,6 +2641,7 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o
createHash() {
this.logger.time("hashing: initialize hash");
const chunkGraph = this.chunkGraph;
const runtimeTemplate = this.runtimeTemplate;
const outputOptions = this.outputOptions;
const hashFunction = outputOptions.hashFunction;
const hashDigest = outputOptions.hashDigest;
Expand Down Expand Up @@ -2707,7 +2726,8 @@ This prevents using hashes of each other and should be avoided.`
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash, {
chunkGraph,
runtime: chunk.runtime
runtime: chunk.runtime,
runtimeTemplate
});
const moduleHashDigest = /** @type {string} */ (moduleHash.digest(
hashDigest
Expand Down Expand Up @@ -2769,7 +2789,8 @@ This prevents using hashes of each other and should be avoided.`
const moduleHash = createHash(hashFunction);
module.updateHash(moduleHash, {
chunkGraph,
runtime: chunk.runtime
runtime: chunk.runtime,
runtimeTemplate
});
const moduleHashDigest = /** @type {string} */ (moduleHash.digest(
hashDigest
Expand Down
109 changes: 109 additions & 0 deletions lib/ConditionalInitFragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/

"use strict";

const { ConcatSource, PrefixSource } = require("webpack-sources");
const InitFragment = require("./InitFragment");
const Template = require("./Template");
const { mergeRuntime } = require("./util/runtime");

/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("./Generator").GenerateContext} GenerateContext */
/** @typedef {import("./util/runtime").RuntimeSpec} RuntimeSpec */

const wrapInCondition = (condition, source) => {
if (typeof source === "string") {
return Template.asString([
`if (${condition}) {`,
Template.indent(source),
"}",
""
]);
} else {
return new ConcatSource(
`if (${condition}) {\n`,
new PrefixSource("\t", source),
"}\n"
);
}
};

class ConditionalInitFragment extends InitFragment {
/**
* @param {string|Source} content the source code that will be included as initialization code
* @param {number} stage category of initialization code (contribute to order)
* @param {number} position position in the category (contribute to order)
* @param {string} key unique key to avoid emitting the same initialization code twice
* @param {RuntimeSpec | boolean} runtimeCondition in which runtime this fragment should be executed
* @param {string|Source=} endContent the source code that will be included at the end of the module
*/
constructor(
content,
stage,
position,
key,
runtimeCondition = true,
endContent
) {
super(content, stage, position, key, endContent);
this.runtimeCondition = runtimeCondition;
}

/**
* @param {GenerateContext} generateContext context for generate
* @returns {string|Source} the source code that will be included as initialization code
*/
getContent(generateContext) {
if (this.runtimeCondition === false || !this.content) return "";
if (this.runtimeCondition === true) return this.content;
const expr = generateContext.runtimeTemplate.runtimeConditionExpression({
chunkGraph: generateContext.chunkGraph,
runtimeRequirements: generateContext.runtimeRequirements,
runtime: generateContext.runtime,
runtimeCondition: this.runtimeCondition
});
if (expr === "true") return this.content;
return wrapInCondition(expr, this.content);
}

/**
* @param {GenerateContext} generateContext context for generate
* @returns {string|Source=} the source code that will be included at the end of the module
*/
getEndContent(generateContext) {
if (this.runtimeCondition === false || !this.endContent) return "";
if (this.runtimeCondition === true) return this.endContent;
const expr = generateContext.runtimeTemplate.runtimeConditionExpression({
chunkGraph: generateContext.chunkGraph,
runtimeRequirements: generateContext.runtimeRequirements,
runtime: generateContext.runtime,
runtimeCondition: this.runtimeCondition
});
if (expr === "true") return this.endContent;
return wrapInCondition(expr, this.endContent);
}

merge(other) {
if (this.runtimeCondition === true) return this;
if (other.runtimeCondition === true) return other;
if (this.runtimeCondition === false) return other;
if (other.runtimeCondition === false) return this;
const runtimeCondition = mergeRuntime(
this.runtimeCondition,
other.runtimeCondition
);
return new ConditionalInitFragment(
this.content,
this.stage,
this.position,
this.key,
runtimeCondition,
this.endContent
);
}
}

module.exports = ConditionalInitFragment;
1 change: 1 addition & 0 deletions lib/Dependency.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* @typedef {Object} UpdateHashContext
* @property {ChunkGraph} chunkGraph
* @property {RuntimeSpec} runtime
* @property {RuntimeTemplate=} runtimeTemplate
*/

/**
Expand Down
5 changes: 5 additions & 0 deletions lib/RuntimeGlobals.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ exports.loadScript = "__webpack_require__.l";
*/
exports.chunkName = "__webpack_require__.cn";

/**
* the runtime id of the current runtime
*/
exports.runtimeId = "__webpack_require__.j";

/**
* the filename of the script part of the chunk
*/
Expand Down
8 changes: 8 additions & 0 deletions lib/RuntimePlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const HasOwnPropertyRuntimeModule = require("./runtime/HasOwnPropertyRuntimeModu
const LoadScriptRuntimeModule = require("./runtime/LoadScriptRuntimeModule");
const MakeNamespaceObjectRuntimeModule = require("./runtime/MakeNamespaceObjectRuntimeModule");
const PublicPathRuntimeModule = require("./runtime/PublicPathRuntimeModule");
const RuntimeIdRuntimeModule = require("./runtime/RuntimeIdRuntimeModule");
const SystemContextRuntimeModule = require("./runtime/SystemContextRuntimeModule");
const ShareRuntimeModule = require("./sharing/ShareRuntimeModule");
const StringXor = require("./util/StringXor");
Expand All @@ -31,6 +32,7 @@ const StringXor = require("./util/StringXor");

const GLOBALS_ON_REQUIRE = [
RuntimeGlobals.chunkName,
RuntimeGlobals.runtimeId,
RuntimeGlobals.compatGetDefaultExport,
RuntimeGlobals.createFakeNamespaceObject,
RuntimeGlobals.definePropertyGetters,
Expand Down Expand Up @@ -156,6 +158,12 @@ class RuntimePlugin {
);
return true;
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.runtimeId)
.tap("RuntimePlugin", chunk => {
compilation.addRuntimeModule(chunk, new RuntimeIdRuntimeModule());
return true;
});
compilation.hooks.runtimeRequirementInTree
.for(RuntimeGlobals.publicPath)
.tap("RuntimePlugin", (chunk, set) => {
Expand Down
37 changes: 37 additions & 0 deletions lib/RuntimeTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ const InitFragment = require("./InitFragment");
const RuntimeGlobals = require("./RuntimeGlobals");
const Template = require("./Template");
const { equals } = require("./util/ArrayHelpers");
const compileBooleanMatcher = require("./util/compileBooleanMatcher");
const propertyAccess = require("./util/propertyAccess");
const { forEachRuntime, subtractRuntime } = require("./util/runtime");

/** @typedef {import("../declarations/WebpackOptions").OutputNormalized} OutputOptions */
/** @typedef {import("./AsyncDependenciesBlock")} AsyncDependenciesBlock */
Expand Down Expand Up @@ -562,6 +564,39 @@ class RuntimeTemplate {
return `${promise || "Promise.resolve()"}${appending}`;
}

/**
* @param {Object} options options object
* @param {ChunkGraph} options.chunkGraph the chunk graph
* @param {RuntimeSpec=} options.runtime runtime for which this code will be generated
* @param {RuntimeSpec | boolean=} options.runtimeCondition only execute the statement in some runtimes
* @param {Set<string>} options.runtimeRequirements if set, will be filled with runtime requirements
* @returns {string} expression
*/
runtimeConditionExpression({
chunkGraph,
runtimeCondition,
runtime,
runtimeRequirements
}) {
if (runtimeCondition === undefined) return "true";
if (typeof runtimeCondition === "boolean") return `${runtimeCondition}`;
/** @type {Set<string>} */
const positiveRuntimeIds = new Set();
forEachRuntime(runtimeCondition, runtime =>
positiveRuntimeIds.add(`${chunkGraph.getRuntimeId(runtime)}`)
);
/** @type {Set<string>} */
const negativeRuntimeIds = new Set();
forEachRuntime(subtractRuntime(runtime, runtimeCondition), runtime =>
negativeRuntimeIds.add(`${chunkGraph.getRuntimeId(runtime)}`)
);
runtimeRequirements.add(RuntimeGlobals.runtimeId);
return compileBooleanMatcher.fromLists(
Array.from(positiveRuntimeIds),
Array.from(negativeRuntimeIds)
)(RuntimeGlobals.runtimeId);
}

/**
*
* @param {Object} options options object
Expand All @@ -572,6 +607,8 @@ class RuntimeTemplate {
* @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 {RuntimeSpec=} options.runtime runtime for which this code will be generated
* @param {RuntimeSpec | boolean=} options.runtimeCondition only execute the statement in some runtimes
* @param {Set<string>} options.runtimeRequirements if set, will be filled with runtime requirements
* @returns {[string, string]} the import statement and the compat statement
*/
Expand Down
4 changes: 3 additions & 1 deletion lib/WebpackOptionsApply.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,9 @@ class WebpackOptionsApply extends OptionsApply {
}
if (options.optimization.sideEffects) {
const SideEffectsFlagPlugin = require("./optimize/SideEffectsFlagPlugin");
new SideEffectsFlagPlugin().apply(compiler);
new SideEffectsFlagPlugin(
options.optimization.sideEffects === true
).apply(compiler);
}
if (options.optimization.providedExports) {
const FlagDependencyExportsPlugin = require("./FlagDependencyExportsPlugin");
Expand Down
2 changes: 1 addition & 1 deletion lib/config/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,7 @@ const applyOptimizationDefaults = (
if (development) return "named";
return "natural";
});
D(optimization, "sideEffects", true);
F(optimization, "sideEffects", () => (production ? true : "flag"));
D(optimization, "providedExports", true);
D(optimization, "usedExports", production);
D(optimization, "innerGraph", production);
Expand Down

0 comments on commit 923be31

Please sign in to comment.