Skip to content

Commit

Permalink
allow inner graph shaking for new URL()
Browse files Browse the repository at this point in the history
fixes #11818
  • Loading branch information
sokra committed Jan 19, 2021
1 parent 8918ab1 commit 4ec418c
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 27 deletions.
19 changes: 2 additions & 17 deletions lib/dependencies/HarmonyImportSpecifierDependency.js
Expand Up @@ -6,7 +6,7 @@
"use strict";

const Dependency = require("../Dependency");
const { UsageState } = require("../ExportsInfo");
const { isDependencyUsedByExports } = require("../optimize/InnerGraph");
const makeSerializable = require("../util/makeSerializable");
const propertyAccess = require("../util/propertyAccess");
const HarmonyImportDependency = require("./HarmonyImportDependency");
Expand Down Expand Up @@ -84,7 +84,7 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
*/
getCondition(moduleGraph) {
return (connection, runtime) =>
this.checkUsedByExports(moduleGraph, runtime);
isDependencyUsedByExports(this, this.usedByExports, moduleGraph, runtime);
}

/**
Expand All @@ -95,21 +95,6 @@ class HarmonyImportSpecifierDependency extends HarmonyImportDependency {
return false;
}

checkUsedByExports(moduleGraph, runtime) {
if (this.usedByExports === false) return false;
if (this.usedByExports !== true && this.usedByExports !== undefined) {
const selfModule = moduleGraph.getParentModule(this);
const exportsInfo = moduleGraph.getExportsInfo(selfModule);
let used = false;
for (const exportName of this.usedByExports) {
if (exportsInfo.getUsed(exportName, runtime) !== UsageState.Unused)
used = true;
}
if (!used) return false;
}
return true;
}

/**
* Returns list of exports referenced by this dependency
* @param {ModuleGraph} moduleGraph module graph
Expand Down
48 changes: 45 additions & 3 deletions lib/dependencies/URLDependency.js
Expand Up @@ -6,6 +6,7 @@
"use strict";

const RuntimeGlobals = require("../RuntimeGlobals");
const { isDependencyUsedByExports } = require("../optimize/InnerGraph");
const makeSerializable = require("../util/makeSerializable");
const ModuleDependency = require("./ModuleDependency");

Expand All @@ -15,16 +16,23 @@ const ModuleDependency = require("./ModuleDependency");
/** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */
/** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */
/** @typedef {import("../util/Hash")} Hash */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */

