weapp-vite 6.16.1 → 6.16.2
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 +17 -3
- package/dist/auto-routes.mjs +1 -1
- package/dist/cli.mjs +961 -52
- package/dist/{config-BKXx-hW-.d.mts → config-BiVvWW9d.d.mts} +60 -1
- package/dist/config.d.mts +1 -1
- package/dist/{createContext-C-05IQc1.mjs → createContext-D69xinBq.mjs} +298 -68
- package/dist/docs/README.md +17 -3
- package/dist/docs/ai-workflows.md +19 -0
- package/dist/docs/mcp.md +23 -5
- package/dist/docs/weapp-config.md +28 -0
- package/dist/file-C0XHk9ks.mjs +2 -0
- package/dist/{file-BC_RNbAI.mjs → file-Dnwa6tm_.mjs} +1 -1
- package/dist/getInstance-GyYMx1dz.mjs +2 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/json.d.mts +1 -1
- package/dist/mcp.d.mts +1 -1
- package/dist/types.d.mts +2 -2
- package/package.json +4 -4
- package/dist/file-djYgQoCM.mjs +0 -2
- package/dist/getInstance-BzppVHkY.mjs +0 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { C as isPathInside, S as shouldPassPlatformArgToIdeOpen, _ as createCjsConfigLoadError, b as normalizeMiniPlatform, c as createSharedBuildConfig, f as resolveWeappConfigFile, g as parseCommentJson, h as loadViteConfigFile, l as SHARED_CHUNK_VIRTUAL_PREFIX, m as getProjectConfigFileName, n as syncProjectSupportFiles, p as checkRuntime, r as syncManagedTsconfigBootstrapFiles, s as formatBytes, t as createCompilerContext, u as resolveHmrProfileJsonPath, v as DEFAULT_MP_PLATFORM, x as resolveMiniPlatform, y as getDefaultIdeProjectRoot } from "./createContext-
|
|
1
|
+
import { C as isPathInside, S as shouldPassPlatformArgToIdeOpen, _ as createCjsConfigLoadError, b as normalizeMiniPlatform, c as createSharedBuildConfig, f as resolveWeappConfigFile, g as parseCommentJson, h as loadViteConfigFile, l as SHARED_CHUNK_VIRTUAL_PREFIX, m as getProjectConfigFileName, n as syncProjectSupportFiles, p as checkRuntime, r as syncManagedTsconfigBootstrapFiles, s as formatBytes, t as createCompilerContext, u as resolveHmrProfileJsonPath, v as DEFAULT_MP_PLATFORM, x as resolveMiniPlatform, y as getDefaultIdeProjectRoot } from "./createContext-D69xinBq.mjs";
|
|
2
2
|
import { r as logger_default, t as colors } from "./logger-CgxdNjvb.mjs";
|
|
3
|
-
import { h as VERSION } from "./file-
|
|
3
|
+
import { h as VERSION } from "./file-Dnwa6tm_.mjs";
|
|
4
4
|
import { a as resolveWeappMcpConfig, o as startWeappViteMcpServer } from "./mcp-BzcrPiku.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import path, { posix } from "pathe";
|
|
@@ -14,6 +14,7 @@ import os from "node:os";
|
|
|
14
14
|
import { execFile } from "node:child_process";
|
|
15
15
|
import { Buffer } from "node:buffer";
|
|
16
16
|
import { cac } from "cac";
|
|
17
|
+
import { brotliCompressSync, gzipSync } from "node:zlib";
|
|
17
18
|
import { resolveCommand } from "package-manager-detector/commands";
|
|
18
19
|
import { RETRY_CANCEL_KEYS, RETRY_CONFIRM_KEYS, bootstrapWechatDevtoolsSettings, buildWechatIdeNpm, clearWechatIdeCache, clearWechatIdeCacheByAutomator, closeSharedMiniProgram, closeWechatIdeProject, compileWechatIdeByAutomator, connectOpenedAutomator, createSharedInputSession, dispatchWechatCliCommand, formatAutomatorLoginError, getConfig, getWechatIdeTestAccounts, getWechatIdeTicket, getWechatIdeToolInfo, isAutomatorLoginError, isWeappIdeTopLevelCommand, isWechatIdeLoginRequiredError, launchAutomator, openWechatIdeProjectByHttp, parse, promptRetryKeypress, promptWechatIdeLoginRetry, quitWechatIde, refreshWechatIdeTicket, resetWechatIdeFileUtilsByHttp, runRetryableCommand, runWechatIdeEngineBuild, runWithSuspendedSharedInput, setWechatIdeTicket, startForwardConsole, takeScreenshot } from "weapp-ide-cli";
|
|
19
20
|
import { promisify } from "node:util";
|
|
@@ -122,6 +123,223 @@ async function analyzeHmrProfile(options) {
|
|
|
122
123
|
};
|
|
123
124
|
}
|
|
124
125
|
//#endregion
|
|
126
|
+
//#region src/analyze/components/suggestions.ts
|
|
127
|
+
function createComponentSuggestion(usage) {
|
|
128
|
+
if (usage.componentPackage !== "__main__" || usage.crossPackageUsageCount === 0) return;
|
|
129
|
+
if (usage.crossPackageUsageCount === usage.placeholderCoveredCrossPackageUsageCount) return;
|
|
130
|
+
const pagePackages = Array.from(new Set(Array.from(usage.pages.values()).map((page) => page.packageId))).sort((left, right) => {
|
|
131
|
+
if (left === "__main__") return -1;
|
|
132
|
+
if (right === "__main__") return 1;
|
|
133
|
+
return left.localeCompare(right);
|
|
134
|
+
});
|
|
135
|
+
const subPackageIds = pagePackages.filter((packageId) => packageId !== "__main__");
|
|
136
|
+
const usedByMain = pagePackages.includes("__main__");
|
|
137
|
+
if (subPackageIds.length === 1 && !usedByMain) {
|
|
138
|
+
const targetPackage = subPackageIds[0];
|
|
139
|
+
return {
|
|
140
|
+
kind: "move-to-subpackage",
|
|
141
|
+
component: usage.component,
|
|
142
|
+
componentPackage: usage.componentPackage,
|
|
143
|
+
targetPackage,
|
|
144
|
+
pagePackages,
|
|
145
|
+
message: `主包组件 ${usage.component} 仅被分包 ${targetPackage} 使用,建议评估移动到该分包。`
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (subPackageIds.length > 1) return {
|
|
149
|
+
kind: "shared-subpackage-or-placeholder",
|
|
150
|
+
component: usage.component,
|
|
151
|
+
componentPackage: usage.componentPackage,
|
|
152
|
+
pagePackages,
|
|
153
|
+
message: `主包组件 ${usage.component} 被多个分包使用,建议评估分包归属、共享策略或 componentPlaceholder。`
|
|
154
|
+
};
|
|
155
|
+
if (usedByMain && subPackageIds.length > 0) return {
|
|
156
|
+
kind: "split-or-async",
|
|
157
|
+
component: usage.component,
|
|
158
|
+
componentPackage: usage.componentPackage,
|
|
159
|
+
pagePackages,
|
|
160
|
+
message: `主包组件 ${usage.component} 同时被主包和分包使用,建议评估组件拆分、归属或异步化策略。`
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
//#region src/analyze/components/index.ts
|
|
165
|
+
function normalizeRoute(value) {
|
|
166
|
+
return posix.normalize(value.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\.json$/, ""));
|
|
167
|
+
}
|
|
168
|
+
function isRecord(value) {
|
|
169
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
|
+
}
|
|
171
|
+
function toStringArray(value) {
|
|
172
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim() !== "") : [];
|
|
173
|
+
}
|
|
174
|
+
function readUsingComponents(config) {
|
|
175
|
+
if (!isRecord(config) || !isRecord(config.usingComponents)) return [];
|
|
176
|
+
return Object.entries(config.usingComponents).filter((entry) => typeof entry[1] === "string" && entry[1].trim() !== "");
|
|
177
|
+
}
|
|
178
|
+
function readComponentPlaceholder(config) {
|
|
179
|
+
if (!isRecord(config) || !isRecord(config.componentPlaceholder)) return /* @__PURE__ */ new Set();
|
|
180
|
+
return new Set(Object.keys(config.componentPlaceholder));
|
|
181
|
+
}
|
|
182
|
+
function resolveComponentRoute(owner, request) {
|
|
183
|
+
const normalizedRequest = request.replace(/\\/g, "/").trim();
|
|
184
|
+
if (!normalizedRequest || normalizedRequest.startsWith("plugin://")) return;
|
|
185
|
+
if (normalizedRequest.startsWith(".")) return normalizeRoute(posix.join(posix.dirname(owner), normalizedRequest));
|
|
186
|
+
if (normalizedRequest.startsWith("/")) return normalizeRoute(normalizedRequest);
|
|
187
|
+
return normalizeRoute(normalizedRequest);
|
|
188
|
+
}
|
|
189
|
+
function resolvePackageId(route, subPackages) {
|
|
190
|
+
return subPackages.map((item) => item.root).filter(Boolean).sort((left, right) => right.length - left.length).find((root) => route === root || route.startsWith(`${root}/`)) ?? "__main__";
|
|
191
|
+
}
|
|
192
|
+
function collectAppPages(configs) {
|
|
193
|
+
const appJson = configs.get("app");
|
|
194
|
+
if (!isRecord(appJson)) return /* @__PURE__ */ new Set();
|
|
195
|
+
const pages = /* @__PURE__ */ new Set();
|
|
196
|
+
for (const page of toStringArray(appJson.pages)) pages.add(normalizeRoute(page));
|
|
197
|
+
const subPackages = Array.isArray(appJson.subPackages) ? appJson.subPackages : Array.isArray(appJson.subpackages) ? appJson.subpackages : [];
|
|
198
|
+
for (const item of subPackages) {
|
|
199
|
+
if (!isRecord(item) || typeof item.root !== "string") continue;
|
|
200
|
+
for (const page of toStringArray(item.pages)) pages.add(normalizeRoute(posix.join(item.root, page)));
|
|
201
|
+
}
|
|
202
|
+
return pages;
|
|
203
|
+
}
|
|
204
|
+
function registerUsage(usageMap, edge, page, pagePackage, componentPackage) {
|
|
205
|
+
const usage = usageMap.get(edge.component) ?? {
|
|
206
|
+
component: edge.component,
|
|
207
|
+
componentPackage,
|
|
208
|
+
totalUsageCount: 0,
|
|
209
|
+
pages: /* @__PURE__ */ new Map(),
|
|
210
|
+
crossPackageUsageCount: 0,
|
|
211
|
+
placeholderCoveredCrossPackageUsageCount: 0
|
|
212
|
+
};
|
|
213
|
+
usage.totalUsageCount += 1;
|
|
214
|
+
const pageUsage = usage.pages.get(page) ?? {
|
|
215
|
+
page,
|
|
216
|
+
packageId: pagePackage,
|
|
217
|
+
usageCount: 0
|
|
218
|
+
};
|
|
219
|
+
pageUsage.usageCount += 1;
|
|
220
|
+
usage.pages.set(page, pageUsage);
|
|
221
|
+
if (pagePackage !== componentPackage) {
|
|
222
|
+
usage.crossPackageUsageCount += 1;
|
|
223
|
+
if (edge.placeholderCovered) usage.placeholderCoveredCrossPackageUsageCount += 1;
|
|
224
|
+
}
|
|
225
|
+
usageMap.set(edge.component, usage);
|
|
226
|
+
}
|
|
227
|
+
function collectAnalyzeComponentJsonConfigs(output) {
|
|
228
|
+
if (!output) return [];
|
|
229
|
+
const configs = [];
|
|
230
|
+
for (const item of output.output ?? []) {
|
|
231
|
+
if (item.type !== "asset" || !item.fileName.endsWith(".json")) continue;
|
|
232
|
+
const asset = item;
|
|
233
|
+
if (typeof asset.source !== "string") continue;
|
|
234
|
+
try {
|
|
235
|
+
configs.push({
|
|
236
|
+
file: normalizeRoute(asset.fileName),
|
|
237
|
+
config: JSON.parse(asset.source)
|
|
238
|
+
});
|
|
239
|
+
} catch {}
|
|
240
|
+
}
|
|
241
|
+
return configs;
|
|
242
|
+
}
|
|
243
|
+
function analyzeComponentUsage(options) {
|
|
244
|
+
const configs = /* @__PURE__ */ new Map();
|
|
245
|
+
for (const item of options.jsonConfigs) configs.set(normalizeRoute(item.file), item.config);
|
|
246
|
+
const pages = collectAppPages(configs);
|
|
247
|
+
const graph = /* @__PURE__ */ new Map();
|
|
248
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
249
|
+
for (const route of configs.keys()) packageMap.set(route, resolvePackageId(route, options.subPackages));
|
|
250
|
+
for (const [owner, config] of configs) {
|
|
251
|
+
const placeholders = readComponentPlaceholder(config);
|
|
252
|
+
const edges = readUsingComponents(config).map(([name, request]) => {
|
|
253
|
+
const component = resolveComponentRoute(owner, request);
|
|
254
|
+
return component && configs.has(component) ? {
|
|
255
|
+
owner,
|
|
256
|
+
component,
|
|
257
|
+
placeholderCovered: placeholders.has(name)
|
|
258
|
+
} : void 0;
|
|
259
|
+
}).filter((edge) => Boolean(edge));
|
|
260
|
+
if (edges.length > 0) graph.set(owner, edges);
|
|
261
|
+
}
|
|
262
|
+
const usageMap = /* @__PURE__ */ new Map();
|
|
263
|
+
const visit = (owner, page, stack) => {
|
|
264
|
+
for (const edge of graph.get(owner) ?? []) {
|
|
265
|
+
const componentPackage = packageMap.get(edge.component) ?? "__main__";
|
|
266
|
+
registerUsage(usageMap, edge, page, packageMap.get(page) ?? "__main__", componentPackage);
|
|
267
|
+
if (stack.has(edge.component)) continue;
|
|
268
|
+
const nextStack = new Set(stack);
|
|
269
|
+
nextStack.add(edge.component);
|
|
270
|
+
visit(edge.component, page, nextStack);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
for (const page of pages) visit(page, page, new Set([page]));
|
|
274
|
+
return Array.from(usageMap.values()).map((usage) => {
|
|
275
|
+
const pages = Array.from(usage.pages.values()).sort((left, right) => left.page.localeCompare(right.page));
|
|
276
|
+
const suggestions = [createComponentSuggestion(usage)].filter((item) => Boolean(item));
|
|
277
|
+
return {
|
|
278
|
+
component: usage.component,
|
|
279
|
+
componentPackage: usage.componentPackage,
|
|
280
|
+
totalUsageCount: usage.totalUsageCount,
|
|
281
|
+
pageUsageCount: pages.length,
|
|
282
|
+
pages,
|
|
283
|
+
suggestions
|
|
284
|
+
};
|
|
285
|
+
}).sort((left, right) => {
|
|
286
|
+
const usageDelta = right.totalUsageCount - left.totalUsageCount;
|
|
287
|
+
return usageDelta !== 0 ? usageDelta : left.component.localeCompare(right.component);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
//#endregion
|
|
291
|
+
//#region src/analyze/subpackages/metadata.ts
|
|
292
|
+
const defaultTotalBudgetBytes = 20 * 1024 * 1024;
|
|
293
|
+
const defaultPackageBudgetBytes = 2 * 1024 * 1024;
|
|
294
|
+
const defaultWarningRatio = .85;
|
|
295
|
+
const defaultHistoryDir = ".weapp-vite/analyze-history";
|
|
296
|
+
const defaultHistoryLimit = 20;
|
|
297
|
+
function resolveBudgetValue(value, fallback) {
|
|
298
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
299
|
+
}
|
|
300
|
+
function resolveHistoryLimit(value) {
|
|
301
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : defaultHistoryLimit;
|
|
302
|
+
}
|
|
303
|
+
function resolveAnalyzeBudgets(configService) {
|
|
304
|
+
const budgets = configService.weappViteConfig.analyze?.budgets;
|
|
305
|
+
const legacyPackageBudget = configService.weappViteConfig.packageSizeWarningBytes;
|
|
306
|
+
const packageFallback = resolveBudgetValue(legacyPackageBudget, defaultPackageBudgetBytes);
|
|
307
|
+
return {
|
|
308
|
+
totalBytes: resolveBudgetValue(budgets?.totalBytes, defaultTotalBudgetBytes),
|
|
309
|
+
mainBytes: resolveBudgetValue(budgets?.mainBytes, packageFallback),
|
|
310
|
+
subPackageBytes: resolveBudgetValue(budgets?.subPackageBytes, packageFallback),
|
|
311
|
+
independentBytes: resolveBudgetValue(budgets?.independentBytes, packageFallback),
|
|
312
|
+
warningRatio: resolveBudgetValue(budgets?.warningRatio, defaultWarningRatio),
|
|
313
|
+
source: budgets ? "config" : "default"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function resolveAnalyzeHistoryMetadata(configService) {
|
|
317
|
+
const history = configService.weappViteConfig.analyze?.history;
|
|
318
|
+
if (history === false) return {
|
|
319
|
+
enabled: false,
|
|
320
|
+
dir: path.resolve(configService.cwd, defaultHistoryDir),
|
|
321
|
+
limit: defaultHistoryLimit
|
|
322
|
+
};
|
|
323
|
+
const historyConfig = typeof history === "object" && history ? history : {};
|
|
324
|
+
const rawDir = typeof historyConfig.dir === "string" && historyConfig.dir.trim() ? historyConfig.dir.trim() : defaultHistoryDir;
|
|
325
|
+
return {
|
|
326
|
+
enabled: historyConfig.enabled !== false,
|
|
327
|
+
dir: path.isAbsolute(rawDir) ? rawDir : path.resolve(configService.cwd, rawDir),
|
|
328
|
+
limit: resolveHistoryLimit(historyConfig.limit)
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function createAnalyzeMetadata(configService, now = /* @__PURE__ */ new Date()) {
|
|
332
|
+
const history = resolveAnalyzeHistoryMetadata(configService);
|
|
333
|
+
return {
|
|
334
|
+
generatedAt: now.toISOString(),
|
|
335
|
+
budgets: resolveAnalyzeBudgets(configService),
|
|
336
|
+
history: {
|
|
337
|
+
...history,
|
|
338
|
+
dir: configService.relativeCwd(history.dir)
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
//#endregion
|
|
125
343
|
//#region src/analyze/subpackages/classifier.ts
|
|
126
344
|
const VIRTUAL_MODULE_INDICATOR = "\0";
|
|
127
345
|
const VIRTUAL_PREFIX = `${SHARED_CHUNK_VIRTUAL_PREFIX}/`;
|
|
@@ -139,6 +357,10 @@ function resolvePluginAssetAbsolute(normalizedFileName, pluginRoot) {
|
|
|
139
357
|
const absolute = posix.resolve(pluginRoot, relative);
|
|
140
358
|
return isPathInside(pluginRoot, absolute) ? absolute : void 0;
|
|
141
359
|
}
|
|
360
|
+
function resolveSubPackageRoot$1(fileName, context) {
|
|
361
|
+
const normalized = posix.normalize(fileName);
|
|
362
|
+
return Array.from(context.subPackageRoots).filter(Boolean).sort((left, right) => right.length - left.length).find((root) => normalized === root || normalized.startsWith(`${root}/`));
|
|
363
|
+
}
|
|
142
364
|
function classifyPackage(fileName, origin, context) {
|
|
143
365
|
if (fileName.startsWith(VIRTUAL_PREFIX)) {
|
|
144
366
|
const combination = fileName.slice(VIRTUAL_PREFIX.length).split("/")[0] || "shared";
|
|
@@ -148,8 +370,8 @@ function classifyPackage(fileName, origin, context) {
|
|
|
148
370
|
type: "virtual"
|
|
149
371
|
};
|
|
150
372
|
}
|
|
151
|
-
const rootCandidate = fileName
|
|
152
|
-
if (rootCandidate
|
|
373
|
+
const rootCandidate = resolveSubPackageRoot$1(fileName, context);
|
|
374
|
+
if (rootCandidate) {
|
|
153
375
|
const isIndependent = context.independentRoots.has(rootCandidate);
|
|
154
376
|
return {
|
|
155
377
|
id: rootCandidate,
|
|
@@ -231,9 +453,17 @@ function registerModuleInPackage(modules, moduleId, source, sourceType, packageI
|
|
|
231
453
|
}
|
|
232
454
|
//#endregion
|
|
233
455
|
//#region src/analyze/subpackages/output.ts
|
|
234
|
-
function
|
|
235
|
-
if (typeof asset.source === "string") return Buffer.
|
|
236
|
-
if (asset.source instanceof Uint8Array) return asset.source
|
|
456
|
+
function getAssetBuffer(asset) {
|
|
457
|
+
if (typeof asset.source === "string") return Buffer.from(asset.source, "utf8");
|
|
458
|
+
if (asset.source instanceof Uint8Array) return Buffer.from(asset.source);
|
|
459
|
+
}
|
|
460
|
+
function getCompressedSizes$1(content) {
|
|
461
|
+
if (content === void 0) return {};
|
|
462
|
+
const buffer = typeof content === "string" ? Buffer.from(content, "utf8") : Buffer.from(content);
|
|
463
|
+
return {
|
|
464
|
+
gzipSize: gzipSync(buffer).byteLength,
|
|
465
|
+
brotliSize: brotliCompressSync(buffer).byteLength
|
|
466
|
+
};
|
|
237
467
|
}
|
|
238
468
|
function processChunk(chunk, origin, ctx, classifierContext, packages, modules) {
|
|
239
469
|
const classification = classifyPackage(chunk.fileName, origin, classifierContext);
|
|
@@ -243,6 +473,7 @@ function processChunk(chunk, origin, ctx, classifierContext, packages, modules)
|
|
|
243
473
|
type: "chunk",
|
|
244
474
|
from: origin,
|
|
245
475
|
size: typeof chunk.code === "string" ? Buffer.byteLength(chunk.code, "utf8") : void 0,
|
|
476
|
+
...getCompressedSizes$1(chunk.code),
|
|
246
477
|
isEntry: chunk.isEntry,
|
|
247
478
|
modules: []
|
|
248
479
|
};
|
|
@@ -267,11 +498,13 @@ function processChunk(chunk, origin, ctx, classifierContext, packages, modules)
|
|
|
267
498
|
function processAsset(asset, origin, ctx, classifierContext, packages, modules) {
|
|
268
499
|
const classification = classifyPackage(asset.fileName, origin, classifierContext);
|
|
269
500
|
const packageEntry = ensurePackage(packages, classification);
|
|
501
|
+
const assetBuffer = getAssetBuffer(asset);
|
|
270
502
|
const entry = {
|
|
271
503
|
file: asset.fileName,
|
|
272
504
|
type: "asset",
|
|
273
505
|
from: origin,
|
|
274
|
-
size:
|
|
506
|
+
size: assetBuffer?.byteLength,
|
|
507
|
+
...getCompressedSizes$1(assetBuffer)
|
|
275
508
|
};
|
|
276
509
|
const assetSource = resolveAssetSource(asset.fileName, ctx);
|
|
277
510
|
if (assetSource) {
|
|
@@ -411,22 +644,470 @@ async function analyzeSubpackages(ctx) {
|
|
|
411
644
|
const mainOutputs = Array.isArray(mainResult) ? mainResult : [mainResult];
|
|
412
645
|
const packages = /* @__PURE__ */ new Map();
|
|
413
646
|
const modules = /* @__PURE__ */ new Map();
|
|
414
|
-
|
|
415
|
-
for (const
|
|
647
|
+
const componentJsonConfigs = [];
|
|
648
|
+
for (const output of mainOutputs) {
|
|
649
|
+
processOutput(output, "main", ctx, classifierContext, packages, modules);
|
|
650
|
+
componentJsonConfigs.push(...collectAnalyzeComponentJsonConfigs(output));
|
|
651
|
+
}
|
|
652
|
+
for (const root of independentRoots) {
|
|
653
|
+
const output = buildService.getIndependentOutput(root);
|
|
654
|
+
processOutput(output, "independent", ctx, classifierContext, packages, modules);
|
|
655
|
+
componentJsonConfigs.push(...collectAnalyzeComponentJsonConfigs(output));
|
|
656
|
+
}
|
|
416
657
|
expandVirtualModulePlacements(modules, packages, classifierContext);
|
|
658
|
+
const subPackages = summarizeSubPackages(subPackageMetas);
|
|
417
659
|
return {
|
|
660
|
+
metadata: createAnalyzeMetadata(ctx.configService),
|
|
418
661
|
packages: summarizePackages(packages),
|
|
419
662
|
modules: summarizeModules(modules),
|
|
420
|
-
subPackages
|
|
663
|
+
subPackages,
|
|
664
|
+
components: analyzeComponentUsage({
|
|
665
|
+
jsonConfigs: componentJsonConfigs,
|
|
666
|
+
subPackages
|
|
667
|
+
})
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region src/analyze/subpackages/history.ts
|
|
672
|
+
const jsonExtension = ".json";
|
|
673
|
+
function createSnapshotFileName(date = /* @__PURE__ */ new Date()) {
|
|
674
|
+
return `${date.toISOString().replace(/[:.]/g, "-")}${jsonExtension}`;
|
|
675
|
+
}
|
|
676
|
+
function isAnalyzeResult(value) {
|
|
677
|
+
return Boolean(value && typeof value === "object" && Array.isArray(value.packages) && Array.isArray(value.modules) && Array.isArray(value.subPackages));
|
|
678
|
+
}
|
|
679
|
+
async function listSnapshotFiles(dir) {
|
|
680
|
+
try {
|
|
681
|
+
return (await fs$2.readdir(dir, { withFileTypes: true })).filter((entry) => entry.isFile() && entry.name.endsWith(jsonExtension)).map((entry) => path.join(dir, entry.name)).sort((a, b) => b.localeCompare(a));
|
|
682
|
+
} catch {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async function trimSnapshotFiles(dir, limit) {
|
|
687
|
+
const staleFiles = (await listSnapshotFiles(dir)).slice(limit);
|
|
688
|
+
await Promise.all(staleFiles.map(async (file) => {
|
|
689
|
+
try {
|
|
690
|
+
await fs$2.unlink(file);
|
|
691
|
+
} catch {}
|
|
692
|
+
}));
|
|
693
|
+
}
|
|
694
|
+
async function readLatestAnalyzeHistorySnapshot(configService) {
|
|
695
|
+
const history = resolveAnalyzeHistoryMetadata(configService);
|
|
696
|
+
if (!history.enabled) return null;
|
|
697
|
+
const files = await listSnapshotFiles(history.dir);
|
|
698
|
+
for (const file of files) try {
|
|
699
|
+
const parsed = JSON.parse(await fs$2.readFile(file, "utf8"));
|
|
700
|
+
if (isAnalyzeResult(parsed)) return parsed;
|
|
701
|
+
} catch {}
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
async function writeAnalyzeHistorySnapshot(result, configService, now = /* @__PURE__ */ new Date()) {
|
|
705
|
+
const history = resolveAnalyzeHistoryMetadata(configService);
|
|
706
|
+
if (!history.enabled) return;
|
|
707
|
+
await fs$2.mkdir(history.dir, { recursive: true });
|
|
708
|
+
const snapshotPath = path.join(history.dir, createSnapshotFileName(now));
|
|
709
|
+
if (result.metadata) result.metadata = {
|
|
710
|
+
...result.metadata,
|
|
711
|
+
history: {
|
|
712
|
+
...history,
|
|
713
|
+
dir: configService.relativeCwd(history.dir),
|
|
714
|
+
latestSnapshot: configService.relativeCwd(snapshotPath)
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
await fs$2.writeFile(snapshotPath, `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
718
|
+
await trimSnapshotFiles(history.dir, history.limit);
|
|
719
|
+
return snapshotPath;
|
|
720
|
+
}
|
|
721
|
+
//#endregion
|
|
722
|
+
//#region src/analyze/subpackages/report.ts
|
|
723
|
+
function formatAnalyzeBytes(bytes) {
|
|
724
|
+
if (!bytes || Number.isNaN(bytes)) return "0 B";
|
|
725
|
+
const units = [
|
|
726
|
+
"B",
|
|
727
|
+
"KB",
|
|
728
|
+
"MB",
|
|
729
|
+
"GB"
|
|
730
|
+
];
|
|
731
|
+
let value = bytes;
|
|
732
|
+
let unitIndex = 0;
|
|
733
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
734
|
+
value /= 1024;
|
|
735
|
+
unitIndex++;
|
|
736
|
+
}
|
|
737
|
+
return `${value.toFixed(value >= 100 || unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}`;
|
|
738
|
+
}
|
|
739
|
+
function formatDelta(bytes) {
|
|
740
|
+
if (typeof bytes !== "number" || Number.isNaN(bytes) || bytes === 0) return "无变化";
|
|
741
|
+
return `${bytes > 0 ? "+" : "-"}${formatAnalyzeBytes(Math.abs(bytes))}`;
|
|
742
|
+
}
|
|
743
|
+
function getFileSize(file) {
|
|
744
|
+
return file.size ?? 0;
|
|
745
|
+
}
|
|
746
|
+
function getCompressedSize(file) {
|
|
747
|
+
return file.brotliSize ?? file.gzipSize ?? 0;
|
|
748
|
+
}
|
|
749
|
+
function getBudgetLimit(type, budgets) {
|
|
750
|
+
if (!budgets) return;
|
|
751
|
+
if (type === "main") return budgets.mainBytes;
|
|
752
|
+
if (type === "subPackage") return budgets.subPackageBytes;
|
|
753
|
+
if (type === "independent") return budgets.independentBytes;
|
|
754
|
+
}
|
|
755
|
+
function createPackageSizeMap(result) {
|
|
756
|
+
return new Map((result?.packages ?? []).map((pkg) => {
|
|
757
|
+
const totalBytes = pkg.files.reduce((sum, file) => sum + getFileSize(file), 0);
|
|
758
|
+
return [pkg.id, totalBytes];
|
|
759
|
+
}));
|
|
760
|
+
}
|
|
761
|
+
function createFileKey(packageId, fileName) {
|
|
762
|
+
return `${packageId}\u0000${fileName}`;
|
|
763
|
+
}
|
|
764
|
+
function createFileSizeMap(result) {
|
|
765
|
+
const map = /* @__PURE__ */ new Map();
|
|
766
|
+
for (const pkg of result?.packages ?? []) for (const file of pkg.files) map.set(createFileKey(pkg.id, file.file), getFileSize(file));
|
|
767
|
+
return map;
|
|
768
|
+
}
|
|
769
|
+
function createPackageTypeMap(result) {
|
|
770
|
+
return new Map(result.packages.map((pkg) => [pkg.id, pkg.type]));
|
|
771
|
+
}
|
|
772
|
+
function createModuleByteMap(result) {
|
|
773
|
+
const map = /* @__PURE__ */ new Map();
|
|
774
|
+
for (const pkg of result.packages) for (const file of pkg.files) for (const mod of file.modules ?? []) {
|
|
775
|
+
const bytes = mod.bytes ?? mod.originalBytes ?? 0;
|
|
776
|
+
map.set(mod.id, Math.max(map.get(mod.id) ?? 0, bytes));
|
|
777
|
+
}
|
|
778
|
+
return map;
|
|
779
|
+
}
|
|
780
|
+
function classifyIncrementCategory(source, sourceType) {
|
|
781
|
+
if (source.includes("wevu") || source.includes("@weapp-vite/dashboard")) return "WeVu / runtime";
|
|
782
|
+
if (sourceType === "node_modules" || source.includes("node_modules")) return "第三方依赖";
|
|
783
|
+
if (sourceType === "workspace") return "工作区包";
|
|
784
|
+
if (sourceType === "plugin") return "插件生成";
|
|
785
|
+
if (source.endsWith(".wxss") || source.endsWith(".css") || source.endsWith(".scss")) return "样式资源";
|
|
786
|
+
if (source.endsWith(".wxml") || source.endsWith(".json")) return "页面结构";
|
|
787
|
+
return "业务源码";
|
|
788
|
+
}
|
|
789
|
+
function createIncrementAdvice(item) {
|
|
790
|
+
if (item.category === "第三方依赖") return "检查依赖是否只在必要分包引用,必要时收敛到公共入口或替换轻量实现。";
|
|
791
|
+
if (item.category === "WeVu / runtime") return "确认运行时能力是否被新页面引入,优先排查组件和 API 引用边界。";
|
|
792
|
+
if (item.category === "样式资源") return "检查样式重复、原子类生成范围和组件样式裁剪。";
|
|
793
|
+
if (item.type === "new-file" || item.type === "new-module") return "确认是否为本次需求必要新增,评估分包归属和懒加载边界。";
|
|
794
|
+
return "对比本次变更,优先查看新增引用、共享模块和大对象常量。";
|
|
795
|
+
}
|
|
796
|
+
function createModuleSizeMap(result) {
|
|
797
|
+
const map = /* @__PURE__ */ new Map();
|
|
798
|
+
for (const pkg of result?.packages ?? []) for (const file of pkg.files) for (const module of file.modules ?? []) {
|
|
799
|
+
const bytes = module.bytes ?? module.originalBytes ?? 0;
|
|
800
|
+
const existing = map.get(module.id);
|
|
801
|
+
if (existing && existing.bytes >= bytes) continue;
|
|
802
|
+
map.set(module.id, {
|
|
803
|
+
source: module.source,
|
|
804
|
+
sourceType: module.sourceType,
|
|
805
|
+
bytes,
|
|
806
|
+
packageLabel: pkg.label,
|
|
807
|
+
file: file.file
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
return map;
|
|
811
|
+
}
|
|
812
|
+
function createDuplicateAdvice(sourceType, packageIds, packageTypeMap, estimatedSavingBytes) {
|
|
813
|
+
if (packageIds.some((packageId) => packageTypeMap.get(packageId) === "independent")) return estimatedSavingBytes > 0 ? "含独立分包,先确认隔离要求,再评估是否抽公共入口。" : "含独立分包,重复可能来自隔离边界。";
|
|
814
|
+
if (sourceType === "node_modules") return "依赖被多个包带入,检查引用边界或考虑主包公共入口。";
|
|
815
|
+
if (sourceType === "src" || sourceType === "workspace") return "共享源码跨包重复,优先抽公共模块或调整分包归属。";
|
|
816
|
+
if (sourceType === "plugin") return "插件生成内容跨包重复,检查插件产物输出策略。";
|
|
817
|
+
return "检查该模块是否需要在多个包内重复存在。";
|
|
818
|
+
}
|
|
819
|
+
function formatBudgetStatus(item) {
|
|
820
|
+
if (item.status === "ok") return "正常";
|
|
821
|
+
return `${item.status === "exceeded" ? "超预算" : "接近预算"} ${(item.ratio * 100).toFixed(1)}%`;
|
|
822
|
+
}
|
|
823
|
+
function createActionItems(options) {
|
|
824
|
+
const actions = [];
|
|
825
|
+
const exceededItems = options.budgetItems.filter((item) => item.status === "exceeded");
|
|
826
|
+
const warningItems = options.budgetItems.filter((item) => item.status === "warning");
|
|
827
|
+
const topDuplicate = options.duplicateInsights.find((item) => item.estimatedSavingBytes > 0);
|
|
828
|
+
if (exceededItems.length > 0) actions.push(`处理 ${exceededItems[0].label} 预算超限:当前 ${formatAnalyzeBytes(exceededItems[0].currentBytes)},限制 ${formatAnalyzeBytes(exceededItems[0].limitBytes)}。`);
|
|
829
|
+
else if (warningItems.length > 0) actions.push(`关注 ${warningItems[0].label} 预算接近阈值:当前 ${(warningItems[0].ratio * 100).toFixed(1)}%。`);
|
|
830
|
+
if (topDuplicate) actions.push(`优先处理重复模块 ${topDuplicate.source},估算可节省 ${formatAnalyzeBytes(topDuplicate.estimatedSavingBytes)}。`);
|
|
831
|
+
if (actions.length === 0) actions.push("当前没有预算超限或高收益重复模块,保持观察即可。");
|
|
832
|
+
return actions;
|
|
833
|
+
}
|
|
834
|
+
function createAnalyzeBudgetCheck(result) {
|
|
835
|
+
const budgets = result.metadata?.budgets;
|
|
836
|
+
if (!budgets) return [];
|
|
837
|
+
const items = [];
|
|
838
|
+
const totalBytes = result.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
|
|
839
|
+
const warningRatio = budgets.warningRatio;
|
|
840
|
+
const createItem = (options) => {
|
|
841
|
+
const ratio = options.limitBytes > 0 ? options.currentBytes / options.limitBytes : 0;
|
|
842
|
+
const status = ratio >= 1 ? "exceeded" : ratio >= warningRatio ? "warning" : "ok";
|
|
843
|
+
return {
|
|
844
|
+
...options,
|
|
845
|
+
ratio,
|
|
846
|
+
status
|
|
847
|
+
};
|
|
421
848
|
};
|
|
849
|
+
items.push(createItem({
|
|
850
|
+
id: "__total__",
|
|
851
|
+
label: "总包",
|
|
852
|
+
scope: "total",
|
|
853
|
+
currentBytes: totalBytes,
|
|
854
|
+
limitBytes: budgets.totalBytes
|
|
855
|
+
}));
|
|
856
|
+
for (const pkg of result.packages) {
|
|
857
|
+
const limitBytes = getBudgetLimit(pkg.type, budgets);
|
|
858
|
+
if (!limitBytes) continue;
|
|
859
|
+
items.push(createItem({
|
|
860
|
+
id: pkg.id,
|
|
861
|
+
label: pkg.label,
|
|
862
|
+
scope: pkg.type,
|
|
863
|
+
currentBytes: pkg.files.reduce((sum, file) => sum + getFileSize(file), 0),
|
|
864
|
+
limitBytes
|
|
865
|
+
}));
|
|
866
|
+
}
|
|
867
|
+
return items.sort((a, b) => b.ratio - a.ratio || a.label.localeCompare(b.label));
|
|
868
|
+
}
|
|
869
|
+
function createDuplicateModuleInsights(result) {
|
|
870
|
+
const moduleByteMap = createModuleByteMap(result);
|
|
871
|
+
const packageTypeMap = createPackageTypeMap(result);
|
|
872
|
+
return result.modules.filter((module) => module.packages.length > 1).map((module) => {
|
|
873
|
+
const bytes = moduleByteMap.get(module.id) ?? 0;
|
|
874
|
+
const packageIds = module.packages.map((pkg) => pkg.packageId);
|
|
875
|
+
const estimatedSavingBytes = bytes * Math.max(module.packages.length - 1, 0);
|
|
876
|
+
return {
|
|
877
|
+
id: module.id,
|
|
878
|
+
source: module.source,
|
|
879
|
+
sourceType: module.sourceType,
|
|
880
|
+
packageCount: module.packages.length,
|
|
881
|
+
bytes,
|
|
882
|
+
estimatedSavingBytes,
|
|
883
|
+
packages: packageIds,
|
|
884
|
+
advice: createDuplicateAdvice(module.sourceType, packageIds, packageTypeMap, estimatedSavingBytes)
|
|
885
|
+
};
|
|
886
|
+
}).sort((a, b) => b.estimatedSavingBytes - a.estimatedSavingBytes || b.packageCount - a.packageCount || a.source.localeCompare(b.source));
|
|
887
|
+
}
|
|
888
|
+
function createAnalyzeIncrementAttribution(result, previousResult) {
|
|
889
|
+
if (!previousResult) return [];
|
|
890
|
+
const previousFiles = createFileSizeMap(previousResult);
|
|
891
|
+
const previousModules = createModuleSizeMap(previousResult);
|
|
892
|
+
const currentModules = createModuleSizeMap(result);
|
|
893
|
+
const items = [];
|
|
894
|
+
for (const pkg of result.packages) for (const file of pkg.files) {
|
|
895
|
+
const currentBytes = getFileSize(file);
|
|
896
|
+
const previousBytes = previousFiles.get(createFileKey(pkg.id, file.file)) ?? 0;
|
|
897
|
+
const deltaBytes = currentBytes - previousBytes;
|
|
898
|
+
if (deltaBytes <= 0) continue;
|
|
899
|
+
const type = previousBytes > 0 ? "increased-file" : "new-file";
|
|
900
|
+
const item = {
|
|
901
|
+
key: `file:${pkg.id}:${file.file}`,
|
|
902
|
+
type,
|
|
903
|
+
label: file.file,
|
|
904
|
+
category: classifyIncrementCategory(file.source ?? file.file),
|
|
905
|
+
packageLabel: pkg.label,
|
|
906
|
+
file: file.file,
|
|
907
|
+
currentBytes,
|
|
908
|
+
previousBytes,
|
|
909
|
+
deltaBytes,
|
|
910
|
+
advice: ""
|
|
911
|
+
};
|
|
912
|
+
items.push({
|
|
913
|
+
...item,
|
|
914
|
+
advice: createIncrementAdvice(item)
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
for (const [id, module] of currentModules) {
|
|
918
|
+
const previousBytes = previousModules.get(id)?.bytes ?? 0;
|
|
919
|
+
const deltaBytes = module.bytes - previousBytes;
|
|
920
|
+
if (deltaBytes <= 0) continue;
|
|
921
|
+
const type = previousBytes > 0 ? "increased-module" : "new-module";
|
|
922
|
+
const item = {
|
|
923
|
+
key: `module:${id}`,
|
|
924
|
+
type,
|
|
925
|
+
label: module.source,
|
|
926
|
+
category: classifyIncrementCategory(module.source, module.sourceType),
|
|
927
|
+
packageLabel: module.packageLabel,
|
|
928
|
+
file: module.file,
|
|
929
|
+
currentBytes: module.bytes,
|
|
930
|
+
previousBytes,
|
|
931
|
+
deltaBytes,
|
|
932
|
+
advice: ""
|
|
933
|
+
};
|
|
934
|
+
items.push({
|
|
935
|
+
...item,
|
|
936
|
+
advice: createIncrementAdvice(item)
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return items.sort((a, b) => b.deltaBytes - a.deltaBytes || a.category.localeCompare(b.category) || a.label.localeCompare(b.label));
|
|
940
|
+
}
|
|
941
|
+
function createAnalyzeIncrementCategorySummary(items) {
|
|
942
|
+
const map = /* @__PURE__ */ new Map();
|
|
943
|
+
for (const item of items) {
|
|
944
|
+
const entry = map.get(item.category) ?? {
|
|
945
|
+
category: item.category,
|
|
946
|
+
count: 0,
|
|
947
|
+
deltaBytes: 0
|
|
948
|
+
};
|
|
949
|
+
entry.count += 1;
|
|
950
|
+
entry.deltaBytes += item.deltaBytes;
|
|
951
|
+
map.set(item.category, entry);
|
|
952
|
+
}
|
|
953
|
+
return [...map.values()].sort((a, b) => b.deltaBytes - a.deltaBytes || b.count - a.count || a.category.localeCompare(b.category));
|
|
954
|
+
}
|
|
955
|
+
function createAnalyzePrMarkdownReport(result, previousResult) {
|
|
956
|
+
const files = result.packages.flatMap((pkg) => pkg.files.map((file) => ({
|
|
957
|
+
pkg,
|
|
958
|
+
file
|
|
959
|
+
})));
|
|
960
|
+
const totalBytes = files.reduce((sum, item) => sum + getFileSize(item.file), 0);
|
|
961
|
+
const compressedBytes = files.reduce((sum, item) => sum + getCompressedSize(item.file), 0);
|
|
962
|
+
const previousTotalBytes = previousResult?.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
|
|
963
|
+
const incrementItems = createAnalyzeIncrementAttribution(result, previousResult);
|
|
964
|
+
const incrementSummary = createAnalyzeIncrementCategorySummary(incrementItems);
|
|
965
|
+
const duplicateInsights = createDuplicateModuleInsights(result);
|
|
966
|
+
const budgetItems = createAnalyzeBudgetCheck(result);
|
|
967
|
+
const budgetIssues = budgetItems.filter((item) => item.status !== "ok");
|
|
968
|
+
const actionItems = createActionItems({
|
|
969
|
+
budgetItems,
|
|
970
|
+
duplicateInsights
|
|
971
|
+
}).slice(0, 3);
|
|
972
|
+
const budgetRows = budgetIssues.slice(0, 5).map((item) => `| ${item.label} | ${formatAnalyzeBytes(item.currentBytes)} | ${formatAnalyzeBytes(item.limitBytes)} | ${formatBudgetStatus(item)} |`).join("\n");
|
|
973
|
+
const incrementRows = incrementItems.slice(0, 8).map((item) => `| ${item.label} | ${item.category} | ${item.packageLabel} | ${formatAnalyzeBytes(item.deltaBytes)} | ${item.advice} |`).join("\n");
|
|
974
|
+
const sourceRows = incrementSummary.slice(0, 6).map((item) => `| ${item.category} | ${item.count} | ${formatAnalyzeBytes(item.deltaBytes)} |`).join("\n");
|
|
975
|
+
const duplicateRows = duplicateInsights.slice(0, 5).map((module) => `| ${module.source} | ${module.packageCount} | ${formatAnalyzeBytes(module.estimatedSavingBytes)} | ${module.advice} |`).join("\n");
|
|
976
|
+
return [
|
|
977
|
+
"## weapp-vite analyze PR 摘要",
|
|
978
|
+
"",
|
|
979
|
+
`- 总产物体积:${formatAnalyzeBytes(totalBytes)}(较上次 ${formatDelta(typeof previousTotalBytes === "number" ? totalBytes - previousTotalBytes : void 0)})`,
|
|
980
|
+
`- 压缩后体积:${formatAnalyzeBytes(compressedBytes)}`,
|
|
981
|
+
`- 预算告警:${budgetIssues.length}`,
|
|
982
|
+
`- 增量归因:${incrementItems.length > 0 ? `${incrementItems.length} 项正向增长` : "无正向增长"}`,
|
|
983
|
+
`- 跨包复用:${duplicateInsights.length}`,
|
|
984
|
+
"",
|
|
985
|
+
"### 建议动作",
|
|
986
|
+
"",
|
|
987
|
+
...actionItems.map((item) => `- ${item}`),
|
|
988
|
+
"",
|
|
989
|
+
"### 预算状态",
|
|
990
|
+
"",
|
|
991
|
+
"| 对象 | 当前体积 | 预算 | 状态 |",
|
|
992
|
+
"| --- | ---: | ---: | --- |",
|
|
993
|
+
budgetRows || "| - | 0 B | 0 B | 正常 |",
|
|
994
|
+
"",
|
|
995
|
+
"### 增量来源",
|
|
996
|
+
"",
|
|
997
|
+
"| 来源 | 项数 | 增量 |",
|
|
998
|
+
"| --- | ---: | ---: |",
|
|
999
|
+
sourceRows || "| - | 0 | 0 B |",
|
|
1000
|
+
"",
|
|
1001
|
+
"### Top 增量",
|
|
1002
|
+
"",
|
|
1003
|
+
"| 文件/模块 | 来源 | 包 | 增量 | 建议 |",
|
|
1004
|
+
"| --- | --- | --- | ---: | --- |",
|
|
1005
|
+
incrementRows || "| - | - | - | 0 B | - |",
|
|
1006
|
+
"",
|
|
1007
|
+
"### 重复模块",
|
|
1008
|
+
"",
|
|
1009
|
+
"| 模块 | 包数量 | 估算可节省 | 建议 |",
|
|
1010
|
+
"| --- | ---: | ---: | --- |",
|
|
1011
|
+
duplicateRows || "| - | 0 | 0 B | - |",
|
|
1012
|
+
""
|
|
1013
|
+
].join("\n");
|
|
1014
|
+
}
|
|
1015
|
+
function createAnalyzeMarkdownReport(result, previousResult) {
|
|
1016
|
+
const files = result.packages.flatMap((pkg) => pkg.files.map((file) => ({
|
|
1017
|
+
pkg,
|
|
1018
|
+
file
|
|
1019
|
+
})));
|
|
1020
|
+
const totalBytes = files.reduce((sum, item) => sum + getFileSize(item.file), 0);
|
|
1021
|
+
const compressedBytes = files.reduce((sum, item) => sum + getCompressedSize(item.file), 0);
|
|
1022
|
+
const duplicateInsights = createDuplicateModuleInsights(result);
|
|
1023
|
+
const incrementItems = createAnalyzeIncrementAttribution(result, previousResult);
|
|
1024
|
+
const incrementSummary = createAnalyzeIncrementCategorySummary(incrementItems);
|
|
1025
|
+
const budgetItems = createAnalyzeBudgetCheck(result);
|
|
1026
|
+
const previousTotalBytes = previousResult?.packages.flatMap((pkg) => pkg.files).reduce((sum, file) => sum + getFileSize(file), 0);
|
|
1027
|
+
const previousPackageSizes = createPackageSizeMap(previousResult);
|
|
1028
|
+
const budgets = result.metadata?.budgets;
|
|
1029
|
+
const budgetIssues = budgetItems.filter((item) => item.status !== "ok");
|
|
1030
|
+
const actionItems = createActionItems({
|
|
1031
|
+
budgetItems,
|
|
1032
|
+
duplicateInsights
|
|
1033
|
+
});
|
|
1034
|
+
const packageRows = result.packages.map((pkg) => {
|
|
1035
|
+
const size = pkg.files.reduce((sum, file) => sum + getFileSize(file), 0);
|
|
1036
|
+
const compressed = pkg.files.reduce((sum, file) => sum + getCompressedSize(file), 0);
|
|
1037
|
+
const previousSize = previousPackageSizes.get(pkg.id);
|
|
1038
|
+
const budgetStatus = budgetItems.find((item) => item.id === pkg.id);
|
|
1039
|
+
return `| ${pkg.label} | ${pkg.type} | ${formatAnalyzeBytes(size)} | ${formatAnalyzeBytes(compressed)} | ${formatDelta(typeof previousSize === "number" ? size - previousSize : void 0)} | ${budgetStatus ? formatBudgetStatus(budgetStatus) : "正常"} |`;
|
|
1040
|
+
}).join("\n");
|
|
1041
|
+
const topFileRows = files.sort((a, b) => getFileSize(b.file) - getFileSize(a.file) || a.file.file.localeCompare(b.file.file)).slice(0, 10).map((item) => `| ${item.file.file} | ${item.pkg.label} | ${item.file.type} | ${formatAnalyzeBytes(getFileSize(item.file))} | ${formatAnalyzeBytes(getCompressedSize(item.file))} |`).join("\n");
|
|
1042
|
+
const duplicateRows = duplicateInsights.slice(0, 10).map((module) => `| ${module.source} | ${module.sourceType} | ${module.packageCount} | ${formatAnalyzeBytes(module.estimatedSavingBytes)} | ${module.advice} |`).join("\n");
|
|
1043
|
+
const budgetRows = budgetIssues.map((item) => `| ${item.label} | ${item.scope} | ${formatAnalyzeBytes(item.currentBytes)} | ${formatAnalyzeBytes(item.limitBytes)} | ${formatBudgetStatus(item)} |`).join("\n");
|
|
1044
|
+
const incrementRows = incrementItems.slice(0, 10).map((item) => `| ${item.label} | ${item.category} | ${item.packageLabel} | ${formatAnalyzeBytes(item.deltaBytes)} | ${item.advice} |`).join("\n");
|
|
1045
|
+
const incrementSummaryRows = incrementSummary.slice(0, 8).map((item) => `| ${item.category} | ${item.count} | ${formatAnalyzeBytes(item.deltaBytes)} |`).join("\n");
|
|
1046
|
+
return [
|
|
1047
|
+
"# weapp-vite analyze 报告",
|
|
1048
|
+
"",
|
|
1049
|
+
`生成时间:${result.metadata?.generatedAt ?? (/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1050
|
+
"",
|
|
1051
|
+
"## 本次变化摘要",
|
|
1052
|
+
"",
|
|
1053
|
+
`- 总产物体积:${formatAnalyzeBytes(totalBytes)}`,
|
|
1054
|
+
`- 压缩后体积:${formatAnalyzeBytes(compressedBytes)}`,
|
|
1055
|
+
`- 较上次:${formatDelta(typeof previousTotalBytes === "number" ? totalBytes - previousTotalBytes : void 0)}`,
|
|
1056
|
+
`- 包体数量:${result.packages.length}`,
|
|
1057
|
+
`- 源码模块:${result.modules.length}`,
|
|
1058
|
+
`- 跨包复用:${duplicateInsights.length}`,
|
|
1059
|
+
`- 预算来源:${budgets?.source === "config" ? "配置" : "默认"}`,
|
|
1060
|
+
"",
|
|
1061
|
+
"## 预算告警",
|
|
1062
|
+
"",
|
|
1063
|
+
"| 对象 | 范围 | 当前体积 | 预算 | 状态 |",
|
|
1064
|
+
"| --- | --- | ---: | ---: | --- |",
|
|
1065
|
+
budgetRows || "| - | - | 0 B | 0 B | 正常 |",
|
|
1066
|
+
"",
|
|
1067
|
+
"## 建议动作",
|
|
1068
|
+
"",
|
|
1069
|
+
...actionItems.map((item) => `- ${item}`),
|
|
1070
|
+
"",
|
|
1071
|
+
"## 增量归因",
|
|
1072
|
+
"",
|
|
1073
|
+
"| 来源 | 项数 | 增量 |",
|
|
1074
|
+
"| --- | ---: | ---: |",
|
|
1075
|
+
incrementSummaryRows || "| - | 0 | 0 B |",
|
|
1076
|
+
"",
|
|
1077
|
+
"| 文件/模块 | 来源 | 包 | 增量 | 建议 |",
|
|
1078
|
+
"| --- | --- | --- | ---: | --- |",
|
|
1079
|
+
incrementRows || "| - | - | - | 0 B | - |",
|
|
1080
|
+
"",
|
|
1081
|
+
"## 包体预算",
|
|
1082
|
+
"",
|
|
1083
|
+
"| 包 | 类型 | 体积 | 压缩后 | 较上次 | 预算 |",
|
|
1084
|
+
"| --- | --- | ---: | ---: | ---: | --- |",
|
|
1085
|
+
packageRows || "| - | - | 0 B | 0 B | 无变化 | 正常 |",
|
|
1086
|
+
"",
|
|
1087
|
+
"## Top 文件",
|
|
1088
|
+
"",
|
|
1089
|
+
"| 文件 | 包 | 类型 | 体积 | 压缩后 |",
|
|
1090
|
+
"| --- | --- | --- | ---: | ---: |",
|
|
1091
|
+
topFileRows || "| - | - | - | 0 B | 0 B |",
|
|
1092
|
+
"",
|
|
1093
|
+
"## 重复模块",
|
|
1094
|
+
"",
|
|
1095
|
+
"| 模块 | 来源 | 包数量 | 估算可节省 | 建议 |",
|
|
1096
|
+
"| --- | --- | ---: | ---: | --- |",
|
|
1097
|
+
duplicateRows || "| - | - | 0 | 0 B | - |",
|
|
1098
|
+
""
|
|
1099
|
+
].join("\n");
|
|
422
1100
|
}
|
|
423
1101
|
//#endregion
|
|
424
1102
|
//#region src/cli/analyze/dashboard.ts
|
|
425
1103
|
const ANALYZE_GLOBAL_KEY = "__WEAPP_VITE_ANALYZE_RESULT__";
|
|
1104
|
+
const PREVIOUS_ANALYZE_GLOBAL_KEY = "__WEAPP_VITE_PREVIOUS_ANALYZE_RESULT__";
|
|
426
1105
|
const DASHBOARD_EVENTS_GLOBAL_KEY = "__WEAPP_VITE_DASHBOARD_EVENTS__";
|
|
427
1106
|
const ANALYZE_DASHBOARD_PACKAGE_NAME = "@weapp-vite/dashboard";
|
|
428
1107
|
const ANALYZE_SSE_PATH = "/__weapp_vite_analyze";
|
|
1108
|
+
const FILE_CONTENT_PATH = "/__weapp_vite_file_content";
|
|
429
1109
|
const DASHBOARD_EVENT_NAME = "weapp-dashboard:event";
|
|
1110
|
+
const MAX_FILE_CONTENT_BYTES = 2 * 1024 * 1024;
|
|
430
1111
|
const require = createRequire(import.meta.url);
|
|
431
1112
|
function createInstallCommand(agent) {
|
|
432
1113
|
const resolved = resolveCommand(agent ?? "npm", "install", [ANALYZE_DASHBOARD_PACKAGE_NAME]);
|
|
@@ -462,6 +1143,18 @@ function resolveDashboardDistRoot(packageRoot, manifest) {
|
|
|
462
1143
|
if (!fs$1.existsSync(distRoot)) return;
|
|
463
1144
|
return { root: distRoot };
|
|
464
1145
|
}
|
|
1146
|
+
function resolveDashboardDevRoot(packageRoot, manifest) {
|
|
1147
|
+
const devRoot = manifest?.weappViteDashboard?.devRoot;
|
|
1148
|
+
const devConfigFile = manifest?.weappViteDashboard?.devConfigFile;
|
|
1149
|
+
if (!devRoot || !devConfigFile) return;
|
|
1150
|
+
const root = path.resolve(packageRoot, devRoot);
|
|
1151
|
+
const configFile = path.resolve(root, devConfigFile);
|
|
1152
|
+
if (!fs$1.existsSync(root) || !fs$1.existsSync(configFile)) return;
|
|
1153
|
+
return {
|
|
1154
|
+
root,
|
|
1155
|
+
configFile
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
465
1158
|
function resolveDashboardRoot(options) {
|
|
466
1159
|
const resolvePaths = options?.cwd && options.cwd !== process.cwd() ? [options.cwd, process.cwd()] : options?.cwd ? [options.cwd] : void 0;
|
|
467
1160
|
let dashboardPackageRoot;
|
|
@@ -475,18 +1168,121 @@ function resolveDashboardRoot(options) {
|
|
|
475
1168
|
dashboardManifest = void 0;
|
|
476
1169
|
}
|
|
477
1170
|
if (dashboardPackageRoot) {
|
|
1171
|
+
const devResolved = resolveDashboardDevRoot(dashboardPackageRoot, dashboardManifest);
|
|
1172
|
+
if (devResolved) return devResolved;
|
|
478
1173
|
const distResolved = resolveDashboardDistRoot(dashboardPackageRoot, dashboardManifest);
|
|
479
1174
|
if (distResolved) return distResolved;
|
|
480
1175
|
}
|
|
481
1176
|
logger_default.warn(`[weapp-vite ui] 未安装可选仪表盘包 ${colors.bold(colors.green(ANALYZE_DASHBOARD_PACKAGE_NAME))},已自动降级关闭 dashboard 能力。`);
|
|
482
1177
|
logger_default.info(`如需启用,请执行 ${colors.bold(colors.green(createInstallCommand(options?.packageManagerAgent)))}`);
|
|
483
1178
|
}
|
|
484
|
-
function
|
|
1179
|
+
function normalizeDashboardRelativePath(value) {
|
|
1180
|
+
return value.replaceAll("\\", "/");
|
|
1181
|
+
}
|
|
1182
|
+
function stripDashboardFileQuery(value) {
|
|
1183
|
+
const queryIndex = value.indexOf("?");
|
|
1184
|
+
return queryIndex === -1 ? value : value.slice(0, queryIndex);
|
|
1185
|
+
}
|
|
1186
|
+
function addDashboardAllowedPath(paths, value) {
|
|
1187
|
+
if (!value || value.includes("\0")) return;
|
|
1188
|
+
const normalizedPath = normalizeDashboardRelativePath(stripDashboardFileQuery(value));
|
|
1189
|
+
if (!normalizedPath || path.isAbsolute(normalizedPath)) return;
|
|
1190
|
+
paths.add(normalizedPath);
|
|
1191
|
+
}
|
|
1192
|
+
function createDashboardContentAllowlist(result) {
|
|
1193
|
+
const artifactPaths = /* @__PURE__ */ new Set();
|
|
1194
|
+
const sourcePaths = /* @__PURE__ */ new Set();
|
|
1195
|
+
for (const packageReport of result.packages) for (const file of packageReport.files) {
|
|
1196
|
+
addDashboardAllowedPath(artifactPaths, file.file);
|
|
1197
|
+
addDashboardAllowedPath(sourcePaths, file.source);
|
|
1198
|
+
for (const module of file.modules ?? []) addDashboardAllowedPath(sourcePaths, module.source);
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
artifactPaths,
|
|
1202
|
+
sourcePaths
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function resolveDashboardContentPath(root, requestPath, options) {
|
|
1206
|
+
if (!root || !requestPath || requestPath.includes("\0")) return;
|
|
1207
|
+
const normalizedRequestPath = normalizeDashboardRelativePath(stripDashboardFileQuery(requestPath));
|
|
1208
|
+
if (path.isAbsolute(normalizedRequestPath)) return;
|
|
1209
|
+
if (!options.allowedPaths.has(normalizedRequestPath)) return;
|
|
1210
|
+
const resolvedRoot = path.resolve(root);
|
|
1211
|
+
const absolutePath = path.resolve(resolvedRoot, normalizedRequestPath);
|
|
1212
|
+
const relativePath = path.relative(resolvedRoot, absolutePath);
|
|
1213
|
+
if (!relativePath) return;
|
|
1214
|
+
if (!options.allowParent && (relativePath.startsWith("..") || path.isAbsolute(relativePath))) return;
|
|
1215
|
+
return {
|
|
1216
|
+
absolutePath,
|
|
1217
|
+
relativePath: options.allowParent ? normalizedRequestPath : normalizeDashboardRelativePath(relativePath)
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
function resolveDashboardFileLanguage(filePath) {
|
|
1221
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
1222
|
+
if (extension === ".js" || extension === ".mjs" || extension === ".cjs" || extension === ".wxs" || extension === ".sjs") return "javascript";
|
|
1223
|
+
if (extension === ".ts" || extension === ".mts" || extension === ".cts") return "typescript";
|
|
1224
|
+
if (extension === ".json" || extension === ".map") return "json";
|
|
1225
|
+
if (extension === ".css" || extension === ".wxss" || extension === ".scss" || extension === ".sass" || extension === ".less") return "css";
|
|
1226
|
+
if (extension === ".vue" || extension === ".wxml" || extension === ".html") return "html";
|
|
1227
|
+
return "plaintext";
|
|
1228
|
+
}
|
|
1229
|
+
function sendDashboardJson(res, statusCode, payload) {
|
|
1230
|
+
res.statusCode = statusCode;
|
|
1231
|
+
res.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
1232
|
+
res.end(JSON.stringify(payload));
|
|
1233
|
+
}
|
|
1234
|
+
async function sendDashboardFileContent(res, roots, allowlist, kind, requestPath) {
|
|
1235
|
+
const resolved = resolveDashboardContentPath(kind === "artifact" ? roots.artifactRoot : kind === "source" ? roots.sourceRoot : void 0, requestPath, {
|
|
1236
|
+
allowParent: kind === "source",
|
|
1237
|
+
allowedPaths: kind === "artifact" ? allowlist.artifactPaths : allowlist.sourcePaths
|
|
1238
|
+
});
|
|
1239
|
+
if (!resolved || kind !== "source" && kind !== "artifact") {
|
|
1240
|
+
sendDashboardJson(res, 400, {
|
|
1241
|
+
error: "invalid_request",
|
|
1242
|
+
message: "必须传入合法的 kind 和相对路径。"
|
|
1243
|
+
});
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
const stat = await fs$1.promises.stat(resolved.absolutePath);
|
|
1248
|
+
if (!stat.isFile()) {
|
|
1249
|
+
sendDashboardJson(res, 400, {
|
|
1250
|
+
error: "not_file",
|
|
1251
|
+
message: "目标路径不是文件。"
|
|
1252
|
+
});
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (stat.size > MAX_FILE_CONTENT_BYTES) {
|
|
1256
|
+
sendDashboardJson(res, 413, {
|
|
1257
|
+
error: "file_too_large",
|
|
1258
|
+
message: `文件超过 ${MAX_FILE_CONTENT_BYTES} 字节,已拒绝读取。`
|
|
1259
|
+
});
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
sendDashboardJson(res, 200, {
|
|
1263
|
+
kind,
|
|
1264
|
+
path: resolved.relativePath,
|
|
1265
|
+
language: resolveDashboardFileLanguage(resolved.relativePath),
|
|
1266
|
+
size: stat.size,
|
|
1267
|
+
content: await fs$1.promises.readFile(resolved.absolutePath, "utf8")
|
|
1268
|
+
});
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
|
|
1271
|
+
sendDashboardJson(res, code === "ENOENT" ? 404 : 500, {
|
|
1272
|
+
error: code === "ENOENT" ? "not_found" : "read_failed",
|
|
1273
|
+
message: code === "ENOENT" ? "文件不存在。" : "读取文件失败。"
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function createAnalyzeHtmlPlugin(state, runtimeEvents, contentRoots, contentAllowlist, onServerInstance, onBroadcastReady) {
|
|
485
1278
|
const sseClients = /* @__PURE__ */ new Set();
|
|
486
1279
|
const hotBridgeScript = `
|
|
487
1280
|
const applyAnalyzePayload = (payload) => {
|
|
488
|
-
|
|
489
|
-
|
|
1281
|
+
const current = payload?.current ?? payload
|
|
1282
|
+
const previous = payload?.previous ?? window.${PREVIOUS_ANALYZE_GLOBAL_KEY} ?? null
|
|
1283
|
+
window.${ANALYZE_GLOBAL_KEY} = current
|
|
1284
|
+
window.${PREVIOUS_ANALYZE_GLOBAL_KEY} = previous
|
|
1285
|
+
window.dispatchEvent(new CustomEvent('weapp-analyze:update', { detail: { current, previous } }))
|
|
490
1286
|
}
|
|
491
1287
|
const applyDashboardEvents = (payload) => {
|
|
492
1288
|
const events = Array.isArray(payload) ? payload : [payload]
|
|
@@ -532,6 +1328,11 @@ function createAnalyzeHtmlPlugin(state, runtimeEvents, onServerInstance, onBroad
|
|
|
532
1328
|
children: `window.${ANALYZE_GLOBAL_KEY} = ${JSON.stringify(state.current)}`,
|
|
533
1329
|
injectTo: "head-prepend"
|
|
534
1330
|
},
|
|
1331
|
+
{
|
|
1332
|
+
tag: "script",
|
|
1333
|
+
children: `window.${PREVIOUS_ANALYZE_GLOBAL_KEY} = ${JSON.stringify(state.previous)}`,
|
|
1334
|
+
injectTo: "head-prepend"
|
|
1335
|
+
},
|
|
535
1336
|
{
|
|
536
1337
|
tag: "script",
|
|
537
1338
|
children: `window.${DASHBOARD_EVENTS_GLOBAL_KEY} = ${JSON.stringify(runtimeEvents.current)}`,
|
|
@@ -557,7 +1358,12 @@ function createAnalyzeHtmlPlugin(state, runtimeEvents, onServerInstance, onBroad
|
|
|
557
1358
|
configureServer(server) {
|
|
558
1359
|
onServerInstance(server);
|
|
559
1360
|
server.middlewares.use((req, res, next) => {
|
|
560
|
-
|
|
1361
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
1362
|
+
if (url.pathname === FILE_CONTENT_PATH) {
|
|
1363
|
+
sendDashboardFileContent(res, contentRoots, contentAllowlist.current, url.searchParams.get("kind"), url.searchParams.get("path"));
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (url.pathname !== ANALYZE_SSE_PATH) {
|
|
561
1367
|
next();
|
|
562
1368
|
return;
|
|
563
1369
|
}
|
|
@@ -565,7 +1371,7 @@ function createAnalyzeHtmlPlugin(state, runtimeEvents, onServerInstance, onBroad
|
|
|
565
1371
|
res.setHeader("Content-Type", "text/event-stream");
|
|
566
1372
|
res.setHeader("Cache-Control", "no-cache, no-transform");
|
|
567
1373
|
res.setHeader("Connection", "keep-alive");
|
|
568
|
-
res.write(`data: ${JSON.stringify(state
|
|
1374
|
+
res.write(`data: ${JSON.stringify(state)}\n\n`);
|
|
569
1375
|
sseClients.add(res);
|
|
570
1376
|
req.on("close", () => {
|
|
571
1377
|
sseClients.delete(res);
|
|
@@ -604,17 +1410,24 @@ async function startAnalyzeDashboard(result, options) {
|
|
|
604
1410
|
const resolved = resolveDashboardRoot(options);
|
|
605
1411
|
if (!resolved) return;
|
|
606
1412
|
const { root, configFile } = resolved;
|
|
607
|
-
const state = {
|
|
1413
|
+
const state = {
|
|
1414
|
+
current: result,
|
|
1415
|
+
previous: options?.previousResult ?? null
|
|
1416
|
+
};
|
|
1417
|
+
const contentAllowlist = { current: createDashboardContentAllowlist(result) };
|
|
608
1418
|
const runtimeEvents = { current: [createDashboardRuntimeEvent({
|
|
609
1419
|
kind: "command",
|
|
610
1420
|
level: "success",
|
|
611
1421
|
title: options?.watch ? "dashboard watch session started" : "dashboard static session started",
|
|
612
1422
|
detail: options?.watch ? "weapp-vite UI 已进入实时分析模式,后续 analyze 结果会继续推送到 dashboard。" : "weapp-vite UI 已进入静态分析模式,当前页面展示的是一次性分析结果。",
|
|
613
1423
|
tags: options?.watch ? ["watch", "analyze"] : ["static", "analyze"]
|
|
614
|
-
})] };
|
|
1424
|
+
}), ...(options?.initialEvents ?? []).map((event) => createDashboardRuntimeEvent(event))] };
|
|
615
1425
|
let serverRef;
|
|
616
1426
|
let broadcastAnalyzeResult;
|
|
617
|
-
const plugins = [createAnalyzeHtmlPlugin(state, runtimeEvents,
|
|
1427
|
+
const plugins = [createAnalyzeHtmlPlugin(state, runtimeEvents, {
|
|
1428
|
+
artifactRoot: options?.artifactRoot ?? (options?.cwd ? path.resolve(options.cwd, "dist") : void 0),
|
|
1429
|
+
sourceRoot: options?.cwd
|
|
1430
|
+
}, contentAllowlist, (server) => {
|
|
618
1431
|
serverRef = server;
|
|
619
1432
|
}, (broadcast) => {
|
|
620
1433
|
broadcastAnalyzeResult = broadcast;
|
|
@@ -648,7 +1461,7 @@ async function startAnalyzeDashboard(result, options) {
|
|
|
648
1461
|
serverRef.ws.send({
|
|
649
1462
|
type: "custom",
|
|
650
1463
|
event: "weapp-analyze:update",
|
|
651
|
-
data: state
|
|
1464
|
+
data: state
|
|
652
1465
|
});
|
|
653
1466
|
serverRef.ws.send({
|
|
654
1467
|
type: "custom",
|
|
@@ -656,7 +1469,7 @@ async function startAnalyzeDashboard(result, options) {
|
|
|
656
1469
|
data: runtimeEvents.current
|
|
657
1470
|
});
|
|
658
1471
|
}
|
|
659
|
-
broadcastAnalyzeResult?.(state
|
|
1472
|
+
broadcastAnalyzeResult?.(state);
|
|
660
1473
|
const emitRuntimeEvents = (events) => {
|
|
661
1474
|
if (events.length === 0) return;
|
|
662
1475
|
const nextEvents = events.map((event) => createDashboardRuntimeEvent(event));
|
|
@@ -668,8 +1481,10 @@ async function startAnalyzeDashboard(result, options) {
|
|
|
668
1481
|
});
|
|
669
1482
|
};
|
|
670
1483
|
const handle = {
|
|
671
|
-
async update(nextResult) {
|
|
1484
|
+
async update(nextResult, previousResult) {
|
|
1485
|
+
state.previous = previousResult ?? state.current;
|
|
672
1486
|
state.current = nextResult;
|
|
1487
|
+
contentAllowlist.current = createDashboardContentAllowlist(nextResult);
|
|
673
1488
|
emitRuntimeEvents([{
|
|
674
1489
|
kind: "build",
|
|
675
1490
|
level: "info",
|
|
@@ -680,9 +1495,9 @@ async function startAnalyzeDashboard(result, options) {
|
|
|
680
1495
|
if (serverRef) serverRef.ws.send({
|
|
681
1496
|
type: "custom",
|
|
682
1497
|
event: "weapp-analyze:update",
|
|
683
|
-
data:
|
|
1498
|
+
data: state
|
|
684
1499
|
});
|
|
685
|
-
broadcastAnalyzeResult?.(
|
|
1500
|
+
broadcastAnalyzeResult?.(state);
|
|
686
1501
|
},
|
|
687
1502
|
emitRuntimeEvents,
|
|
688
1503
|
waitForExit: () => waitPromise,
|
|
@@ -870,6 +1685,13 @@ function printAnalysisSummary(result) {
|
|
|
870
1685
|
logger_default.info(`- ${segments.join(",")}`);
|
|
871
1686
|
}
|
|
872
1687
|
}
|
|
1688
|
+
const componentUsages = result.components ?? [];
|
|
1689
|
+
if (componentUsages.length > 0) {
|
|
1690
|
+
const suggestions = componentUsages.flatMap((component) => component.suggestions);
|
|
1691
|
+
logger_default.info(`组件依赖:${componentUsages.length} 个组件,${suggestions.length} 条分包优化建议`);
|
|
1692
|
+
for (const suggestion of suggestions.slice(0, 5)) logger_default.info(`- ${suggestion.message}`);
|
|
1693
|
+
if (suggestions.length > 5) logger_default.info(`- …其余 ${suggestions.length - 5} 条组件建议请使用 ${colors.bold(colors.green("weapp-vite analyze --json"))} 查看`);
|
|
1694
|
+
}
|
|
873
1695
|
const duplicates = result.modules.filter((module) => module.packages.length > 1);
|
|
874
1696
|
if (duplicates.length === 0) {
|
|
875
1697
|
logger_default.info("未检测到跨包复用的源码模块。");
|
|
@@ -886,6 +1708,16 @@ function printAnalysisSummary(result) {
|
|
|
886
1708
|
}
|
|
887
1709
|
if (duplicates.length > limit) logger_default.info(`- …其余 ${duplicates.length - limit} 项请使用 ${colors.bold(colors.green("weapp-vite analyze --json"))} 查看`);
|
|
888
1710
|
}
|
|
1711
|
+
function printBudgetCheckSummary(result) {
|
|
1712
|
+
const exceededItems = createAnalyzeBudgetCheck(result).filter((item) => item.status === "exceeded");
|
|
1713
|
+
if (exceededItems.length === 0) {
|
|
1714
|
+
logger_default.success("包体预算检查通过");
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
logger_default.error(`包体预算检查失败:${exceededItems.length} 项超限`);
|
|
1718
|
+
for (const item of exceededItems) logger_default.error(`- ${item.label}:${formatAnalyzeBytes(item.currentBytes)} / ${formatAnalyzeBytes(item.limitBytes)} (${(item.ratio * 100).toFixed(1)}%)`);
|
|
1719
|
+
return true;
|
|
1720
|
+
}
|
|
889
1721
|
function printWebAnalysisSummary(result) {
|
|
890
1722
|
logger_default.success("Web 静态分析完成");
|
|
891
1723
|
logger_default.info(`- 配置状态:${result.web.enabled ? "已启用 weapp.web" : "未启用 weapp.web"}`);
|
|
@@ -899,12 +1731,13 @@ function printWebAnalysisSummary(result) {
|
|
|
899
1731
|
logger_default.warn(`- 未支持范围:${result.unsupportedScopes.join(";")}`);
|
|
900
1732
|
for (const limitation of result.limitations) logger_default.warn(`- 限制:${limitation}`);
|
|
901
1733
|
}
|
|
902
|
-
async function writeAnalyzeResult(result, outputOption, configService) {
|
|
1734
|
+
async function writeAnalyzeResult(result, outputOption, configService, format = "json", previousResult) {
|
|
903
1735
|
if (!outputOption) return;
|
|
904
1736
|
const baseDir = configService.cwd;
|
|
905
1737
|
const resolvedOutputPath = path.isAbsolute(outputOption) ? outputOption : path.resolve(baseDir, outputOption);
|
|
906
1738
|
await fs.ensureDir(path.dirname(resolvedOutputPath));
|
|
907
|
-
|
|
1739
|
+
const content = format === "markdown" && "packages" in result ? createAnalyzeMarkdownReport(result, previousResult) : format === "pr" && "packages" in result ? createAnalyzePrMarkdownReport(result, previousResult) : JSON.stringify(result, null, 2);
|
|
1740
|
+
await fs.writeFile(resolvedOutputPath, `${content}\n`, "utf8");
|
|
908
1741
|
const relativeOutput = configService.relativeCwd(resolvedOutputPath);
|
|
909
1742
|
logger_default.success(`分析结果已写入 ${colors.green(relativeOutput)}`);
|
|
910
1743
|
return resolvedOutputPath;
|
|
@@ -944,10 +1777,15 @@ function printHmrProfileAnalysisSummary(result, configService) {
|
|
|
944
1777
|
}
|
|
945
1778
|
}
|
|
946
1779
|
function registerAnalyzeCommand(cli) {
|
|
947
|
-
cli.command("analyze [root]", "analyze 两端包体与源码映射").option("--hmr-profile [file]", `[string | boolean] 分析 HMR JSONL profile,省略值时优先读取配置,否则回退到默认路径`).option("--json", `[boolean] 输出 JSON 结果`).option("--output <file>", `[string] 将分析结果写入指定文件(JSON)`).option("-p, --platform <platform>", `[string] target platform (weapp | h5)`).option("--project-config <path>", `[string] project config path (miniprogram only)`).action(async (root, options) => {
|
|
1780
|
+
cli.command("analyze [root]", "analyze 两端包体与源码映射").option("--hmr-profile [file]", `[string | boolean] 分析 HMR JSONL profile,省略值时优先读取配置,否则回退到默认路径`).option("--json", `[boolean] 输出 JSON 结果`).option("--markdown", `[boolean] 输出 Markdown 报告`).option("--report <type>", `[string] 输出指定报告类型(pr)`).option("--budget-check", `[boolean] 检查 analyze 预算,超过预算时返回非 0 退出码`).option("--output <file>", `[string] 将分析结果写入指定文件(JSON 或 Markdown)`).option("-p, --platform <platform>", `[string] target platform (weapp | h5)`).option("--project-config <path>", `[string] project config path (miniprogram only)`).action(async (root, options) => {
|
|
948
1781
|
filterDuplicateOptions(options);
|
|
949
1782
|
const configFile = resolveConfigFile(options);
|
|
950
1783
|
const outputJson = coerceBooleanOption(options.json);
|
|
1784
|
+
const outputMarkdown = coerceBooleanOption(options.markdown);
|
|
1785
|
+
const reportType = typeof options.report === "string" ? options.report.trim() : "";
|
|
1786
|
+
const outputPrReport = reportType === "pr";
|
|
1787
|
+
if (reportType && !outputPrReport) throw new Error(`不支持的 analyze report 类型:${reportType}`);
|
|
1788
|
+
const budgetCheck = coerceBooleanOption(options.budgetCheck);
|
|
951
1789
|
const targets = resolveRuntimeTargets(options);
|
|
952
1790
|
const inlineConfig = createInlineConfig(targets.mpPlatform);
|
|
953
1791
|
try {
|
|
@@ -960,7 +1798,7 @@ function registerAnalyzeCommand(cli) {
|
|
|
960
1798
|
projectConfigPath: options.projectConfig
|
|
961
1799
|
});
|
|
962
1800
|
logRuntimeTarget(targets, {
|
|
963
|
-
silent: outputJson,
|
|
1801
|
+
silent: outputJson || outputMarkdown,
|
|
964
1802
|
resolvedConfigPlatform: ctx.configService.platform
|
|
965
1803
|
});
|
|
966
1804
|
const outputOption = typeof options.output === "string" ? options.output.trim() : "";
|
|
@@ -991,15 +1829,26 @@ function registerAnalyzeCommand(cli) {
|
|
|
991
1829
|
logger_default.warn("当前命令不支持该平台,请通过 --platform weapp 或 --platform h5 指定目标。");
|
|
992
1830
|
return;
|
|
993
1831
|
}
|
|
1832
|
+
const previousResult = await readLatestAnalyzeHistorySnapshot(ctx.configService);
|
|
994
1833
|
const result = await analyzeSubpackages(ctx);
|
|
995
|
-
|
|
996
|
-
|
|
1834
|
+
await writeAnalyzeHistorySnapshot(result, ctx.configService);
|
|
1835
|
+
const writtenPath = await writeAnalyzeResult(result, outputOption, ctx.configService, outputPrReport ? "pr" : outputMarkdown ? "markdown" : "json", previousResult);
|
|
1836
|
+
if (outputPrReport) {
|
|
1837
|
+
if (!writtenPath) process.stdout.write(`${createAnalyzePrMarkdownReport(result, previousResult)}\n`);
|
|
1838
|
+
} else if (outputMarkdown) {
|
|
1839
|
+
if (!writtenPath) process.stdout.write(`${createAnalyzeMarkdownReport(result, previousResult)}\n`);
|
|
1840
|
+
} else if (outputJson) {
|
|
997
1841
|
if (!writtenPath) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
998
|
-
}
|
|
1842
|
+
}
|
|
1843
|
+
if (budgetCheck ? printBudgetCheckSummary(result) : false) process.exitCode = 1;
|
|
1844
|
+
if (budgetCheck) return;
|
|
1845
|
+
if (!outputPrReport && !outputMarkdown && !outputJson) {
|
|
999
1846
|
printAnalysisSummary(result);
|
|
1000
1847
|
await startAnalyzeDashboard(result, {
|
|
1848
|
+
artifactRoot: ctx.configService.outDir,
|
|
1001
1849
|
cwd: ctx.configService.cwd,
|
|
1002
|
-
packageManagerAgent: ctx.configService.packageManager.agent
|
|
1850
|
+
packageManagerAgent: ctx.configService.packageManager.agent,
|
|
1851
|
+
previousResult
|
|
1003
1852
|
});
|
|
1004
1853
|
}
|
|
1005
1854
|
} catch (error) {
|
|
@@ -1541,20 +2390,33 @@ function registerBuildCommand(cli) {
|
|
|
1541
2390
|
warningBytes: configService.weappViteConfig.packageSizeWarningBytes
|
|
1542
2391
|
});
|
|
1543
2392
|
if (enableAnalyze) {
|
|
2393
|
+
const analyzeStartedAt = Date.now();
|
|
2394
|
+
const previousAnalyzeResult = await readLatestAnalyzeHistorySnapshot(configService);
|
|
1544
2395
|
const analyzeResult = await analyzeSubpackages(ctx);
|
|
2396
|
+
await writeAnalyzeHistorySnapshot(analyzeResult, configService);
|
|
2397
|
+
const analyzeDurationMs = Date.now() - analyzeStartedAt;
|
|
1545
2398
|
analyzeHandle = await startAnalyzeDashboard(analyzeResult, {
|
|
1546
2399
|
watch: true,
|
|
2400
|
+
artifactRoot: configService.outDir,
|
|
1547
2401
|
cwd: configService.cwd,
|
|
1548
|
-
packageManagerAgent: configService.packageManager.agent
|
|
2402
|
+
packageManagerAgent: configService.packageManager.agent,
|
|
2403
|
+
previousResult: previousAnalyzeResult,
|
|
2404
|
+
initialEvents: [{
|
|
2405
|
+
kind: "build",
|
|
2406
|
+
level: "success",
|
|
2407
|
+
title: "mini build completed",
|
|
2408
|
+
detail: `生产构建已完成,当前 analyze 结果包含 ${analyzeResult.packages.length} 个包。`,
|
|
2409
|
+
durationMs: miniBuildDurationMs,
|
|
2410
|
+
tags: ["build", "mini"]
|
|
2411
|
+
}, {
|
|
2412
|
+
kind: "build",
|
|
2413
|
+
level: "success",
|
|
2414
|
+
title: "analyze completed",
|
|
2415
|
+
detail: `分析已完成,当前包含 ${analyzeResult.packages.length} 个包与 ${analyzeResult.modules.length} 个模块。`,
|
|
2416
|
+
durationMs: analyzeDurationMs,
|
|
2417
|
+
tags: ["build", "analyze"]
|
|
2418
|
+
}]
|
|
1549
2419
|
}) ?? void 0;
|
|
1550
|
-
emitDashboardEvents$1(analyzeHandle, [{
|
|
1551
|
-
kind: "build",
|
|
1552
|
-
level: "success",
|
|
1553
|
-
title: "mini build completed",
|
|
1554
|
-
detail: `生产构建已完成,当前 analyze 结果包含 ${analyzeResult.packages.length} 个包。`,
|
|
1555
|
-
durationMs: miniBuildDurationMs,
|
|
1556
|
-
tags: ["build", "mini"]
|
|
1557
|
-
}]);
|
|
1558
2420
|
}
|
|
1559
2421
|
}
|
|
1560
2422
|
const webConfig = configService.weappWebConfig;
|
|
@@ -2784,7 +3646,7 @@ function resolveRunnableHotkeyDefinition(input) {
|
|
|
2784
3646
|
}
|
|
2785
3647
|
//#endregion
|
|
2786
3648
|
//#region package.json
|
|
2787
|
-
var version = "6.16.
|
|
3649
|
+
var version = "6.16.2";
|
|
2788
3650
|
//#endregion
|
|
2789
3651
|
//#region src/cli/devHotkeys/format.ts
|
|
2790
3652
|
const FULLWIDTH_ASCII_START = 65281;
|
|
@@ -3099,6 +3961,12 @@ async function collectOutputFiles(root) {
|
|
|
3099
3961
|
}
|
|
3100
3962
|
return files;
|
|
3101
3963
|
}
|
|
3964
|
+
function getCompressedSizes(content) {
|
|
3965
|
+
return {
|
|
3966
|
+
gzipSize: gzipSync(content).byteLength,
|
|
3967
|
+
brotliSize: brotliCompressSync(content).byteLength
|
|
3968
|
+
};
|
|
3969
|
+
}
|
|
3102
3970
|
async function analyzeUiFallback(ctx) {
|
|
3103
3971
|
const { configService, scanService } = ctx;
|
|
3104
3972
|
await scanService.loadAppEntry();
|
|
@@ -3136,16 +4004,19 @@ async function analyzeUiFallback(ctx) {
|
|
|
3136
4004
|
const relativeFile = path.relative(distRoot, absoluteFile).replace(REG_DIST_POSIX_SEP, "/");
|
|
3137
4005
|
const packageInfo = classifyPackage(relativeFile);
|
|
3138
4006
|
const stat = await fs$2.stat(absoluteFile);
|
|
4007
|
+
const content = await fs$2.readFile(absoluteFile);
|
|
3139
4008
|
ensurePackage(packageInfo.id, packageInfo.type).files.push({
|
|
3140
4009
|
file: relativeFile,
|
|
3141
4010
|
type: relativeFile.endsWith(".js") ? "chunk" : "asset",
|
|
3142
4011
|
from: packageInfo.type === "independent" ? "independent" : "main",
|
|
3143
4012
|
size: stat.size,
|
|
4013
|
+
...getCompressedSizes(content),
|
|
3144
4014
|
isEntry: relativeFile === "app.js" || REG_DIST_PAGE_ENTRY.test(relativeFile),
|
|
3145
4015
|
source: relativeFile.endsWith(".js") ? void 0 : relativeFile
|
|
3146
4016
|
});
|
|
3147
4017
|
}
|
|
3148
4018
|
return {
|
|
4019
|
+
metadata: createAnalyzeMetadata(configService),
|
|
3149
4020
|
packages: Array.from(packages.values()).sort((a, b) => {
|
|
3150
4021
|
if (a.id === "__main__") return -1;
|
|
3151
4022
|
if (b.id === "__main__") return 1;
|
|
@@ -3166,6 +4037,7 @@ function createAnalyzeController(options) {
|
|
|
3166
4037
|
let analyzeHandle;
|
|
3167
4038
|
const runAnalyze = async () => {
|
|
3168
4039
|
const startedAt = Date.now();
|
|
4040
|
+
const previousResult = await readLatestAnalyzeHistorySnapshot(configService);
|
|
3169
4041
|
try {
|
|
3170
4042
|
const result = await analyzeSubpackages(await createCompilerContext({
|
|
3171
4043
|
key: `serve-ui-analyze:${process.pid}:${++analyzeRunId}`,
|
|
@@ -3178,23 +4050,33 @@ function createAnalyzeController(options) {
|
|
|
3178
4050
|
projectConfigPath: cliOptions.projectConfig,
|
|
3179
4051
|
syncSupportFiles: false
|
|
3180
4052
|
}));
|
|
3181
|
-
if (hasAnalyzeData(result))
|
|
3182
|
-
result,
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
4053
|
+
if (hasAnalyzeData(result)) {
|
|
4054
|
+
await writeAnalyzeHistorySnapshot(result, configService);
|
|
4055
|
+
return {
|
|
4056
|
+
result,
|
|
4057
|
+
previousResult,
|
|
4058
|
+
durationMs: Date.now() - startedAt,
|
|
4059
|
+
mode: "full"
|
|
4060
|
+
};
|
|
4061
|
+
}
|
|
3186
4062
|
} catch (error) {
|
|
3187
4063
|
const message = error instanceof Error ? error.message : String(error);
|
|
3188
4064
|
logger_default.warn(`[ui] 完整分析失败,已回退到 dist 文件扫描:${message}`);
|
|
4065
|
+
const result = await analyzeUiFallback(ctx);
|
|
4066
|
+
await writeAnalyzeHistorySnapshot(result, configService);
|
|
3189
4067
|
return {
|
|
3190
|
-
result
|
|
4068
|
+
result,
|
|
4069
|
+
previousResult,
|
|
3191
4070
|
durationMs: Date.now() - startedAt,
|
|
3192
4071
|
mode: "fallback",
|
|
3193
4072
|
fallbackReason: message
|
|
3194
4073
|
};
|
|
3195
4074
|
}
|
|
4075
|
+
const result = await analyzeUiFallback(ctx);
|
|
4076
|
+
await writeAnalyzeHistorySnapshot(result, configService);
|
|
3196
4077
|
return {
|
|
3197
|
-
result
|
|
4078
|
+
result,
|
|
4079
|
+
previousResult,
|
|
3198
4080
|
durationMs: Date.now() - startedAt,
|
|
3199
4081
|
mode: "fallback",
|
|
3200
4082
|
fallbackReason: "完整分析结果为空,已回退到 dist 文件扫描。"
|
|
@@ -3218,7 +4100,7 @@ function createAnalyzeController(options) {
|
|
|
3218
4100
|
durationMs: next.durationMs,
|
|
3219
4101
|
tags: ["analyze", "fallback"]
|
|
3220
4102
|
}]);
|
|
3221
|
-
await analyzeHandle.update(next.result);
|
|
4103
|
+
await analyzeHandle.update(next.result, next.previousResult);
|
|
3222
4104
|
emitDashboardEvents(analyzeHandle, [{
|
|
3223
4105
|
kind: next.mode === "fallback" ? "diagnostic" : "build",
|
|
3224
4106
|
level: next.mode === "fallback" ? "warning" : "success",
|
|
@@ -3251,9 +4133,23 @@ function createAnalyzeController(options) {
|
|
|
3251
4133
|
const initialAnalyze = await runAnalyze();
|
|
3252
4134
|
analyzeHandle = await startDashboard(initialAnalyze.result, {
|
|
3253
4135
|
watch: true,
|
|
4136
|
+
artifactRoot: configService.outDir,
|
|
3254
4137
|
cwd: configService.cwd,
|
|
3255
4138
|
packageManagerAgent: configService.packageManager.agent,
|
|
3256
|
-
silentStartupLog: true
|
|
4139
|
+
silentStartupLog: true,
|
|
4140
|
+
previousResult: initialAnalyze.previousResult,
|
|
4141
|
+
initialEvents: [{
|
|
4142
|
+
kind: initialAnalyze.mode === "fallback" ? "diagnostic" : "build",
|
|
4143
|
+
level: initialAnalyze.mode === "fallback" ? "warning" : "success",
|
|
4144
|
+
title: initialAnalyze.mode === "fallback" ? "initial analyze fallback completed" : "initial analyze completed",
|
|
4145
|
+
detail: initialAnalyze.mode === "fallback" ? initialAnalyze.fallbackReason ?? "完整分析不可用,已回退到 dist 文件扫描。" : `开发态首份 analyze 结果已生成,包含 ${initialAnalyze.result.packages.length} 个包与 ${initialAnalyze.result.modules.length} 个模块。`,
|
|
4146
|
+
durationMs: initialAnalyze.durationMs,
|
|
4147
|
+
tags: initialAnalyze.mode === "fallback" ? [
|
|
4148
|
+
"analyze",
|
|
4149
|
+
"fallback",
|
|
4150
|
+
"initial"
|
|
4151
|
+
] : ["analyze", "initial"]
|
|
4152
|
+
}]
|
|
3257
4153
|
}) ?? void 0;
|
|
3258
4154
|
emitDashboardEvents(analyzeHandle, [{
|
|
3259
4155
|
kind: "command",
|
|
@@ -3413,10 +4309,23 @@ function registerServeCommand(cli) {
|
|
|
3413
4309
|
if (targets.runMini) {
|
|
3414
4310
|
const miniBuildStartedAt = Date.now();
|
|
3415
4311
|
const buildResult = await buildService.build(options);
|
|
3416
|
-
|
|
4312
|
+
const miniBuildDurationMs = Date.now() - miniBuildStartedAt;
|
|
4313
|
+
logger_default.success(`小程序初次构建完成,耗时:${formatDuration(miniBuildDurationMs)}`);
|
|
3417
4314
|
if (enableAnalyze) {
|
|
3418
4315
|
await analyzeController.startDashboard(startAnalyzeDashboard);
|
|
3419
4316
|
analyzeHandle = analyzeController.getHandle();
|
|
4317
|
+
analyzeController.emitRuntimeEvents([{
|
|
4318
|
+
kind: "build",
|
|
4319
|
+
level: "success",
|
|
4320
|
+
title: "mini initial build completed",
|
|
4321
|
+
detail: "小程序开发态初次构建已完成,dashboard 可继续监听后续刷新。",
|
|
4322
|
+
durationMs: miniBuildDurationMs,
|
|
4323
|
+
tags: [
|
|
4324
|
+
"dev",
|
|
4325
|
+
"mini",
|
|
4326
|
+
"initial"
|
|
4327
|
+
]
|
|
4328
|
+
}]);
|
|
3420
4329
|
await analyzeController.bindWatcher(buildResult).runInitialUpdate();
|
|
3421
4330
|
}
|
|
3422
4331
|
}
|