weapp-vite 7.4.0 → 7.5.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 +51 -1
- package/bin/bootstrap.js +14 -1
- package/dist/adapters-CcHrsYaj.mjs +1 -0
- package/dist/analyze-ZJUvEdaY.mjs +248 -0
- package/dist/auto-import-components/resolvers.mjs +1 -2
- package/dist/auto-import-presets/wevu-router.mjs +1 -2
- package/dist/auto-import-presets/wevu.mjs +1 -2
- package/dist/auto-routes.mjs +2 -1
- package/dist/budget-BD7VqLCl.d.mts +178 -0
- package/dist/chunkStrategy-DRA5WpB-.mjs +1436 -0
- package/dist/cli-acceptance.d.mts +1 -0
- package/dist/cli-acceptance.mjs +21 -0
- package/dist/cli.mjs +1420 -1554
- package/dist/compatibility.mjs +1 -2
- package/dist/{config-Dbvj5yK7.d.mts → config-V0L-IAJf.d.mts} +87 -30
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +1 -1
- package/dist/{constants-WOsG3Eqd.mjs → constants-DEZdcsMn.mjs} +1 -1
- package/dist/{createContext-UOEgeYnA.mjs → createCompilerContextInstance-DJH_P_nZ.mjs} +31278 -32148
- package/dist/createContext-BqYzHhY9.mjs +12 -0
- package/dist/dashboard/analyze.d.mts +48 -0
- package/dist/dashboard/analyze.mjs +1 -0
- package/dist/dashboard/vite.d.mts +1 -1
- package/dist/dashboard-DvqF3Key.mjs +1842 -0
- package/dist/dashboard.d.mts +3 -2
- package/dist/dashboard.mjs +1 -2
- package/dist/dashboardViteBridge-Dy3nAD4m.mjs +299 -0
- package/dist/dev-fR5GiJRO.mjs +63 -0
- package/dist/devProvider-BGBkw28J.mjs +1050 -0
- package/dist/docs/README.md +51 -1
- package/dist/docs/ai-workflows.md +4 -0
- package/dist/docs/doctor.md +38 -0
- package/dist/docs/getting-started.md +3 -1
- package/dist/docs/index.md +3 -1
- package/dist/docs/mcp.md +94 -1
- package/dist/docs/testing.md +29 -2
- package/dist/docs/troubleshooting.md +8 -0
- package/dist/docs/uni-app-component-libraries.md +8 -7
- package/dist/docs/upload.md +40 -3
- package/dist/docs/vite-plugin.md +171 -0
- package/dist/docs/vue-sfc.md +19 -0
- package/dist/docs/weapp-config.md +54 -0
- package/dist/docs/wevu-authoring.md +10 -0
- package/dist/doctor-worker.d.mts +1 -0
- package/dist/doctor-worker.mjs +59 -0
- package/dist/doctor.d.mts +144 -0
- package/dist/doctor.mjs +848 -0
- package/dist/eslint.mjs +1 -2
- package/dist/file-DRb4eWqw.mjs +1 -0
- package/dist/{file-giE9UMZa.mjs → file-sC3jD7Fh.mjs} +15 -9
- package/dist/files-BqxFIPRp.mjs +45 -0
- package/dist/getInstance-DiGSzBNI.mjs +28 -0
- package/dist/getInstance-SiE6WZRh.mjs +1 -0
- package/dist/index-C9HjUX5c.d.mts +741 -0
- package/dist/index.d.mts +10 -8
- package/dist/index.mjs +3 -2
- package/dist/json.d.mts +1 -1
- package/dist/json.mjs +1 -2
- package/dist/lifecycle-BROTlwSL.mjs +320 -0
- package/dist/loadConfig-C3BQhz7F.mjs +59 -0
- package/dist/loadConfig-VGtBF4qm.mjs +1 -0
- package/dist/logger-DGn5iEVN.mjs +1 -0
- package/dist/{mcp-BG6TliEg.mjs → mcp-Cj8Maqo6.mjs} +16 -7
- package/dist/mcp-D3Yh24Cj.mjs +529 -0
- package/dist/mcp.d.mts +6 -3
- package/dist/mcp.mjs +1 -2
- package/dist/package-7swhyLmf.mjs +91 -0
- package/dist/path-doV773b7.mjs +103 -0
- package/dist/{platform-DTWs5QRi.mjs → platform-CnkySBKz.mjs} +1 -1
- package/dist/processCleanup-B6KA30Zc.mjs +77 -0
- package/dist/protocol-vg8HGJ4n.mjs +138 -0
- package/dist/{resolvers-icNxkpcy.mjs → resolvers-Dod-euqX.mjs} +4 -1
- package/dist/resolvers.mjs +1 -2
- package/dist/runtime.mjs +1 -2
- package/dist/session-PskhD-nZ.mjs +390 -0
- package/dist/test.d.mts +1 -0
- package/dist/test.mjs +201 -44
- package/dist/types.d.mts +2 -2
- package/dist/{upload-D0RcEWrq.mjs → upload-D4msblBf.mjs} +174 -15
- package/dist/upload-worker.mjs +136 -3
- package/dist/utils-CJSCgEj6.mjs +1094 -0
- package/dist/vite.d.mts +9 -0
- package/dist/vite.mjs +270 -0
- package/dist/volar.d.mts +2 -2
- package/dist/web-apis.mjs +1 -2
- package/package.json +67 -41
- package/dist/adapters-BwnMMIZR.mjs +0 -2
- package/dist/dashboard-BzaVesmQ.mjs +0 -558
- package/dist/file-D19nbDoN.mjs +0 -2
- package/dist/getInstance-CYF1vl5l.mjs +0 -2
- package/dist/index-DbqVupg0.d.mts +0 -246
- package/dist/logger-mt4mSTqV.mjs +0 -2
package/dist/doctor.mjs
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { l as getProjectPlatformOptions, m as resolveMiniPlatform, o as getPlatformOutputExtensions } from "./platform-CnkySBKz.mjs";
|
|
2
|
+
import { i as createAnalyzeBudgetCheck } from "./analyze-ZJUvEdaY.mjs";
|
|
3
|
+
import { n as classifyPackage, t as version } from "./package-7swhyLmf.mjs";
|
|
4
|
+
import { n as parseObject, r as readDoctorFiles, t as objectValue } from "./files-BqxFIPRp.mjs";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { constants } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import path, { posix } from "node:path";
|
|
9
|
+
import { parseJsLike, traverse } from "@weapp-vite/ast/babel";
|
|
10
|
+
import { access, readFile, stat } from "node:fs/promises";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
import { Parser } from "htmlparser2";
|
|
13
|
+
import postcss from "postcss";
|
|
14
|
+
import valueParser from "postcss-value-parser";
|
|
15
|
+
import { fork } from "node:child_process";
|
|
16
|
+
import { connectOpenedAutomator, queryWechatIdeLogin, resolveProjectAutomatorPort } from "weapp-ide-cli";
|
|
17
|
+
import { createConnection } from "node:net";
|
|
18
|
+
import { miniProgramRuntimePlugin, wevuCompatibilityPlugin } from "@weapp-vite/eslint";
|
|
19
|
+
import tsParser from "@typescript-eslint/parser";
|
|
20
|
+
import { Linter } from "eslint";
|
|
21
|
+
import vueParser from "vue-eslint-parser";
|
|
22
|
+
import { OperationLifecycle, classifyOperationError } from "@weapp-vite/miniprogram-automator";
|
|
23
|
+
//#region src/doctor/report.ts
|
|
24
|
+
function addDiagnostic(report, diagnostic) {
|
|
25
|
+
const fingerprint = createHash("sha256").update(JSON.stringify([
|
|
26
|
+
diagnostic.ruleId,
|
|
27
|
+
diagnostic.target,
|
|
28
|
+
diagnostic.layer,
|
|
29
|
+
diagnostic.location,
|
|
30
|
+
diagnostic.evidence
|
|
31
|
+
])).digest("hex");
|
|
32
|
+
if (!report.diagnostics.some((item) => item.fingerprint === fingerprint)) report.diagnostics.push({
|
|
33
|
+
...diagnostic,
|
|
34
|
+
fingerprint
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function finishReport(report) {
|
|
38
|
+
report.diagnostics.sort((a, b) => a.fingerprint.localeCompare(b.fingerprint));
|
|
39
|
+
report.exitCode = report.coverage.some((item) => item.status === "incomplete") ? 2 : report.diagnostics.some((item) => item.severity === "error") ? 1 : 0;
|
|
40
|
+
return report;
|
|
41
|
+
}
|
|
42
|
+
function formatDoctorReport(report, format = "terminal") {
|
|
43
|
+
if (format === "json") return JSON.stringify(report, null, 2);
|
|
44
|
+
if (format === "sarif") return JSON.stringify({
|
|
45
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
46
|
+
version: "2.1.0",
|
|
47
|
+
runs: [{
|
|
48
|
+
tool: { driver: {
|
|
49
|
+
name: "weapp-vite doctor",
|
|
50
|
+
rules: [...new Set(report.diagnostics.map((d) => d.ruleId))].map((id) => ({ id }))
|
|
51
|
+
} },
|
|
52
|
+
invocations: [{ executionSuccessful: report.exitCode !== 2 }],
|
|
53
|
+
properties: {
|
|
54
|
+
coverage: report.coverage,
|
|
55
|
+
exitCode: report.exitCode,
|
|
56
|
+
artifacts: report.artifacts,
|
|
57
|
+
runtime: report.runtime
|
|
58
|
+
},
|
|
59
|
+
results: report.diagnostics.map((d) => ({
|
|
60
|
+
ruleId: d.ruleId,
|
|
61
|
+
level: d.severity,
|
|
62
|
+
message: { text: d.message },
|
|
63
|
+
partialFingerprints: { "doctor/v1": d.fingerprint },
|
|
64
|
+
locations: d.location ? [{ physicalLocation: {
|
|
65
|
+
artifactLocation: { uri: d.location.file.split("/").map(encodeURIComponent).join("/") },
|
|
66
|
+
region: d.location.line ? {
|
|
67
|
+
startLine: d.location.line,
|
|
68
|
+
startColumn: d.location.column ?? 1
|
|
69
|
+
} : void 0
|
|
70
|
+
} }] : [],
|
|
71
|
+
properties: {
|
|
72
|
+
target: d.target,
|
|
73
|
+
layer: d.layer,
|
|
74
|
+
evidence: d.evidence,
|
|
75
|
+
responsibility: d.responsibility,
|
|
76
|
+
suggestion: d.suggestion
|
|
77
|
+
}
|
|
78
|
+
}))
|
|
79
|
+
}]
|
|
80
|
+
}, null, 2);
|
|
81
|
+
return [
|
|
82
|
+
`Doctor: exit ${report.exitCode}`,
|
|
83
|
+
...report.coverage.map((c) => `[${c.target}] ${c.layer}/${c.check}: ${c.status}${c.reason ? ` — ${c.reason}` : ""}`),
|
|
84
|
+
...Object.entries(report.runtime).flatMap(([target, evidence]) => evidence.bundle ? [
|
|
85
|
+
`[${target}] framework=${evidence.bundle.frameworkVersion} node=${evidence.bundle.nodeVersion} IDE=${evidence.bundle.versions.ide ?? "unknown"} SDK=${evidence.bundle.versions.sdk ?? "unknown"}`,
|
|
86
|
+
`最后成功阶段:${evidence.bundle.lastSuccessfulStage ?? "unknown"}`,
|
|
87
|
+
`复现:${evidence.bundle.reproduction}`
|
|
88
|
+
] : []),
|
|
89
|
+
...report.diagnostics.map((d) => `[${d.severity}] ${d.target} ${d.ruleId} ${d.location?.file ?? ""} ${d.message}\n ${d.suggestion}`)
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/doctor/source.ts
|
|
94
|
+
function checkDoctorScripts(report, target, files, layer) {
|
|
95
|
+
const linter = new Linter();
|
|
96
|
+
let incomplete = false;
|
|
97
|
+
const eligible = files.filter((file) => layer === "artifact" ? /\.js$/.test(file.path) : /\.(?:[cm]?[jt]sx?|vue)$/.test(file.path) && !/(?:^|\/)(?:scripts|generated|node_modules)\/|\.web\.|(?:^|\/)node\.|\.(?:test|spec)\.|\.d\.[cm]?ts$/.test(file.path));
|
|
98
|
+
for (const file of eligible) {
|
|
99
|
+
const messages = linter.verify(file.text ?? "", {
|
|
100
|
+
files: ["**/*.{js,mjs,cjs,jsx,ts,mts,cts,tsx,vue}"],
|
|
101
|
+
languageOptions: {
|
|
102
|
+
parser: file.path.endsWith(".vue") ? vueParser : tsParser,
|
|
103
|
+
parserOptions: {
|
|
104
|
+
parser: tsParser,
|
|
105
|
+
ecmaVersion: "latest",
|
|
106
|
+
sourceType: "module",
|
|
107
|
+
ecmaFeatures: { jsx: true }
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
plugins: {
|
|
111
|
+
"mini-program": miniProgramRuntimePlugin,
|
|
112
|
+
"wevu": wevuCompatibilityPlugin
|
|
113
|
+
},
|
|
114
|
+
rules: {
|
|
115
|
+
"no-eval": "error",
|
|
116
|
+
"no-new-func": "error",
|
|
117
|
+
...target === "weapp" ? {
|
|
118
|
+
"mini-program/no-unsupported-runtime-api": layer === "artifact" ? "error" : "warn",
|
|
119
|
+
"mini-program/no-implicit-runtime-polyfill": "warn"
|
|
120
|
+
} : {},
|
|
121
|
+
...layer === "source" ? {
|
|
122
|
+
"wevu/no-unsupported-api": "warn",
|
|
123
|
+
"wevu/no-risky-api": "warn",
|
|
124
|
+
"wevu/no-unsupported-template-feature": "warn"
|
|
125
|
+
} : {}
|
|
126
|
+
}
|
|
127
|
+
}, { filename: file.path });
|
|
128
|
+
for (const message of messages) {
|
|
129
|
+
if (message.fatal) incomplete = true;
|
|
130
|
+
addDiagnostic(report, {
|
|
131
|
+
ruleId: `doctor/${layer}/${message.ruleId ?? "parse"}`,
|
|
132
|
+
layer,
|
|
133
|
+
target,
|
|
134
|
+
severity: message.severity === 2 ? "error" : "warning",
|
|
135
|
+
message: message.message,
|
|
136
|
+
location: {
|
|
137
|
+
file: file.path,
|
|
138
|
+
line: message.line,
|
|
139
|
+
column: message.column
|
|
140
|
+
},
|
|
141
|
+
evidence: {
|
|
142
|
+
expected: "可解析且符合所选执行域的静态契约",
|
|
143
|
+
actual: message.message
|
|
144
|
+
},
|
|
145
|
+
responsibility: {
|
|
146
|
+
owner: "unknown",
|
|
147
|
+
confidence: layer === "source" ? "suspected" : "confirmed"
|
|
148
|
+
},
|
|
149
|
+
suggestion: layer === "source" ? "核对编译转换与兼容层,再检查最终产物;源码风险不等同于宿主失败。" : "定位对应源码及构建插件,保留最小复现并在目标宿主验证。"
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
report.coverage.push({
|
|
154
|
+
target,
|
|
155
|
+
layer,
|
|
156
|
+
check: "script-syntax-and-static-risks",
|
|
157
|
+
status: incomplete ? "incomplete" : "complete",
|
|
158
|
+
reason: `检查 ${eligible.length} 个脚本;不推断动态类型或将静态扫描等同于完整运行正确性。`
|
|
159
|
+
});
|
|
160
|
+
if (target !== "weapp") report.coverage.push({
|
|
161
|
+
target,
|
|
162
|
+
layer,
|
|
163
|
+
check: "host-api-baseline",
|
|
164
|
+
status: "incomplete",
|
|
165
|
+
reason: "所选平台尚无可复用的完整 API 基线;未套用微信规则。"
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function incompleteCoverage(report, target, layer, check, reason) {
|
|
169
|
+
report.coverage.push({
|
|
170
|
+
target,
|
|
171
|
+
layer,
|
|
172
|
+
check,
|
|
173
|
+
status: "incomplete",
|
|
174
|
+
reason
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
//#endregion
|
|
178
|
+
//#region src/doctor/references.ts
|
|
179
|
+
function checkArtifactReferences(report, target, snapshot, classifier) {
|
|
180
|
+
const files = new Set(snapshot.files.map((file) => file.path));
|
|
181
|
+
const ext = getPlatformOutputExtensions(target);
|
|
182
|
+
const reference = (owner, specifier, script = false) => {
|
|
183
|
+
if (/^[a-z]+:\/\//i.test(specifier) || specifier.includes("{{")) {
|
|
184
|
+
incompleteCoverage(report, target, "artifact", "references", "动态或外部引用不能由本地静态快照验证。");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const base = posix.normalize(specifier.startsWith("/") ? specifier.slice(1) : posix.join(posix.dirname(owner), specifier));
|
|
188
|
+
const found = (script ? [
|
|
189
|
+
base,
|
|
190
|
+
`${base}.js`,
|
|
191
|
+
`${base}.json`,
|
|
192
|
+
`${base}/index.js`
|
|
193
|
+
] : [base]).find((candidate) => files.has(candidate));
|
|
194
|
+
const from = classifyPackage(owner, "main", classifier);
|
|
195
|
+
const to = found && classifyPackage(found, "main", classifier);
|
|
196
|
+
const crosses = to && (from.type === "independent" && from.id !== to.id || to.type !== "main" && from.id !== to.id);
|
|
197
|
+
if (!found || crosses) addDiagnostic(report, {
|
|
198
|
+
ruleId: `doctor/artifact/${crosses ? "package-import" : "missing-import"}`,
|
|
199
|
+
target,
|
|
200
|
+
layer: "artifact",
|
|
201
|
+
severity: "error",
|
|
202
|
+
message: crosses ? "产物引用跨越包边界" : "产物引用指向不存在的文件",
|
|
203
|
+
location: { file: owner },
|
|
204
|
+
evidence: {
|
|
205
|
+
expected: "引用可在所属包及允许的依赖范围内解析",
|
|
206
|
+
actual: specifier
|
|
207
|
+
},
|
|
208
|
+
responsibility: {
|
|
209
|
+
owner: "unknown",
|
|
210
|
+
confidence: "confirmed"
|
|
211
|
+
},
|
|
212
|
+
suggestion: "检查最终产物路径、native npm 输出目录及独立分包归属;不要只修改源码导入规避编译器问题。"
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
for (const file of snapshot.files) {
|
|
216
|
+
if (file.text === void 0) continue;
|
|
217
|
+
try {
|
|
218
|
+
if (file.path.endsWith(`.${ext.js}`)) {
|
|
219
|
+
const ast = parseJsLike(file.text);
|
|
220
|
+
traverse(ast, {
|
|
221
|
+
ImportDeclaration(p) {
|
|
222
|
+
reference(file.path, p.node.source.value, true);
|
|
223
|
+
},
|
|
224
|
+
ExportNamedDeclaration(p) {
|
|
225
|
+
if (p.node.source) reference(file.path, p.node.source.value, true);
|
|
226
|
+
},
|
|
227
|
+
ExportAllDeclaration(p) {
|
|
228
|
+
reference(file.path, p.node.source.value, true);
|
|
229
|
+
},
|
|
230
|
+
CallExpression(p) {
|
|
231
|
+
if (!p.get("callee").isIdentifier({ name: "require" }) || p.scope.getBinding("require")) return;
|
|
232
|
+
const arg = p.node.arguments[0];
|
|
233
|
+
if (arg?.type === "StringLiteral") reference(file.path, arg.value, true);
|
|
234
|
+
else incompleteCoverage(report, target, "artifact", "dynamic-import", "动态 require 无法由静态快照解析。");
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
} else if (file.path.endsWith(`.${ext.wxml}`)) new Parser({ onopentag(name, attributes) {
|
|
238
|
+
if ([
|
|
239
|
+
"import",
|
|
240
|
+
"include",
|
|
241
|
+
"wxs",
|
|
242
|
+
"sjs",
|
|
243
|
+
"import-sjs"
|
|
244
|
+
].includes(name) && attributes.src) reference(file.path, attributes.src);
|
|
245
|
+
} }, { xmlMode: true }).end(file.text);
|
|
246
|
+
else if (file.path.endsWith(`.${ext.wxss}`)) postcss.parse(file.text).walkAtRules("import", (rule) => {
|
|
247
|
+
const first = valueParser(rule.params).nodes[0];
|
|
248
|
+
const value = first?.type === "string" ? first.value : void 0;
|
|
249
|
+
if (value) reference(file.path, value);
|
|
250
|
+
else incompleteCoverage(report, target, "artifact", "style-import", "非字符串样式导入需要目标宿主或适配器验证。");
|
|
251
|
+
});
|
|
252
|
+
} catch {
|
|
253
|
+
incompleteCoverage(report, target, "artifact", "reference-parse", "产物无法解析,未将引用图视为完整。");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
report.coverage.push({
|
|
257
|
+
target,
|
|
258
|
+
layer: "artifact",
|
|
259
|
+
check: "static-references",
|
|
260
|
+
status: "complete",
|
|
261
|
+
reason: "覆盖静态脚本、模板和样式引用;动态/外部引用另记 incomplete。"
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/doctor/artifact.ts
|
|
266
|
+
function checkDoctorArtifact(report, target, snapshot) {
|
|
267
|
+
const files = new Map(snapshot.files.map((file) => [file.path, file]));
|
|
268
|
+
const ext = getPlatformOutputExtensions(target);
|
|
269
|
+
const fail = (rule, file, expected, actual) => addDiagnostic(report, {
|
|
270
|
+
ruleId: `doctor/artifact/${rule}`,
|
|
271
|
+
target,
|
|
272
|
+
layer: "artifact",
|
|
273
|
+
severity: "error",
|
|
274
|
+
message: `${file}: ${expected}`,
|
|
275
|
+
location: { file },
|
|
276
|
+
evidence: {
|
|
277
|
+
expected,
|
|
278
|
+
actual
|
|
279
|
+
},
|
|
280
|
+
responsibility: {
|
|
281
|
+
owner: "unknown",
|
|
282
|
+
confidence: "confirmed"
|
|
283
|
+
},
|
|
284
|
+
suggestion: "比较来源声明和最终编译产物,确定配置、插件或编译器责任后补充最小回归。"
|
|
285
|
+
});
|
|
286
|
+
const readJson = (file) => {
|
|
287
|
+
try {
|
|
288
|
+
return parseObject(files.get(file)?.text);
|
|
289
|
+
} catch {
|
|
290
|
+
fail("json", file, "存在合法的 JSON 对象", files.has(file) ? "无法解析 JSON 对象" : "文件缺失");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const app = readJson("app.json");
|
|
295
|
+
if (!app) {
|
|
296
|
+
incompleteCoverage(report, target, "artifact", "reachable-graph", "app.json 不可读,无法遍历页面图。");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (!files.has(`app.${ext.js}`)) fail("entry-file", `app.${ext.js}`, "应用入口脚本存在", "文件缺失");
|
|
300
|
+
const packages = Array.isArray(app.subPackages ?? app.subpackages) ? app.subPackages ?? app.subpackages : [];
|
|
301
|
+
if ((app.subPackages ?? app.subpackages) !== void 0 && !Array.isArray(app.subPackages ?? app.subpackages)) fail("subpackage", "app.json", "subPackages 为数组", "非法分包声明");
|
|
302
|
+
const subPackageRoots = /* @__PURE__ */ new Set();
|
|
303
|
+
const independentRoots = /* @__PURE__ */ new Set();
|
|
304
|
+
const pages = [];
|
|
305
|
+
const addPages = (value, root = "") => {
|
|
306
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
307
|
+
fail("pages", "app.json", "pages 为字符串数组", "页面列表缺失或类型错误");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
for (const item of value) pages.push(root ? `${root}/${item}` : item);
|
|
311
|
+
};
|
|
312
|
+
addPages(app.pages);
|
|
313
|
+
for (const value of packages) {
|
|
314
|
+
const pkg = objectValue(value);
|
|
315
|
+
if (!pkg || typeof pkg.root !== "string" || !pkg.root || posix.normalize(pkg.root) !== pkg.root || /^[./]/.test(pkg.root)) {
|
|
316
|
+
fail("subpackage", "app.json", "分包 root 为规范的相对路径", "非法分包声明");
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
subPackageRoots.add(pkg.root);
|
|
320
|
+
if (pkg.independent === true) independentRoots.add(pkg.root);
|
|
321
|
+
addPages(pkg.pages, pkg.root);
|
|
322
|
+
}
|
|
323
|
+
const classifier = {
|
|
324
|
+
subPackageRoots,
|
|
325
|
+
independentRoots
|
|
326
|
+
};
|
|
327
|
+
const checked = /* @__PURE__ */ new Set();
|
|
328
|
+
const queued = pages.map((base) => ({
|
|
329
|
+
base,
|
|
330
|
+
component: false
|
|
331
|
+
}));
|
|
332
|
+
const resolveReference = (owner, reference) => posix.normalize(reference.startsWith("/") ? reference.slice(1) : posix.join(posix.dirname(owner), reference));
|
|
333
|
+
const checkComponents = (owner, config) => {
|
|
334
|
+
const components = objectValue(config.usingComponents);
|
|
335
|
+
if (config.usingComponents !== void 0 && !components) fail("components", owner, "usingComponents 为路径映射", "非法组件映射");
|
|
336
|
+
for (const [name, value] of Object.entries(components ?? {})) {
|
|
337
|
+
if (typeof value !== "string") {
|
|
338
|
+
fail("components", owner, "组件引用为字符串", name);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (/^[a-z]+:\/\//i.test(value)) {
|
|
342
|
+
incompleteCoverage(report, target, "artifact", "external-component", "外部插件组件不在本地产物快照中。");
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const base = resolveReference(owner, value);
|
|
346
|
+
const from = classifyPackage(owner, "main", classifier);
|
|
347
|
+
const to = classifyPackage(base, "main", classifier);
|
|
348
|
+
if (from.type === "independent" && from.id !== to.id || to.type !== "main" && from.id !== to.id) fail("package-boundary", owner, "组件引用不跨越独立包或其他分包边界", base);
|
|
349
|
+
queued.push({
|
|
350
|
+
base,
|
|
351
|
+
component: true
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
checkComponents("app.json", app);
|
|
356
|
+
while (queued.length) {
|
|
357
|
+
const { base, component } = queued.shift();
|
|
358
|
+
if (base.startsWith("../") || base.startsWith("/") || posix.normalize(base) !== base) {
|
|
359
|
+
fail("path", "app.json", "引用位于产物根目录内且已规范化", base);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (checked.has(base)) continue;
|
|
363
|
+
checked.add(base);
|
|
364
|
+
for (const extension of [
|
|
365
|
+
ext.js,
|
|
366
|
+
ext.json,
|
|
367
|
+
ext.wxml
|
|
368
|
+
]) if (!files.has(`${base}.${extension}`)) fail("entry-file", `${base}.${extension}`, "页面/组件的宿主必需文件存在", "文件缺失");
|
|
369
|
+
const config = readJson(`${base}.${ext.json}`);
|
|
370
|
+
if (config) {
|
|
371
|
+
if (component && config.component !== true) fail("component-flag", `${base}.${ext.json}`, "被引用的组件声明 component: true", "未声明组件");
|
|
372
|
+
checkComponents(`${base}.${ext.json}`, config);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (typeof app.workers === "string" && !snapshot.files.some((file) => file.path.startsWith(`${app.workers}/`) && file.path.endsWith(ext.js))) fail("worker-root", "app.json", "workers 目录内有脚本产物", app.workers);
|
|
376
|
+
const packageMap = /* @__PURE__ */ new Map();
|
|
377
|
+
for (const file of snapshot.files) {
|
|
378
|
+
const pkg = classifyPackage(file.path, "main", classifier);
|
|
379
|
+
const current = packageMap.get(pkg.id) ?? {
|
|
380
|
+
...pkg,
|
|
381
|
+
files: []
|
|
382
|
+
};
|
|
383
|
+
current.files.push({
|
|
384
|
+
file: file.path,
|
|
385
|
+
size: file.size,
|
|
386
|
+
type: file.path.endsWith(ext.js) ? "chunk" : "asset",
|
|
387
|
+
from: "main"
|
|
388
|
+
});
|
|
389
|
+
packageMap.set(pkg.id, current);
|
|
390
|
+
}
|
|
391
|
+
if (snapshot.budgets) {
|
|
392
|
+
for (const item of createAnalyzeBudgetCheck({
|
|
393
|
+
packages: [...packageMap.values()],
|
|
394
|
+
metadata: {
|
|
395
|
+
generatedAt: snapshot.capturedAt,
|
|
396
|
+
budgets: snapshot.budgets,
|
|
397
|
+
history: {
|
|
398
|
+
enabled: false,
|
|
399
|
+
dir: "",
|
|
400
|
+
limit: 0
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
})) if (item.status === "exceeded") fail("budget", item.id, `包体不超过 ${item.limitBytes} 字节`, `${item.currentBytes} 字节`);
|
|
404
|
+
} else incompleteCoverage(report, target, "artifact", "budgets", "复用目录未提供编译器实际预算;不套用其他平台默认预算。");
|
|
405
|
+
report.coverage.push({
|
|
406
|
+
target,
|
|
407
|
+
layer: "artifact",
|
|
408
|
+
check: "pages-components-packages-workers",
|
|
409
|
+
status: "complete"
|
|
410
|
+
});
|
|
411
|
+
checkArtifactReferences(report, target, snapshot, classifier);
|
|
412
|
+
checkDoctorScripts(report, target, snapshot.files, "artifact");
|
|
413
|
+
}
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/doctor/build.ts
|
|
416
|
+
/** 将显式构建隔离到子进程,避免全局编译上下文和插件日志污染并发 API/JSON 输出。 */
|
|
417
|
+
async function buildDoctorSnapshot(options, target) {
|
|
418
|
+
const worker = path.join(path.dirname(fileURLToPath(import.meta.resolve("weapp-vite/package.json"))), "dist/doctor-worker.mjs");
|
|
419
|
+
return new Promise((resolve, reject) => {
|
|
420
|
+
const child = fork(worker, [], {
|
|
421
|
+
cwd: options.cwd,
|
|
422
|
+
stdio: [
|
|
423
|
+
"ignore",
|
|
424
|
+
"pipe",
|
|
425
|
+
"pipe",
|
|
426
|
+
"ipc"
|
|
427
|
+
],
|
|
428
|
+
execArgv: []
|
|
429
|
+
});
|
|
430
|
+
let result;
|
|
431
|
+
const timer = setTimeout(() => {
|
|
432
|
+
child.kill();
|
|
433
|
+
reject(/* @__PURE__ */ new Error("Doctor 构建超时"));
|
|
434
|
+
}, 18e4);
|
|
435
|
+
child.stdout?.on("data", (chunk) => options.onBuildLog?.(String(chunk)));
|
|
436
|
+
child.stderr?.on("data", (chunk) => options.onBuildLog?.(String(chunk)));
|
|
437
|
+
child.on("message", (message) => {
|
|
438
|
+
result = message.snapshot;
|
|
439
|
+
});
|
|
440
|
+
child.on("error", (error) => {
|
|
441
|
+
clearTimeout(timer);
|
|
442
|
+
reject(error);
|
|
443
|
+
});
|
|
444
|
+
child.on("close", (code) => {
|
|
445
|
+
clearTimeout(timer);
|
|
446
|
+
if (code === 0 && result) resolve(result);
|
|
447
|
+
else reject(/* @__PURE__ */ new Error("Doctor 构建未完成"));
|
|
448
|
+
});
|
|
449
|
+
child.send({
|
|
450
|
+
cwd: options.cwd,
|
|
451
|
+
configFile: options.configFile,
|
|
452
|
+
target
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/doctor/runtimeProbes.ts
|
|
458
|
+
/** 仅检查文件及平台执行条件,不执行 CLI 或项目配置。 */
|
|
459
|
+
async function inspectDoctorCli(cliPath) {
|
|
460
|
+
try {
|
|
461
|
+
if (!(await stat(cliPath)).isFile()) return false;
|
|
462
|
+
if (process.platform === "win32" && ![
|
|
463
|
+
".exe",
|
|
464
|
+
".cmd",
|
|
465
|
+
".bat",
|
|
466
|
+
".com"
|
|
467
|
+
].includes(path.extname(cliPath).toLowerCase())) return false;
|
|
468
|
+
await access(cliPath, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
469
|
+
return true;
|
|
470
|
+
} catch {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/** TCP 可达只证明监听存在,不证明宿主身份;仅销毁本次创建的 socket。 */
|
|
475
|
+
async function inspectDoctorListener(port, signal) {
|
|
476
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) return "invalid-port";
|
|
477
|
+
signal?.throwIfAborted();
|
|
478
|
+
return new Promise((resolve) => {
|
|
479
|
+
let abort;
|
|
480
|
+
const socket = createConnection({
|
|
481
|
+
host: "127.0.0.1",
|
|
482
|
+
port
|
|
483
|
+
});
|
|
484
|
+
let settled = false;
|
|
485
|
+
function finish(result) {
|
|
486
|
+
if (!settled) {
|
|
487
|
+
settled = true;
|
|
488
|
+
signal?.removeEventListener("abort", abort);
|
|
489
|
+
socket.destroy();
|
|
490
|
+
resolve(result);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
abort = () => finish("timeout");
|
|
494
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
495
|
+
socket.setTimeout(1e3, () => finish("timeout"));
|
|
496
|
+
socket.once("connect", () => finish("listening"));
|
|
497
|
+
socket.once("error", (error) => finish(error.code === "ECONNREFUSED" ? "not-listening" : "unavailable"));
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/** 宿主版本只保留数值版本,不透传任意 Tool.getInfo 字段。 */
|
|
501
|
+
function readDoctorHostVersions(info) {
|
|
502
|
+
if (!info || typeof info !== "object") return {};
|
|
503
|
+
const version = (value) => typeof value === "string" && value.length <= 64 && /^\d+(?:\.\d+){1,3}$/.test(value) ? value : void 0;
|
|
504
|
+
return {
|
|
505
|
+
ide: version("version" in info ? info.version : void 0),
|
|
506
|
+
sdk: version("SDKVersion" in info ? info.SDKVersion : void 0)
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
/** 页面查询参数可能包含业务凭据,只保留相对逻辑路由。 */
|
|
510
|
+
function readDoctorRoute(value) {
|
|
511
|
+
if (typeof value !== "string") return;
|
|
512
|
+
const route = value.split(/[?#]/, 1)[0];
|
|
513
|
+
if (!route || route.startsWith("/") || route.length > 256 || route.includes("..") || /[\\:\s]/.test(route)) return;
|
|
514
|
+
return route;
|
|
515
|
+
}
|
|
516
|
+
//#endregion
|
|
517
|
+
//#region src/doctor/runtime.ts
|
|
518
|
+
const stages = [
|
|
519
|
+
"cli-executable",
|
|
520
|
+
"service-listener",
|
|
521
|
+
"login-query",
|
|
522
|
+
"login-state",
|
|
523
|
+
"project-connection",
|
|
524
|
+
"tool-info",
|
|
525
|
+
"host-version",
|
|
526
|
+
"sdk-version",
|
|
527
|
+
"current-page",
|
|
528
|
+
"session-release"
|
|
529
|
+
];
|
|
530
|
+
/** 每个探针保留局部上限,并受同一个总 deadline 约束。 */
|
|
531
|
+
async function probeStep(scope, stage, cap, run, disposeLate) {
|
|
532
|
+
const phase = new OperationLifecycle(scope.remainingMs(cap), stage, scope.signal);
|
|
533
|
+
return await scope.step(() => phase.run(() => phase.step(() => run(phase.signal, phase.remainingMs()), {
|
|
534
|
+
stage,
|
|
535
|
+
disposeLate
|
|
536
|
+
})), { stage });
|
|
537
|
+
}
|
|
538
|
+
/** 默认只连接已有项目;原生登录查询单独显式授权,不改变路由或业务状态。 */
|
|
539
|
+
async function probeDoctorRuntime(cwd, target, port, options = {}) {
|
|
540
|
+
const scope = new OperationLifecycle(options.timeout ?? 1e4, "Doctor runtime", options.signal);
|
|
541
|
+
let finalized = false;
|
|
542
|
+
const facts = stages.map((stage) => ({
|
|
543
|
+
stage,
|
|
544
|
+
status: "not-run",
|
|
545
|
+
code: "not-requested"
|
|
546
|
+
}));
|
|
547
|
+
const events = [];
|
|
548
|
+
const provider = target === "weapp" ? "devtools" : "unsupported";
|
|
549
|
+
const bundle = {
|
|
550
|
+
frameworkVersion: version,
|
|
551
|
+
nodeVersion: process.version,
|
|
552
|
+
versions: {},
|
|
553
|
+
configuration: {
|
|
554
|
+
target,
|
|
555
|
+
provider,
|
|
556
|
+
explicitCli: !!options.cliPath,
|
|
557
|
+
explicitAutomatorPort: port !== void 0,
|
|
558
|
+
explicitServicePort: options.servicePort !== void 0,
|
|
559
|
+
loginRequested: options.login === true
|
|
560
|
+
},
|
|
561
|
+
lastSuccessfulStage: void 0,
|
|
562
|
+
events,
|
|
563
|
+
operation: scope.diagnostics,
|
|
564
|
+
reproduction: `wv doctor --runtime --platform ${target} --format json${port !== void 0 ? " --runtime-port <automator-port>" : ""}${options.cliPath ? " --runtime-cli <selected-cli>" : ""}${options.servicePort !== void 0 ? " --runtime-service-port <service-port>" : ""}${options.login ? " --runtime-login" : ""}`
|
|
565
|
+
};
|
|
566
|
+
const evidence = {
|
|
567
|
+
host: "unknown",
|
|
568
|
+
route: "",
|
|
569
|
+
provider,
|
|
570
|
+
checks: [],
|
|
571
|
+
facts,
|
|
572
|
+
bundle,
|
|
573
|
+
complete: false
|
|
574
|
+
};
|
|
575
|
+
const record = (stage, status, code) => {
|
|
576
|
+
if (finalized) return;
|
|
577
|
+
const fact = {
|
|
578
|
+
stage,
|
|
579
|
+
status,
|
|
580
|
+
code
|
|
581
|
+
};
|
|
582
|
+
facts[stages.indexOf(stage)] = fact;
|
|
583
|
+
events.push(fact);
|
|
584
|
+
if (status === "passed" && stage !== "session-release") bundle.lastSuccessfulStage = stage;
|
|
585
|
+
};
|
|
586
|
+
if (target !== "weapp") {
|
|
587
|
+
record("project-connection", "unknown", "unsupported-host");
|
|
588
|
+
return evidence;
|
|
589
|
+
}
|
|
590
|
+
if (port !== void 0 && (!Number.isInteger(port) || port < 1 || port > 65535)) {
|
|
591
|
+
record("project-connection", "failed", "invalid-port");
|
|
592
|
+
return evidence;
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
return await scope.run(async () => {
|
|
596
|
+
scope.attempt();
|
|
597
|
+
let executable = false;
|
|
598
|
+
if (options.cliPath) {
|
|
599
|
+
executable = await probeStep(scope, "cli-executable", 1e3, () => inspectDoctorCli(options.cliPath));
|
|
600
|
+
record("cli-executable", executable ? "passed" : "failed", executable ? "executable" : "not-executable");
|
|
601
|
+
}
|
|
602
|
+
if (options.servicePort !== void 0) {
|
|
603
|
+
const result = await probeStep(scope, "service-listener", 1e3, (signal) => inspectDoctorListener(options.servicePort, signal));
|
|
604
|
+
record("service-listener", result === "listening" ? "passed" : result === "not-listening" || result === "invalid-port" ? "failed" : "unknown", result);
|
|
605
|
+
}
|
|
606
|
+
if (options.login) {
|
|
607
|
+
if (!executable || !options.cliPath) {
|
|
608
|
+
record("login-query", "not-run", "explicit-executable-required");
|
|
609
|
+
record("login-state", "unknown", "query-not-run");
|
|
610
|
+
} else {
|
|
611
|
+
const result = await probeStep(scope, "login-query", 3e3, (signal, timeout) => queryWechatIdeLogin(options.cliPath, {
|
|
612
|
+
timeout,
|
|
613
|
+
signal
|
|
614
|
+
}));
|
|
615
|
+
record("login-query", result.status === "success" ? "passed" : "unknown", result.status === "success" ? "response-received" : result.reason);
|
|
616
|
+
record("login-state", result.status === "success" ? result.login ? "passed" : "failed" : "unknown", result.status === "success" ? result.login ? "logged-in" : "logged-out" : "query-incomplete");
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
let session;
|
|
620
|
+
try {
|
|
621
|
+
session = await probeStep(scope, "project-connection", 3e3, async (signal, timeout) => await connectOpenedAutomator({
|
|
622
|
+
projectPath: cwd,
|
|
623
|
+
cliPath: options.cliPath,
|
|
624
|
+
port: port ?? resolveProjectAutomatorPort(cwd),
|
|
625
|
+
timeout,
|
|
626
|
+
signal
|
|
627
|
+
}), (session) => session.disconnect());
|
|
628
|
+
record("project-connection", "passed", "connected");
|
|
629
|
+
} catch (error) {
|
|
630
|
+
record("project-connection", "failed", classifyOperationError(error) === "timeout" ? "timeout" : "connection-failed");
|
|
631
|
+
for (const stage of [
|
|
632
|
+
"tool-info",
|
|
633
|
+
"host-version",
|
|
634
|
+
"sdk-version",
|
|
635
|
+
"current-page"
|
|
636
|
+
]) record(stage, "not-run", "connection-unavailable");
|
|
637
|
+
return evidence;
|
|
638
|
+
}
|
|
639
|
+
let released = false;
|
|
640
|
+
const release = () => {
|
|
641
|
+
if (released) return;
|
|
642
|
+
released = true;
|
|
643
|
+
try {
|
|
644
|
+
session.disconnect();
|
|
645
|
+
record("session-release", "passed", "disconnected");
|
|
646
|
+
} catch {
|
|
647
|
+
record("session-release", "failed", "disconnect-failed");
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
scope.own(release, "doctor-websocket");
|
|
651
|
+
const checks = [];
|
|
652
|
+
const read = async (stage, check, operation, accept) => {
|
|
653
|
+
checks.push(check);
|
|
654
|
+
try {
|
|
655
|
+
const valid = accept(await probeStep(scope, stage, 5e3, operation));
|
|
656
|
+
record(stage, valid ? "passed" : "failed", valid ? "snapshot-read" : "invalid-response");
|
|
657
|
+
} catch (error) {
|
|
658
|
+
record(stage, "failed", classifyOperationError(error) === "timeout" ? "timeout" : "rpc-failed");
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
try {
|
|
662
|
+
await Promise.all([read("tool-info", "Tool.getInfo", () => session.toolInfo(), (info) => {
|
|
663
|
+
if (!info || typeof info !== "object") return false;
|
|
664
|
+
evidence.host = "wechat-devtools";
|
|
665
|
+
bundle.versions = readDoctorHostVersions(info);
|
|
666
|
+
record("host-version", bundle.versions.ide ? "passed" : "unknown", bundle.versions.ide ? "version-read" : "version-unavailable");
|
|
667
|
+
record("sdk-version", bundle.versions.sdk ? "passed" : "unknown", bundle.versions.sdk ? "version-read" : "version-unavailable");
|
|
668
|
+
return true;
|
|
669
|
+
}), read("current-page", "App.getCurrentPage", () => session.currentPage(), (page) => {
|
|
670
|
+
const route = readDoctorRoute(page?.path);
|
|
671
|
+
if (!route) return false;
|
|
672
|
+
evidence.route = route;
|
|
673
|
+
return true;
|
|
674
|
+
})]);
|
|
675
|
+
} finally {
|
|
676
|
+
release();
|
|
677
|
+
}
|
|
678
|
+
for (const stage of ["host-version", "sdk-version"]) if (facts.find((fact) => fact.stage === stage)?.status === "not-run") record(stage, "unknown", "tool-info-unavailable");
|
|
679
|
+
evidence.checks = checks;
|
|
680
|
+
evidence.complete = facts.every((fact) => fact.status === "passed" || fact.status === "not-run");
|
|
681
|
+
return evidence;
|
|
682
|
+
});
|
|
683
|
+
} catch (error) {
|
|
684
|
+
record(stages.includes(scope.diagnostics.stage) ? scope.diagnostics.stage : "project-connection", "failed", classifyOperationError(error));
|
|
685
|
+
evidence.complete = false;
|
|
686
|
+
return evidence;
|
|
687
|
+
} finally {
|
|
688
|
+
bundle.operation = scope.diagnostics;
|
|
689
|
+
finalized = true;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/doctor/runtimeReport.ts
|
|
694
|
+
const suggestions = {
|
|
695
|
+
"cli-executable": "确认所选 CLI 是可执行文件;不会自动回退到其他安装。",
|
|
696
|
+
"service-listener": "核对所选安装的服务端口与监听状态;TCP 可达不能证明监听者身份。",
|
|
697
|
+
"login-query": "确认所选 CLI 可响应查询;未知结果不能推断未登录。",
|
|
698
|
+
"login-state": "仅在原生查询明确未登录时完成正常登录;其他情况先排查查询链路。",
|
|
699
|
+
"project-connection": "确认目标项目已有可连接的 automator 会话及正确端口,不清理其他项目。",
|
|
700
|
+
"tool-info": "确认目标项目工具协议可用;缺失版本不能归因于编译器。",
|
|
701
|
+
"host-version": "从实际连接宿主取得 IDE 版本,不用 CLI 路径或历史记录代替。",
|
|
702
|
+
"sdk-version": "从实际宿主取得基础库版本,不将项目配置值视为实际运行版本。",
|
|
703
|
+
"current-page": "确认目标项目已打开有效页面;页面快照不代替应用功能 E2E。",
|
|
704
|
+
"session-release": "保留环境现场并检查本次 websocket 释放,不终止共享 IDE。"
|
|
705
|
+
};
|
|
706
|
+
/** 部分失败保留已有事实,按稳定阶段诊断输出;不推断平台或框架责任。 */
|
|
707
|
+
function appendDoctorRuntimeEvidence(report, target, evidence) {
|
|
708
|
+
report.runtime[target] = evidence;
|
|
709
|
+
const incomplete = evidence.complete === false || evidence.facts?.some((fact) => fact.status === "failed" || fact.status === "unknown");
|
|
710
|
+
report.coverage.push({
|
|
711
|
+
target,
|
|
712
|
+
layer: "runtime",
|
|
713
|
+
check: "connected-page-snapshot",
|
|
714
|
+
status: incomplete ? "incomplete" : "complete",
|
|
715
|
+
reason: incomplete ? "请求的宿主探针没有全部完成,已保留部分事实。" : "仅证明请求的工具信息和当前页面可读取;不代表完整应用功能验收。"
|
|
716
|
+
});
|
|
717
|
+
for (const fact of evidence.facts ?? []) {
|
|
718
|
+
report.coverage.push({
|
|
719
|
+
target,
|
|
720
|
+
layer: "runtime",
|
|
721
|
+
check: fact.stage,
|
|
722
|
+
status: fact.status === "passed" ? "complete" : fact.status === "not-run" && fact.code === "not-requested" ? "not-requested" : "incomplete",
|
|
723
|
+
reason: fact.code
|
|
724
|
+
});
|
|
725
|
+
if (fact.status === "failed" || fact.status === "unknown") addDiagnostic(report, {
|
|
726
|
+
ruleId: `doctor/runtime/${fact.stage}`,
|
|
727
|
+
target,
|
|
728
|
+
layer: "runtime",
|
|
729
|
+
severity: "warning",
|
|
730
|
+
message: `宿主探针 ${fact.stage}:${fact.code}`,
|
|
731
|
+
evidence: {
|
|
732
|
+
expected: "取得该阶段明确事实",
|
|
733
|
+
actual: fact.code
|
|
734
|
+
},
|
|
735
|
+
responsibility: {
|
|
736
|
+
owner: "unknown",
|
|
737
|
+
confidence: "suspected"
|
|
738
|
+
},
|
|
739
|
+
suggestion: suggestions[fact.stage]
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/doctor/index.ts
|
|
745
|
+
/** 分层收集事实;静态路径永不调用配置加载器、构建或宿主连接。 */
|
|
746
|
+
async function runDoctor(options = {}, adapters = {}) {
|
|
747
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
748
|
+
const report = {
|
|
749
|
+
schemaVersion: 1,
|
|
750
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
751
|
+
targets: [...new Set(options.targets ?? ["weapp"])],
|
|
752
|
+
coverage: [],
|
|
753
|
+
diagnostics: [],
|
|
754
|
+
artifacts: {},
|
|
755
|
+
runtime: {},
|
|
756
|
+
exitCode: 0
|
|
757
|
+
};
|
|
758
|
+
for (const requested of report.targets) {
|
|
759
|
+
const target = resolveMiniPlatform(requested);
|
|
760
|
+
if (!target) {
|
|
761
|
+
incompleteCoverage(report, requested, "project", "target", "当前编译器没有注册该目标。");
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const configName = getProjectPlatformOptions(target).projectConfigFileName;
|
|
765
|
+
for (const file of ["package.json", configName]) try {
|
|
766
|
+
parseObject(await readFile(path.join(cwd, file), "utf8"));
|
|
767
|
+
report.coverage.push({
|
|
768
|
+
target,
|
|
769
|
+
layer: "project",
|
|
770
|
+
check: file,
|
|
771
|
+
status: "complete",
|
|
772
|
+
reason: "只读 JSON 结构;未执行项目配置或插件。"
|
|
773
|
+
});
|
|
774
|
+
} catch {
|
|
775
|
+
incompleteCoverage(report, target, "project", file, "文件缺失、不可读或不是合法 JSON 对象。");
|
|
776
|
+
}
|
|
777
|
+
let sourceRoot = options.source;
|
|
778
|
+
if (options.build && options.artifact) incompleteCoverage(report, target, "artifact", "selection", "build 与 artifact 不能同时选择。");
|
|
779
|
+
else if (options.build || options.artifact) try {
|
|
780
|
+
const snapshot = options.build ? await (adapters.build ?? buildDoctorSnapshot)({
|
|
781
|
+
...options,
|
|
782
|
+
cwd
|
|
783
|
+
}, target) : {
|
|
784
|
+
files: await readDoctorFiles(path.resolve(cwd, options.artifact)),
|
|
785
|
+
origin: "existing",
|
|
786
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
787
|
+
freshness: "unverified"
|
|
788
|
+
};
|
|
789
|
+
report.artifacts[target] = {
|
|
790
|
+
...snapshot,
|
|
791
|
+
files: snapshot.files.map(({ text: _text, ...file }) => file)
|
|
792
|
+
};
|
|
793
|
+
sourceRoot ??= snapshot.sourceRoot;
|
|
794
|
+
for (const limitation of snapshot.limitations ?? []) incompleteCoverage(report, target, "artifact", "compiler-scope", limitation);
|
|
795
|
+
checkDoctorArtifact(report, target, snapshot);
|
|
796
|
+
} catch {
|
|
797
|
+
incompleteCoverage(report, target, "artifact", "collection", "构建或最终产物快照失败;没有采用旧产物作为本轮通过证据。");
|
|
798
|
+
}
|
|
799
|
+
else report.coverage.push({
|
|
800
|
+
target,
|
|
801
|
+
layer: "artifact",
|
|
802
|
+
check: "collection",
|
|
803
|
+
status: "not-requested"
|
|
804
|
+
});
|
|
805
|
+
try {
|
|
806
|
+
checkDoctorScripts(report, target, await readDoctorFiles(path.resolve(cwd, sourceRoot ?? "src"), true), "source");
|
|
807
|
+
} catch {
|
|
808
|
+
incompleteCoverage(report, target, "source", "scan", "源码目录无法完整读取;可用 source 显式指定目录。");
|
|
809
|
+
}
|
|
810
|
+
if (options.runtime) try {
|
|
811
|
+
const probe = adapters.runtime ?? probeDoctorRuntime;
|
|
812
|
+
appendDoctorRuntimeEvidence(report, target, options.runtimeCliPath !== void 0 || options.runtimeLogin !== void 0 || options.runtimeServicePort !== void 0 ? await probe(cwd, target, options.runtimePort, {
|
|
813
|
+
cliPath: options.runtimeCliPath,
|
|
814
|
+
login: options.runtimeLogin,
|
|
815
|
+
servicePort: options.runtimeServicePort
|
|
816
|
+
}) : await probe(cwd, target, options.runtimePort));
|
|
817
|
+
} catch {
|
|
818
|
+
incompleteCoverage(report, target, "runtime", "connected-page-snapshot", "没有可连接的受支持宿主或探针执行失败;请先打开目标项目并启用服务端口。");
|
|
819
|
+
}
|
|
820
|
+
else report.coverage.push({
|
|
821
|
+
target,
|
|
822
|
+
layer: "runtime",
|
|
823
|
+
check: "connected-page-snapshot",
|
|
824
|
+
status: "not-requested"
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
if (!report.targets.length) incompleteCoverage(report, "unknown", "project", "target", "至少需要一个注册目标。");
|
|
828
|
+
if (!options.runtime && (options.runtimeCliPath !== void 0 || options.runtimeLogin || options.runtimeServicePort !== void 0)) incompleteCoverage(report, "all", "runtime", "selection", "宿主检查选项需要显式 runtime;本轮未执行宿主操作。");
|
|
829
|
+
if (options.build && options.artifact) addDiagnostic(report, {
|
|
830
|
+
ruleId: "doctor/options",
|
|
831
|
+
target: "all",
|
|
832
|
+
layer: "project",
|
|
833
|
+
severity: "error",
|
|
834
|
+
message: "build 与 artifact 不能同时选择",
|
|
835
|
+
evidence: {
|
|
836
|
+
expected: "一个产物来源",
|
|
837
|
+
actual: "两个产物来源"
|
|
838
|
+
},
|
|
839
|
+
responsibility: {
|
|
840
|
+
owner: "project",
|
|
841
|
+
confidence: "confirmed"
|
|
842
|
+
},
|
|
843
|
+
suggestion: "选择重新构建或复用目录。"
|
|
844
|
+
});
|
|
845
|
+
return finishReport(report);
|
|
846
|
+
}
|
|
847
|
+
//#endregion
|
|
848
|
+
export { formatDoctorReport, runDoctor };
|