class URLDependency extends ModuleDependency {
/**
* @param {string} request request
* @param {[number, number]} range range
* @param {[number, number]} range range of the arguments of new URL( |> ... <| )
* @param {[number, number]} outerRange range of the full |> new URL(...) <|
*/
constructor(request, range) {
constructor(request, range, outerRange) {
super(request);
this.range = range;
this.outerRange = outerRange;
/** @type {Set<string> | boolean} */
this.usedByExports = undefined;
}

get type() {
Expand All @@ -34,6 +42,29 @@ class URLDependency extends ModuleDependency {
get category() {
return "url";
}

/**
* @param {ModuleGraph} moduleGraph module graph
* @returns {function(ModuleGraphConnection, RuntimeSpec): ConnectionState} function to determine if the connection is active
*/
getCondition(moduleGraph) {
return (connection, runtime) =>
isDependencyUsedByExports(this, this.usedByExports, moduleGraph, runtime);
}

serialize(context) {
const { write } = context;
write(this.outerRange);
write(this.usedByExports);
super.serialize(context);
}

deserialize(context) {
const { read } = context;
this.outerRange = read();
this.usedByExports = read();
super.deserialize(context);
}
}

URLDependency.Template = class URLDependencyTemplate extends (
Expand All @@ -50,9 +81,20 @@ URLDependency.Template = class URLDependencyTemplate extends (
chunkGraph,
moduleGraph,
runtimeRequirements,
runtimeTemplate
runtimeTemplate,
runtime
} = templateContext;
const dep = /** @type {URLDependency} */ (dependency);
const connection = moduleGraph.getConnection(dep);
// Skip rendering depending when dependency is conditional
if (connection && !connection.isTargetActive(runtime)) {
source.replace(
dep.outerRange[0],
dep.outerRange[1] - 1,
"/* unused asset import */ undefined"
);
return;
}

runtimeRequirements.add(RuntimeGlobals.baseURI);
runtimeRequirements.add(RuntimeGlobals.require);
Expand Down
40 changes: 33 additions & 7 deletions lib/dependencies/URLPlugin.js
Expand Up @@ -6,6 +6,7 @@
"use strict";

const { approve } = require("../javascript/JavascriptParserHelpers");
const InnerGraph = require("../optimize/InnerGraph");
const URLDependency = require("./URLDependency");

/** @typedef {import("estree").NewExpression} NewExpressionNode */
Expand All @@ -32,10 +33,12 @@ class URLPlugin {
*/
const parserCallback = (parser, parserOptions) => {
if (parserOptions.url === false) return;
parser.hooks.canRename.for("URL").tap("URLPlugin", approve);
parser.hooks.new.for("URL").tap("URLPlugin", _expr => {
const expr = /** @type {NewExpressionNode} */ (_expr);

/**
* @param {NewExpressionNode} expr expression
* @returns {undefined | string} request
*/
const getUrlRequest = expr => {
if (expr.arguments.length !== 2) return;

const [arg1, arg2] = expr.arguments;
Expand All @@ -59,16 +62,39 @@ class URLPlugin {

const request = parser.evaluateExpression(arg1).asString();

return request;
};

parser.hooks.canRename.for("URL").tap("URLPlugin", approve);
parser.hooks.new.for("URL").tap("URLPlugin", _expr => {
const expr = /** @type {NewExpressionNode} */ (_expr);

const request = getUrlRequest(expr);

if (!request) return;

const dep = new URLDependency(request, [
arg1.range[0],
arg2.range[1]
]);
const [arg1, arg2] = expr.arguments;
const dep = new URLDependency(
request,
[arg1.range[0], arg2.range[1]],
expr.range
);
dep.loc = expr.loc;
parser.state.module.addDependency(dep);
InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
return true;
});
parser.hooks.isPure.for("NewExpression").tap("URLPlugin", _expr => {
const expr = /** @type {NewExpressionNode} */ (_expr);
const { callee } = expr;
if (callee.type !== "Identifier") return;
const calleeInfo = parser.getFreeInfoFromVariable(callee.name);
if (!calleeInfo || calleeInfo.name !== "URL") return;

const request = getUrlRequest(expr);

if (request) return true;
});
};

normalModuleFactory.hooks.parser
Expand Down
32 changes: 32 additions & 0 deletions lib/optimize/InnerGraph.js
Expand Up @@ -5,9 +5,14 @@

"use strict";

const { UsageState } = require("../ExportsInfo");

/** @typedef {import("estree").Node} AnyNode */
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../ModuleGraph")} ModuleGraph */
/** @typedef {import("../Parser").ParserState} ParserState */
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */

/** @typedef {Map<TopLevelSymbol, Set<string | TopLevelSymbol> | true>} InnerGraph */
/** @typedef {function(boolean | Set<string> | undefined): void} UsageCallback */
Expand Down Expand Up @@ -254,6 +259,33 @@ exports.tagTopLevelSymbol = (parser, name) => {
return fn;
};

/**
* @param {Dependency} dependency the dependency
* @param {Set<string> | boolean} usedByExports usedByExports info
* @param {ModuleGraph} moduleGraph moduleGraph
* @param {RuntimeSpec} runtime runtime
* @returns {boolean} false, when unused. Otherwise true
*/
exports.isDependencyUsedByExports = (
dependency,
usedByExports,
moduleGraph,
runtime
) => {
if (usedByExports === false) return false;
if (usedByExports !== true && usedByExports !== undefined) {
const selfModule = moduleGraph.getParentModule(dependency);
const exportsInfo = moduleGraph.getExportsInfo(selfModule);
let used = false;
for (const exportName of usedByExports) {
if (exportsInfo.getUsed(exportName, runtime) !== UsageState.Unused)
used = true;
}
if (!used) return false;
}
return true;
};

class TopLevelSymbol {
/**
* @param {string} name name of the variable
Expand Down
Binary file added test/configCases/side-effects/url/file.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions test/configCases/side-effects/url/index.js
@@ -0,0 +1,31 @@
import { used } from "./module";

it("should not include unused assets", () => {
expect(used.href).toMatch(/png/);
expect(__STATS__.modules.find(m => m.name.includes("file.png?used"))).toEqual(
expect.objectContaining({
orphan: false
})
);
expect(
__STATS__.modules.find(m => m.name.includes("file.png?default"))
).toEqual(
expect.objectContaining({
orphan: true
})
);
expect(
__STATS__.modules.find(m => m.name.includes("file.png?named"))
).toEqual(
expect.objectContaining({
orphan: true
})
);
expect(
__STATS__.modules.find(m => m.name.includes("file.png?indirect"))
).toEqual(
expect.objectContaining({
orphan: true
})
);
});
8 changes: 8 additions & 0 deletions test/configCases/side-effects/url/module.js
@@ -0,0 +1,8 @@
export default new URL("file.png?default", import.meta.url);
export const named = new URL("file.png?named", import.meta.url);
export const indirect = fn;
export const used = new URL("file.png?used", import.meta.url);

function fn() {
return new URL("file.png?indirect", import.meta.url);
}
7 changes: 7 additions & 0 deletions test/configCases/side-effects/url/webpack.config.js
@@ -0,0 +1,7 @@
/** @type {import("../../../../").Configuration} */
module.exports = {
optimization: {
sideEffects: true,
innerGraph: true
}
};

0 comments on commit 4ec418c

Please sign in to comment.