@aipanel/core 1.2.21 → 1.2.23
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/es/node/diagnostics.d.ts +13 -5
- package/es/node/diagnostics.mjs +230 -57
- package/lib/node/diagnostics.cjs +233 -58
- package/lib/node/diagnostics.d.ts +13 -5
- package/package.json +1 -1
package/es/node/diagnostics.d.ts
CHANGED
|
@@ -27,10 +27,14 @@ export interface TscResult {
|
|
|
27
27
|
rawOutput: string;
|
|
28
28
|
exitCode: number;
|
|
29
29
|
diagnostics?: DiagnosticItem[];
|
|
30
|
+
/** 实际使用的类型检查引擎名("tsc" / "vue-tsc"),用于分区标题;解析失败等极端场景缺省 */
|
|
31
|
+
source?: string;
|
|
30
32
|
}
|
|
31
33
|
export interface EslintOutput {
|
|
32
34
|
text?: string;
|
|
33
35
|
diagnostics?: DiagnosticItem[];
|
|
36
|
+
/** 实际运行的 lint 引擎("ESLint" / "oxlint");缺省视为 ESLint(兼容旧调用方) */
|
|
37
|
+
engines?: string[];
|
|
34
38
|
}
|
|
35
39
|
export interface DiagnosticsResult {
|
|
36
40
|
eslintOutput: EslintOutput;
|
|
@@ -45,14 +49,18 @@ export declare function lintFiles(pattern: string, cwd: string, warnLimit?: numb
|
|
|
45
49
|
export declare function findTsconfigDir(filePath: string): string | null;
|
|
46
50
|
/** 在工作区子目录中查找 tsconfig.json(不包括根目录;调用方已处理根目录场景) */
|
|
47
51
|
export declare function findAllTsconfigDirs(workspace: string): string[];
|
|
48
|
-
/** 运行 vue-tsc
|
|
49
|
-
export declare function
|
|
50
|
-
/** 并行运行 ESLint +
|
|
52
|
+
/** 运行 TypeScript 类型检查(tsc / vue-tsc 按项目自动选择)--build --noEmit,返回原始输出 */
|
|
53
|
+
export declare function runTypeCheck(filePath: string | undefined, cwd: string): Promise<TscResult>;
|
|
54
|
+
/** 并行运行 ESLint + 类型检查(单文件或 glob) */
|
|
51
55
|
export declare function runAllChecks(pattern: string, cwd: string): Promise<DiagnosticsResult>;
|
|
52
56
|
/**
|
|
53
|
-
* 全量项目诊断:优先从根 tsconfig
|
|
57
|
+
* 全量项目诊断:优先从根 tsconfig 运行一次类型检查 --build,
|
|
54
58
|
* 根无 tsconfig 时回退到逐个子目录 build;ESLint 以 "." 全量扫描。
|
|
55
59
|
*/
|
|
56
60
|
export declare function runProjectDiagnostics(workspace: string): Promise<DiagnosticsResult>;
|
|
57
|
-
/**
|
|
61
|
+
/** 类型检查分区标题(单一来源:跟随实际引擎 tsc / vue-tsc) */
|
|
62
|
+
export declare function tscSectionTitle(tscOutput: TscResult): string;
|
|
63
|
+
/** Lint 分区标题(单一来源:跟随实际运行的引擎组合,如 "ESLint + oxlint") */
|
|
64
|
+
export declare function lintSectionTitle(lintOutput: EslintOutput): string;
|
|
65
|
+
/** 组装统一的分区诊断文本(Lint / 类型检查,空结果显示占位文案) */
|
|
58
66
|
export declare function formatDiagnosticsSections(title: string, eslintOutput: EslintOutput, tscOutput: TscResult): string;
|
package/es/node/diagnostics.mjs
CHANGED
|
@@ -20,10 +20,10 @@ function isJsFile(filePath) {
|
|
|
20
20
|
return JS_EXTENSIONS.has(path.extname(filePath));
|
|
21
21
|
}
|
|
22
22
|
const DIAGNOSTICS_TOOL_DESCRIPTION = [
|
|
23
|
-
"\u8FD0\u884C
|
|
23
|
+
"\u8FD0\u884C Lint\uFF08ESLint / oxlint\uFF09\u4E0E TypeScript \u7C7B\u578B\u8BCA\u65AD\uFF0C\u8FD4\u56DE\u8BCA\u65AD\u7ED3\u679C\u3002",
|
|
24
24
|
"",
|
|
25
25
|
"**\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B**\uFF1A",
|
|
26
|
-
`-
|
|
26
|
+
`- Lint\uFF1AJavaScript / TypeScript / Vue \u6E90\u7801\uFF08${[...JS_EXTENSIONS].map((e) => `*${e}`).join(" ")}\uFF09\uFF1B\u4E24\u5F15\u64CE\u5747\u5B89\u88C5\u65F6\u5E76\u884C\u4E92\u8865\u8FD0\u884C\uFF08\u8BCA\u65AD\u6309\u6765\u6E90\u6807\u6CE8\uFF09`,
|
|
27
27
|
"- TypeScript \u7C7B\u578B\u68C0\u67E5\uFF1A*.ts *.tsx *.vue",
|
|
28
28
|
"",
|
|
29
29
|
"**\u4F55\u65F6\u4F7F\u7528\u6B64\u5DE5\u5177**\uFF1A",
|
|
@@ -47,12 +47,68 @@ function loadESLint(workspace) {
|
|
|
47
47
|
}
|
|
48
48
|
async function lintFiles(pattern, cwd, warnLimit = 5) {
|
|
49
49
|
loadESLint(cwd);
|
|
50
|
-
|
|
50
|
+
const [eslintOutput, oxlintOutput] = await Promise.all([
|
|
51
|
+
ESLintClass ? runEslintFiles(pattern, cwd, warnLimit) : Promise.resolve(null),
|
|
52
|
+
runOxlintFiles(pattern, cwd, warnLimit)
|
|
53
|
+
]);
|
|
54
|
+
const engines = [];
|
|
55
|
+
const texts = [];
|
|
56
|
+
const diagnostics = [];
|
|
57
|
+
for (const output of [eslintOutput, oxlintOutput]) {
|
|
58
|
+
if (!output) continue;
|
|
59
|
+
engines.push(...output.engines ?? []);
|
|
60
|
+
if (output.text) texts.push(output.text);
|
|
61
|
+
diagnostics.push(...output.diagnostics ?? []);
|
|
62
|
+
}
|
|
63
|
+
if (engines.length === 0) {
|
|
51
64
|
return {
|
|
52
|
-
text: `[
|
|
65
|
+
text: `[Lint] \u672A\u8FD0\u884C\uFF1A\u65E0\u6CD5\u5728 workspace "${cwd}" \u89E3\u6790\u5230 eslint \u6216 oxlint`,
|
|
53
66
|
diagnostics: []
|
|
54
67
|
};
|
|
55
68
|
}
|
|
69
|
+
return { text: texts.join("\n\n") || void 0, diagnostics, engines };
|
|
70
|
+
}
|
|
71
|
+
function formatLintMessages(messages, engine, warnLimit) {
|
|
72
|
+
if (messages.length === 0) return { engines: [engine.label] };
|
|
73
|
+
const ESLINT_ERROR = 2;
|
|
74
|
+
const ESLINT_WARN = 1;
|
|
75
|
+
const lines = [];
|
|
76
|
+
const errors = messages.filter((m) => m.severity === ESLINT_ERROR);
|
|
77
|
+
const warnings = messages.filter((m) => m.severity === ESLINT_WARN);
|
|
78
|
+
if (errors.length > 0) {
|
|
79
|
+
lines.push(
|
|
80
|
+
...errors.map(
|
|
81
|
+
(m) => `ERROR [${m.filePath}:${m.line}:${m.column}] ${m.message} (${m.ruleId})`
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (warnings.length > 0) {
|
|
86
|
+
lines.push(
|
|
87
|
+
...warnings.slice(0, warnLimit).map((m) => `WARN [${m.filePath}:${m.line}:${m.column}] ${m.message} (${m.ruleId})`)
|
|
88
|
+
);
|
|
89
|
+
if (warnings.length > warnLimit)
|
|
90
|
+
lines.push(`... and ${warnings.length - warnLimit} more warnings`);
|
|
91
|
+
}
|
|
92
|
+
const diagnostics = messages.map((m) => ({
|
|
93
|
+
severity: m.severity === ESLINT_ERROR ? SEVERITY_ERROR : m.severity === ESLINT_WARN ? SEVERITY_WARN : m.severity,
|
|
94
|
+
file: m.filePath,
|
|
95
|
+
range: {
|
|
96
|
+
start: { line: (m.line || 1) - 1, character: (m.column || 1) - 1 },
|
|
97
|
+
end: {
|
|
98
|
+
line: (m.endLine || m.line || 1) - 1,
|
|
99
|
+
character: (m.endColumn || m.column || 1) - 1
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
message: `[${engine.label}] ${m.message} (${m.ruleId})`,
|
|
103
|
+
source: engine.source
|
|
104
|
+
}));
|
|
105
|
+
return {
|
|
106
|
+
text: lines.length > 0 ? lines.join("\n") : void 0,
|
|
107
|
+
diagnostics,
|
|
108
|
+
engines: [engine.label]
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function runEslintFiles(pattern, cwd, warnLimit) {
|
|
56
112
|
try {
|
|
57
113
|
const eslint = new ESLintClass({ cwd });
|
|
58
114
|
const results = await eslint.lintFiles(pattern);
|
|
@@ -64,48 +120,111 @@ async function lintFiles(pattern, cwd, warnLimit = 5) {
|
|
|
64
120
|
fileCount: results.length,
|
|
65
121
|
messageCount: messages.length
|
|
66
122
|
});
|
|
67
|
-
|
|
68
|
-
const ESLINT_ERROR = 2;
|
|
69
|
-
const ESLINT_WARN = 1;
|
|
70
|
-
const lines = [];
|
|
71
|
-
const errors = messages.filter((m) => m.severity === ESLINT_ERROR);
|
|
72
|
-
const warnings = messages.filter((m) => m.severity === ESLINT_WARN);
|
|
73
|
-
if (errors.length > 0) {
|
|
74
|
-
lines.push(
|
|
75
|
-
...errors.map(
|
|
76
|
-
(m) => `ERROR [${m.filePath}:${m.line}:${m.column}] ${m.message} (${m.ruleId})`
|
|
77
|
-
)
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
if (warnings.length > 0) {
|
|
81
|
-
lines.push(
|
|
82
|
-
...warnings.slice(0, warnLimit).map((m) => `WARN [${m.filePath}:${m.line}:${m.column}] ${m.message} (${m.ruleId})`)
|
|
83
|
-
);
|
|
84
|
-
if (warnings.length > warnLimit)
|
|
85
|
-
lines.push(`... and ${warnings.length - warnLimit} more warnings`);
|
|
86
|
-
}
|
|
87
|
-
const diagnostics = messages.map((m) => ({
|
|
88
|
-
severity: m.severity === ESLINT_ERROR ? SEVERITY_ERROR : m.severity === ESLINT_WARN ? SEVERITY_WARN : m.severity,
|
|
89
|
-
file: m.filePath,
|
|
90
|
-
range: {
|
|
91
|
-
start: { line: (m.line || 1) - 1, character: (m.column || 1) - 1 },
|
|
92
|
-
end: {
|
|
93
|
-
line: (m.endLine || m.line || 1) - 1,
|
|
94
|
-
character: (m.endColumn || m.column || 1) - 1
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
message: `[ESLint] ${m.message} (${m.ruleId})`,
|
|
98
|
-
source: "eslint"
|
|
99
|
-
}));
|
|
100
|
-
return { text: lines.length > 0 ? lines.join("\n") : void 0, diagnostics };
|
|
123
|
+
return formatLintMessages(messages, { label: "ESLint", source: "eslint" }, warnLimit);
|
|
101
124
|
} catch (err) {
|
|
102
125
|
log.warn("ESLint failed", { pattern, error: err.message });
|
|
103
126
|
return {
|
|
104
127
|
text: `[ESLint] \u8FD0\u884C\u5931\u8D25\uFF1A${err.message}\uFF08\u4EC5\u663E\u793A TypeScript \u8BCA\u65AD\uFF09`,
|
|
105
|
-
diagnostics: []
|
|
128
|
+
diagnostics: [],
|
|
129
|
+
engines: ["ESLint"]
|
|
106
130
|
};
|
|
107
131
|
}
|
|
108
132
|
}
|
|
133
|
+
let _oxlintBin;
|
|
134
|
+
function resolveOxlintBin(workspace) {
|
|
135
|
+
if (_oxlintBin !== void 0) return _oxlintBin;
|
|
136
|
+
try {
|
|
137
|
+
const req = createRequire(path.join(workspace, "package.json"));
|
|
138
|
+
const pkgJsonPath = req.resolve("oxlint/package.json");
|
|
139
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
140
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.oxlint;
|
|
141
|
+
_oxlintBin = binRel ? path.join(path.dirname(pkgJsonPath), binRel) : null;
|
|
142
|
+
} catch {
|
|
143
|
+
_oxlintBin = null;
|
|
144
|
+
}
|
|
145
|
+
return _oxlintBin;
|
|
146
|
+
}
|
|
147
|
+
function oxlintRuleName(code) {
|
|
148
|
+
if (!code) return null;
|
|
149
|
+
const match = /\(([^)]*)\)$/.exec(code);
|
|
150
|
+
return match ? match[1] : code;
|
|
151
|
+
}
|
|
152
|
+
function parseOxlintOutput(stdout, cwd) {
|
|
153
|
+
let parsed;
|
|
154
|
+
try {
|
|
155
|
+
parsed = JSON.parse(stdout);
|
|
156
|
+
} catch {
|
|
157
|
+
const start = stdout.indexOf("{");
|
|
158
|
+
const end = stdout.lastIndexOf("}");
|
|
159
|
+
if (start >= 0 && end > start) {
|
|
160
|
+
try {
|
|
161
|
+
parsed = JSON.parse(stdout.slice(start, end + 1));
|
|
162
|
+
} catch {
|
|
163
|
+
parsed = void 0;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!parsed) throw new Error("\u65E0\u6CD5\u89E3\u6790 oxlint JSON \u8F93\u51FA");
|
|
168
|
+
return (parsed.diagnostics ?? []).flatMap((d) => {
|
|
169
|
+
const filePath = d.filename ? path.isAbsolute(d.filename) ? d.filename : path.resolve(cwd, d.filename) : "";
|
|
170
|
+
if (!filePath) return [];
|
|
171
|
+
const span = d.labels?.[0]?.span;
|
|
172
|
+
const line = span?.line ?? 1;
|
|
173
|
+
const column = span?.column ?? 1;
|
|
174
|
+
return [
|
|
175
|
+
{
|
|
176
|
+
// oxlint 无 endLine/endColumn,同行按 span.length 延伸近似
|
|
177
|
+
severity: d.severity === "error" ? 2 : 1,
|
|
178
|
+
line,
|
|
179
|
+
column,
|
|
180
|
+
endLine: line,
|
|
181
|
+
endColumn: column + (span?.length ?? 0),
|
|
182
|
+
message: d.message ?? "\u672A\u77E5\u8BCA\u65AD",
|
|
183
|
+
ruleId: oxlintRuleName(d.code),
|
|
184
|
+
filePath
|
|
185
|
+
}
|
|
186
|
+
];
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
async function runOxlintFiles(pattern, cwd, warnLimit) {
|
|
190
|
+
const bin = resolveOxlintBin(cwd);
|
|
191
|
+
if (!bin) return {};
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
exec(
|
|
194
|
+
// 与 ESLint 默认行为对齐:忽略 node_modules(oxlint 默认不排除)
|
|
195
|
+
`node "${bin}" --format=json --ignore-pattern node_modules "${pattern}"`,
|
|
196
|
+
{ cwd, timeout: 6e4, maxBuffer: 50 * 1024 * 1024 },
|
|
197
|
+
(error, stdout, stderr) => {
|
|
198
|
+
const killed = error?.killed;
|
|
199
|
+
if (killed) {
|
|
200
|
+
log.warn("oxlint timed out", { pattern });
|
|
201
|
+
resolve({
|
|
202
|
+
text: "[oxlint] \u8FD0\u884C\u5931\u8D25\uFF1A\u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u3002",
|
|
203
|
+
diagnostics: [],
|
|
204
|
+
engines: ["oxlint"]
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const messages = parseOxlintOutput(stdout, cwd);
|
|
210
|
+
log.debug("oxlint lint", {
|
|
211
|
+
pattern,
|
|
212
|
+
messageCount: messages.length,
|
|
213
|
+
stderr: stderr || void 0
|
|
214
|
+
});
|
|
215
|
+
resolve(formatLintMessages(messages, { label: "oxlint", source: "oxlint" }, warnLimit));
|
|
216
|
+
} catch (e) {
|
|
217
|
+
log.warn("oxlint failed", { pattern, error: e.message });
|
|
218
|
+
resolve({
|
|
219
|
+
text: `[oxlint] \u8FD0\u884C\u5931\u8D25\uFF1A${e.message}`,
|
|
220
|
+
diagnostics: [],
|
|
221
|
+
engines: ["oxlint"]
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
109
228
|
let _vueTscBin;
|
|
110
229
|
function resolveVueTscBin() {
|
|
111
230
|
if (_vueTscBin !== void 0) return _vueTscBin;
|
|
@@ -117,6 +236,46 @@ function resolveVueTscBin() {
|
|
|
117
236
|
}
|
|
118
237
|
return _vueTscBin;
|
|
119
238
|
}
|
|
239
|
+
const _tscBinByPkgDir = /* @__PURE__ */ new Map();
|
|
240
|
+
function nearestPackageJsonDir(startDir) {
|
|
241
|
+
let dir = path.resolve(startDir);
|
|
242
|
+
while (true) {
|
|
243
|
+
if (fs.existsSync(path.join(dir, "package.json"))) return dir;
|
|
244
|
+
const parent = path.dirname(dir);
|
|
245
|
+
if (parent === dir) return null;
|
|
246
|
+
dir = parent;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function isVuePackage(pkgDir) {
|
|
250
|
+
try {
|
|
251
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
|
|
252
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
253
|
+
return "vue" in deps || "nuxt" in deps;
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function resolveTypeCheckBin(projectDir) {
|
|
259
|
+
const pkgDir = nearestPackageJsonDir(projectDir);
|
|
260
|
+
if (!pkgDir || isVuePackage(pkgDir)) {
|
|
261
|
+
const bin2 = resolveVueTscBin();
|
|
262
|
+
return bin2 ? { bin: bin2, source: "vue-tsc" } : null;
|
|
263
|
+
}
|
|
264
|
+
let bin = _tscBinByPkgDir.get(pkgDir);
|
|
265
|
+
if (bin === void 0) {
|
|
266
|
+
try {
|
|
267
|
+
const req = createRequire(path.join(pkgDir, "package.json"));
|
|
268
|
+
bin = req.resolve("typescript/bin/tsc");
|
|
269
|
+
} catch {
|
|
270
|
+
bin = null;
|
|
271
|
+
}
|
|
272
|
+
_tscBinByPkgDir.set(pkgDir, bin);
|
|
273
|
+
if (!bin) log.debug("workspace tsc not resolvable, fallback to vue-tsc", { pkgDir });
|
|
274
|
+
}
|
|
275
|
+
if (bin) return { bin, source: "tsc" };
|
|
276
|
+
const vueBin = resolveVueTscBin();
|
|
277
|
+
return vueBin ? { bin: vueBin, source: "vue-tsc" } : null;
|
|
278
|
+
}
|
|
120
279
|
function findTsconfigDir(filePath) {
|
|
121
280
|
const resolved = path.resolve(filePath);
|
|
122
281
|
let dir = path.dirname(resolved);
|
|
@@ -162,7 +321,7 @@ function findAllTsconfigDirs(workspace) {
|
|
|
162
321
|
});
|
|
163
322
|
return dirs;
|
|
164
323
|
}
|
|
165
|
-
function parseTscDiags(rawOutput, filePath, projectDir) {
|
|
324
|
+
function parseTscDiags(rawOutput, filePath, projectDir, source = "tsc") {
|
|
166
325
|
const errorLinePat = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.+)$/;
|
|
167
326
|
const diags = [];
|
|
168
327
|
const resolved = filePath ? path.resolve(filePath) : void 0;
|
|
@@ -183,40 +342,40 @@ function parseTscDiags(rawOutput, filePath, projectDir) {
|
|
|
183
342
|
end: { line: Number(lineNum) - 1, character: Number(col) - 1 }
|
|
184
343
|
},
|
|
185
344
|
message: `[TS${code}] ${message}`,
|
|
186
|
-
source
|
|
345
|
+
source
|
|
187
346
|
});
|
|
188
347
|
}
|
|
189
348
|
}
|
|
190
349
|
return diags;
|
|
191
350
|
}
|
|
192
|
-
async function
|
|
351
|
+
async function runTypeCheck(filePath, cwd) {
|
|
193
352
|
const dir = cwd;
|
|
194
353
|
const projectDir = filePath ? findTsconfigDir(filePath) ?? dir : dir;
|
|
195
|
-
log.debug("
|
|
354
|
+
log.debug("runTypeCheck", {
|
|
196
355
|
filePath: filePath || "(all)",
|
|
197
356
|
cwd: dir,
|
|
198
357
|
projectDir,
|
|
199
358
|
processCwd: process.cwd()
|
|
200
359
|
});
|
|
201
|
-
const
|
|
202
|
-
if (!
|
|
203
|
-
log.warn("
|
|
360
|
+
const engine = resolveTypeCheckBin(projectDir);
|
|
361
|
+
if (!engine) {
|
|
362
|
+
log.warn("type-check bin not found", { projectDir });
|
|
204
363
|
return { rawOutput: "", exitCode: 0 };
|
|
205
364
|
}
|
|
206
365
|
const timeout = filePath ? 6e4 : 12e4;
|
|
207
366
|
const maxBuffer = filePath ? 10 * 1024 * 1024 : 50 * 1024 * 1024;
|
|
208
367
|
return new Promise((resolve) => {
|
|
209
368
|
exec(
|
|
210
|
-
`node "${bin}" --build --noEmit --pretty false`,
|
|
369
|
+
`node "${engine.bin}" --build --noEmit --pretty false`,
|
|
211
370
|
{ cwd: projectDir, timeout, maxBuffer },
|
|
212
371
|
(error, stdout, stderr) => {
|
|
213
372
|
let rawOutput = stdout + stderr;
|
|
214
373
|
const killed = error?.killed;
|
|
215
374
|
const exitCode = typeof error?.code === "number" ? error.code : killed ? 1 : 0;
|
|
216
375
|
if (killed && !rawOutput) {
|
|
217
|
-
rawOutput =
|
|
376
|
+
rawOutput = `${engine.source} \u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u6216\u4F18\u5316\u9879\u76EE\u914D\u7F6E\u3002`;
|
|
218
377
|
}
|
|
219
|
-
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir);
|
|
378
|
+
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir, engine.source);
|
|
220
379
|
if (filePath) {
|
|
221
380
|
const resolved = path.resolve(filePath);
|
|
222
381
|
const errorLinePat = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+TS\d+:/;
|
|
@@ -234,12 +393,13 @@ async function runVueTsc(filePath, cwd) {
|
|
|
234
393
|
}
|
|
235
394
|
rawOutput = filtered.join("\n");
|
|
236
395
|
}
|
|
237
|
-
log.debug("
|
|
396
|
+
log.debug("type-check finished", {
|
|
397
|
+
engine: engine.source,
|
|
238
398
|
filePath: filePath || "(all)",
|
|
239
399
|
exitCode,
|
|
240
400
|
outputLength: rawOutput.length
|
|
241
401
|
});
|
|
242
|
-
resolve({ rawOutput, exitCode, diagnostics });
|
|
402
|
+
resolve({ rawOutput, exitCode, diagnostics, source: engine.source });
|
|
243
403
|
}
|
|
244
404
|
);
|
|
245
405
|
});
|
|
@@ -248,7 +408,7 @@ async function runAllChecks(pattern, cwd) {
|
|
|
248
408
|
log.debug("runAllChecks", { pattern, cwd });
|
|
249
409
|
const [eslintOutput, tscOutput] = await Promise.all([
|
|
250
410
|
lintFiles(pattern, cwd),
|
|
251
|
-
|
|
411
|
+
runTypeCheck(pattern, cwd)
|
|
252
412
|
]);
|
|
253
413
|
return { eslintOutput, tscOutput };
|
|
254
414
|
}
|
|
@@ -257,20 +417,31 @@ async function runProjectDiagnostics(workspace) {
|
|
|
257
417
|
log.debug("Tsc dirs to check", { count: tscDirs.length, dirs: tscDirs });
|
|
258
418
|
const [eslintOutput, ...tscOutputs] = await Promise.all([
|
|
259
419
|
lintFiles(".", workspace, 10),
|
|
260
|
-
...tscDirs.map((dir) =>
|
|
420
|
+
...tscDirs.map((dir) => runTypeCheck(void 0, dir))
|
|
261
421
|
]);
|
|
262
422
|
const mergedTsc = {
|
|
263
423
|
rawOutput: tscOutputs.flatMap((o) => o.rawOutput).filter(Boolean).join("\n"),
|
|
264
424
|
exitCode: tscOutputs.reduce((max, o) => Math.max(max, o.exitCode), 0),
|
|
265
|
-
diagnostics: tscOutputs.flatMap((o) => o.diagnostics ?? [])
|
|
425
|
+
diagnostics: tscOutputs.flatMap((o) => o.diagnostics ?? []),
|
|
426
|
+
source: tscOutputs.find((o) => o.source)?.source
|
|
266
427
|
};
|
|
267
428
|
return { eslintOutput, tscOutput: mergedTsc };
|
|
268
429
|
}
|
|
430
|
+
function tscSectionTitle(tscOutput) {
|
|
431
|
+
return tscOutput.source ?? "tsc";
|
|
432
|
+
}
|
|
433
|
+
function lintSectionTitle(lintOutput) {
|
|
434
|
+
return lintOutput.engines?.length ? lintOutput.engines.join(" + ") : "ESLint";
|
|
435
|
+
}
|
|
269
436
|
function formatDiagnosticsSections(title, eslintOutput, tscOutput) {
|
|
270
437
|
const parts = [];
|
|
271
|
-
parts.push(
|
|
438
|
+
parts.push(`## ${lintSectionTitle(eslintOutput)}
|
|
439
|
+
|
|
440
|
+
` + (eslintOutput.text || "\u6CA1\u6709\u53D1\u73B0\u95EE\u9898"));
|
|
272
441
|
const tscLines = tscOutput.rawOutput.trim();
|
|
273
|
-
parts.push(
|
|
442
|
+
parts.push(`## ${tscSectionTitle(tscOutput)}
|
|
443
|
+
|
|
444
|
+
` + (tscLines || "\u6CA1\u6709\u53D1\u73B0\u7C7B\u578B\u9519\u8BEF"));
|
|
274
445
|
return `${title}
|
|
275
446
|
|
|
276
447
|
` + parts.join("\n\n");
|
|
@@ -282,7 +453,9 @@ export {
|
|
|
282
453
|
formatDiagnosticsSections,
|
|
283
454
|
isJsFile,
|
|
284
455
|
lintFiles,
|
|
456
|
+
lintSectionTitle,
|
|
285
457
|
runAllChecks,
|
|
286
458
|
runProjectDiagnostics,
|
|
287
|
-
|
|
459
|
+
runTypeCheck,
|
|
460
|
+
tscSectionTitle
|
|
288
461
|
};
|
package/lib/node/diagnostics.cjs
CHANGED
|
@@ -33,9 +33,11 @@ __export(diagnostics_exports, {
|
|
|
33
33
|
formatDiagnosticsSections: () => formatDiagnosticsSections,
|
|
34
34
|
isJsFile: () => isJsFile,
|
|
35
35
|
lintFiles: () => lintFiles,
|
|
36
|
+
lintSectionTitle: () => lintSectionTitle,
|
|
36
37
|
runAllChecks: () => runAllChecks,
|
|
37
38
|
runProjectDiagnostics: () => runProjectDiagnostics,
|
|
38
|
-
|
|
39
|
+
runTypeCheck: () => runTypeCheck,
|
|
40
|
+
tscSectionTitle: () => tscSectionTitle
|
|
39
41
|
});
|
|
40
42
|
module.exports = __toCommonJS(diagnostics_exports);
|
|
41
43
|
var import_node_fs = __toESM(require("node:fs"));
|
|
@@ -61,10 +63,10 @@ function isJsFile(filePath) {
|
|
|
61
63
|
return JS_EXTENSIONS.has(import_node_path.default.extname(filePath));
|
|
62
64
|
}
|
|
63
65
|
const DIAGNOSTICS_TOOL_DESCRIPTION = [
|
|
64
|
-
"\u8FD0\u884C
|
|
66
|
+
"\u8FD0\u884C Lint\uFF08ESLint / oxlint\uFF09\u4E0E TypeScript \u7C7B\u578B\u8BCA\u65AD\uFF0C\u8FD4\u56DE\u8BCA\u65AD\u7ED3\u679C\u3002",
|
|
65
67
|
"",
|
|
66
68
|
"**\u652F\u6301\u7684\u6587\u4EF6\u7C7B\u578B**\uFF1A",
|
|
67
|
-
`-
|
|
69
|
+
`- Lint\uFF1AJavaScript / TypeScript / Vue \u6E90\u7801\uFF08${[...JS_EXTENSIONS].map((e) => `*${e}`).join(" ")}\uFF09\uFF1B\u4E24\u5F15\u64CE\u5747\u5B89\u88C5\u65F6\u5E76\u884C\u4E92\u8865\u8FD0\u884C\uFF08\u8BCA\u65AD\u6309\u6765\u6E90\u6807\u6CE8\uFF09`,
|
|
68
70
|
"- TypeScript \u7C7B\u578B\u68C0\u67E5\uFF1A*.ts *.tsx *.vue",
|
|
69
71
|
"",
|
|
70
72
|
"**\u4F55\u65F6\u4F7F\u7528\u6B64\u5DE5\u5177**\uFF1A",
|
|
@@ -88,12 +90,68 @@ function loadESLint(workspace) {
|
|
|
88
90
|
}
|
|
89
91
|
async function lintFiles(pattern, cwd, warnLimit = 5) {
|
|
90
92
|
loadESLint(cwd);
|
|
91
|
-
|
|
93
|
+
const [eslintOutput, oxlintOutput] = await Promise.all([
|
|
94
|
+
ESLintClass ? runEslintFiles(pattern, cwd, warnLimit) : Promise.resolve(null),
|
|
95
|
+
runOxlintFiles(pattern, cwd, warnLimit)
|
|
96
|
+
]);
|
|
97
|
+
const engines = [];
|
|
98
|
+
const texts = [];
|
|
99
|
+
const diagnostics = [];
|
|
100
|
+
for (const output of [eslintOutput, oxlintOutput]) {
|
|
101
|
+
if (!output) continue;
|
|
102
|
+
engines.push(...output.engines ?? []);
|
|
103
|
+
if (output.text) texts.push(output.text);
|
|
104
|
+
diagnostics.push(...output.diagnostics ?? []);
|
|
105
|
+
}
|
|
106
|
+
if (engines.length === 0) {
|
|
92
107
|
return {
|
|
93
|
-
text: `[
|
|
108
|
+
text: `[Lint] \u672A\u8FD0\u884C\uFF1A\u65E0\u6CD5\u5728 workspace "${cwd}" \u89E3\u6790\u5230 eslint \u6216 oxlint`,
|
|
94
109
|
diagnostics: []
|
|
95
110
|
};
|
|
96
111
|
}
|
|
112
|
+
return { text: texts.join("\n\n") || void 0, diagnostics, engines };
|
|
113
|
+
}
|
|
114
|
+
function formatLintMessages(messages, engine, warnLimit) {
|
|
115
|
+
if (messages.length === 0) return { engines: [engine.label] };
|
|
116
|
+
const ESLINT_ERROR = 2;
|
|
117
|
+
const ESLINT_WARN = 1;
|
|
118
|
+
const lines = [];
|
|
119
|
+
const errors = messages.filter((m) => m.severity === ESLINT_ERROR);
|
|
120
|
+
const warnings = messages.filter((m) => m.severity === ESLINT_WARN);
|
|
121
|
+
if (errors.length > 0) {
|
|
122
|
+
lines.push(
|
|
123
|
+
...errors.map(
|
|
124
|
+
(m) => `ERROR [${m.filePath}:${m.line}:${m.column}] ${m.message} (${m.ruleId})`
|
|
125
|
+
)
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (warnings.length > 0) {
|
|
129
|
+
lines.push(
|
|
130
|
+
...warnings.slice(0, warnLimit).map((m) => `WARN [${m.filePath}:${m.line}:${m.column}] ${m.message} (${m.ruleId})`)
|
|
131
|
+
);
|
|
132
|
+
if (warnings.length > warnLimit)
|
|
133
|
+
lines.push(`... and ${warnings.length - warnLimit} more warnings`);
|
|
134
|
+
}
|
|
135
|
+
const diagnostics = messages.map((m) => ({
|
|
136
|
+
severity: m.severity === ESLINT_ERROR ? import_constants.SEVERITY_ERROR : m.severity === ESLINT_WARN ? import_constants.SEVERITY_WARN : m.severity,
|
|
137
|
+
file: m.filePath,
|
|
138
|
+
range: {
|
|
139
|
+
start: { line: (m.line || 1) - 1, character: (m.column || 1) - 1 },
|
|
140
|
+
end: {
|
|
141
|
+
line: (m.endLine || m.line || 1) - 1,
|
|
142
|
+
character: (m.endColumn || m.column || 1) - 1
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
message: `[${engine.label}] ${m.message} (${m.ruleId})`,
|
|
146
|
+
source: engine.source
|
|
147
|
+
}));
|
|
148
|
+
return {
|
|
149
|
+
text: lines.length > 0 ? lines.join("\n") : void 0,
|
|
150
|
+
diagnostics,
|
|
151
|
+
engines: [engine.label]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function runEslintFiles(pattern, cwd, warnLimit) {
|
|
97
155
|
try {
|
|
98
156
|
const eslint = new ESLintClass({ cwd });
|
|
99
157
|
const results = await eslint.lintFiles(pattern);
|
|
@@ -105,48 +163,111 @@ async function lintFiles(pattern, cwd, warnLimit = 5) {
|
|
|
105
163
|
fileCount: results.length,
|
|
106
164
|
messageCount: messages.length
|
|
107
165
|
});
|
|
108
|
-
|
|
109
|
-
const ESLINT_ERROR = 2;
|
|
110
|
-
const ESLINT_WARN = 1;
|
|
111
|
-
const lines = [];
|
|
112
|
-
const errors = messages.filter((m) => m.severity === ESLINT_ERROR);
|
|
113
|
-
const warnings = messages.filter((m) => m.severity === ESLINT_WARN);
|
|
114
|
-
if (errors.length > 0) {
|
|
115
|
-
lines.push(
|
|
116
|
-
...errors.map(
|
|
117
|
-
(m) => `ERROR [${m.filePath}:${m.line}:${m.column}] ${m.message} (${m.ruleId})`
|
|
118
|
-
)
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
if (warnings.length > 0) {
|
|
122
|
-
lines.push(
|
|
123
|
-
...warnings.slice(0, warnLimit).map((m) => `WARN [${m.filePath}:${m.line}:${m.column}] ${m.message} (${m.ruleId})`)
|
|
124
|
-
);
|
|
125
|
-
if (warnings.length > warnLimit)
|
|
126
|
-
lines.push(`... and ${warnings.length - warnLimit} more warnings`);
|
|
127
|
-
}
|
|
128
|
-
const diagnostics = messages.map((m) => ({
|
|
129
|
-
severity: m.severity === ESLINT_ERROR ? import_constants.SEVERITY_ERROR : m.severity === ESLINT_WARN ? import_constants.SEVERITY_WARN : m.severity,
|
|
130
|
-
file: m.filePath,
|
|
131
|
-
range: {
|
|
132
|
-
start: { line: (m.line || 1) - 1, character: (m.column || 1) - 1 },
|
|
133
|
-
end: {
|
|
134
|
-
line: (m.endLine || m.line || 1) - 1,
|
|
135
|
-
character: (m.endColumn || m.column || 1) - 1
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
message: `[ESLint] ${m.message} (${m.ruleId})`,
|
|
139
|
-
source: "eslint"
|
|
140
|
-
}));
|
|
141
|
-
return { text: lines.length > 0 ? lines.join("\n") : void 0, diagnostics };
|
|
166
|
+
return formatLintMessages(messages, { label: "ESLint", source: "eslint" }, warnLimit);
|
|
142
167
|
} catch (err) {
|
|
143
168
|
log.warn("ESLint failed", { pattern, error: err.message });
|
|
144
169
|
return {
|
|
145
170
|
text: `[ESLint] \u8FD0\u884C\u5931\u8D25\uFF1A${err.message}\uFF08\u4EC5\u663E\u793A TypeScript \u8BCA\u65AD\uFF09`,
|
|
146
|
-
diagnostics: []
|
|
171
|
+
diagnostics: [],
|
|
172
|
+
engines: ["ESLint"]
|
|
147
173
|
};
|
|
148
174
|
}
|
|
149
175
|
}
|
|
176
|
+
let _oxlintBin;
|
|
177
|
+
function resolveOxlintBin(workspace) {
|
|
178
|
+
if (_oxlintBin !== void 0) return _oxlintBin;
|
|
179
|
+
try {
|
|
180
|
+
const req = (0, import_node_module.createRequire)(import_node_path.default.join(workspace, "package.json"));
|
|
181
|
+
const pkgJsonPath = req.resolve("oxlint/package.json");
|
|
182
|
+
const pkg = JSON.parse(import_node_fs.default.readFileSync(pkgJsonPath, "utf8"));
|
|
183
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.oxlint;
|
|
184
|
+
_oxlintBin = binRel ? import_node_path.default.join(import_node_path.default.dirname(pkgJsonPath), binRel) : null;
|
|
185
|
+
} catch {
|
|
186
|
+
_oxlintBin = null;
|
|
187
|
+
}
|
|
188
|
+
return _oxlintBin;
|
|
189
|
+
}
|
|
190
|
+
function oxlintRuleName(code) {
|
|
191
|
+
if (!code) return null;
|
|
192
|
+
const match = /\(([^)]*)\)$/.exec(code);
|
|
193
|
+
return match ? match[1] : code;
|
|
194
|
+
}
|
|
195
|
+
function parseOxlintOutput(stdout, cwd) {
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(stdout);
|
|
199
|
+
} catch {
|
|
200
|
+
const start = stdout.indexOf("{");
|
|
201
|
+
const end = stdout.lastIndexOf("}");
|
|
202
|
+
if (start >= 0 && end > start) {
|
|
203
|
+
try {
|
|
204
|
+
parsed = JSON.parse(stdout.slice(start, end + 1));
|
|
205
|
+
} catch {
|
|
206
|
+
parsed = void 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (!parsed) throw new Error("\u65E0\u6CD5\u89E3\u6790 oxlint JSON \u8F93\u51FA");
|
|
211
|
+
return (parsed.diagnostics ?? []).flatMap((d) => {
|
|
212
|
+
const filePath = d.filename ? import_node_path.default.isAbsolute(d.filename) ? d.filename : import_node_path.default.resolve(cwd, d.filename) : "";
|
|
213
|
+
if (!filePath) return [];
|
|
214
|
+
const span = d.labels?.[0]?.span;
|
|
215
|
+
const line = span?.line ?? 1;
|
|
216
|
+
const column = span?.column ?? 1;
|
|
217
|
+
return [
|
|
218
|
+
{
|
|
219
|
+
// oxlint 无 endLine/endColumn,同行按 span.length 延伸近似
|
|
220
|
+
severity: d.severity === "error" ? 2 : 1,
|
|
221
|
+
line,
|
|
222
|
+
column,
|
|
223
|
+
endLine: line,
|
|
224
|
+
endColumn: column + (span?.length ?? 0),
|
|
225
|
+
message: d.message ?? "\u672A\u77E5\u8BCA\u65AD",
|
|
226
|
+
ruleId: oxlintRuleName(d.code),
|
|
227
|
+
filePath
|
|
228
|
+
}
|
|
229
|
+
];
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async function runOxlintFiles(pattern, cwd, warnLimit) {
|
|
233
|
+
const bin = resolveOxlintBin(cwd);
|
|
234
|
+
if (!bin) return {};
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
(0, import_node_child_process.exec)(
|
|
237
|
+
// 与 ESLint 默认行为对齐:忽略 node_modules(oxlint 默认不排除)
|
|
238
|
+
`node "${bin}" --format=json --ignore-pattern node_modules "${pattern}"`,
|
|
239
|
+
{ cwd, timeout: 6e4, maxBuffer: 50 * 1024 * 1024 },
|
|
240
|
+
(error, stdout, stderr) => {
|
|
241
|
+
const killed = error?.killed;
|
|
242
|
+
if (killed) {
|
|
243
|
+
log.warn("oxlint timed out", { pattern });
|
|
244
|
+
resolve({
|
|
245
|
+
text: "[oxlint] \u8FD0\u884C\u5931\u8D25\uFF1A\u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u3002",
|
|
246
|
+
diagnostics: [],
|
|
247
|
+
engines: ["oxlint"]
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const messages = parseOxlintOutput(stdout, cwd);
|
|
253
|
+
log.debug("oxlint lint", {
|
|
254
|
+
pattern,
|
|
255
|
+
messageCount: messages.length,
|
|
256
|
+
stderr: stderr || void 0
|
|
257
|
+
});
|
|
258
|
+
resolve(formatLintMessages(messages, { label: "oxlint", source: "oxlint" }, warnLimit));
|
|
259
|
+
} catch (e) {
|
|
260
|
+
log.warn("oxlint failed", { pattern, error: e.message });
|
|
261
|
+
resolve({
|
|
262
|
+
text: `[oxlint] \u8FD0\u884C\u5931\u8D25\uFF1A${e.message}`,
|
|
263
|
+
diagnostics: [],
|
|
264
|
+
engines: ["oxlint"]
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
150
271
|
let _vueTscBin;
|
|
151
272
|
function resolveVueTscBin() {
|
|
152
273
|
if (_vueTscBin !== void 0) return _vueTscBin;
|
|
@@ -158,6 +279,46 @@ function resolveVueTscBin() {
|
|
|
158
279
|
}
|
|
159
280
|
return _vueTscBin;
|
|
160
281
|
}
|
|
282
|
+
const _tscBinByPkgDir = /* @__PURE__ */ new Map();
|
|
283
|
+
function nearestPackageJsonDir(startDir) {
|
|
284
|
+
let dir = import_node_path.default.resolve(startDir);
|
|
285
|
+
while (true) {
|
|
286
|
+
if (import_node_fs.default.existsSync(import_node_path.default.join(dir, "package.json"))) return dir;
|
|
287
|
+
const parent = import_node_path.default.dirname(dir);
|
|
288
|
+
if (parent === dir) return null;
|
|
289
|
+
dir = parent;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function isVuePackage(pkgDir) {
|
|
293
|
+
try {
|
|
294
|
+
const pkg = JSON.parse(import_node_fs.default.readFileSync(import_node_path.default.join(pkgDir, "package.json"), "utf8"));
|
|
295
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
296
|
+
return "vue" in deps || "nuxt" in deps;
|
|
297
|
+
} catch {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function resolveTypeCheckBin(projectDir) {
|
|
302
|
+
const pkgDir = nearestPackageJsonDir(projectDir);
|
|
303
|
+
if (!pkgDir || isVuePackage(pkgDir)) {
|
|
304
|
+
const bin2 = resolveVueTscBin();
|
|
305
|
+
return bin2 ? { bin: bin2, source: "vue-tsc" } : null;
|
|
306
|
+
}
|
|
307
|
+
let bin = _tscBinByPkgDir.get(pkgDir);
|
|
308
|
+
if (bin === void 0) {
|
|
309
|
+
try {
|
|
310
|
+
const req = (0, import_node_module.createRequire)(import_node_path.default.join(pkgDir, "package.json"));
|
|
311
|
+
bin = req.resolve("typescript/bin/tsc");
|
|
312
|
+
} catch {
|
|
313
|
+
bin = null;
|
|
314
|
+
}
|
|
315
|
+
_tscBinByPkgDir.set(pkgDir, bin);
|
|
316
|
+
if (!bin) log.debug("workspace tsc not resolvable, fallback to vue-tsc", { pkgDir });
|
|
317
|
+
}
|
|
318
|
+
if (bin) return { bin, source: "tsc" };
|
|
319
|
+
const vueBin = resolveVueTscBin();
|
|
320
|
+
return vueBin ? { bin: vueBin, source: "vue-tsc" } : null;
|
|
321
|
+
}
|
|
161
322
|
function findTsconfigDir(filePath) {
|
|
162
323
|
const resolved = import_node_path.default.resolve(filePath);
|
|
163
324
|
let dir = import_node_path.default.dirname(resolved);
|
|
@@ -203,7 +364,7 @@ function findAllTsconfigDirs(workspace) {
|
|
|
203
364
|
});
|
|
204
365
|
return dirs;
|
|
205
366
|
}
|
|
206
|
-
function parseTscDiags(rawOutput, filePath, projectDir) {
|
|
367
|
+
function parseTscDiags(rawOutput, filePath, projectDir, source = "tsc") {
|
|
207
368
|
const errorLinePat = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+TS(\d+):\s+(.+)$/;
|
|
208
369
|
const diags = [];
|
|
209
370
|
const resolved = filePath ? import_node_path.default.resolve(filePath) : void 0;
|
|
@@ -224,40 +385,40 @@ function parseTscDiags(rawOutput, filePath, projectDir) {
|
|
|
224
385
|
end: { line: Number(lineNum) - 1, character: Number(col) - 1 }
|
|
225
386
|
},
|
|
226
387
|
message: `[TS${code}] ${message}`,
|
|
227
|
-
source
|
|
388
|
+
source
|
|
228
389
|
});
|
|
229
390
|
}
|
|
230
391
|
}
|
|
231
392
|
return diags;
|
|
232
393
|
}
|
|
233
|
-
async function
|
|
394
|
+
async function runTypeCheck(filePath, cwd) {
|
|
234
395
|
const dir = cwd;
|
|
235
396
|
const projectDir = filePath ? findTsconfigDir(filePath) ?? dir : dir;
|
|
236
|
-
log.debug("
|
|
397
|
+
log.debug("runTypeCheck", {
|
|
237
398
|
filePath: filePath || "(all)",
|
|
238
399
|
cwd: dir,
|
|
239
400
|
projectDir,
|
|
240
401
|
processCwd: process.cwd()
|
|
241
402
|
});
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
244
|
-
log.warn("
|
|
403
|
+
const engine = resolveTypeCheckBin(projectDir);
|
|
404
|
+
if (!engine) {
|
|
405
|
+
log.warn("type-check bin not found", { projectDir });
|
|
245
406
|
return { rawOutput: "", exitCode: 0 };
|
|
246
407
|
}
|
|
247
408
|
const timeout = filePath ? 6e4 : 12e4;
|
|
248
409
|
const maxBuffer = filePath ? 10 * 1024 * 1024 : 50 * 1024 * 1024;
|
|
249
410
|
return new Promise((resolve) => {
|
|
250
411
|
(0, import_node_child_process.exec)(
|
|
251
|
-
`node "${bin}" --build --noEmit --pretty false`,
|
|
412
|
+
`node "${engine.bin}" --build --noEmit --pretty false`,
|
|
252
413
|
{ cwd: projectDir, timeout, maxBuffer },
|
|
253
414
|
(error, stdout, stderr) => {
|
|
254
415
|
let rawOutput = stdout + stderr;
|
|
255
416
|
const killed = error?.killed;
|
|
256
417
|
const exitCode = typeof error?.code === "number" ? error.code : killed ? 1 : 0;
|
|
257
418
|
if (killed && !rawOutput) {
|
|
258
|
-
rawOutput =
|
|
419
|
+
rawOutput = `${engine.source} \u68C0\u67E5\u8D85\u65F6\uFF0C\u8BF7\u5C1D\u8BD5\u7F29\u5C0F\u68C0\u67E5\u8303\u56F4\u6216\u4F18\u5316\u9879\u76EE\u914D\u7F6E\u3002`;
|
|
259
420
|
}
|
|
260
|
-
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir);
|
|
421
|
+
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir, engine.source);
|
|
261
422
|
if (filePath) {
|
|
262
423
|
const resolved = import_node_path.default.resolve(filePath);
|
|
263
424
|
const errorLinePat = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+TS\d+:/;
|
|
@@ -275,12 +436,13 @@ async function runVueTsc(filePath, cwd) {
|
|
|
275
436
|
}
|
|
276
437
|
rawOutput = filtered.join("\n");
|
|
277
438
|
}
|
|
278
|
-
log.debug("
|
|
439
|
+
log.debug("type-check finished", {
|
|
440
|
+
engine: engine.source,
|
|
279
441
|
filePath: filePath || "(all)",
|
|
280
442
|
exitCode,
|
|
281
443
|
outputLength: rawOutput.length
|
|
282
444
|
});
|
|
283
|
-
resolve({ rawOutput, exitCode, diagnostics });
|
|
445
|
+
resolve({ rawOutput, exitCode, diagnostics, source: engine.source });
|
|
284
446
|
}
|
|
285
447
|
);
|
|
286
448
|
});
|
|
@@ -289,7 +451,7 @@ async function runAllChecks(pattern, cwd) {
|
|
|
289
451
|
log.debug("runAllChecks", { pattern, cwd });
|
|
290
452
|
const [eslintOutput, tscOutput] = await Promise.all([
|
|
291
453
|
lintFiles(pattern, cwd),
|
|
292
|
-
|
|
454
|
+
runTypeCheck(pattern, cwd)
|
|
293
455
|
]);
|
|
294
456
|
return { eslintOutput, tscOutput };
|
|
295
457
|
}
|
|
@@ -298,20 +460,31 @@ async function runProjectDiagnostics(workspace) {
|
|
|
298
460
|
log.debug("Tsc dirs to check", { count: tscDirs.length, dirs: tscDirs });
|
|
299
461
|
const [eslintOutput, ...tscOutputs] = await Promise.all([
|
|
300
462
|
lintFiles(".", workspace, 10),
|
|
301
|
-
...tscDirs.map((dir) =>
|
|
463
|
+
...tscDirs.map((dir) => runTypeCheck(void 0, dir))
|
|
302
464
|
]);
|
|
303
465
|
const mergedTsc = {
|
|
304
466
|
rawOutput: tscOutputs.flatMap((o) => o.rawOutput).filter(Boolean).join("\n"),
|
|
305
467
|
exitCode: tscOutputs.reduce((max, o) => Math.max(max, o.exitCode), 0),
|
|
306
|
-
diagnostics: tscOutputs.flatMap((o) => o.diagnostics ?? [])
|
|
468
|
+
diagnostics: tscOutputs.flatMap((o) => o.diagnostics ?? []),
|
|
469
|
+
source: tscOutputs.find((o) => o.source)?.source
|
|
307
470
|
};
|
|
308
471
|
return { eslintOutput, tscOutput: mergedTsc };
|
|
309
472
|
}
|
|
473
|
+
function tscSectionTitle(tscOutput) {
|
|
474
|
+
return tscOutput.source ?? "tsc";
|
|
475
|
+
}
|
|
476
|
+
function lintSectionTitle(lintOutput) {
|
|
477
|
+
return lintOutput.engines?.length ? lintOutput.engines.join(" + ") : "ESLint";
|
|
478
|
+
}
|
|
310
479
|
function formatDiagnosticsSections(title, eslintOutput, tscOutput) {
|
|
311
480
|
const parts = [];
|
|
312
|
-
parts.push(
|
|
481
|
+
parts.push(`## ${lintSectionTitle(eslintOutput)}
|
|
482
|
+
|
|
483
|
+
` + (eslintOutput.text || "\u6CA1\u6709\u53D1\u73B0\u95EE\u9898"));
|
|
313
484
|
const tscLines = tscOutput.rawOutput.trim();
|
|
314
|
-
parts.push(
|
|
485
|
+
parts.push(`## ${tscSectionTitle(tscOutput)}
|
|
486
|
+
|
|
487
|
+
` + (tscLines || "\u6CA1\u6709\u53D1\u73B0\u7C7B\u578B\u9519\u8BEF"));
|
|
315
488
|
return `${title}
|
|
316
489
|
|
|
317
490
|
` + parts.join("\n\n");
|
|
@@ -324,7 +497,9 @@ function formatDiagnosticsSections(title, eslintOutput, tscOutput) {
|
|
|
324
497
|
formatDiagnosticsSections,
|
|
325
498
|
isJsFile,
|
|
326
499
|
lintFiles,
|
|
500
|
+
lintSectionTitle,
|
|
327
501
|
runAllChecks,
|
|
328
502
|
runProjectDiagnostics,
|
|
329
|
-
|
|
503
|
+
runTypeCheck,
|
|
504
|
+
tscSectionTitle
|
|
330
505
|
});
|
|
@@ -27,10 +27,14 @@ export interface TscResult {
|
|
|
27
27
|
rawOutput: string;
|
|
28
28
|
exitCode: number;
|
|
29
29
|
diagnostics?: DiagnosticItem[];
|
|
30
|
+
/** 实际使用的类型检查引擎名("tsc" / "vue-tsc"),用于分区标题;解析失败等极端场景缺省 */
|
|
31
|
+
source?: string;
|
|
30
32
|
}
|
|
31
33
|
export interface EslintOutput {
|
|
32
34
|
text?: string;
|
|
33
35
|
diagnostics?: DiagnosticItem[];
|
|
36
|
+
/** 实际运行的 lint 引擎("ESLint" / "oxlint");缺省视为 ESLint(兼容旧调用方) */
|
|
37
|
+
engines?: string[];
|
|
34
38
|
}
|
|
35
39
|
export interface DiagnosticsResult {
|
|
36
40
|
eslintOutput: EslintOutput;
|
|
@@ -45,14 +49,18 @@ export declare function lintFiles(pattern: string, cwd: string, warnLimit?: numb
|
|
|
45
49
|
export declare function findTsconfigDir(filePath: string): string | null;
|
|
46
50
|
/** 在工作区子目录中查找 tsconfig.json(不包括根目录;调用方已处理根目录场景) */
|
|
47
51
|
export declare function findAllTsconfigDirs(workspace: string): string[];
|
|
48
|
-
/** 运行 vue-tsc
|
|
49
|
-
export declare function
|
|
50
|
-
/** 并行运行 ESLint +
|
|
52
|
+
/** 运行 TypeScript 类型检查(tsc / vue-tsc 按项目自动选择)--build --noEmit,返回原始输出 */
|
|
53
|
+
export declare function runTypeCheck(filePath: string | undefined, cwd: string): Promise<TscResult>;
|
|
54
|
+
/** 并行运行 ESLint + 类型检查(单文件或 glob) */
|
|
51
55
|
export declare function runAllChecks(pattern: string, cwd: string): Promise<DiagnosticsResult>;
|
|
52
56
|
/**
|
|
53
|
-
* 全量项目诊断:优先从根 tsconfig
|
|
57
|
+
* 全量项目诊断:优先从根 tsconfig 运行一次类型检查 --build,
|
|
54
58
|
* 根无 tsconfig 时回退到逐个子目录 build;ESLint 以 "." 全量扫描。
|
|
55
59
|
*/
|
|
56
60
|
export declare function runProjectDiagnostics(workspace: string): Promise<DiagnosticsResult>;
|
|
57
|
-
/**
|
|
61
|
+
/** 类型检查分区标题(单一来源:跟随实际引擎 tsc / vue-tsc) */
|
|
62
|
+
export declare function tscSectionTitle(tscOutput: TscResult): string;
|
|
63
|
+
/** Lint 分区标题(单一来源:跟随实际运行的引擎组合,如 "ESLint + oxlint") */
|
|
64
|
+
export declare function lintSectionTitle(lintOutput: EslintOutput): string;
|
|
65
|
+
/** 组装统一的分区诊断文本(Lint / 类型检查,空结果显示占位文案) */
|
|
58
66
|
export declare function formatDiagnosticsSections(title: string, eslintOutput: EslintOutput, tscOutput: TscResult): string;
|