vite-plugin-resource-waste 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +290 -0
- package/dist/index.cjs +929 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +76 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +892 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
default: () => plugin_default,
|
|
34
|
+
resourceWaste: () => resourceWaste
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/plugin.ts
|
|
39
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
40
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
41
|
+
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
42
|
+
|
|
43
|
+
// src/constants.ts
|
|
44
|
+
var DEFAULT_OPTIONS = {
|
|
45
|
+
root: "src",
|
|
46
|
+
reportDir: "resource-waste",
|
|
47
|
+
wasteThresholdKb: 300,
|
|
48
|
+
failOnThreshold: false,
|
|
49
|
+
htmlReport: true,
|
|
50
|
+
scanImportPatterns: true,
|
|
51
|
+
scanUnreachable: true,
|
|
52
|
+
scanCacheStrategy: true,
|
|
53
|
+
scanDuplicateDeps: true,
|
|
54
|
+
include: [],
|
|
55
|
+
exclude: ["**/*.spec.*", "**/*.test.*", "**/__tests__/**", "**/node_modules/**"],
|
|
56
|
+
silent: false
|
|
57
|
+
};
|
|
58
|
+
var SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".vue", ".svelte"];
|
|
59
|
+
var CACHEABLE_EXTENSIONS = [
|
|
60
|
+
".js",
|
|
61
|
+
".css",
|
|
62
|
+
".woff",
|
|
63
|
+
".woff2",
|
|
64
|
+
".ttf",
|
|
65
|
+
".png",
|
|
66
|
+
".jpg",
|
|
67
|
+
".jpeg",
|
|
68
|
+
".gif",
|
|
69
|
+
".webp",
|
|
70
|
+
".svg",
|
|
71
|
+
".ico"
|
|
72
|
+
];
|
|
73
|
+
var HASH_IN_FILENAME = /\.[a-f0-9]{8,}\.(js|css|woff2?|ttf|png|jpe?g|gif|webp|svg|ico)$/i;
|
|
74
|
+
var FULL_IMPORT_PATTERNS = [
|
|
75
|
+
{
|
|
76
|
+
pattern: /import\s+_\s+from\s+['"]lodash['"]/,
|
|
77
|
+
packageName: "lodash",
|
|
78
|
+
suggestion: "\u6539\u4E3A import debounce from 'lodash-es/debounce' \u6216\u6309\u9700\u5BFC\u5165",
|
|
79
|
+
severity: "high",
|
|
80
|
+
wasteKb: 68
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
pattern: /import\s+\*\s+as\s+\w+\s+from\s+['"]lodash['"]/,
|
|
84
|
+
packageName: "lodash",
|
|
85
|
+
suggestion: "\u6539\u4E3A lodash-es \u6309\u9700\u5BFC\u5165",
|
|
86
|
+
severity: "high",
|
|
87
|
+
wasteKb: 68
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
pattern: /import\s+\w+\s+from\s+['"]moment['"]/,
|
|
91
|
+
packageName: "moment",
|
|
92
|
+
suggestion: "\u6539\u7528 dayjs \u6216 date-fns \u6309\u9700\u5BFC\u5165",
|
|
93
|
+
severity: "high",
|
|
94
|
+
wasteKb: 65
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
pattern: /import\s+\*\s+as\s+\w+\s+from\s+['"]antd['"]/,
|
|
98
|
+
packageName: "antd",
|
|
99
|
+
suggestion: "\u6539\u7528 antd \u6309\u9700\u5BFC\u5165\u6216 unplugin-vue-components \u81EA\u52A8\u5BFC\u5165",
|
|
100
|
+
severity: "high",
|
|
101
|
+
wasteKb: 200
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
pattern: /import\s+\*\s+as\s+\w+\s+from\s+['"]@ant-design\/icons['"]/,
|
|
105
|
+
packageName: "@ant-design/icons",
|
|
106
|
+
suggestion: "\u6539\u4E3A @ant-design/icons-vue \u6309\u9700\u5BFC\u5165\u5177\u4F53\u56FE\u6807",
|
|
107
|
+
severity: "medium",
|
|
108
|
+
wasteKb: 80
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
pattern: /import\s+['"][^'"]+\.css['"]\s*;\s*\/\/\s*unused/i,
|
|
112
|
+
packageName: "css-side-effect",
|
|
113
|
+
suggestion: "\u79FB\u9664\u672A\u4F7F\u7528\u7684 CSS \u526F\u4F5C\u7528 import",
|
|
114
|
+
severity: "medium",
|
|
115
|
+
wasteKb: 10
|
|
116
|
+
}
|
|
117
|
+
];
|
|
118
|
+
var CATEGORY_LABELS = {
|
|
119
|
+
"static-unused-js": "\u9759\u6001\u672A\u4F7F\u7528 JS",
|
|
120
|
+
"unreachable-component": "\u4E0D\u53EF\u8FBE\u7EC4\u4EF6/\u9875\u9762",
|
|
121
|
+
"cache-miss": "\u7F13\u5B58\u7B56\u7565\u98CE\u9669",
|
|
122
|
+
"duplicate-dependency": "\u91CD\u590D\u4F9D\u8D56",
|
|
123
|
+
"import-pattern": "\u5BFC\u5165\u6A21\u5F0F\u6D6A\u8D39",
|
|
124
|
+
"prefetch-waste": "\u9884\u52A0\u8F7D\u6D6A\u8D39"
|
|
125
|
+
};
|
|
126
|
+
var SEVERITY_LABELS = {
|
|
127
|
+
high: "\u9AD8",
|
|
128
|
+
medium: "\u4E2D",
|
|
129
|
+
low: "\u4F4E"
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// src/utils/fs.ts
|
|
133
|
+
var import_node_fs = __toESM(require("fs"), 1);
|
|
134
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
135
|
+
function normalizePath(p) {
|
|
136
|
+
return p.split(import_node_path.default.sep).join("/");
|
|
137
|
+
}
|
|
138
|
+
function readTextSafe(filePath) {
|
|
139
|
+
try {
|
|
140
|
+
return import_node_fs.default.readFileSync(filePath, "utf-8");
|
|
141
|
+
} catch {
|
|
142
|
+
return "";
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function fileSizeKb(filePath) {
|
|
146
|
+
try {
|
|
147
|
+
const stat = import_node_fs.default.statSync(filePath);
|
|
148
|
+
return Math.round(stat.size / 1024 * 100) / 100;
|
|
149
|
+
} catch {
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function estimateParseMs(kb) {
|
|
154
|
+
return Math.round(kb * 0.45 * 10) / 10;
|
|
155
|
+
}
|
|
156
|
+
function extractPackageFromModuleId(moduleId) {
|
|
157
|
+
const normalized = normalizePath(moduleId);
|
|
158
|
+
const nodeModulesIdx = normalized.lastIndexOf("node_modules/");
|
|
159
|
+
if (nodeModulesIdx === -1) return null;
|
|
160
|
+
const nodeModulesRoot = normalized.slice(0, nodeModulesIdx + "node_modules/".length);
|
|
161
|
+
const rest = normalized.slice(nodeModulesIdx + "node_modules/".length);
|
|
162
|
+
const parts = rest.split("/");
|
|
163
|
+
if (parts[0]?.startsWith("@") && parts[1]) {
|
|
164
|
+
const name = `${parts[0]}/${parts[1]}`;
|
|
165
|
+
const version = readPackageVersion(import_node_path.default.join(nodeModulesRoot, name));
|
|
166
|
+
return { name, version };
|
|
167
|
+
}
|
|
168
|
+
if (parts[0]) {
|
|
169
|
+
const name = parts[0];
|
|
170
|
+
const version = readPackageVersion(import_node_path.default.join(nodeModulesRoot, name));
|
|
171
|
+
return { name, version };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
function readPackageVersion(packageDir) {
|
|
176
|
+
try {
|
|
177
|
+
const pkgPath = import_node_path.default.join(packageDir, "package.json");
|
|
178
|
+
const pkg = JSON.parse(import_node_fs.default.readFileSync(pkgPath, "utf-8"));
|
|
179
|
+
return pkg.version;
|
|
180
|
+
} catch {
|
|
181
|
+
return void 0;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function resolveFromRoot(projectRoot, maybeRelative) {
|
|
185
|
+
if (import_node_path.default.isAbsolute(maybeRelative)) return maybeRelative;
|
|
186
|
+
return import_node_path.default.resolve(projectRoot, maybeRelative);
|
|
187
|
+
}
|
|
188
|
+
function uniqueId(prefix, index) {
|
|
189
|
+
return `${prefix}-${index}`;
|
|
190
|
+
}
|
|
191
|
+
function ensureDir(dir) {
|
|
192
|
+
import_node_fs.default.mkdirSync(dir, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
function toRelative(projectRoot, absPath) {
|
|
195
|
+
return normalizePath(import_node_path.default.relative(projectRoot, absPath));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/analyzers/cache-analyzer.ts
|
|
199
|
+
function analyzeCacheStrategy(ctx) {
|
|
200
|
+
if (!ctx.options.scanCacheStrategy) return [];
|
|
201
|
+
const issues = [];
|
|
202
|
+
let index = 0;
|
|
203
|
+
for (const file of ctx.bundleFiles) {
|
|
204
|
+
if (file.type !== "asset") continue;
|
|
205
|
+
const ext = getExtension(file.fileName);
|
|
206
|
+
if (!CACHEABLE_EXTENSIONS.includes(ext)) continue;
|
|
207
|
+
const sizeKb = Math.round(file.size / 1024 * 100) / 100;
|
|
208
|
+
const hasHash = HASH_IN_FILENAME.test(file.fileName);
|
|
209
|
+
if (!hasHash) {
|
|
210
|
+
const isFont = /\.(woff2?|ttf|otf)$/i.test(file.fileName);
|
|
211
|
+
const isLargeAsset = sizeKb > 100;
|
|
212
|
+
issues.push({
|
|
213
|
+
id: uniqueId("cache-miss", index++),
|
|
214
|
+
category: "cache-miss",
|
|
215
|
+
severity: isLargeAsset || isFont ? "high" : "medium",
|
|
216
|
+
title: "\u4EA7\u7269\u6587\u4EF6\u540D\u7F3A\u5C11 content hash",
|
|
217
|
+
file: file.fileName,
|
|
218
|
+
detail: `\u9759\u6001\u8D44\u6E90 ${file.fileName} (${sizeKb} KB) \u672A\u5305\u542B content hash\uFF0C\u6D4F\u89C8\u5668\u96BE\u4EE5\u5B89\u5168\u957F\u671F\u7F13\u5B58\uFF0C\u56DE\u8BBF\u7528\u6237\u53EF\u80FD\u91CD\u590D\u4E0B\u8F7D\u3002`,
|
|
219
|
+
suggestion: isFont ? "\u914D\u7F6E vite build.assetsInlineLimit \u4E0E rollup assetFileNames \u542B [hash]\uFF1BCDN \u8BBE\u7F6E immutable \u7F13\u5B58\u3002" : "\u5728 vite.config \u4E2D\u8BBE\u7F6E build.rollupOptions.output.assetFileNames \u5305\u542B [hash] \u5360\u4F4D\u7B26\u3002",
|
|
220
|
+
cost: {
|
|
221
|
+
transferKb: sizeKb,
|
|
222
|
+
parseMsEstimate: isFont ? estimateParseMs(sizeKb * 0.1) : 0,
|
|
223
|
+
description: isLargeAsset ? `\u56DE\u8BBF\u7528\u6237\u53EF\u80FD\u91CD\u590D\u4F20\u8F93 ${sizeKb} KB` : "\u7F13\u5B58\u547D\u4E2D\u7387\u4F4E\u5BFC\u81F4\u91CD\u590D\u9A8C\u8BC1\u6210\u672C"
|
|
224
|
+
},
|
|
225
|
+
metadata: {
|
|
226
|
+
sizeKb,
|
|
227
|
+
extension: ext,
|
|
228
|
+
hasContentHash: false
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const htmlFiles = ctx.bundleFiles.filter((f) => f.fileName.endsWith(".html"));
|
|
234
|
+
for (const html of htmlFiles) {
|
|
235
|
+
issues.push({
|
|
236
|
+
id: uniqueId("cache-info", index++),
|
|
237
|
+
category: "cache-miss",
|
|
238
|
+
severity: "low",
|
|
239
|
+
title: "HTML \u5165\u53E3\u5E94\u4F7F\u7528\u77ED\u7F13\u5B58",
|
|
240
|
+
file: html.fileName,
|
|
241
|
+
detail: `${html.fileName} \u662F HTML \u5165\u53E3\uFF0C\u901A\u5E38\u5E94\u914D\u7F6E\u8F83\u77ED max-age \u6216 no-cache\uFF0C\u4EE5\u786E\u4FDD\u7528\u6237\u83B7\u53D6\u6700\u65B0\u8D44\u6E90\u5F15\u7528\u3002`,
|
|
242
|
+
suggestion: "\u90E8\u7F72\u65F6\u4E3A HTML \u8BBE\u7F6E Cache-Control: no-cache\uFF1BJS/CSS \u7B49\u5E26 hash \u8D44\u6E90\u8BBE\u7F6E\u957F\u671F\u7F13\u5B58\u3002",
|
|
243
|
+
cost: {
|
|
244
|
+
transferKb: Math.round(html.size / 1024 * 100) / 100,
|
|
245
|
+
parseMsEstimate: 0,
|
|
246
|
+
description: "\u914D\u7F6E\u5EFA\u8BAE\uFF0C\u975E\u6D6A\u8D39"
|
|
247
|
+
},
|
|
248
|
+
metadata: { informational: true }
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return issues;
|
|
252
|
+
}
|
|
253
|
+
function getExtension(fileName) {
|
|
254
|
+
const idx = fileName.lastIndexOf(".");
|
|
255
|
+
return idx === -1 ? "" : fileName.slice(idx).toLowerCase();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/analyzers/duplicate-deps-analyzer.ts
|
|
259
|
+
function analyzeDuplicateDependencies(ctx) {
|
|
260
|
+
if (!ctx.options.scanDuplicateDeps) return [];
|
|
261
|
+
const packageVersions = /* @__PURE__ */ new Map();
|
|
262
|
+
let index = 0;
|
|
263
|
+
const issues = [];
|
|
264
|
+
for (const bundleFile of ctx.bundleFiles) {
|
|
265
|
+
if (bundleFile.type !== "chunk") continue;
|
|
266
|
+
for (const moduleId of bundleFile.modules ?? []) {
|
|
267
|
+
const pkg = extractPackageFromModuleId(moduleId);
|
|
268
|
+
if (!pkg?.version) continue;
|
|
269
|
+
if (!packageVersions.has(pkg.name)) {
|
|
270
|
+
packageVersions.set(pkg.name, /* @__PURE__ */ new Map());
|
|
271
|
+
}
|
|
272
|
+
const versions = packageVersions.get(pkg.name);
|
|
273
|
+
versions.set(pkg.version, (versions.get(pkg.version) ?? 0) + 1);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const [name, versions] of packageVersions) {
|
|
277
|
+
if (versions.size <= 1) continue;
|
|
278
|
+
const versionList = [...versions.entries()];
|
|
279
|
+
const totalRefs = versionList.reduce((sum, [, count]) => sum + count, 0);
|
|
280
|
+
const wasteKb = Math.max(5, Math.round(totalRefs * 3));
|
|
281
|
+
issues.push({
|
|
282
|
+
id: uniqueId("duplicate-dep", index++),
|
|
283
|
+
category: "duplicate-dependency",
|
|
284
|
+
severity: "medium",
|
|
285
|
+
title: `\u91CD\u590D\u4F9D\u8D56\u7248\u672C: ${name}`,
|
|
286
|
+
file: name,
|
|
287
|
+
detail: `\u4F9D\u8D56 ${name} \u5728\u4EA7\u7269\u4E2D\u51FA\u73B0\u591A\u4E2A\u7248\u672C: ${versionList.map(([v, c]) => `${v}(${c}\u6B21)`).join(", ")}\u3002`,
|
|
288
|
+
suggestion: `\u4F7F\u7528 package.json overrides / pnpm.overrides \u7EDF\u4E00 ${name} \u7248\u672C\uFF0C\u907F\u514D\u91CD\u590D\u6253\u5305\u4E0E\u7F13\u5B58\u5931\u6548\u3002`,
|
|
289
|
+
cost: {
|
|
290
|
+
transferKb: wasteKb,
|
|
291
|
+
parseMsEstimate: estimateParseMs(wasteKb),
|
|
292
|
+
description: `\u9884\u4F30\u91CD\u590D\u4F20\u8F93\u7EA6 ${wasteKb} KB`
|
|
293
|
+
},
|
|
294
|
+
metadata: {
|
|
295
|
+
packageName: name,
|
|
296
|
+
versions: Object.fromEntries(versionList)
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return issues;
|
|
301
|
+
}
|
|
302
|
+
function analyzeUnusedExports(ctx) {
|
|
303
|
+
const issues = [];
|
|
304
|
+
let index = 0;
|
|
305
|
+
for (const [moduleId, mod] of ctx.parsedModules) {
|
|
306
|
+
if (moduleId.includes("node_modules") || moduleId.startsWith("\0")) continue;
|
|
307
|
+
if (!mod.code) continue;
|
|
308
|
+
const exportMatches = [...mod.code.matchAll(/export\s+(?:const|function|class|type|interface)\s+(\w+)/g)];
|
|
309
|
+
if (exportMatches.length === 0) continue;
|
|
310
|
+
if (exportMatches.length >= 8 && mod.importers.size <= 1) {
|
|
311
|
+
const sizeEstimate = Math.round(mod.code.length / 1024 * 0.3);
|
|
312
|
+
issues.push({
|
|
313
|
+
id: uniqueId("unused-export", index++),
|
|
314
|
+
category: "static-unused-js",
|
|
315
|
+
severity: "low",
|
|
316
|
+
title: "\u7591\u4F3C\u8FC7\u591A\u672A\u4F7F\u7528 export",
|
|
317
|
+
file: moduleId,
|
|
318
|
+
detail: `${moduleId} \u5BFC\u51FA\u4E86 ${exportMatches.length} \u4E2A\u7B26\u53F7\uFF0C\u4F46\u4EC5\u88AB ${mod.importers.size} \u4E2A\u6A21\u5757\u5F15\u7528\uFF0C\u53EF\u80FD\u5B58\u5728\u672A\u4F7F\u7528\u7684 export\u3002`,
|
|
319
|
+
suggestion: "\u4F7F\u7528 Knip \u6216 ESLint \u8FDB\u4E00\u6B65\u786E\u8BA4\uFF0C\u79FB\u9664\u672A\u4F7F\u7528\u7684 export \u4EE5\u6539\u5584 tree-shaking\u3002",
|
|
320
|
+
cost: {
|
|
321
|
+
transferKb: sizeEstimate,
|
|
322
|
+
parseMsEstimate: estimateParseMs(sizeEstimate),
|
|
323
|
+
description: "\u4FDD\u5B88\u4F30\u8BA1\uFF0C\u9700\u7ED3\u5408\u9759\u6001\u5206\u6790\u5DE5\u5177\u786E\u8BA4"
|
|
324
|
+
},
|
|
325
|
+
metadata: {
|
|
326
|
+
exportCount: exportMatches.length,
|
|
327
|
+
importerCount: mod.importers.size
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return issues;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/analyzers/import-pattern-analyzer.ts
|
|
336
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
337
|
+
var import_fast_glob = __toESM(require("fast-glob"), 1);
|
|
338
|
+
async function analyzeImportPatterns(ctx) {
|
|
339
|
+
if (!ctx.options.scanImportPatterns) return [];
|
|
340
|
+
const issues = [];
|
|
341
|
+
let index = 0;
|
|
342
|
+
for (const file of ctx.sourceFiles) {
|
|
343
|
+
const content = readTextSafe(file);
|
|
344
|
+
if (!content) continue;
|
|
345
|
+
const relativeFile = toRelative(ctx.projectRoot, file);
|
|
346
|
+
for (const rule of FULL_IMPORT_PATTERNS) {
|
|
347
|
+
if (!rule.pattern.test(content)) continue;
|
|
348
|
+
const gzipEstimate = Math.round(rule.wasteKb * 0.35);
|
|
349
|
+
issues.push({
|
|
350
|
+
id: uniqueId("import-pattern", index++),
|
|
351
|
+
category: "import-pattern",
|
|
352
|
+
severity: rule.severity,
|
|
353
|
+
title: `\u5168\u91CF/\u4F4E\u6548\u5BFC\u5165: ${rule.packageName}`,
|
|
354
|
+
file: relativeFile,
|
|
355
|
+
detail: `\u6587\u4EF6 ${relativeFile} \u4E2D\u5B58\u5728\u53EF\u80FD\u5BFC\u81F4 tree-shake \u5931\u6548\u7684\u5BFC\u5165\u6A21\u5F0F\uFF08${rule.packageName}\uFF09\u3002`,
|
|
356
|
+
suggestion: rule.suggestion,
|
|
357
|
+
cost: {
|
|
358
|
+
transferKb: gzipEstimate,
|
|
359
|
+
parseMsEstimate: estimateParseMs(gzipEstimate),
|
|
360
|
+
description: `\u9884\u4F30 gzip \u540E\u989D\u5916\u4F20\u8F93\u7EA6 ${gzipEstimate} KB`
|
|
361
|
+
},
|
|
362
|
+
metadata: {
|
|
363
|
+
packageName: rule.packageName,
|
|
364
|
+
pattern: rule.pattern.source
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
const hasDefaultExport = /export\s+default/.test(content);
|
|
369
|
+
const hasNamedExport = /export\s+(const|function|class|type|interface|\{)/.test(content);
|
|
370
|
+
const isIndexBarrel = /export\s+\*\s+from/.test(content);
|
|
371
|
+
if (isIndexBarrel) {
|
|
372
|
+
issues.push({
|
|
373
|
+
id: uniqueId("import-pattern", index++),
|
|
374
|
+
category: "import-pattern",
|
|
375
|
+
severity: "medium",
|
|
376
|
+
title: "Barrel \u6587\u4EF6\u53EF\u80FD\u5BFC\u81F4 tree-shake \u5931\u6548",
|
|
377
|
+
file: relativeFile,
|
|
378
|
+
detail: `${relativeFile} \u4F7F\u7528\u4E86 export * from \u6A21\u5F0F\uFF0C\u5BB9\u6613\u5BFC\u81F4\u672A\u4F7F\u7528\u7684\u6A21\u5757\u88AB\u4E00\u5E76\u6253\u5305\u3002`,
|
|
379
|
+
suggestion: "\u6539\u4E3A\u663E\u5F0F\u5177\u540D\u5BFC\u51FA\uFF0C\u6216\u76F4\u63A5\u4ECE\u6E90\u6587\u4EF6\u6309\u9700\u5BFC\u5165\u3002",
|
|
380
|
+
cost: {
|
|
381
|
+
transferKb: 15,
|
|
382
|
+
parseMsEstimate: estimateParseMs(15),
|
|
383
|
+
description: "Barrel \u6587\u4EF6\u95F4\u63A5\u5F15\u5165\u7684\u5197\u4F59\u4F53\u79EF\u96BE\u4EE5\u7CBE\u786E\u91CF\u5316\uFF0C\u6B64\u4E3A\u4FDD\u5B88\u4F30\u8BA1"
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (!hasDefaultExport && !hasNamedExport && relativeFile.includes("/components/")) {
|
|
388
|
+
if (content.trim().length < 20) {
|
|
389
|
+
issues.push({
|
|
390
|
+
id: uniqueId("static-unused", index++),
|
|
391
|
+
category: "static-unused-js",
|
|
392
|
+
severity: "low",
|
|
393
|
+
title: "\u7591\u4F3C\u7A7A\u7EC4\u4EF6\u6587\u4EF6",
|
|
394
|
+
file: relativeFile,
|
|
395
|
+
detail: `${relativeFile} \u51E0\u4E4E\u6CA1\u6709\u6709\u6548\u5185\u5BB9\uFF0C\u53EF\u80FD\u662F\u9057\u7559\u6587\u4EF6\u3002`,
|
|
396
|
+
suggestion: "\u786E\u8BA4\u662F\u5426\u4ECD\u9700\u8981\uFF0C\u4E0D\u9700\u8981\u5219\u5220\u9664\u3002",
|
|
397
|
+
cost: {
|
|
398
|
+
transferKb: 0,
|
|
399
|
+
parseMsEstimate: 0,
|
|
400
|
+
description: "\u7EF4\u62A4\u6210\u672C\u6D6A\u8D39"
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return issues;
|
|
407
|
+
}
|
|
408
|
+
async function collectSourceFiles(ctx) {
|
|
409
|
+
const rootDir = import_node_path2.default.resolve(ctx.projectRoot, ctx.options.root ?? "src");
|
|
410
|
+
const patterns = [
|
|
411
|
+
...SOURCE_EXTENSIONS.map((ext) => `**/*${ext}`),
|
|
412
|
+
...ctx.options.include ?? []
|
|
413
|
+
];
|
|
414
|
+
const files = await (0, import_fast_glob.default)(patterns, {
|
|
415
|
+
cwd: rootDir,
|
|
416
|
+
absolute: true,
|
|
417
|
+
ignore: ctx.options.exclude,
|
|
418
|
+
onlyFiles: true
|
|
419
|
+
});
|
|
420
|
+
return files.map((f) => normalizePath(f));
|
|
421
|
+
}
|
|
422
|
+
function getReachableModules(ctx) {
|
|
423
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
424
|
+
const queue = [...ctx.entries];
|
|
425
|
+
while (queue.length) {
|
|
426
|
+
const current = queue.shift();
|
|
427
|
+
if (reachable.has(current)) continue;
|
|
428
|
+
reachable.add(current);
|
|
429
|
+
const mod = ctx.parsedModules.get(current);
|
|
430
|
+
if (!mod) continue;
|
|
431
|
+
for (const dep of mod.importedIds) {
|
|
432
|
+
if (!reachable.has(dep)) queue.push(dep);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return reachable;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/analyzers/unreachable-analyzer.ts
|
|
439
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
440
|
+
function analyzeUnreachableFiles(ctx) {
|
|
441
|
+
if (!ctx.options.scanUnreachable) return [];
|
|
442
|
+
const issues = [];
|
|
443
|
+
const reachable = getReachableModules(ctx);
|
|
444
|
+
let index = 0;
|
|
445
|
+
const reachableAbsPaths = /* @__PURE__ */ new Set();
|
|
446
|
+
for (const moduleId of reachable) {
|
|
447
|
+
if (moduleId.startsWith("\0") || moduleId.includes("virtual:")) continue;
|
|
448
|
+
const abs = import_node_path3.default.isAbsolute(moduleId) ? normalizePath(moduleId) : normalizePath(import_node_path3.default.resolve(ctx.projectRoot, moduleId));
|
|
449
|
+
reachableAbsPaths.add(abs);
|
|
450
|
+
}
|
|
451
|
+
for (const sourceFile of ctx.sourceFiles) {
|
|
452
|
+
const normalized = normalizePath(sourceFile);
|
|
453
|
+
const isEntry = ctx.entries.some((e) => normalizePath(import_node_path3.default.resolve(e)) === normalized);
|
|
454
|
+
if (isEntry) continue;
|
|
455
|
+
const isReachable = [...reachableAbsPaths].some(
|
|
456
|
+
(r) => r === normalized || normalized.endsWith(r) || r.endsWith(normalized)
|
|
457
|
+
);
|
|
458
|
+
const mod = ctx.parsedModules.get(normalized) ?? ctx.parsedModules.get(sourceFile);
|
|
459
|
+
const hasImporters = mod && mod.importers.size > 0;
|
|
460
|
+
if (isReachable || hasImporters) continue;
|
|
461
|
+
const sizeKb = fileSizeKb(sourceFile);
|
|
462
|
+
const relativeFile = toRelative(ctx.projectRoot, sourceFile);
|
|
463
|
+
const isPageOrComponent = /pages?|views?|components?|routes?/i.test(relativeFile) && /\.(vue|tsx|jsx)$/.test(relativeFile);
|
|
464
|
+
issues.push({
|
|
465
|
+
id: uniqueId("unreachable", index++),
|
|
466
|
+
category: "unreachable-component",
|
|
467
|
+
severity: isPageOrComponent ? "high" : "medium",
|
|
468
|
+
title: isPageOrComponent ? "\u4E0D\u53EF\u8FBE\u9875\u9762/\u7EC4\u4EF6" : "\u4E0D\u53EF\u8FBE\u6E90\u6587\u4EF6",
|
|
469
|
+
file: relativeFile,
|
|
470
|
+
detail: `${relativeFile} \u672A\u51FA\u73B0\u5728\u4EFB\u4F55\u6784\u5EFA\u5165\u53E3\u7684 module graph \u4E2D\uFF0C\u5C5E\u4E8E\u9759\u6001\u4E0D\u53EF\u8FBE\u4EE3\u7801\u3002`,
|
|
471
|
+
suggestion: isPageOrComponent ? "\u82E5\u4E1A\u52A1\u5DF2\u4E0B\u7EBF\uFF0C\u5220\u9664\u8BE5\u9875\u9762/\u7EC4\u4EF6\u53CA\u5173\u8054\u9759\u6001\u8D44\u6E90\u3002" : "\u786E\u8BA4\u662F\u5426\u4ECD\u88AB\u52A8\u6001\u8DEF\u5F84\u5F15\u7528\uFF1B\u82E5\u65E0\uFF0C\u8BF7\u5220\u9664\u6216\u79FB\u51FA src\u3002",
|
|
472
|
+
cost: {
|
|
473
|
+
transferKb: sizeKb,
|
|
474
|
+
parseMsEstimate: estimateParseMs(sizeKb),
|
|
475
|
+
description: `\u6587\u4EF6\u672C\u4F53 ${sizeKb} KB\uFF1B\u82E5\u88AB glob \u626B\u5165\u53EF\u80FD\u4EA7\u751F\u66F4\u5927 chunk \u6D6A\u8D39`
|
|
476
|
+
},
|
|
477
|
+
metadata: {
|
|
478
|
+
isPageOrComponent
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return issues;
|
|
483
|
+
}
|
|
484
|
+
function analyzeOrphanChunks(ctx) {
|
|
485
|
+
const issues = [];
|
|
486
|
+
let index = 0;
|
|
487
|
+
const entryChunks = ctx.bundleFiles.filter((f) => f.isEntry);
|
|
488
|
+
const allChunkNames = new Set(ctx.bundleFiles.filter((f) => f.type === "chunk").map((f) => f.fileName));
|
|
489
|
+
const reachableChunks = /* @__PURE__ */ new Set();
|
|
490
|
+
const queue = entryChunks.map((c) => c.fileName);
|
|
491
|
+
while (queue.length) {
|
|
492
|
+
const name = queue.shift();
|
|
493
|
+
if (reachableChunks.has(name)) continue;
|
|
494
|
+
reachableChunks.add(name);
|
|
495
|
+
const chunk = ctx.bundleFiles.find((f) => f.fileName === name);
|
|
496
|
+
if (!chunk) continue;
|
|
497
|
+
for (const imp of chunk.imports ?? []) {
|
|
498
|
+
if (allChunkNames.has(imp) && !reachableChunks.has(imp)) queue.push(imp);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
for (const chunk of ctx.bundleFiles) {
|
|
502
|
+
if (chunk.type !== "chunk" || chunk.isEntry) continue;
|
|
503
|
+
if (reachableChunks.has(chunk.fileName)) continue;
|
|
504
|
+
const sizeKb = Math.round(chunk.size / 1024 * 100) / 100;
|
|
505
|
+
const isDynamicOnly = chunk.fileName.includes("legacy") || chunk.fileName.includes("admin");
|
|
506
|
+
issues.push({
|
|
507
|
+
id: uniqueId("orphan-chunk", index++),
|
|
508
|
+
category: "static-unused-js",
|
|
509
|
+
severity: "low",
|
|
510
|
+
title: "\u72EC\u7ACB\u5F02\u6B65 chunk\uFF08\u672A\u88AB\u9759\u6001\u5F15\u7528\uFF09",
|
|
511
|
+
file: chunk.fileName,
|
|
512
|
+
detail: `\u4EA7\u7269 chunk ${chunk.fileName} (${sizeKb} KB) \u4EC5\u901A\u8FC7\u52A8\u6001 import \u52A0\u8F7D\uFF0C\u672C\u8EAB\u4E0D\u662F\u6D6A\u8D39\uFF0C\u4F46\u82E5\u88AB\u5168\u7AD9 prefetch \u5219\u4F1A\u4EA7\u751F\u52A0\u8F7D\u6D6A\u8D39\u3002`,
|
|
513
|
+
suggestion: "\u68C0\u67E5\u662F\u5426\u5BF9\u8BE5 chunk \u914D\u7F6E\u4E86\u4E0D\u5FC5\u8981\u7684 prefetch/preload\u3002",
|
|
514
|
+
cost: {
|
|
515
|
+
transferKb: 0,
|
|
516
|
+
parseMsEstimate: 0,
|
|
517
|
+
description: "\u672C\u8EAB\u4E3A\u6309\u9700\u52A0\u8F7D\uFF1B\u6D6A\u8D39\u53D6\u51B3\u4E8E prefetch \u7B56\u7565"
|
|
518
|
+
},
|
|
519
|
+
metadata: {
|
|
520
|
+
chunkSizeKb: sizeKb,
|
|
521
|
+
dynamicOnly: true,
|
|
522
|
+
suspiciousPrefetch: isDynamicOnly
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return issues;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/analyzers/index.ts
|
|
530
|
+
async function runAllAnalyzers(ctx) {
|
|
531
|
+
const results = await Promise.all([
|
|
532
|
+
analyzeImportPatterns(ctx),
|
|
533
|
+
// 低效 import 模式、barrel 文件等
|
|
534
|
+
Promise.resolve(analyzeUnreachableFiles(ctx)),
|
|
535
|
+
// 静态不可达源文件
|
|
536
|
+
Promise.resolve(analyzeOrphanChunks(ctx)),
|
|
537
|
+
// 仅动态引用的 async chunk
|
|
538
|
+
Promise.resolve(analyzeCacheStrategy(ctx)),
|
|
539
|
+
// 产物 cache 策略
|
|
540
|
+
Promise.resolve(analyzeDuplicateDependencies(ctx)),
|
|
541
|
+
// 重复 npm 依赖版本
|
|
542
|
+
Promise.resolve(analyzeUnusedExports(ctx))
|
|
543
|
+
// 疑似过多 export
|
|
544
|
+
]);
|
|
545
|
+
return dedupeIssues(results.flat());
|
|
546
|
+
}
|
|
547
|
+
function dedupeIssues(issues) {
|
|
548
|
+
const seen = /* @__PURE__ */ new Set();
|
|
549
|
+
return issues.filter((issue) => {
|
|
550
|
+
const key = `${issue.category}:${issue.file}:${issue.title}`;
|
|
551
|
+
if (seen.has(key)) return false;
|
|
552
|
+
seen.add(key);
|
|
553
|
+
return true;
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
function sortIssues(issues) {
|
|
557
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
558
|
+
return [...issues].sort((a, b) => {
|
|
559
|
+
const s = severityOrder[a.severity] - severityOrder[b.severity];
|
|
560
|
+
if (s !== 0) return s;
|
|
561
|
+
return b.cost.transferKb - a.cost.transferKb;
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// src/analyzers/module-graph-analyzer.ts
|
|
566
|
+
function recordModuleParsed(parsedModules, id, code, importedIds, isEntry) {
|
|
567
|
+
const normalizedId = normalizePath(id);
|
|
568
|
+
const existing = parsedModules.get(normalizedId) ?? {
|
|
569
|
+
id: normalizedId,
|
|
570
|
+
importers: /* @__PURE__ */ new Set(),
|
|
571
|
+
importedIds: /* @__PURE__ */ new Set(),
|
|
572
|
+
dynamicImporters: /* @__PURE__ */ new Set(),
|
|
573
|
+
isEntry: false,
|
|
574
|
+
code: void 0
|
|
575
|
+
};
|
|
576
|
+
existing.isEntry = existing.isEntry || isEntry;
|
|
577
|
+
if (code) existing.code = code;
|
|
578
|
+
for (const dep of importedIds) {
|
|
579
|
+
existing.importedIds.add(normalizePath(dep));
|
|
580
|
+
}
|
|
581
|
+
parsedModules.set(normalizedId, existing);
|
|
582
|
+
for (const dep of importedIds) {
|
|
583
|
+
const depId = normalizePath(dep);
|
|
584
|
+
const depMod = parsedModules.get(depId) ?? {
|
|
585
|
+
id: depId,
|
|
586
|
+
importers: /* @__PURE__ */ new Set(),
|
|
587
|
+
importedIds: /* @__PURE__ */ new Set(),
|
|
588
|
+
dynamicImporters: /* @__PURE__ */ new Set(),
|
|
589
|
+
isEntry: false
|
|
590
|
+
};
|
|
591
|
+
depMod.importers.add(normalizedId);
|
|
592
|
+
parsedModules.set(depId, depMod);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function extractBundleFiles(bundle) {
|
|
596
|
+
const files = [];
|
|
597
|
+
for (const [fileName, item] of Object.entries(bundle)) {
|
|
598
|
+
if (item.type === "chunk") {
|
|
599
|
+
const chunk = item;
|
|
600
|
+
files.push({
|
|
601
|
+
fileName,
|
|
602
|
+
type: "chunk",
|
|
603
|
+
size: Buffer.byteLength(chunk.code, "utf-8"),
|
|
604
|
+
modules: Object.keys(chunk.modules ?? {}),
|
|
605
|
+
isEntry: chunk.isEntry ?? false,
|
|
606
|
+
imports: chunk.imports,
|
|
607
|
+
dynamicImports: chunk.dynamicImports
|
|
608
|
+
});
|
|
609
|
+
} else {
|
|
610
|
+
const asset = item;
|
|
611
|
+
const source = asset.source;
|
|
612
|
+
const size = typeof source === "string" ? Buffer.byteLength(source, "utf-8") : source instanceof Uint8Array ? source.byteLength : 0;
|
|
613
|
+
files.push({
|
|
614
|
+
fileName,
|
|
615
|
+
type: "asset",
|
|
616
|
+
size
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return files;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/report/reporter.ts
|
|
624
|
+
function buildReport(issues, meta) {
|
|
625
|
+
const byCategory = emptyCategoryCount();
|
|
626
|
+
const bySeverity = { high: 0, medium: 0, low: 0 };
|
|
627
|
+
let totalWasteTransferKb = 0;
|
|
628
|
+
let totalParseMsEstimate = 0;
|
|
629
|
+
for (const issue of issues) {
|
|
630
|
+
byCategory[issue.category]++;
|
|
631
|
+
bySeverity[issue.severity]++;
|
|
632
|
+
if (issue.metadata?.informational) continue;
|
|
633
|
+
totalWasteTransferKb += issue.cost.transferKb;
|
|
634
|
+
totalParseMsEstimate += issue.cost.parseMsEstimate;
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
638
|
+
projectRoot: meta.projectRoot,
|
|
639
|
+
mode: meta.mode,
|
|
640
|
+
outDir: meta.outDir,
|
|
641
|
+
summary: {
|
|
642
|
+
totalIssues: issues.length,
|
|
643
|
+
totalWasteTransferKb: Math.round(totalWasteTransferKb * 100) / 100,
|
|
644
|
+
totalParseMsEstimate: Math.round(totalParseMsEstimate * 10) / 10,
|
|
645
|
+
byCategory,
|
|
646
|
+
bySeverity
|
|
647
|
+
},
|
|
648
|
+
issues
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
function emptyCategoryCount() {
|
|
652
|
+
return {
|
|
653
|
+
"static-unused-js": 0,
|
|
654
|
+
"unreachable-component": 0,
|
|
655
|
+
"cache-miss": 0,
|
|
656
|
+
"duplicate-dependency": 0,
|
|
657
|
+
"import-pattern": 0,
|
|
658
|
+
"prefetch-waste": 0
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function renderHtmlReport(report) {
|
|
662
|
+
const { summary, issues } = report;
|
|
663
|
+
const issueRows = issues.map(
|
|
664
|
+
(issue) => `
|
|
665
|
+
<tr class="severity-${issue.severity}">
|
|
666
|
+
<td><span class="badge ${issue.severity}">${SEVERITY_LABELS[issue.severity]}</span></td>
|
|
667
|
+
<td>${CATEGORY_LABELS[issue.category]}</td>
|
|
668
|
+
<td><strong>${escapeHtml(issue.title)}</strong><br/><small>${escapeHtml(issue.detail)}</small></td>
|
|
669
|
+
<td><code>${escapeHtml(issue.file ?? "-")}</code></td>
|
|
670
|
+
<td>${issue.cost.transferKb > 0 ? `${issue.cost.transferKb} KB` : "-"}</td>
|
|
671
|
+
<td>${issue.cost.parseMsEstimate > 0 ? `${issue.cost.parseMsEstimate} ms` : "-"}</td>
|
|
672
|
+
<td>${escapeHtml(issue.suggestion)}</td>
|
|
673
|
+
</tr>`
|
|
674
|
+
).join("");
|
|
675
|
+
return `<!DOCTYPE html>
|
|
676
|
+
<html lang="zh-CN">
|
|
677
|
+
<head>
|
|
678
|
+
<meta charset="UTF-8" />
|
|
679
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
680
|
+
<title>Resource Cost Report</title>
|
|
681
|
+
<style>
|
|
682
|
+
:root {
|
|
683
|
+
--bg: #0f172a;
|
|
684
|
+
--card: #1e293b;
|
|
685
|
+
--text: #e2e8f0;
|
|
686
|
+
--muted: #94a3b8;
|
|
687
|
+
--high: #ef4444;
|
|
688
|
+
--medium: #f59e0b;
|
|
689
|
+
--low: #22c55e;
|
|
690
|
+
--accent: #38bdf8;
|
|
691
|
+
}
|
|
692
|
+
* { box-sizing: border-box; }
|
|
693
|
+
body {
|
|
694
|
+
margin: 0;
|
|
695
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
696
|
+
background: var(--bg);
|
|
697
|
+
color: var(--text);
|
|
698
|
+
line-height: 1.5;
|
|
699
|
+
}
|
|
700
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 24px; }
|
|
701
|
+
h1 { margin: 0 0 8px; font-size: 28px; }
|
|
702
|
+
.sub { color: var(--muted); margin-bottom: 24px; }
|
|
703
|
+
.cards {
|
|
704
|
+
display: grid;
|
|
705
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
706
|
+
gap: 16px;
|
|
707
|
+
margin-bottom: 24px;
|
|
708
|
+
}
|
|
709
|
+
.card {
|
|
710
|
+
background: var(--card);
|
|
711
|
+
border-radius: 12px;
|
|
712
|
+
padding: 16px;
|
|
713
|
+
border: 1px solid #334155;
|
|
714
|
+
}
|
|
715
|
+
.card .label { color: var(--muted); font-size: 13px; }
|
|
716
|
+
.card .value { font-size: 24px; font-weight: 700; margin-top: 4px; }
|
|
717
|
+
table {
|
|
718
|
+
width: 100%;
|
|
719
|
+
border-collapse: collapse;
|
|
720
|
+
background: var(--card);
|
|
721
|
+
border-radius: 12px;
|
|
722
|
+
overflow: hidden;
|
|
723
|
+
border: 1px solid #334155;
|
|
724
|
+
}
|
|
725
|
+
th, td {
|
|
726
|
+
padding: 12px;
|
|
727
|
+
text-align: left;
|
|
728
|
+
vertical-align: top;
|
|
729
|
+
border-bottom: 1px solid #334155;
|
|
730
|
+
font-size: 14px;
|
|
731
|
+
}
|
|
732
|
+
th { background: #0b1220; color: var(--muted); font-weight: 600; }
|
|
733
|
+
tr:last-child td { border-bottom: none; }
|
|
734
|
+
code { background: #0b1220; padding: 2px 6px; border-radius: 4px; font-size: 12px; }
|
|
735
|
+
.badge {
|
|
736
|
+
display: inline-block;
|
|
737
|
+
padding: 2px 8px;
|
|
738
|
+
border-radius: 999px;
|
|
739
|
+
font-size: 12px;
|
|
740
|
+
font-weight: 600;
|
|
741
|
+
}
|
|
742
|
+
.badge.high { background: rgba(239,68,68,.2); color: var(--high); }
|
|
743
|
+
.badge.medium { background: rgba(245,158,11,.2); color: var(--medium); }
|
|
744
|
+
.badge.low { background: rgba(34,197,94,.2); color: var(--low); }
|
|
745
|
+
.legend { margin-top: 16px; color: var(--muted); font-size: 13px; }
|
|
746
|
+
</style>
|
|
747
|
+
</head>
|
|
748
|
+
<body>
|
|
749
|
+
<div class="container">
|
|
750
|
+
<h1>\u524D\u7AEF\u8D44\u6E90\u6D6A\u8D39\u5206\u6790\u62A5\u544A</h1>
|
|
751
|
+
<p class="sub">\u751F\u6210\u65F6\u95F4: ${report.generatedAt} \xB7 \u6A21\u5F0F: ${report.mode}</p>
|
|
752
|
+
|
|
753
|
+
<div class="cards">
|
|
754
|
+
<div class="card"><div class="label">\u95EE\u9898\u603B\u6570</div><div class="value">${summary.totalIssues}</div></div>
|
|
755
|
+
<div class="card"><div class="label">\u9884\u4F30\u6D6A\u8D39\u4F20\u8F93</div><div class="value">${summary.totalWasteTransferKb} KB</div></div>
|
|
756
|
+
<div class="card"><div class="label">\u9884\u4F30\u6D6A\u8D39\u89E3\u6790</div><div class="value">${summary.totalParseMsEstimate} ms</div></div>
|
|
757
|
+
<div class="card"><div class="label">\u9AD8\u5371\u95EE\u9898</div><div class="value">${summary.bySeverity.high}</div></div>
|
|
758
|
+
</div>
|
|
759
|
+
|
|
760
|
+
<table>
|
|
761
|
+
<thead>
|
|
762
|
+
<tr>
|
|
763
|
+
<th>\u7EA7\u522B</th>
|
|
764
|
+
<th>\u7C7B\u522B</th>
|
|
765
|
+
<th>\u95EE\u9898</th>
|
|
766
|
+
<th>\u6587\u4EF6</th>
|
|
767
|
+
<th>\u4F20\u8F93\u6D6A\u8D39</th>
|
|
768
|
+
<th>\u89E3\u6790\u6D6A\u8D39</th>
|
|
769
|
+
<th>\u5EFA\u8BAE</th>
|
|
770
|
+
</tr>
|
|
771
|
+
</thead>
|
|
772
|
+
<tbody>
|
|
773
|
+
${issueRows || '<tr><td colspan="7">\u672A\u68C0\u6D4B\u5230\u8D44\u6E90\u6D6A\u8D39\u95EE\u9898 \u{1F389}</td></tr>'}
|
|
774
|
+
</tbody>
|
|
775
|
+
</table>
|
|
776
|
+
|
|
777
|
+
<p class="legend">
|
|
778
|
+
\u8BF4\u660E\uFF1A\u62A5\u544A\u5305\u542B\u6784\u5EFA\u671F\u9759\u6001\u5206\u6790\u7ED3\u679C\u3002\u672A\u4F7F\u7528\u7684 export \u5728 tree-shake \u6709\u6548\u65F6\u4E0D\u4E00\u5B9A\u589E\u5927\u9996\u5305\uFF1B
|
|
779
|
+
\u7F13\u5B58\u7C7B\u95EE\u9898\u5728\u56DE\u8BBF\u7528\u6237\u573A\u666F\u4E0B\u6D6A\u8D39\u66F4\u663E\u8457\u3002
|
|
780
|
+
</p>
|
|
781
|
+
</div>
|
|
782
|
+
</body>
|
|
783
|
+
</html>`;
|
|
784
|
+
}
|
|
785
|
+
function escapeHtml(input) {
|
|
786
|
+
return input.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
787
|
+
}
|
|
788
|
+
function printTerminalSummary(report, thresholdKb) {
|
|
789
|
+
const { summary, issues } = report;
|
|
790
|
+
const top = issues.slice(0, 5);
|
|
791
|
+
console.log("\n[vite-plugin-resource-waste] Resource waste analysis complete\n");
|
|
792
|
+
console.log(` Issues : ${summary.totalIssues}`);
|
|
793
|
+
console.log(` Waste (est.) : ${summary.totalWasteTransferKb} KB transfer / ${summary.totalParseMsEstimate} ms parse`);
|
|
794
|
+
console.log(` Threshold : ${thresholdKb} KB`);
|
|
795
|
+
if (top.length > 0) {
|
|
796
|
+
console.log("\n Top issues:");
|
|
797
|
+
for (const issue of top) {
|
|
798
|
+
console.log(` [${issue.severity.toUpperCase()}] ${issue.title}${issue.file ? ` (${issue.file})` : ""}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
console.log("");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// src/plugin.ts
|
|
805
|
+
function resourceWaste(options = {}) {
|
|
806
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
807
|
+
const state = {};
|
|
808
|
+
let projectRoot = process.cwd();
|
|
809
|
+
let outDir = "dist";
|
|
810
|
+
let mode = "production";
|
|
811
|
+
let entries = [];
|
|
812
|
+
let sourceFiles = [];
|
|
813
|
+
const parsedModules = /* @__PURE__ */ new Map();
|
|
814
|
+
const createContext = (bundleFiles = []) => ({
|
|
815
|
+
root: opts.root ?? "src",
|
|
816
|
+
projectRoot,
|
|
817
|
+
outDir,
|
|
818
|
+
mode,
|
|
819
|
+
entries,
|
|
820
|
+
sourceFiles,
|
|
821
|
+
parsedModules,
|
|
822
|
+
bundleFiles,
|
|
823
|
+
options: opts
|
|
824
|
+
});
|
|
825
|
+
async function writeReport() {
|
|
826
|
+
if (!state.bundle) return;
|
|
827
|
+
const bundleFiles = extractBundleFiles(state.bundle);
|
|
828
|
+
const ctx = createContext(bundleFiles);
|
|
829
|
+
const issues = sortIssues(await runAllAnalyzers(ctx));
|
|
830
|
+
const report = buildReport(issues, { projectRoot, mode, outDir });
|
|
831
|
+
const reportBaseDir = import_node_path4.default.resolve(projectRoot, outDir, opts.reportDir ?? "resource-waste");
|
|
832
|
+
ensureDir(reportBaseDir);
|
|
833
|
+
const jsonPath = import_node_path4.default.join(reportBaseDir, "report.json");
|
|
834
|
+
import_node_fs2.default.writeFileSync(jsonPath, JSON.stringify(report, null, 2), "utf-8");
|
|
835
|
+
if (opts.htmlReport) {
|
|
836
|
+
import_node_fs2.default.writeFileSync(import_node_path4.default.join(reportBaseDir, "report.html"), renderHtmlReport(report), "utf-8");
|
|
837
|
+
}
|
|
838
|
+
if (!opts.silent) {
|
|
839
|
+
printTerminalSummary(report, opts.wasteThresholdKb ?? 300);
|
|
840
|
+
console.log(` Report JSON : ${normalizePath(import_node_path4.default.relative(projectRoot, jsonPath))}`);
|
|
841
|
+
if (opts.htmlReport) {
|
|
842
|
+
console.log(
|
|
843
|
+
` Report HTML : ${normalizePath(import_node_path4.default.relative(projectRoot, import_node_path4.default.join(reportBaseDir, "report.html")))}`
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (opts.failOnThreshold && report.summary.totalWasteTransferKb > (opts.wasteThresholdKb ?? 300)) {
|
|
848
|
+
throw new Error(
|
|
849
|
+
`[vite-plugin-resource-waste] Estimated waste ${report.summary.totalWasteTransferKb} KB exceeds threshold ${opts.wasteThresholdKb} KB`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
name: "vite-plugin-resource-waste",
|
|
855
|
+
enforce: "post",
|
|
856
|
+
// 在其他插件之后执行,确保 module graph 和 bundle 已完整
|
|
857
|
+
/** Vite 配置解析完成后,收集构建入口等信息 */
|
|
858
|
+
configResolved(config) {
|
|
859
|
+
projectRoot = config.root;
|
|
860
|
+
outDir = config.build.outDir;
|
|
861
|
+
mode = config.mode;
|
|
862
|
+
const rollupInput = config.build.rollupOptions.input;
|
|
863
|
+
if (typeof rollupInput === "string") {
|
|
864
|
+
entries = [normalizePath(resolveFromRoot(projectRoot, rollupInput))];
|
|
865
|
+
} else if (Array.isArray(rollupInput)) {
|
|
866
|
+
entries = rollupInput.map((e) => normalizePath(resolveFromRoot(projectRoot, e)));
|
|
867
|
+
} else if (rollupInput && typeof rollupInput === "object") {
|
|
868
|
+
entries = Object.values(rollupInput).map((e) => normalizePath(resolveFromRoot(projectRoot, e)));
|
|
869
|
+
} else {
|
|
870
|
+
entries = [normalizePath(resolveFromRoot(projectRoot, "index.html"))];
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
/** 构建开始时 glob 扫描 src 下全部源文件 */
|
|
874
|
+
async buildStart() {
|
|
875
|
+
sourceFiles = await collectSourceFiles(createContext());
|
|
876
|
+
},
|
|
877
|
+
/**
|
|
878
|
+
* 每个模块被 Rollup 解析后触发
|
|
879
|
+
* 用于构建 import 依赖图(谁引用了谁)
|
|
880
|
+
*/
|
|
881
|
+
moduleParsed(moduleInfo) {
|
|
882
|
+
if (!moduleInfo.id) return;
|
|
883
|
+
recordModuleParsed(
|
|
884
|
+
parsedModules,
|
|
885
|
+
moduleInfo.id,
|
|
886
|
+
moduleInfo.code ?? null,
|
|
887
|
+
[...moduleInfo.importedIds, ...moduleInfo.dynamicallyImportedIds ?? []],
|
|
888
|
+
moduleInfo.isEntry
|
|
889
|
+
);
|
|
890
|
+
},
|
|
891
|
+
/** 构建产出阶段:缓存 bundle 对象,closeBundle 时再分析 */
|
|
892
|
+
generateBundle(_options, bundle) {
|
|
893
|
+
state.bundle = bundle;
|
|
894
|
+
},
|
|
895
|
+
/** 构建结束:执行分析并写报告 */
|
|
896
|
+
async closeBundle() {
|
|
897
|
+
await writeReport();
|
|
898
|
+
},
|
|
899
|
+
/**
|
|
900
|
+
* dev server 启动后:做轻量静态分析(无 bundle 数据)
|
|
901
|
+
* 报告写入 node_modules/.cache/resource-waste/dev-report.json
|
|
902
|
+
*/
|
|
903
|
+
configureServer(server) {
|
|
904
|
+
server.httpServer?.once("listening", async () => {
|
|
905
|
+
if (mode === "production") return;
|
|
906
|
+
sourceFiles = await collectSourceFiles(createContext());
|
|
907
|
+
const issues = sortIssues(await runAllAnalyzers(createContext()));
|
|
908
|
+
if (!opts.silent && issues.length > 0) {
|
|
909
|
+
console.log(
|
|
910
|
+
import_picocolors.default.cyan("\n[vite-plugin-resource-waste]") + import_picocolors.default.yellow(
|
|
911
|
+
` Dev mode: found ${issues.length} potential resource waste issue(s). Run "vite build" for full report.
|
|
912
|
+
`
|
|
913
|
+
)
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
const cacheDir = import_node_path4.default.resolve(projectRoot, "node_modules/.cache/resource-waste");
|
|
917
|
+
ensureDir(cacheDir);
|
|
918
|
+
const report = buildReport(issues, { projectRoot, mode, outDir });
|
|
919
|
+
import_node_fs2.default.writeFileSync(import_node_path4.default.join(cacheDir, "dev-report.json"), JSON.stringify(report, null, 2), "utf-8");
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
var plugin_default = resourceWaste;
|
|
925
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
926
|
+
0 && (module.exports = {
|
|
927
|
+
resourceWaste
|
|
928
|
+
});
|
|
929
|
+
//# sourceMappingURL=index.cjs.map
|