为什么 Webpack 5 包含我未使用的 TypeScript 枚举导出,即使启用了 Tree Shaking?
Why does Webpack 5 include my unused TypeScript enum exports, even when Tree Shaking is enabled?
我的 TypeScript 枚举是这样定义的,as in this file:
export enum HueColors {
"red" = "hsl(0, 100%, 50%)",
"orange" = "hsl(30, 100%, 50%)",
// ...
"pink" = "hsl(330, 100%, 50%)",
}
export enum RGBExtended { /* ... */ }
export enum WebSafe { /* ... */ }
Setup/Config
// package.json
{
...
"main": "./index.js",
"types": "./index.d.ts",
"files": [
"**/*.{js,ts, map}"
],
"sideEffects": false,
"scripts": {
...
"build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config config/webpack.config.js",
...
},
"babel": {
"extends": "./config/.babelrc.json"
},
...
"devDependencies": {
"@babel/core": "^7.14.8",
"@babel/preset-env": "^7.14.8",
"@types/jest": "^26.0.24",
"@types/node": "^16.4.0",
"@typescript-eslint/eslint-plugin": "^4.28.4",
"@typescript-eslint/parser": "^4.28.4",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^7.0.3",
"eslint": "^7.31.0",
"eslint-plugin-jest": "^24.4.0",
"jest": "^27.0.6",
"prettier": "^2.3.2",
"terser-webpack-plugin": "^5.1.4",
"ts-jest": "^27.0.4",
"ts-loader": "^9.2.4",
"ts-node": "^10.1.0",
"typedoc": "^0.21.4",
"typescript": "^4.3.5",
"webpack": "^5.46.0",
"webpack-cli": "^4.7.2"
}
}
// config/.babelrc.json
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
},
"modules": false
}
]
]
}
// config/tsconfig.json
{
"compilerOptions": {
"target": "ES6",
"module": "ES6",
"lib": ["DOM", "DOM.Iterable", "ES2017"],
"moduleResolution": "node",
"outDir": "../dist",
"noEmit": false,
"declaration": true,
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"removeComments": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true
},
"include": ["../src"],
"exclude": ["../node_modules", "../tests", "../coverage", "../src/debug.ts"]
}
// config/webpack.config.js
/* eslint-disable @typescript-eslint/no-var-requires */
const CopyPlugin = require("copy-webpack-plugin");
const path = require("path");
const basePath = path.resolve(__dirname, "../");
module.exports = {
entry: path.join(basePath, "src", "index.ts"),
mode: process.env.NODE_ENV,
devtool: process.env.NODE_ENV === "production" ? "source-map" : false,
module: {
rules: [
{
test: /\.ts$/,
loader: "ts-loader",
options: {
configFile: path.join(__dirname, "tsconfig.json")
},
exclude: /node_modules/
}
]
},
plugins: [
new CopyPlugin({
patterns: [
... // not important for question
]
})
],
optimization: {
minimize: process.env.NODE_ENV === "production",
minimizer: [
(compiler) => {
const TerserPlugin = require("terser-webpack-plugin");
new TerserPlugin({
terserOptions: {
ecma: 5,
mangle: true,
module: false
}
}).apply(compiler);
}
],
usedExports: true,
sideEffects: true,
innerGraph: true
},
stats: {
usedExports: true,
providedExports: true,
env: true
},
resolve: {
extensions: [".ts"]
},
output: {
filename: "index.js",
path: path.join(basePath, "dist"),
library: "colormaster",
libraryTarget: "umd",
globalObject: "this",
clean: true
}
};
开发构建输出
我在控制台中看到以下内容:
...
./src/enums/colors.ts 17.6 KiB [built] [code generated]
[exports: HueColors, RGBExtended, WebSafe]
[only some exports used: HueColors] // ← indicates that tree shaking should occur in production build
webpack 5.46.0 compiled successfully in 2368 ms
我在生成的 dist 文件夹输出中看到以下内容:
// dist/index.js → mode === development
/***/ "./src/enums/colors.ts":
/*!*****************************!*\
!*** ./src/enums/colors.ts ***!
\*****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "HueColors": () => (/* binding */ HueColors)
/* harmony export */ });
/* unused harmony exports RGBExtended, WebSafe */ // ← indicates that tree shaking should occur in production
var HueColors;
(function (HueColors) {
HueColors["red"] = "hsl(0, 100%, 50%)";
...
HueColors["pink"] = "hsl(330, 100%, 50%)";
})(HueColors || (HueColors = {}));
var RGBExtended;
(function (RGBExtended) {
RGBExtended["maroon"] = "rgb(128,0,0)";
...
RGBExtended["white"] = "rgb(255,255,255)";
})(RGBExtended || (RGBExtended = {}));
var WebSafe;
(function (WebSafe) {
WebSafe["#000000"] = "rgb(0,0,0)";
...
WebSafe["#FFFFFF"] = "rgb(255,255,255)";
})(WebSafe || (WebSafe = {}));
生产构建输出
但是,在生产构建输出中,我看到以下内容:
这显然还包括未使用的导出。
如何避免这个问题?
解决方案
感谢@Jeff Bowman 的广泛回应,我们能够推断出根本原因是 TypeScript 将 enum
编译成 IIFE。
只需将 enum
变量替换为 const
(记录实用程序)即可解决问题,并且在生产包中可以看到 Tree Shaking。
这是因为 Terser 无法推断出 colors.ts
枚举 中的副作用,因此 Terser 保留了所有三个定义,即使它只导出一个其中
如果这些不是转译的 TypeScript 枚举,我建议简化声明,最好通过标记每个函数 /*#__PURE__*/
并使其成为 return 的预期值。但是,由于它们 是 TypeScript 枚举,您可能需要将它们转换为 object literals as const
,对于 Terser 来说当然更容易推理并且可能足以满足您的需求。
如果我没看错你的输出,你试图删除的数组在开发和运行时构建中都存在;你用“...”省略了它们,但它们就在那里。
根据您的 package.json
,您正在使用 Webpack tree-shaking feature set 的 sideEffects
和 usedExports
。 sideEffects
正确断言除了导出之外您没有更改任何内容,因此如果您的项目消耗了 none 的导出,Webpack 可以安全地跳过整个模块。但是,usedExports
可能没有您希望的那么聪明:
usedExports
relies on terser to detect side effects in statements. It is a difficult task in JavaScript and not as effective as straightforward sideEffects
flag. It also can't skip subtree/dependencies since the spec says that side effects need to be evaluated.
对于开发和生产来说,Webpack 似乎足够聪明,可以检测到你的 HueColors 是你消费的唯一导出,但 Terser 不够聪明,无法确定每个自初始化的 IIFE 没有副作用会影响到其他人。从技术上讲,作为一个人,我也无法对此进行推理:即使您的函数没有,其他一些代码也可能以奇怪的方式更改了对象或数组原型在您的 IIFE 中使用内联赋值或修改封闭范围的同名阴影变量。
使用 in-browser copy of terser 我已经能够重现您的问题。
首先,切换到 const 对象文字将完全有效:
const foo = {foo: "foo"};
const bar = {bar: "bar"};
const baz = {baz: "baz"};
window.alert(foo);
// output: window.alert({foo:"foo"})
// correctly minifed
相同的定义,在您的格式中,表现出您试图避免的行为:
var foo;
(function(x) {
x.foo = "foo";
})(foo || (foo = {}));
var bar;
(function(x) {
x.bar = "bar";
})(bar || (bar = {}));
var baz;
(function(x) {
x.baz = "baz";
})(baz || (baz = {}));
window.alert(foo);
// output: o,n,a;(o||(o={})).foo="foo",function(o){o.bar="bar"}(n||(n={})),function(o){o.baz="baz"}(a||(a={})),window.alert(o)
// incorrectly minified; foo, bar, and baz all survive
仅仅避免内联定义是不够的,尽管这是一个好的开始:
var foo = {};
(function(x) {
x.foo = "foo";
})(foo);
var bar = {};
(function(x) {
x.bar = "bar";
})(bar);
var baz = {};
(function(x) {
x.baz = "baz";
})(baz);
window.alert(foo);
// output: o={};o.foo="foo";!function(o){o.bar="bar"}({});!function(o){o.baz="baz"}({}),window.alert(o)
// incorrectly minified: definitions remain, but
// ! shows how terser is just looking for side effects
就足够了,如果你让每个函数 return 一个值 并且 你用 [=15= 标记函数] 与 webpack documentation and terser documentation 中一样。这对您的枚举没有帮助,但确实表明了如何调整输出以满足 Terser。
var foo = /*#__PURE__*/ (function() {
var x = {};
x.foo = "foo";
return x;
})();
var bar = /*#__PURE__*/ (function() {
var x = {};
x.bar = "bar";
return x;
})();
var baz = /*#__PURE__*/ (function() {
var x = {};
x.baz = "baz";
return x;
})();
window.alert(foo);
// output: let o=function(){var o={foo:"foo"};return o}();window.alert(o)
// correctly minified, though longer than the literal example
我的 TypeScript 枚举是这样定义的,as in this file:
export enum HueColors {
"red" = "hsl(0, 100%, 50%)",
"orange" = "hsl(30, 100%, 50%)",
// ...
"pink" = "hsl(330, 100%, 50%)",
}
export enum RGBExtended { /* ... */ }
export enum WebSafe { /* ... */ }
Setup/Config
// package.json
{
...
"main": "./index.js",
"types": "./index.d.ts",
"files": [
"**/*.{js,ts, map}"
],
"sideEffects": false,
"scripts": {
...
"build:dev": "cross-env NODE_ENV=development webpack --config config/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config config/webpack.config.js",
...
},
"babel": {
"extends": "./config/.babelrc.json"
},
...
"devDependencies": {
"@babel/core": "^7.14.8",
"@babel/preset-env": "^7.14.8",
"@types/jest": "^26.0.24",
"@types/node": "^16.4.0",
"@typescript-eslint/eslint-plugin": "^4.28.4",
"@typescript-eslint/parser": "^4.28.4",
"copy-webpack-plugin": "^9.0.1",
"cross-env": "^7.0.3",
"eslint": "^7.31.0",
"eslint-plugin-jest": "^24.4.0",
"jest": "^27.0.6",
"prettier": "^2.3.2",
"terser-webpack-plugin": "^5.1.4",
"ts-jest": "^27.0.4",
"ts-loader": "^9.2.4",
"ts-node": "^10.1.0",
"typedoc": "^0.21.4",
"typescript": "^4.3.5",
"webpack": "^5.46.0",
"webpack-cli": "^4.7.2"
}
}
// config/.babelrc.json
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
},
"modules": false
}
]
]
}
// config/tsconfig.json
{
"compilerOptions": {
"target": "ES6",
"module": "ES6",
"lib": ["DOM", "DOM.Iterable", "ES2017"],
"moduleResolution": "node",
"outDir": "../dist",
"noEmit": false,
"declaration": true,
"strict": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"removeComments": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true
},
"include": ["../src"],
"exclude": ["../node_modules", "../tests", "../coverage", "../src/debug.ts"]
}
// config/webpack.config.js
/* eslint-disable @typescript-eslint/no-var-requires */
const CopyPlugin = require("copy-webpack-plugin");
const path = require("path");
const basePath = path.resolve(__dirname, "../");
module.exports = {
entry: path.join(basePath, "src", "index.ts"),
mode: process.env.NODE_ENV,
devtool: process.env.NODE_ENV === "production" ? "source-map" : false,
module: {
rules: [
{
test: /\.ts$/,
loader: "ts-loader",
options: {
configFile: path.join(__dirname, "tsconfig.json")
},
exclude: /node_modules/
}
]
},
plugins: [
new CopyPlugin({
patterns: [
... // not important for question
]
})
],
optimization: {
minimize: process.env.NODE_ENV === "production",
minimizer: [
(compiler) => {
const TerserPlugin = require("terser-webpack-plugin");
new TerserPlugin({
terserOptions: {
ecma: 5,
mangle: true,
module: false
}
}).apply(compiler);
}
],
usedExports: true,
sideEffects: true,
innerGraph: true
},
stats: {
usedExports: true,
providedExports: true,
env: true
},
resolve: {
extensions: [".ts"]
},
output: {
filename: "index.js",
path: path.join(basePath, "dist"),
library: "colormaster",
libraryTarget: "umd",
globalObject: "this",
clean: true
}
};
开发构建输出
我在控制台中看到以下内容:
...
./src/enums/colors.ts 17.6 KiB [built] [code generated]
[exports: HueColors, RGBExtended, WebSafe]
[only some exports used: HueColors] // ← indicates that tree shaking should occur in production build
webpack 5.46.0 compiled successfully in 2368 ms
我在生成的 dist 文件夹输出中看到以下内容:
// dist/index.js → mode === development
/***/ "./src/enums/colors.ts":
/*!*****************************!*\
!*** ./src/enums/colors.ts ***!
\*****************************/
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "HueColors": () => (/* binding */ HueColors)
/* harmony export */ });
/* unused harmony exports RGBExtended, WebSafe */ // ← indicates that tree shaking should occur in production
var HueColors;
(function (HueColors) {
HueColors["red"] = "hsl(0, 100%, 50%)";
...
HueColors["pink"] = "hsl(330, 100%, 50%)";
})(HueColors || (HueColors = {}));
var RGBExtended;
(function (RGBExtended) {
RGBExtended["maroon"] = "rgb(128,0,0)";
...
RGBExtended["white"] = "rgb(255,255,255)";
})(RGBExtended || (RGBExtended = {}));
var WebSafe;
(function (WebSafe) {
WebSafe["#000000"] = "rgb(0,0,0)";
...
WebSafe["#FFFFFF"] = "rgb(255,255,255)";
})(WebSafe || (WebSafe = {}));
生产构建输出
但是,在生产构建输出中,我看到以下内容:
这显然还包括未使用的导出。
如何避免这个问题?
解决方案
感谢@Jeff Bowman 的广泛回应,我们能够推断出根本原因是 TypeScript 将 enum
编译成 IIFE。
只需将 enum
变量替换为 const
(记录实用程序)即可解决问题,并且在生产包中可以看到 Tree Shaking。
这是因为 Terser 无法推断出 colors.ts
枚举 中的副作用,因此 Terser 保留了所有三个定义,即使它只导出一个其中
如果这些不是转译的 TypeScript 枚举,我建议简化声明,最好通过标记每个函数 /*#__PURE__*/
并使其成为 return 的预期值。但是,由于它们 是 TypeScript 枚举,您可能需要将它们转换为 object literals as const
,对于 Terser 来说当然更容易推理并且可能足以满足您的需求。
如果我没看错你的输出,你试图删除的数组在开发和运行时构建中都存在;你用“...”省略了它们,但它们就在那里。
根据您的 package.json
,您正在使用 Webpack tree-shaking feature set 的 sideEffects
和 usedExports
。 sideEffects
正确断言除了导出之外您没有更改任何内容,因此如果您的项目消耗了 none 的导出,Webpack 可以安全地跳过整个模块。但是,usedExports
可能没有您希望的那么聪明:
usedExports
relies on terser to detect side effects in statements. It is a difficult task in JavaScript and not as effective as straightforwardsideEffects
flag. It also can't skip subtree/dependencies since the spec says that side effects need to be evaluated.
对于开发和生产来说,Webpack 似乎足够聪明,可以检测到你的 HueColors 是你消费的唯一导出,但 Terser 不够聪明,无法确定每个自初始化的 IIFE 没有副作用会影响到其他人。从技术上讲,作为一个人,我也无法对此进行推理:即使您的函数没有,其他一些代码也可能以奇怪的方式更改了对象或数组原型在您的 IIFE 中使用内联赋值或修改封闭范围的同名阴影变量。
使用 in-browser copy of terser 我已经能够重现您的问题。
首先,切换到 const 对象文字将完全有效:
const foo = {foo: "foo"};
const bar = {bar: "bar"};
const baz = {baz: "baz"};
window.alert(foo);
// output: window.alert({foo:"foo"})
// correctly minifed
相同的定义,在您的格式中,表现出您试图避免的行为:
var foo;
(function(x) {
x.foo = "foo";
})(foo || (foo = {}));
var bar;
(function(x) {
x.bar = "bar";
})(bar || (bar = {}));
var baz;
(function(x) {
x.baz = "baz";
})(baz || (baz = {}));
window.alert(foo);
// output: o,n,a;(o||(o={})).foo="foo",function(o){o.bar="bar"}(n||(n={})),function(o){o.baz="baz"}(a||(a={})),window.alert(o)
// incorrectly minified; foo, bar, and baz all survive
仅仅避免内联定义是不够的,尽管这是一个好的开始:
var foo = {};
(function(x) {
x.foo = "foo";
})(foo);
var bar = {};
(function(x) {
x.bar = "bar";
})(bar);
var baz = {};
(function(x) {
x.baz = "baz";
})(baz);
window.alert(foo);
// output: o={};o.foo="foo";!function(o){o.bar="bar"}({});!function(o){o.baz="baz"}({}),window.alert(o)
// incorrectly minified: definitions remain, but
// ! shows how terser is just looking for side effects
就足够了,如果你让每个函数 return 一个值 并且 你用 [=15= 标记函数] 与 webpack documentation and terser documentation 中一样。这对您的枚举没有帮助,但确实表明了如何调整输出以满足 Terser。
var foo = /*#__PURE__*/ (function() {
var x = {};
x.foo = "foo";
return x;
})();
var bar = /*#__PURE__*/ (function() {
var x = {};
x.bar = "bar";
return x;
})();
var baz = /*#__PURE__*/ (function() {
var x = {};
x.baz = "baz";
return x;
})();
window.alert(foo);
// output: let o=function(){var o={foo:"foo"};return o}();window.alert(o)
// correctly minified, though longer than the literal example