@aipanel/core 1.3.1 → 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.
@@ -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 类型检查(tsc / vue-tsc 按项目自动选择)--build --noEmit,返回原始输出 */
52
+ /** 运行 TypeScript 类型检查(引擎按项目自动选择,优先项目自身版本),返回原始输出 */
53
53
  export declare function runTypeCheck(filePath: string | undefined, cwd: string): Promise<TscResult>;
54
54
  /**
55
- * 多文件类型检查:按最近的 tsconfig 项目分组,每个项目只跑一次 `--build --noEmit`,
55
+ * 多文件类型检查:按最近的 tsconfig 项目分组,每个项目只检查一次
56
+ * (TS >= 5.6 为一次 `--build --noEmit`,更早的 TS 按 references 逐项目 `-p`),
56
57
  * 再把项目输出切分回各文件(与单文件路径共用 filterTscOutputForFile)。
57
58
  * 一次编辑批次里的 N 个文件因此从 N 次项目构建降到 1 次。
58
59
  */
@@ -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
- let ESLintClass;
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
- if (ESLintClass) return;
38
- log.debug("Loading eslint", { workspace });
39
- try {
40
- const req = createRequire(path.join(workspace, "package.json"));
41
- const eslintModule = req("eslint");
42
- ESLintClass ?? (ESLintClass = eslintModule.ESLint ?? eslintModule.FlatESLint);
43
- log.debug("eslint loaded", { hasClass: !!ESLintClass });
44
- } catch (e) {
45
- log.warn("eslint not found", { error: e.message });
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
- ESLintClass ? runEslintFiles(pattern, cwd, warnLimit) : Promise.resolve(null),
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 ESLintClass({ cwd });
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
- 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;
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
- let _vueTscBin;
223
- function resolveVueTscBin() {
224
- if (_vueTscBin !== void 0) return _vueTscBin;
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 resolveTypeCheckBin(projectDir) {
253
- const pkgDir = nearestPackageJsonDir(projectDir);
254
- if (!pkgDir || isVuePackage(pkgDir)) {
255
- const bin2 = resolveVueTscBin();
256
- return bin2 ? { bin: bin2, source: "vue-tsc" } : null;
257
+ function readPackageMeta(pkgJsonPath) {
258
+ try {
259
+ return JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
260
+ } catch {
261
+ return null;
257
262
  }
258
- let bin = _tscBinByPkgDir.get(pkgDir);
259
- if (bin === void 0) {
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
- bin = req.resolve("typescript/bin/tsc");
272
+ return readPackageMeta(req.resolve("typescript/package.json"))?.version ?? null;
263
273
  } catch {
264
- bin = null;
274
+ return null;
265
275
  }
266
- _tscBinByPkgDir.set(pkgDir, bin);
267
- if (!bin) log.debug("workspace tsc not resolvable, fallback to vue-tsc", { pkgDir });
268
- }
269
- if (bin) return { bin, source: "tsc" };
270
- const vueBin = resolveVueTscBin();
271
- return vueBin ? { bin: vueBin, source: "vue-tsc" } : null;
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 = resolveTypeCheckBin(projectDir);
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 result = await execa("node", [engine.bin, "--build", "--noEmit", "--pretty", "false"], {
378
- cwd: projectDir,
379
- timeout,
380
- maxBuffer,
381
- reject: false
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
@@ -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
- let ESLintClass;
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
- if (ESLintClass) return;
84
- log.debug("Loading eslint", { workspace });
85
- try {
86
- const req = (0, import_node_module.createRequire)(import_node_path.default.join(workspace, "package.json"));
87
- const eslintModule = req("eslint");
88
- ESLintClass ?? (ESLintClass = eslintModule.ESLint ?? eslintModule.FlatESLint);
89
- log.debug("eslint loaded", { hasClass: !!ESLintClass });
90
- } catch (e) {
91
- log.warn("eslint not found", { error: e.message });
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
- ESLintClass ? runEslintFiles(pattern, cwd, warnLimit) : Promise.resolve(null),
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 ESLintClass({ cwd });
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
- if (_oxlintBin !== void 0) return _oxlintBin;
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
- let _vueTscBin;
269
- function resolveVueTscBin() {
270
- if (_vueTscBin !== void 0) return _vueTscBin;
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 resolveTypeCheckBin(projectDir) {
299
- const pkgDir = nearestPackageJsonDir(projectDir);
300
- if (!pkgDir || isVuePackage(pkgDir)) {
301
- const bin2 = resolveVueTscBin();
302
- return bin2 ? { bin: bin2, source: "vue-tsc" } : null;
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
- let bin = _tscBinByPkgDir.get(pkgDir);
305
- if (bin === void 0) {
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
- bin = req.resolve("typescript/bin/tsc");
318
+ return readPackageMeta(req.resolve("typescript/package.json"))?.version ?? null;
309
319
  } catch {
310
- bin = null;
320
+ return null;
311
321
  }
312
- _tscBinByPkgDir.set(pkgDir, bin);
313
- if (!bin) log.debug("workspace tsc not resolvable, fallback to vue-tsc", { pkgDir });
314
- }
315
- if (bin) return { bin, source: "tsc" };
316
- const vueBin = resolveVueTscBin();
317
- return vueBin ? { bin: vueBin, source: "vue-tsc" } : null;
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 = resolveTypeCheckBin(projectDir);
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 result = await (0, import_execa.execa)("node", [engine.bin, "--build", "--noEmit", "--pretty", "false"], {
424
- cwd: projectDir,
425
- timeout,
426
- maxBuffer,
427
- reject: false
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 类型检查(tsc / vue-tsc 按项目自动选择)--build --noEmit,返回原始输出 */
52
+ /** 运行 TypeScript 类型检查(引擎按项目自动选择,优先项目自身版本),返回原始输出 */
53
53
  export declare function runTypeCheck(filePath: string | undefined, cwd: string): Promise<TscResult>;
54
54
  /**
55
- * 多文件类型检查:按最近的 tsconfig 项目分组,每个项目只跑一次 `--build --noEmit`,
55
+ * 多文件类型检查:按最近的 tsconfig 项目分组,每个项目只检查一次
56
+ * (TS >= 5.6 为一次 `--build --noEmit`,更早的 TS 按 references 逐项目 `-p`),
56
57
  * 再把项目输出切分回各文件(与单文件路径共用 filterTscOutputForFile)。
57
58
  * 一次编辑批次里的 N 个文件因此从 N 次项目构建降到 1 次。
58
59
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aipanel/core",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "lib/index.cjs",