@aipanel/core 1.3.2 → 1.3.3
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 +3 -2
- package/es/node/diagnostics.mjs +166 -67
- package/lib/node/diagnostics.cjs +166 -67
- package/lib/node/diagnostics.d.ts +3 -2
- package/package.json +1 -1
package/es/node/diagnostics.d.ts
CHANGED
|
@@ -49,10 +49,11 @@ export declare function lintFiles(pattern: string, cwd: string, warnLimit?: numb
|
|
|
49
49
|
export declare function findTsconfigDir(filePath: string): string | null;
|
|
50
50
|
/** 在工作区子目录中查找 tsconfig.json(不包括根目录;调用方已处理根目录场景) */
|
|
51
51
|
export declare function findAllTsconfigDirs(workspace: string): string[];
|
|
52
|
-
/** 运行 TypeScript
|
|
52
|
+
/** 运行 TypeScript 类型检查(引擎按项目自动选择,优先项目自身版本),返回原始输出 */
|
|
53
53
|
export declare function runTypeCheck(filePath: string | undefined, cwd: string): Promise<TscResult>;
|
|
54
54
|
/**
|
|
55
|
-
* 多文件类型检查:按最近的 tsconfig
|
|
55
|
+
* 多文件类型检查:按最近的 tsconfig 项目分组,每个项目只检查一次
|
|
56
|
+
* (TS >= 5.6 为一次 `--build --noEmit`,更早的 TS 按 references 逐项目 `-p`),
|
|
56
57
|
* 再把项目输出切分回各文件(与单文件路径共用 filterTscOutputForFile)。
|
|
57
58
|
* 一次编辑批次里的 N 个文件因此从 N 次项目构建降到 1 次。
|
|
58
59
|
*/
|
package/es/node/diagnostics.mjs
CHANGED
|
@@ -32,23 +32,48 @@ const DIAGNOSTICS_TOOL_DESCRIPTION = [
|
|
|
32
32
|
"- \u6392\u67E5\u7F16\u8F91\u5668\u672A\u663E\u793A\u4F46\u5B9E\u9645\u5B58\u5728\u7684\u7C7B\u578B\u95EE\u9898",
|
|
33
33
|
"- \u4E0D\u4F20\u53C2\u6570\u53EF\u5168\u91CF\u8BCA\u65AD\u6574\u4E2A\u9879\u76EE"
|
|
34
34
|
].join("\n");
|
|
35
|
-
|
|
35
|
+
function cachedResolve(cache, key, resolve) {
|
|
36
|
+
const hit = cache.get(key);
|
|
37
|
+
if (hit !== void 0) return hit;
|
|
38
|
+
const value = resolve();
|
|
39
|
+
cache.set(key, value);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
const _packageBinCache = /* @__PURE__ */ new Map();
|
|
43
|
+
function resolvePackageBin(basePkgJson, pkgName, binName) {
|
|
44
|
+
return cachedResolve(_packageBinCache, `${basePkgJson}\0${pkgName}`, () => {
|
|
45
|
+
try {
|
|
46
|
+
const req = createRequire(basePkgJson);
|
|
47
|
+
const pkgJsonPath = req.resolve(`${pkgName}/package.json`);
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
49
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[binName];
|
|
50
|
+
return binRel ? path.join(path.dirname(pkgJsonPath), binRel) : null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const _eslintClassByBase = /* @__PURE__ */ new Map();
|
|
36
57
|
function loadESLint(workspace) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
const basePkgJson = path.join(workspace, "package.json");
|
|
59
|
+
return cachedResolve(_eslintClassByBase, basePkgJson, () => {
|
|
60
|
+
log.debug("Loading eslint", { workspace });
|
|
61
|
+
try {
|
|
62
|
+
const req = createRequire(basePkgJson);
|
|
63
|
+
const eslintModule = req("eslint");
|
|
64
|
+
const eslintClass = eslintModule.ESLint ?? eslintModule.FlatESLint ?? null;
|
|
65
|
+
log.debug("eslint loaded", { hasClass: !!eslintClass });
|
|
66
|
+
return eslintClass;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
log.warn("eslint not found", { error: e.message });
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
47
72
|
}
|
|
48
73
|
async function lintFiles(pattern, cwd, warnLimit = 5) {
|
|
49
|
-
loadESLint(cwd);
|
|
74
|
+
const eslintClass = loadESLint(cwd);
|
|
50
75
|
const [eslintOutput, oxlintOutput] = await Promise.all([
|
|
51
|
-
|
|
76
|
+
eslintClass ? runEslintFiles(eslintClass, pattern, cwd, warnLimit) : Promise.resolve(null),
|
|
52
77
|
runOxlintFiles(pattern, cwd, warnLimit)
|
|
53
78
|
]);
|
|
54
79
|
const engines = [];
|
|
@@ -108,9 +133,9 @@ function formatLintMessages(messages, engine, warnLimit) {
|
|
|
108
133
|
engines: [engine.label]
|
|
109
134
|
};
|
|
110
135
|
}
|
|
111
|
-
async function runEslintFiles(pattern, cwd, warnLimit) {
|
|
136
|
+
async function runEslintFiles(eslintClass, pattern, cwd, warnLimit) {
|
|
112
137
|
try {
|
|
113
|
-
const eslint = new
|
|
138
|
+
const eslint = new eslintClass({ cwd });
|
|
114
139
|
const results = await eslint.lintFiles(pattern);
|
|
115
140
|
const messages = results.flatMap(
|
|
116
141
|
(r) => (r.messages ?? []).map((m) => ({ ...m, filePath: r.filePath }))
|
|
@@ -130,19 +155,8 @@ async function runEslintFiles(pattern, cwd, warnLimit) {
|
|
|
130
155
|
};
|
|
131
156
|
}
|
|
132
157
|
}
|
|
133
|
-
let _oxlintBin;
|
|
134
158
|
function resolveOxlintBin(workspace) {
|
|
135
|
-
|
|
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;
|
|
159
|
+
return resolvePackageBin(path.join(workspace, "package.json"), "oxlint", "oxlint");
|
|
146
160
|
}
|
|
147
161
|
function oxlintRuleName(code) {
|
|
148
162
|
if (!code) return null;
|
|
@@ -219,18 +233,9 @@ async function runOxlintFiles(pattern, cwd, warnLimit) {
|
|
|
219
233
|
};
|
|
220
234
|
}
|
|
221
235
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
const req = createRequire(import.meta.url);
|
|
227
|
-
_vueTscBin = req.resolve("vue-tsc/bin/vue-tsc.js");
|
|
228
|
-
} catch {
|
|
229
|
-
_vueTscBin = null;
|
|
230
|
-
}
|
|
231
|
-
return _vueTscBin;
|
|
232
|
-
}
|
|
233
|
-
const _tscBinByPkgDir = /* @__PURE__ */ new Map();
|
|
236
|
+
const BUNDLED_ENGINE_KEY = "<bundled>";
|
|
237
|
+
const _engineCache = /* @__PURE__ */ new Map();
|
|
238
|
+
const _tsVersionCache = /* @__PURE__ */ new Map();
|
|
234
239
|
function nearestPackageJsonDir(startDir) {
|
|
235
240
|
let dir = path.resolve(startDir);
|
|
236
241
|
while (true) {
|
|
@@ -249,26 +254,53 @@ function isVuePackage(pkgDir) {
|
|
|
249
254
|
return false;
|
|
250
255
|
}
|
|
251
256
|
}
|
|
252
|
-
function
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return
|
|
257
|
+
function readPackageMeta(pkgJsonPath) {
|
|
258
|
+
try {
|
|
259
|
+
return JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
257
262
|
}
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
}
|
|
264
|
+
function resolveEngineTsVersion(bin) {
|
|
265
|
+
const pkgDir = nearestPackageJsonDir(path.dirname(bin));
|
|
266
|
+
if (!pkgDir) return null;
|
|
267
|
+
return cachedResolve(_tsVersionCache, pkgDir, () => {
|
|
268
|
+
const own = readPackageMeta(path.join(pkgDir, "package.json"));
|
|
269
|
+
if (own?.name === "typescript" && own.version) return own.version;
|
|
260
270
|
try {
|
|
261
271
|
const req = createRequire(path.join(pkgDir, "package.json"));
|
|
262
|
-
|
|
272
|
+
return readPackageMeta(req.resolve("typescript/package.json"))?.version ?? null;
|
|
263
273
|
} catch {
|
|
264
|
-
|
|
274
|
+
return null;
|
|
265
275
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function toEngine(bin, source, origin) {
|
|
279
|
+
return bin ? { bin, source, origin, tsVersion: resolveEngineTsVersion(bin) } : null;
|
|
280
|
+
}
|
|
281
|
+
function resolveWorkspaceEngine(pkgDir, kind) {
|
|
282
|
+
const basePkgJson = path.join(pkgDir, "package.json");
|
|
283
|
+
return kind === "tsc" ? toEngine(resolvePackageBin(basePkgJson, "typescript", "tsc"), "tsc", "workspace") : toEngine(resolvePackageBin(basePkgJson, "vue-tsc", "vue-tsc"), "vue-tsc", "workspace");
|
|
284
|
+
}
|
|
285
|
+
function resolveBundledVueTscEngine() {
|
|
286
|
+
return toEngine(resolvePackageBin(import.meta.url, "vue-tsc", "vue-tsc"), "vue-tsc", "bundled");
|
|
287
|
+
}
|
|
288
|
+
function resolveTypeCheckEngine(projectDir) {
|
|
289
|
+
const pkgDir = nearestPackageJsonDir(projectDir);
|
|
290
|
+
return cachedResolve(_engineCache, pkgDir ?? BUNDLED_ENGINE_KEY, () => {
|
|
291
|
+
if (!pkgDir) return resolveBundledVueTscEngine();
|
|
292
|
+
const candidates = isVuePackage(pkgDir) ? [resolveWorkspaceEngine(pkgDir, "vue-tsc")] : [resolveWorkspaceEngine(pkgDir, "tsc"), resolveWorkspaceEngine(pkgDir, "vue-tsc")];
|
|
293
|
+
for (const engine of candidates) {
|
|
294
|
+
if (engine) return engine;
|
|
295
|
+
}
|
|
296
|
+
return resolveBundledVueTscEngine();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
const BUILD_NO_EMIT_MIN_TS = { major: 5, minor: 6 };
|
|
300
|
+
function supportsBuildNoEmit(tsVersion) {
|
|
301
|
+
if (!tsVersion) return false;
|
|
302
|
+
const [major, minor] = tsVersion.split(".").map((part) => Number.parseInt(part, 10));
|
|
303
|
+
return major > BUILD_NO_EMIT_MIN_TS.major || major === BUILD_NO_EMIT_MIN_TS.major && minor >= BUILD_NO_EMIT_MIN_TS.minor;
|
|
272
304
|
}
|
|
273
305
|
function findTsconfigDir(filePath) {
|
|
274
306
|
const resolved = path.resolve(filePath);
|
|
@@ -358,6 +390,77 @@ function filterTscOutputForFile(rawOutput, filePath, projectDir) {
|
|
|
358
390
|
}
|
|
359
391
|
return filtered.join("\n");
|
|
360
392
|
}
|
|
393
|
+
async function readResolvedTsconfig(engine, configPath, projectDir) {
|
|
394
|
+
const result = await execa(
|
|
395
|
+
"node",
|
|
396
|
+
[
|
|
397
|
+
engine.bin,
|
|
398
|
+
"-p",
|
|
399
|
+
projectRelativeConfig(projectDir, configPath),
|
|
400
|
+
"--showConfig",
|
|
401
|
+
"--pretty",
|
|
402
|
+
"false"
|
|
403
|
+
],
|
|
404
|
+
{ cwd: projectDir, timeout: 3e4, maxBuffer: 10 * 1024 * 1024, reject: false }
|
|
405
|
+
).catch(() => null);
|
|
406
|
+
if (!result || result.timedOut || result.isTerminated || result.isMaxBuffer) return null;
|
|
407
|
+
const stdout = result.stdout ?? "";
|
|
408
|
+
const start = stdout.indexOf("{");
|
|
409
|
+
const end = stdout.lastIndexOf("}");
|
|
410
|
+
if (start < 0 || end <= start) return null;
|
|
411
|
+
try {
|
|
412
|
+
return JSON.parse(stdout.slice(start, end + 1));
|
|
413
|
+
} catch {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async function resolveProjectConfigs(engine, projectDir) {
|
|
418
|
+
const configs = [];
|
|
419
|
+
const visited = /* @__PURE__ */ new Set();
|
|
420
|
+
const visit = async (configPath) => {
|
|
421
|
+
const resolved = path.resolve(configPath);
|
|
422
|
+
if (visited.has(resolved)) return;
|
|
423
|
+
visited.add(resolved);
|
|
424
|
+
const config = await readResolvedTsconfig(engine, resolved, projectDir);
|
|
425
|
+
const hasProgram = !config || (config.files?.length ?? 0) > 0 || (config.include?.length ?? 0) > 0;
|
|
426
|
+
if (hasProgram) configs.push(resolved);
|
|
427
|
+
for (const ref of config?.references ?? []) {
|
|
428
|
+
if (ref.path) await visit(path.resolve(path.dirname(resolved), ref.path));
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
const rootConfig = path.join(projectDir, "tsconfig.json");
|
|
432
|
+
await visit(fs.existsSync(rootConfig) ? rootConfig : projectDir);
|
|
433
|
+
return configs;
|
|
434
|
+
}
|
|
435
|
+
function projectRelativeConfig(projectDir, configPath) {
|
|
436
|
+
return path.relative(projectDir, configPath) || ".";
|
|
437
|
+
}
|
|
438
|
+
async function typeCheckArgGroups(engine, projectDir) {
|
|
439
|
+
const pretty = ["--pretty", "false"];
|
|
440
|
+
if (supportsBuildNoEmit(engine.tsVersion)) return [["--build", "--noEmit", ...pretty]];
|
|
441
|
+
const configs = await resolveProjectConfigs(engine, projectDir);
|
|
442
|
+
return configs.map((config) => [
|
|
443
|
+
"-p",
|
|
444
|
+
projectRelativeConfig(projectDir, config),
|
|
445
|
+
"--noEmit",
|
|
446
|
+
...pretty
|
|
447
|
+
]);
|
|
448
|
+
}
|
|
449
|
+
async function runTypeCheckCli(engine, args, cwd, timeout, maxBuffer) {
|
|
450
|
+
const result = await execa("node", [engine.bin, ...args], {
|
|
451
|
+
cwd,
|
|
452
|
+
timeout,
|
|
453
|
+
maxBuffer,
|
|
454
|
+
reject: false
|
|
455
|
+
});
|
|
456
|
+
let rawOutput = result.stdout + result.stderr;
|
|
457
|
+
const killed = result.timedOut || result.isTerminated || result.isMaxBuffer;
|
|
458
|
+
const exitCode = typeof result.exitCode === "number" ? result.exitCode : killed ? 1 : 0;
|
|
459
|
+
if (killed && !rawOutput) {
|
|
460
|
+
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`;
|
|
461
|
+
}
|
|
462
|
+
return { rawOutput, exitCode };
|
|
463
|
+
}
|
|
361
464
|
async function runTypeCheck(filePath, cwd) {
|
|
362
465
|
const dir = cwd;
|
|
363
466
|
const projectDir = filePath ? findTsconfigDir(filePath) ?? dir : dir;
|
|
@@ -367,31 +470,27 @@ async function runTypeCheck(filePath, cwd) {
|
|
|
367
470
|
projectDir,
|
|
368
471
|
processCwd: process.cwd()
|
|
369
472
|
});
|
|
370
|
-
const engine =
|
|
473
|
+
const engine = resolveTypeCheckEngine(projectDir);
|
|
371
474
|
if (!engine) {
|
|
372
475
|
log.warn("type-check bin not found", { projectDir });
|
|
373
476
|
return { rawOutput: "", exitCode: 0 };
|
|
374
477
|
}
|
|
375
478
|
const timeout = filePath ? 6e4 : 12e4;
|
|
376
479
|
const maxBuffer = filePath ? 10 * 1024 * 1024 : 50 * 1024 * 1024;
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
timeout,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
let rawOutput = result.stdout + result.stderr;
|
|
384
|
-
const killed = result.timedOut || result.isTerminated || result.isMaxBuffer;
|
|
385
|
-
const exitCode = typeof result.exitCode === "number" ? result.exitCode : killed ? 1 : 0;
|
|
386
|
-
if (killed && !rawOutput) {
|
|
387
|
-
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`;
|
|
388
|
-
}
|
|
480
|
+
const argGroups = await typeCheckArgGroups(engine, projectDir);
|
|
481
|
+
const results = await Promise.all(
|
|
482
|
+
argGroups.map((args) => runTypeCheckCli(engine, args, projectDir, timeout, maxBuffer))
|
|
483
|
+
);
|
|
484
|
+
let rawOutput = results.map((result) => result.rawOutput).filter(Boolean).join("\n");
|
|
485
|
+
const exitCode = results.reduce((max, result) => Math.max(max, result.exitCode), 0);
|
|
389
486
|
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir, engine.source);
|
|
390
487
|
if (filePath) {
|
|
391
488
|
rawOutput = filterTscOutputForFile(rawOutput, filePath, projectDir);
|
|
392
489
|
}
|
|
393
490
|
log.debug("type-check finished", {
|
|
394
491
|
engine: engine.source,
|
|
492
|
+
engineOrigin: engine.origin,
|
|
493
|
+
tsVersion: engine.tsVersion,
|
|
395
494
|
filePath: filePath || "(all)",
|
|
396
495
|
exitCode,
|
|
397
496
|
outputLength: rawOutput.length
|
package/lib/node/diagnostics.cjs
CHANGED
|
@@ -78,23 +78,48 @@ const DIAGNOSTICS_TOOL_DESCRIPTION = [
|
|
|
78
78
|
"- \u6392\u67E5\u7F16\u8F91\u5668\u672A\u663E\u793A\u4F46\u5B9E\u9645\u5B58\u5728\u7684\u7C7B\u578B\u95EE\u9898",
|
|
79
79
|
"- \u4E0D\u4F20\u53C2\u6570\u53EF\u5168\u91CF\u8BCA\u65AD\u6574\u4E2A\u9879\u76EE"
|
|
80
80
|
].join("\n");
|
|
81
|
-
|
|
81
|
+
function cachedResolve(cache, key, resolve) {
|
|
82
|
+
const hit = cache.get(key);
|
|
83
|
+
if (hit !== void 0) return hit;
|
|
84
|
+
const value = resolve();
|
|
85
|
+
cache.set(key, value);
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
const _packageBinCache = /* @__PURE__ */ new Map();
|
|
89
|
+
function resolvePackageBin(basePkgJson, pkgName, binName) {
|
|
90
|
+
return cachedResolve(_packageBinCache, `${basePkgJson}\0${pkgName}`, () => {
|
|
91
|
+
try {
|
|
92
|
+
const req = (0, import_node_module.createRequire)(basePkgJson);
|
|
93
|
+
const pkgJsonPath = req.resolve(`${pkgName}/package.json`);
|
|
94
|
+
const pkg = JSON.parse(import_node_fs.default.readFileSync(pkgJsonPath, "utf8"));
|
|
95
|
+
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.[binName];
|
|
96
|
+
return binRel ? import_node_path.default.join(import_node_path.default.dirname(pkgJsonPath), binRel) : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const _eslintClassByBase = /* @__PURE__ */ new Map();
|
|
82
103
|
function loadESLint(workspace) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
const basePkgJson = import_node_path.default.join(workspace, "package.json");
|
|
105
|
+
return cachedResolve(_eslintClassByBase, basePkgJson, () => {
|
|
106
|
+
log.debug("Loading eslint", { workspace });
|
|
107
|
+
try {
|
|
108
|
+
const req = (0, import_node_module.createRequire)(basePkgJson);
|
|
109
|
+
const eslintModule = req("eslint");
|
|
110
|
+
const eslintClass = eslintModule.ESLint ?? eslintModule.FlatESLint ?? null;
|
|
111
|
+
log.debug("eslint loaded", { hasClass: !!eslintClass });
|
|
112
|
+
return eslintClass;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
log.warn("eslint not found", { error: e.message });
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
93
118
|
}
|
|
94
119
|
async function lintFiles(pattern, cwd, warnLimit = 5) {
|
|
95
|
-
loadESLint(cwd);
|
|
120
|
+
const eslintClass = loadESLint(cwd);
|
|
96
121
|
const [eslintOutput, oxlintOutput] = await Promise.all([
|
|
97
|
-
|
|
122
|
+
eslintClass ? runEslintFiles(eslintClass, pattern, cwd, warnLimit) : Promise.resolve(null),
|
|
98
123
|
runOxlintFiles(pattern, cwd, warnLimit)
|
|
99
124
|
]);
|
|
100
125
|
const engines = [];
|
|
@@ -154,9 +179,9 @@ function formatLintMessages(messages, engine, warnLimit) {
|
|
|
154
179
|
engines: [engine.label]
|
|
155
180
|
};
|
|
156
181
|
}
|
|
157
|
-
async function runEslintFiles(pattern, cwd, warnLimit) {
|
|
182
|
+
async function runEslintFiles(eslintClass, pattern, cwd, warnLimit) {
|
|
158
183
|
try {
|
|
159
|
-
const eslint = new
|
|
184
|
+
const eslint = new eslintClass({ cwd });
|
|
160
185
|
const results = await eslint.lintFiles(pattern);
|
|
161
186
|
const messages = results.flatMap(
|
|
162
187
|
(r) => (r.messages ?? []).map((m) => ({ ...m, filePath: r.filePath }))
|
|
@@ -176,19 +201,8 @@ async function runEslintFiles(pattern, cwd, warnLimit) {
|
|
|
176
201
|
};
|
|
177
202
|
}
|
|
178
203
|
}
|
|
179
|
-
let _oxlintBin;
|
|
180
204
|
function resolveOxlintBin(workspace) {
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const req = (0, import_node_module.createRequire)(import_node_path.default.join(workspace, "package.json"));
|
|
184
|
-
const pkgJsonPath = req.resolve("oxlint/package.json");
|
|
185
|
-
const pkg = JSON.parse(import_node_fs.default.readFileSync(pkgJsonPath, "utf8"));
|
|
186
|
-
const binRel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.oxlint;
|
|
187
|
-
_oxlintBin = binRel ? import_node_path.default.join(import_node_path.default.dirname(pkgJsonPath), binRel) : null;
|
|
188
|
-
} catch {
|
|
189
|
-
_oxlintBin = null;
|
|
190
|
-
}
|
|
191
|
-
return _oxlintBin;
|
|
205
|
+
return resolvePackageBin(import_node_path.default.join(workspace, "package.json"), "oxlint", "oxlint");
|
|
192
206
|
}
|
|
193
207
|
function oxlintRuleName(code) {
|
|
194
208
|
if (!code) return null;
|
|
@@ -265,18 +279,9 @@ async function runOxlintFiles(pattern, cwd, warnLimit) {
|
|
|
265
279
|
};
|
|
266
280
|
}
|
|
267
281
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
try {
|
|
272
|
-
const req = (0, import_node_module.createRequire)(import_meta.url);
|
|
273
|
-
_vueTscBin = req.resolve("vue-tsc/bin/vue-tsc.js");
|
|
274
|
-
} catch {
|
|
275
|
-
_vueTscBin = null;
|
|
276
|
-
}
|
|
277
|
-
return _vueTscBin;
|
|
278
|
-
}
|
|
279
|
-
const _tscBinByPkgDir = /* @__PURE__ */ new Map();
|
|
282
|
+
const BUNDLED_ENGINE_KEY = "<bundled>";
|
|
283
|
+
const _engineCache = /* @__PURE__ */ new Map();
|
|
284
|
+
const _tsVersionCache = /* @__PURE__ */ new Map();
|
|
280
285
|
function nearestPackageJsonDir(startDir) {
|
|
281
286
|
let dir = import_node_path.default.resolve(startDir);
|
|
282
287
|
while (true) {
|
|
@@ -295,26 +300,53 @@ function isVuePackage(pkgDir) {
|
|
|
295
300
|
return false;
|
|
296
301
|
}
|
|
297
302
|
}
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return
|
|
303
|
+
function readPackageMeta(pkgJsonPath) {
|
|
304
|
+
try {
|
|
305
|
+
return JSON.parse(import_node_fs.default.readFileSync(pkgJsonPath, "utf8"));
|
|
306
|
+
} catch {
|
|
307
|
+
return null;
|
|
303
308
|
}
|
|
304
|
-
|
|
305
|
-
|
|
309
|
+
}
|
|
310
|
+
function resolveEngineTsVersion(bin) {
|
|
311
|
+
const pkgDir = nearestPackageJsonDir(import_node_path.default.dirname(bin));
|
|
312
|
+
if (!pkgDir) return null;
|
|
313
|
+
return cachedResolve(_tsVersionCache, pkgDir, () => {
|
|
314
|
+
const own = readPackageMeta(import_node_path.default.join(pkgDir, "package.json"));
|
|
315
|
+
if (own?.name === "typescript" && own.version) return own.version;
|
|
306
316
|
try {
|
|
307
317
|
const req = (0, import_node_module.createRequire)(import_node_path.default.join(pkgDir, "package.json"));
|
|
308
|
-
|
|
318
|
+
return readPackageMeta(req.resolve("typescript/package.json"))?.version ?? null;
|
|
309
319
|
} catch {
|
|
310
|
-
|
|
320
|
+
return null;
|
|
311
321
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function toEngine(bin, source, origin) {
|
|
325
|
+
return bin ? { bin, source, origin, tsVersion: resolveEngineTsVersion(bin) } : null;
|
|
326
|
+
}
|
|
327
|
+
function resolveWorkspaceEngine(pkgDir, kind) {
|
|
328
|
+
const basePkgJson = import_node_path.default.join(pkgDir, "package.json");
|
|
329
|
+
return kind === "tsc" ? toEngine(resolvePackageBin(basePkgJson, "typescript", "tsc"), "tsc", "workspace") : toEngine(resolvePackageBin(basePkgJson, "vue-tsc", "vue-tsc"), "vue-tsc", "workspace");
|
|
330
|
+
}
|
|
331
|
+
function resolveBundledVueTscEngine() {
|
|
332
|
+
return toEngine(resolvePackageBin(import_meta.url, "vue-tsc", "vue-tsc"), "vue-tsc", "bundled");
|
|
333
|
+
}
|
|
334
|
+
function resolveTypeCheckEngine(projectDir) {
|
|
335
|
+
const pkgDir = nearestPackageJsonDir(projectDir);
|
|
336
|
+
return cachedResolve(_engineCache, pkgDir ?? BUNDLED_ENGINE_KEY, () => {
|
|
337
|
+
if (!pkgDir) return resolveBundledVueTscEngine();
|
|
338
|
+
const candidates = isVuePackage(pkgDir) ? [resolveWorkspaceEngine(pkgDir, "vue-tsc")] : [resolveWorkspaceEngine(pkgDir, "tsc"), resolveWorkspaceEngine(pkgDir, "vue-tsc")];
|
|
339
|
+
for (const engine of candidates) {
|
|
340
|
+
if (engine) return engine;
|
|
341
|
+
}
|
|
342
|
+
return resolveBundledVueTscEngine();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
const BUILD_NO_EMIT_MIN_TS = { major: 5, minor: 6 };
|
|
346
|
+
function supportsBuildNoEmit(tsVersion) {
|
|
347
|
+
if (!tsVersion) return false;
|
|
348
|
+
const [major, minor] = tsVersion.split(".").map((part) => Number.parseInt(part, 10));
|
|
349
|
+
return major > BUILD_NO_EMIT_MIN_TS.major || major === BUILD_NO_EMIT_MIN_TS.major && minor >= BUILD_NO_EMIT_MIN_TS.minor;
|
|
318
350
|
}
|
|
319
351
|
function findTsconfigDir(filePath) {
|
|
320
352
|
const resolved = import_node_path.default.resolve(filePath);
|
|
@@ -404,6 +436,77 @@ function filterTscOutputForFile(rawOutput, filePath, projectDir) {
|
|
|
404
436
|
}
|
|
405
437
|
return filtered.join("\n");
|
|
406
438
|
}
|
|
439
|
+
async function readResolvedTsconfig(engine, configPath, projectDir) {
|
|
440
|
+
const result = await (0, import_execa.execa)(
|
|
441
|
+
"node",
|
|
442
|
+
[
|
|
443
|
+
engine.bin,
|
|
444
|
+
"-p",
|
|
445
|
+
projectRelativeConfig(projectDir, configPath),
|
|
446
|
+
"--showConfig",
|
|
447
|
+
"--pretty",
|
|
448
|
+
"false"
|
|
449
|
+
],
|
|
450
|
+
{ cwd: projectDir, timeout: 3e4, maxBuffer: 10 * 1024 * 1024, reject: false }
|
|
451
|
+
).catch(() => null);
|
|
452
|
+
if (!result || result.timedOut || result.isTerminated || result.isMaxBuffer) return null;
|
|
453
|
+
const stdout = result.stdout ?? "";
|
|
454
|
+
const start = stdout.indexOf("{");
|
|
455
|
+
const end = stdout.lastIndexOf("}");
|
|
456
|
+
if (start < 0 || end <= start) return null;
|
|
457
|
+
try {
|
|
458
|
+
return JSON.parse(stdout.slice(start, end + 1));
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function resolveProjectConfigs(engine, projectDir) {
|
|
464
|
+
const configs = [];
|
|
465
|
+
const visited = /* @__PURE__ */ new Set();
|
|
466
|
+
const visit = async (configPath) => {
|
|
467
|
+
const resolved = import_node_path.default.resolve(configPath);
|
|
468
|
+
if (visited.has(resolved)) return;
|
|
469
|
+
visited.add(resolved);
|
|
470
|
+
const config = await readResolvedTsconfig(engine, resolved, projectDir);
|
|
471
|
+
const hasProgram = !config || (config.files?.length ?? 0) > 0 || (config.include?.length ?? 0) > 0;
|
|
472
|
+
if (hasProgram) configs.push(resolved);
|
|
473
|
+
for (const ref of config?.references ?? []) {
|
|
474
|
+
if (ref.path) await visit(import_node_path.default.resolve(import_node_path.default.dirname(resolved), ref.path));
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const rootConfig = import_node_path.default.join(projectDir, "tsconfig.json");
|
|
478
|
+
await visit(import_node_fs.default.existsSync(rootConfig) ? rootConfig : projectDir);
|
|
479
|
+
return configs;
|
|
480
|
+
}
|
|
481
|
+
function projectRelativeConfig(projectDir, configPath) {
|
|
482
|
+
return import_node_path.default.relative(projectDir, configPath) || ".";
|
|
483
|
+
}
|
|
484
|
+
async function typeCheckArgGroups(engine, projectDir) {
|
|
485
|
+
const pretty = ["--pretty", "false"];
|
|
486
|
+
if (supportsBuildNoEmit(engine.tsVersion)) return [["--build", "--noEmit", ...pretty]];
|
|
487
|
+
const configs = await resolveProjectConfigs(engine, projectDir);
|
|
488
|
+
return configs.map((config) => [
|
|
489
|
+
"-p",
|
|
490
|
+
projectRelativeConfig(projectDir, config),
|
|
491
|
+
"--noEmit",
|
|
492
|
+
...pretty
|
|
493
|
+
]);
|
|
494
|
+
}
|
|
495
|
+
async function runTypeCheckCli(engine, args, cwd, timeout, maxBuffer) {
|
|
496
|
+
const result = await (0, import_execa.execa)("node", [engine.bin, ...args], {
|
|
497
|
+
cwd,
|
|
498
|
+
timeout,
|
|
499
|
+
maxBuffer,
|
|
500
|
+
reject: false
|
|
501
|
+
});
|
|
502
|
+
let rawOutput = result.stdout + result.stderr;
|
|
503
|
+
const killed = result.timedOut || result.isTerminated || result.isMaxBuffer;
|
|
504
|
+
const exitCode = typeof result.exitCode === "number" ? result.exitCode : killed ? 1 : 0;
|
|
505
|
+
if (killed && !rawOutput) {
|
|
506
|
+
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`;
|
|
507
|
+
}
|
|
508
|
+
return { rawOutput, exitCode };
|
|
509
|
+
}
|
|
407
510
|
async function runTypeCheck(filePath, cwd) {
|
|
408
511
|
const dir = cwd;
|
|
409
512
|
const projectDir = filePath ? findTsconfigDir(filePath) ?? dir : dir;
|
|
@@ -413,31 +516,27 @@ async function runTypeCheck(filePath, cwd) {
|
|
|
413
516
|
projectDir,
|
|
414
517
|
processCwd: process.cwd()
|
|
415
518
|
});
|
|
416
|
-
const engine =
|
|
519
|
+
const engine = resolveTypeCheckEngine(projectDir);
|
|
417
520
|
if (!engine) {
|
|
418
521
|
log.warn("type-check bin not found", { projectDir });
|
|
419
522
|
return { rawOutput: "", exitCode: 0 };
|
|
420
523
|
}
|
|
421
524
|
const timeout = filePath ? 6e4 : 12e4;
|
|
422
525
|
const maxBuffer = filePath ? 10 * 1024 * 1024 : 50 * 1024 * 1024;
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
timeout,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
let rawOutput = result.stdout + result.stderr;
|
|
430
|
-
const killed = result.timedOut || result.isTerminated || result.isMaxBuffer;
|
|
431
|
-
const exitCode = typeof result.exitCode === "number" ? result.exitCode : killed ? 1 : 0;
|
|
432
|
-
if (killed && !rawOutput) {
|
|
433
|
-
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`;
|
|
434
|
-
}
|
|
526
|
+
const argGroups = await typeCheckArgGroups(engine, projectDir);
|
|
527
|
+
const results = await Promise.all(
|
|
528
|
+
argGroups.map((args) => runTypeCheckCli(engine, args, projectDir, timeout, maxBuffer))
|
|
529
|
+
);
|
|
530
|
+
let rawOutput = results.map((result) => result.rawOutput).filter(Boolean).join("\n");
|
|
531
|
+
const exitCode = results.reduce((max, result) => Math.max(max, result.exitCode), 0);
|
|
435
532
|
const diagnostics = parseTscDiags(rawOutput, filePath, projectDir, engine.source);
|
|
436
533
|
if (filePath) {
|
|
437
534
|
rawOutput = filterTscOutputForFile(rawOutput, filePath, projectDir);
|
|
438
535
|
}
|
|
439
536
|
log.debug("type-check finished", {
|
|
440
537
|
engine: engine.source,
|
|
538
|
+
engineOrigin: engine.origin,
|
|
539
|
+
tsVersion: engine.tsVersion,
|
|
441
540
|
filePath: filePath || "(all)",
|
|
442
541
|
exitCode,
|
|
443
542
|
outputLength: rawOutput.length
|
|
@@ -49,10 +49,11 @@ export declare function lintFiles(pattern: string, cwd: string, warnLimit?: numb
|
|
|
49
49
|
export declare function findTsconfigDir(filePath: string): string | null;
|
|
50
50
|
/** 在工作区子目录中查找 tsconfig.json(不包括根目录;调用方已处理根目录场景) */
|
|
51
51
|
export declare function findAllTsconfigDirs(workspace: string): string[];
|
|
52
|
-
/** 运行 TypeScript
|
|
52
|
+
/** 运行 TypeScript 类型检查(引擎按项目自动选择,优先项目自身版本),返回原始输出 */
|
|
53
53
|
export declare function runTypeCheck(filePath: string | undefined, cwd: string): Promise<TscResult>;
|
|
54
54
|
/**
|
|
55
|
-
* 多文件类型检查:按最近的 tsconfig
|
|
55
|
+
* 多文件类型检查:按最近的 tsconfig 项目分组,每个项目只检查一次
|
|
56
|
+
* (TS >= 5.6 为一次 `--build --noEmit`,更早的 TS 按 references 逐项目 `-p`),
|
|
56
57
|
* 再把项目输出切分回各文件(与单文件路径共用 filterTscOutputForFile)。
|
|
57
58
|
* 一次编辑批次里的 N 个文件因此从 N 次项目构建降到 1 次。
|
|
58
59
|
*/
|