@simplysm/sd-cli 14.0.49 → 14.0.52

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.
Files changed (60) hide show
  1. package/dist/angular/angular-compiler.d.ts +3 -0
  2. package/dist/angular/angular-compiler.d.ts.map +1 -1
  3. package/dist/angular/angular-compiler.js +3 -0
  4. package/dist/angular/angular-compiler.js.map +1 -1
  5. package/dist/dev-server/hmr-service.d.ts.map +1 -1
  6. package/dist/dev-server/hmr-service.js +10 -5
  7. package/dist/dev-server/hmr-service.js.map +1 -1
  8. package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
  9. package/dist/engines/EsbuildClientEngine.js +1 -2
  10. package/dist/engines/EsbuildClientEngine.js.map +1 -1
  11. package/dist/esbuild/esbuild-angular-compiler-plugin.js +5 -5
  12. package/dist/esbuild/esbuild-angular-compiler-plugin.js.map +1 -1
  13. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  14. package/dist/esbuild/esbuild-client-config.js +10 -5
  15. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  16. package/dist/lint/lint-core.js.map +1 -1
  17. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  18. package/dist/orchestrators/ServerRuntimeManager.d.ts.map +1 -1
  19. package/dist/orchestrators/ServerRuntimeManager.js +2 -4
  20. package/dist/orchestrators/ServerRuntimeManager.js.map +1 -1
  21. package/dist/ts-compiler/SdTsCompiler.d.ts +1 -7
  22. package/dist/ts-compiler/SdTsCompiler.d.ts.map +1 -1
  23. package/dist/ts-compiler/SdTsCompiler.js +109 -116
  24. package/dist/ts-compiler/SdTsCompiler.js.map +1 -1
  25. package/dist/utils/package-utils.d.ts.map +1 -1
  26. package/dist/utils/package-utils.js.map +1 -1
  27. package/dist/workers/client.worker.d.ts.map +1 -1
  28. package/dist/workers/client.worker.js +6 -1
  29. package/dist/workers/client.worker.js.map +1 -1
  30. package/package.json +5 -5
  31. package/src/angular/angular-compiler.ts +3 -0
  32. package/src/dev-server/hmr-service.ts +10 -5
  33. package/src/engines/EsbuildClientEngine.ts +1 -2
  34. package/src/esbuild/esbuild-angular-compiler-plugin.ts +6 -6
  35. package/src/esbuild/esbuild-client-config.ts +17 -12
  36. package/src/lint/lint-core.ts +1 -1
  37. package/src/orchestrators/DevOrchestrator.ts +3 -3
  38. package/src/orchestrators/ServerRuntimeManager.ts +2 -5
  39. package/src/ts-compiler/SdTsCompiler.ts +136 -154
  40. package/src/utils/package-utils.ts +1 -2
  41. package/src/workers/client.worker.ts +6 -1
  42. package/tests/esbuild/esbuild-angular-compiler-plugin.spec.ts +1 -1
  43. package/tests/esbuild/esbuild-postcss-plugin.acc.spec.ts +1 -1
  44. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +2 -2
  45. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +3 -3
  46. package/tests/esbuild/esbuild-worker-plugin.spec.ts +1 -1
  47. package/tests/orchestrators/build-orchestrator.spec.ts +9 -9
  48. package/tests/orchestrators/dev-orchestrator.spec.ts +4 -4
  49. package/tests/orchestrators/watch-orchestrator.spec.ts +2 -2
  50. package/tests/ts-compiler/SdTsCompiler-crash-handling.verify.md +24 -0
  51. package/tests/utils/copy-src.spec.ts +5 -5
  52. package/tests/utils/esbuild-client-config.acc.spec.ts +1 -1
  53. package/tests/utils/esbuild-client-config.spec.ts +4 -4
  54. package/tests/utils/esbuild-config.spec.ts +3 -3
  55. package/tests/utils/esbuild-postcss-plugin.spec.ts +1 -1
  56. package/tests/utils/hmr-client-script.acc.spec.ts +1 -1
  57. package/tests/utils/ngtsc-build-core.spec.ts +1 -1
  58. package/tests/utils/package-utils.spec.ts +6 -6
  59. package/tests/workers/server-build-worker.spec.ts +1 -1
  60. package/README.md +0 -782
@@ -44,9 +44,12 @@ export function createHmrService(options: HmrServiceOptions): HmrService {
44
44
  let prevOutputs: Map<string, string> | undefined;
45
45
  let debounceTimer: ReturnType<typeof setTimeout> | undefined;
46
46
  let pendingMetafile: esbuild.Metafile | undefined;
47
+ let pendingTemplateKeys: string[] | undefined;
47
48
 
48
49
  function onBuildEnd(metafile: esbuild.Metafile): void {
49
50
  pendingMetafile = metafile;
51
+ pendingTemplateKeys =
52
+ templateUpdates.size > 0 ? [...templateUpdates.keys()] : undefined;
50
53
  if (debounceTimer != null) clearTimeout(debounceTimer);
51
54
  debounceTimer = setTimeout(() => {
52
55
  debounceTimer = undefined;
@@ -62,7 +65,7 @@ export function createHmrService(options: HmrServiceOptions): HmrService {
62
65
  let fingerprint = String(output.bytes);
63
66
  if (outDir != null) {
64
67
  try {
65
- const filePath = path.resolve(outDir, normalizedPath);
68
+ const filePath = path.resolve(normalizedPath);
66
69
  const content = fs.readFileSync(filePath);
67
70
  fingerprint = crypto.createHash("md5").update(content).digest("hex");
68
71
  } catch {
@@ -78,16 +81,18 @@ export function createHmrService(options: HmrServiceOptions): HmrService {
78
81
  function dispatchHmrMessage(): void {
79
82
  if (pendingMetafile == null) return;
80
83
  const metafile = pendingMetafile;
84
+ const snapshotKeys = pendingTemplateKeys;
81
85
  pendingMetafile = undefined;
86
+ pendingTemplateKeys = undefined;
82
87
 
83
88
  const timestamp = Date.now();
84
89
  const currentOutputs = collectOutputs(metafile);
85
90
 
86
- // 1. templateUpdates에 entry가 있으면 component-update
87
- if (templateUpdates.size > 0) {
91
+ // 1. onBuildEnd 시점에 캡처된 templateUpdates가 있으면 component-update
92
+ if (snapshotKeys != null && snapshotKeys.length > 0) {
88
93
  broadcast({
89
94
  type: "component-update",
90
- ids: [...templateUpdates.keys()],
95
+ ids: snapshotKeys,
91
96
  timestamp,
92
97
  });
93
98
  prevOutputs = currentOutputs;
@@ -170,7 +175,7 @@ export function createHmrService(options: HmrServiceOptions): HmrService {
170
175
  }
171
176
 
172
177
  const componentId = parsedUrl.searchParams.get("c") ?? "";
173
- const body = templateUpdates.get(componentId) ?? "";
178
+ const body = templateUpdates.get(encodeURIComponent(componentId)) ?? "";
174
179
 
175
180
  res.writeHead(200, {
176
181
  "Content-Type": "text/javascript",
@@ -96,8 +96,7 @@ export class EsbuildClientEngine implements BuildEngine {
96
96
 
97
97
  // serverReady를 감지하여 포트 캡처 (EsbuildClientEngine 전용)
98
98
  this._worker!.on("serverReady", (data) => {
99
- const event = data as { port: number };
100
- this.port = event.port;
99
+ this.port = data.port;
101
100
  });
102
101
 
103
102
  // 공통 이벤트 처리 (buildStart/build/error)
@@ -96,7 +96,7 @@ export function createCompilerOptionsTransformer(
96
96
  _enableHmr: !!options.templateUpdates,
97
97
  supportTestBed: !!options.includeTestMetadata,
98
98
  supportJitMode: !!options.includeTestMetadata,
99
- } as ts.CompilerOptions;
99
+ };
100
100
  };
101
101
  }
102
102
 
@@ -333,10 +333,10 @@ export function createAngularCompilerPlugin(
333
333
  if (isIncremental) {
334
334
  const useHmr =
335
335
  pluginOptions.templateUpdates != null &&
336
- sourceFileCache.modifiedFiles.size <= HMR_MODIFIED_FILE_LIMIT;
336
+ sourceFileCache.cycleModifiedFiles.size <= HMR_MODIFIED_FILE_LIMIT;
337
337
 
338
338
  if (useHmr && lastResult != null) {
339
- for (const modifiedFile of sourceFileCache.modifiedFiles) {
339
+ for (const modifiedFile of sourceFileCache.cycleModifiedFiles) {
340
340
  const sf = lastResult.program.getSourceFile(modifiedFile);
341
341
  if (sf != null) {
342
342
  staleSourceFiles ??= new Map();
@@ -345,9 +345,9 @@ export function createAngularCompilerPlugin(
345
345
  }
346
346
  }
347
347
 
348
- // referencedFileTracker 확장
348
+ // referencedFileTracker 확장 (per-cycle 변경 파일만 전달하여 불필요한 재순회 방지)
349
349
  expandedModifiedFiles = referencedFileTracker.update(
350
- sourceFileCache.modifiedFiles,
350
+ sourceFileCache.cycleModifiedFiles,
351
351
  );
352
352
 
353
353
  // 변경된 파일의 Worker 결과만 선택적 제거 (변경되지 않은 파일의
@@ -452,7 +452,7 @@ export function createAngularCompilerPlugin(
452
452
  pluginOptions.templateUpdates != null
453
453
  ) {
454
454
  const hmrCandidates = collectHmrCandidates(
455
- sourceFileCache.modifiedFiles,
455
+ sourceFileCache.cycleModifiedFiles,
456
456
  compileResult.ngtscProgram,
457
457
  staleSourceFiles,
458
458
  );
@@ -154,19 +154,24 @@ export async function createClientEsbuildContext(
154
154
  define["import.meta.env"] = JSON.stringify(options.env);
155
155
  }
156
156
 
157
- // import.meta.hot 폴리필 banner (Angular HMR 런타임용)
157
+ // import.meta.hot 폴리필 (Angular HMR 런타임용)
158
158
  // Angular의 compileHmrInitializer가 import.meta.hot.on('angular:component-update', ...)을 사용.
159
- // Vite 없이 동작하도록 import.meta.hot을 폴리필하고, globalThis.__hmr_dispatch로 외부 트리거 제공.
160
- const hmrBanner =
161
- options.templateUpdates != null && options.legacyModule !== true
162
- ? [
163
- 'if(typeof ngHmrMode!=="undefined"&&ngHmrMode){(function(){',
164
- "var _l={};",
165
- "import.meta.hot={on:function(e,c){if(!_l[e])_l[e]=[];_l[e].push(c);},off:function(e,c){var a=_l[e];if(a){var i=a.indexOf(c);if(i!==-1)a.splice(i,1);}}};",
166
- "globalThis.__hmr_dispatch=function(e,d){var a=_l[e];if(a)for(var i=0;i<a.length;i++)a[i](d);};",
167
- "})()}",
168
- ].join("")
169
- : undefined;
159
+ // ES Module에서 import.meta 모듈별로 고유하므로, banner가 아닌 globalThis hot 객체를 저장하고
160
+ // esbuild define으로 import.meta.hot을 globalThis.__hmr_hot으로 치환한다.
161
+ const useHmrPolyfill =
162
+ options.templateUpdates != null && options.legacyModule !== true;
163
+ if (useHmrPolyfill) {
164
+ define["import.meta.hot"] = "globalThis.__hmr_hot";
165
+ }
166
+ const hmrBanner = useHmrPolyfill
167
+ ? [
168
+ "if(!globalThis.__hmr_hot){(function(){",
169
+ "var _l={};",
170
+ "globalThis.__hmr_hot={on:function(e,c){if(!_l[e])_l[e]=[];_l[e].push(c);},off:function(e,c){var a=_l[e];if(a){var i=a.indexOf(c);if(i!==-1)a.splice(i,1);}}};",
171
+ "globalThis.__hmr_dispatch=function(e,d){var a=_l[e];if(a)for(var i=0;i<a.length;i++)a[i](d);};",
172
+ "})()}",
173
+ ].join("")
174
+ : undefined;
170
175
 
171
176
  // esbuild context 생성
172
177
  const context = await esbuild.context({
@@ -52,7 +52,7 @@ function isGlobalIgnoresConfig(item: unknown): item is { ignores: string[] } {
52
52
  if (item == null || typeof item !== "object") return false;
53
53
  if (!("ignores" in item)) return false;
54
54
  if ("files" in item) return false; // files가 있으면 globalIgnores가 아님
55
- const ignores = (item as { ignores: unknown }).ignores;
55
+ const ignores = item.ignores;
56
56
  if (!Array.isArray(ignores)) return false;
57
57
  return ignores.every((i) => typeof i === "string");
58
58
  }
@@ -8,7 +8,7 @@ import type {
8
8
  import { filterPackagesByTargets, classifyDevPackages } from "../utils/package-classify";
9
9
  import { printDiagnostics, printServers } from "../utils/output-utils";
10
10
  import { createBuildEngine } from "../engines/engine-factory";
11
- import type { BuildEngine, ClientPackageInfo, ServerPackageInfo } from "../engines/types";
11
+ import type { BuildEngine } from "../engines/types";
12
12
  import { Capacitor } from "../capacitor/capacitor";
13
13
  import { BaseOrchestrator } from "./BaseOrchestrator";
14
14
  import { ServerRuntimeManager } from "./ServerRuntimeManager";
@@ -85,7 +85,7 @@ export class DevOrchestrator extends BaseOrchestrator implements OrchestratorLif
85
85
  const engineConfig = { ...config, env: { ...this._baseEnv, ...config.env } };
86
86
 
87
87
  const engine = createBuildEngine(
88
- { name, dir, config: engineConfig } as ServerPackageInfo,
88
+ { name, dir, config: engineConfig },
89
89
  {
90
90
  cwd: this._cwd,
91
91
  replaceDeps: this._replaceDeps,
@@ -100,7 +100,7 @@ export class DevOrchestrator extends BaseOrchestrator implements OrchestratorLif
100
100
  for (const { name, dir, config } of this._clientPackages) {
101
101
  const engineConfig = { ...config, env: { ...this._baseEnv, ...config.env } };
102
102
  const engine = createBuildEngine(
103
- { name, dir, config: engineConfig } as ClientPackageInfo,
103
+ { name, dir, config: engineConfig },
104
104
  {
105
105
  cwd: this._cwd,
106
106
  replaceDeps: this._replaceDeps,
@@ -2,7 +2,6 @@ import type { ConsolaInstance } from "consola";
2
2
  import { Worker, type WorkerProxy } from "@simplysm/core-node";
3
3
  import { err as errNs } from "@simplysm/core-common";
4
4
  import type * as ServerRuntimeWorkerModule from "../workers/server-runtime.worker";
5
- import type { ServerReadyEventData, ErrorEventData } from "../runtime/worker-events";
6
5
  import type { ResultCollector } from "../runtime/ResultCollector";
7
6
 
8
7
  /**
@@ -42,25 +41,23 @@ export class ServerRuntimeManager {
42
41
 
43
42
  // 런타임 이벤트 핸들러
44
43
  runtimeWorker.on("serverReady", (readyData) => {
45
- const readyEvent = readyData as ServerReadyEventData;
46
44
  params.resultCollector.add({
47
45
  name: params.serverName,
48
46
  target: "server",
49
47
  type: "server",
50
48
  status: "running",
51
- port: readyEvent.port,
49
+ port: readyData.port,
52
50
  });
53
51
  params.onServerReady();
54
52
  });
55
53
 
56
54
  runtimeWorker.on("error", (errorData) => {
57
- const errorEvent = errorData as ErrorEventData;
58
55
  params.resultCollector.add({
59
56
  name: params.serverName,
60
57
  target: "server",
61
58
  type: "server",
62
59
  status: "error",
63
- message: errorEvent.message,
60
+ message: errorData.message,
64
61
  });
65
62
  });
66
63
 
@@ -2,6 +2,7 @@ import path from "path";
2
2
  import { createHash } from "crypto";
3
3
  import ts from "typescript";
4
4
  import { consola, type ConsolaInstance } from "consola";
5
+ import { SdError } from "@simplysm/core-common";
5
6
  import { pathx } from "@simplysm/core-node";
6
7
  import type { ISdTsCompilerOptions, ISdTsCompilerEmitOptions } from "./sd-ts-compiler-options";
7
8
  import type { ISdTsCompilerResult } from "./sd-ts-compiler-result";
@@ -104,6 +105,21 @@ export class SdTsCompiler {
104
105
  return ctx.stage;
105
106
  }
106
107
 
108
+ private _createCrashDiagnostic(e: unknown): SerializedDiagnostic {
109
+ const contextLabel = this._formatCrashContext();
110
+ const sdError =
111
+ e instanceof Error
112
+ ? new SdError(e, `TsCompiler 내부 크래시 @${contextLabel}`)
113
+ : new SdError(`TsCompiler 내부 크래시 @${contextLabel}`, String(e));
114
+ const message = sdError.stack ?? sdError.message;
115
+ this._logger.debug(message);
116
+ return {
117
+ category: ts.DiagnosticCategory.Error,
118
+ code: 0,
119
+ messageText: message,
120
+ };
121
+ }
122
+
107
123
  private _getScssLoadPaths(): string[] {
108
124
  const { pkgDir, cwd } = this._options;
109
125
  return [path.join(pkgDir, "scss"), path.join(cwd, "node_modules")];
@@ -251,29 +267,40 @@ export class SdTsCompiler {
251
267
  program = builderProgram.getProgram();
252
268
  }
253
269
 
254
- // 9. 위험 구간 (체커 진입TsCompiler 내부 크래시 단일 catch)
270
+ // 9. 위험 구간 (단계별 catch부분 복구)
271
+ const crashDiagnostics: SerializedDiagnostic[] = [];
272
+
255
273
  try {
274
+ // 9-0. analyzeAsync (Angular only)
256
275
  if (isForAngular) {
257
- this._setCrashContext("analyzeAsync");
258
- this._logger.debug(`[${pkgName}] AOT analyzeAsync 시작`);
259
- await angularProgram!.compiler.analyzeAsync();
260
- this._logger.debug(`[${pkgName}] AOT analyzeAsync 완료`);
261
- this._ngtscProgram = angularProgram;
276
+ try {
277
+ this._setCrashContext("analyzeAsync");
278
+ this._logger.debug(`[${pkgName}] AOT analyzeAsync 시작`);
279
+ await angularProgram!.compiler.analyzeAsync();
280
+ this._logger.debug(`[${pkgName}] AOT analyzeAsync 완료`);
281
+ this._ngtscProgram = angularProgram;
282
+ } catch (e) {
283
+ crashDiagnostics.push(this._createCrashDiagnostic(e));
284
+ }
262
285
  }
263
286
 
264
287
  // 9-1. affected files 추적
265
- this._setCrashContext("findAffectedFiles");
266
288
  let affectedFiles: ReadonlySet<string> | undefined;
267
- if (isForAngular) {
268
- const result = this._findAffectedFilesForAngular(
269
- builderProgram,
270
- this._ngtscProgram!.compiler,
271
- this._sourceFileCache,
272
- );
273
- this._affectedSourceFiles = result.affectedSourceFiles;
274
- affectedFiles = result.affectedPaths;
275
- } else {
276
- affectedFiles = this._findAffectedFilesForTsc(builderProgram);
289
+ try {
290
+ this._setCrashContext("findAffectedFiles");
291
+ if (isForAngular && this._ngtscProgram != null) {
292
+ const result = this._findAffectedFilesForAngular(
293
+ builderProgram,
294
+ this._ngtscProgram.compiler,
295
+ this._sourceFileCache,
296
+ );
297
+ this._affectedSourceFiles = result.affectedSourceFiles;
298
+ affectedFiles = result.affectedPaths;
299
+ } else if (!isForAngular) {
300
+ affectedFiles = this._findAffectedFilesForTsc(builderProgram);
301
+ }
302
+ } catch (e) {
303
+ crashDiagnostics.push(this._createCrashDiagnostic(e));
277
304
  }
278
305
 
279
306
  if (affectedFiles != null) {
@@ -289,51 +316,83 @@ export class SdTsCompiler {
289
316
  }
290
317
 
291
318
  // 9-2. emit 처리
292
- this._setCrashContext("emit");
293
319
  let emitResults: EmitResult[] | undefined;
294
- if (isForAngular) {
295
- emitResults = this._emitAngular(
296
- this._ngtscProgram!,
297
- builderProgram,
298
- this._affectedSourceFiles,
299
- emitOptions,
300
- );
301
- } else {
302
- this._emitTsc(builderProgram);
320
+ try {
321
+ this._setCrashContext("emit");
322
+ if (isForAngular && this._ngtscProgram != null) {
323
+ emitResults = this._emitAngular(
324
+ this._ngtscProgram,
325
+ builderProgram,
326
+ this._affectedSourceFiles,
327
+ emitOptions,
328
+ );
329
+ } else if (!isForAngular) {
330
+ this._emitTsc(builderProgram);
331
+ }
332
+ } catch (e) {
333
+ crashDiagnostics.push(this._createCrashDiagnostic(e));
303
334
  }
304
335
 
305
336
  // 9-3. 진단 수집
306
- this._setCrashContext("collectDiagnostics");
307
- const rawDiagnostics = isForAngular
308
- ? this._collectDiagnosticsForAngular(
309
- this._ngtscProgram!,
310
- builderProgram,
311
- this._affectedSourceFiles,
312
- )
313
- : this._collectDiagnosticsForTsc(builderProgram);
314
- const diagResult = this._finalizeDiagnostics(rawDiagnostics);
337
+ let diagResult:
338
+ | {
339
+ diagnostics: SerializedDiagnostic[];
340
+ errorCount: number;
341
+ warningCount: number;
342
+ errors?: string[];
343
+ }
344
+ | undefined;
345
+ try {
346
+ this._setCrashContext("collectDiagnostics");
347
+ const rawDiagnostics =
348
+ isForAngular && this._ngtscProgram != null
349
+ ? this._collectDiagnosticsForAngular(
350
+ this._ngtscProgram,
351
+ builderProgram,
352
+ this._affectedSourceFiles,
353
+ )
354
+ : this._collectDiagnosticsForTsc(builderProgram);
355
+ diagResult = this._finalizeDiagnostics(rawDiagnostics);
356
+ } catch (e) {
357
+ crashDiagnostics.push(this._createCrashDiagnostic(e));
358
+ }
315
359
 
316
360
  // 9-4. 글로벌 SCSS + lint 병렬 실행
317
- this._setCrashContext("lintAndGlobalScss");
318
- const [, lintResult] = await Promise.all([
319
- // globalScss
320
- this._options.globalScss === true
321
- ? Promise.resolve().then(() => {
322
- const loadPaths = this._getScssLoadPaths();
323
- const globalScssErrors = compileGlobalScss(pkgDir, loadPaths);
324
- this._scssErrors.push(...globalScssErrors);
325
- })
326
- : Promise.resolve(),
327
- // lint
328
- this._options.lint === true
329
- ? this._runLint(program, affectedFiles)
330
- : Promise.resolve(undefined),
331
- ]);
361
+ let lintResult: LintWithProgramResult | undefined;
362
+ try {
363
+ this._setCrashContext("lintAndGlobalScss");
364
+ const [, lr] = await Promise.all([
365
+ // globalScss
366
+ this._options.globalScss === true
367
+ ? Promise.resolve().then(() => {
368
+ const loadPaths = this._getScssLoadPaths();
369
+ const globalScssErrors = compileGlobalScss(pkgDir, loadPaths);
370
+ this._scssErrors.push(...globalScssErrors);
371
+ })
372
+ : Promise.resolve(),
373
+ // lint
374
+ this._options.lint === true
375
+ ? this._runLint(program, affectedFiles)
376
+ : Promise.resolve(undefined),
377
+ ]);
378
+ lintResult = lr;
379
+ } catch (e) {
380
+ crashDiagnostics.push(this._createCrashDiagnostic(e));
381
+ }
332
382
 
333
383
  // 9-5. 상태 저장
334
384
  this._builderProgram = builderProgram;
335
385
  this._crashContext = undefined;
336
386
 
387
+ // 결과 조립 (crashDiagnostics 합산)
388
+ const finalDiagnostics = [...(diagResult?.diagnostics ?? []), ...crashDiagnostics];
389
+ const finalErrorCount = (diagResult?.errorCount ?? 0) + crashDiagnostics.length;
390
+ const finalWarningCount = diagResult?.warningCount ?? 0;
391
+ const crashErrors = crashDiagnostics.map((d) =>
392
+ typeof d.messageText === "string" ? d.messageText : "TsCompiler 내부 크래시",
393
+ );
394
+ const finalErrors = [...(diagResult?.errors ?? []), ...crashErrors];
395
+
337
396
  this._logger.debug(`[${pkgName}] compileAsync 완료`);
338
397
 
339
398
  return {
@@ -341,10 +400,10 @@ export class SdTsCompiler {
341
400
  builderProgram,
342
401
  isForAngular,
343
402
  affectedFiles,
344
- diagnostics: diagResult.diagnostics,
345
- errorCount: diagResult.errorCount,
346
- warningCount: diagResult.warningCount,
347
- errors: diagResult.errors,
403
+ diagnostics: finalDiagnostics,
404
+ errorCount: finalErrorCount,
405
+ warningCount: finalWarningCount,
406
+ errors: finalErrors.length > 0 ? finalErrors : undefined,
348
407
  ngtscProgram: this._ngtscProgram,
349
408
  emitResults,
350
409
  lint: lintResult,
@@ -352,35 +411,8 @@ export class SdTsCompiler {
352
411
  scssDependencies: new Map(this._scssDependencies),
353
412
  };
354
413
  } catch (e) {
355
- // TsCompiler 내부 크래시 (TS 5.9 overload 버그 등) — 단일 에러 진단으로 degrade
356
- const contextLabel = this._formatCrashContext();
357
- const rawMsg = e instanceof Error ? (e.stack ?? e.message) : String(e);
358
- this._logger.debug(`[${pkgName}] crash @${contextLabel}: ${rawMsg}`);
359
-
360
- // per-file 프로브: affected 파일 각각을 개별 try-catch로 재체크하여 재현 파일 특정
361
- const probeReport = isForAngular
362
- ? this._probeCrashPerFileAngular(
363
- this._ngtscProgram,
364
- builderProgram,
365
- this._affectedSourceFiles,
366
- )
367
- : this._probeCrashPerFileTsc(builderProgram, this._affectedSourceFiles);
368
-
369
- const parts: string[] = [
370
- `TsCompiler 내부 크래시 @${contextLabel}`,
371
- "",
372
- rawMsg,
373
- ];
374
- if (probeReport.length > 0) {
375
- parts.push("", "크래시 재현 파일 (per-file 프로브):", ...probeReport);
376
- }
377
- const message = parts.join("\n");
378
-
379
- const crashDiag: SerializedDiagnostic = {
380
- category: ts.DiagnosticCategory.Error,
381
- code: 0,
382
- messageText: message,
383
- };
414
+ // 최종 안전망 단계별 catch를 우회한 예상치 못한 에러
415
+ const crashDiag = this._createCrashDiagnostic(e);
384
416
  return {
385
417
  program,
386
418
  builderProgram,
@@ -389,7 +421,11 @@ export class SdTsCompiler {
389
421
  diagnostics: [crashDiag],
390
422
  errorCount: 1,
391
423
  warningCount: 0,
392
- errors: [message],
424
+ errors: [
425
+ typeof crashDiag.messageText === "string"
426
+ ? crashDiag.messageText
427
+ : "TsCompiler 내부 크래시",
428
+ ],
393
429
  ngtscProgram: this._ngtscProgram,
394
430
  emitResults: undefined,
395
431
  lint: undefined,
@@ -399,72 +435,6 @@ export class SdTsCompiler {
399
435
  }
400
436
  }
401
437
 
402
- /**
403
- * 크래시 발생 후, affected sourceFile을 개별 try-catch로 재검사하여 원인 파일을 특정한다.
404
- * Angular: `getDiagnosticsForFile(sf, SingleFile)` + `builderProgram.getSemanticDiagnostics(sf)` 개별 호출.
405
- * 각 파일별로 크래시 재현 여부를 기록. 프로브 자체 크래시는 흡수한다.
406
- */
407
- private _probeCrashPerFileAngular(
408
- ngtscProgram: NgtscProgram | undefined,
409
- builderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram,
410
- affectedSourceFiles: ReadonlySet<ts.SourceFile>,
411
- ): string[] {
412
- const report: string[] = [];
413
- const angularCompiler = ngtscProgram?.compiler;
414
- const { cwd } = this._options;
415
-
416
- for (const sf of affectedSourceFiles) {
417
- if (angularCompiler?.ignoreForDiagnostics.has(sf) === true) continue;
418
-
419
- const rel = path.relative(cwd, sf.fileName);
420
-
421
- try {
422
- builderProgram.getSemanticDiagnostics(sf);
423
- } catch (e) {
424
- report.push(
425
- ` - ${rel} [getSemanticDiagnostics]: ${e instanceof Error ? e.message : String(e)}`,
426
- );
427
- continue;
428
- }
429
-
430
- if (angularCompiler != null && !sf.isDeclarationFile) {
431
- try {
432
- angularCompiler.getDiagnosticsForFile(sf, OptimizeFor.SingleFile);
433
- } catch (e) {
434
- report.push(
435
- ` - ${rel} [getDiagnosticsForFile]: ${e instanceof Error ? e.message : String(e)}`,
436
- );
437
- }
438
- }
439
- }
440
- return report;
441
- }
442
-
443
- private _probeCrashPerFileTsc(
444
- builderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram,
445
- affectedSourceFiles: ReadonlySet<ts.SourceFile>,
446
- ): string[] {
447
- const report: string[] = [];
448
- const { cwd } = this._options;
449
-
450
- const targets =
451
- affectedSourceFiles.size > 0
452
- ? affectedSourceFiles
453
- : new Set(builderProgram.getSourceFiles());
454
-
455
- for (const sf of targets) {
456
- try {
457
- builderProgram.getSemanticDiagnostics(sf);
458
- } catch (e) {
459
- const rel = path.relative(cwd, sf.fileName);
460
- report.push(
461
- ` - ${rel} [getSemanticDiagnostics]: ${e instanceof Error ? e.message : String(e)}`,
462
- );
463
- }
464
- }
465
- return report;
466
- }
467
-
468
438
  private async _runLint(
469
439
  program: ts.Program,
470
440
  affectedFiles?: ReadonlySet<string>,
@@ -615,11 +585,16 @@ export class SdTsCompiler {
615
585
  ): ts.Diagnostic[] {
616
586
  const diagnostics: ts.Diagnostic[] = [
617
587
  ...builderProgram.getConfigFileParsingDiagnostics(),
618
- ...builderProgram.getSyntacticDiagnostics(),
619
588
  ...builderProgram.getOptionsDiagnostics(),
620
589
  ...builderProgram.getGlobalDiagnostics(),
621
- ...builderProgram.getSemanticDiagnostics(),
622
590
  ];
591
+
592
+ for (const sourceFile of builderProgram.getSourceFiles()) {
593
+ this._setCrashContext("collectDiagnostics", sourceFile.fileName);
594
+ diagnostics.push(...builderProgram.getSyntacticDiagnostics(sourceFile));
595
+ diagnostics.push(...builderProgram.getSemanticDiagnostics(sourceFile));
596
+ }
597
+
623
598
  if (!this._options.output.dts) {
624
599
  diagnostics.push(...builderProgram.getProgram().getDeclarationDiagnostics());
625
600
  }
@@ -802,7 +777,13 @@ export class SdTsCompiler {
802
777
  ): ReadonlySet<string> | undefined {
803
778
  const affectedFiles = new Set<string>();
804
779
  while (true) {
805
- const result = builderProgram.getSemanticDiagnosticsOfNextAffectedFile();
780
+ const result = builderProgram.getSemanticDiagnosticsOfNextAffectedFile(
781
+ undefined,
782
+ (sourceFile) => {
783
+ this._setCrashContext("findAffectedFiles", sourceFile.fileName);
784
+ return false;
785
+ },
786
+ );
806
787
  if (result == null) break;
807
788
  if ("fileName" in result.affected) {
808
789
  affectedFiles.add(pathx.posix(result.affected.fileName));
@@ -829,6 +810,7 @@ export class SdTsCompiler {
829
810
  const result = builderProgram.getSemanticDiagnosticsOfNextAffectedFile(
830
811
  undefined,
831
812
  (sourceFile) => {
813
+ this._setCrashContext("findAffectedFiles", sourceFile.fileName);
832
814
  if (
833
815
  angularCompiler.ignoreForDiagnostics.has(sourceFile) &&
834
816
  sourceFile.fileName.endsWith(".ngtypecheck.ts")
@@ -4,7 +4,6 @@ import { SdError } from "@simplysm/core-common";
4
4
  import { pathx } from "@simplysm/core-node";
5
5
  import { createLazyLogger } from "../runtime/lazy-logger";
6
6
  import type {
7
- SdBuildPackageConfig,
8
7
  SdPackageConfig,
9
8
  } from "../sd-config.types";
10
9
 
@@ -90,7 +89,7 @@ export function mergeTestsPackagesIntoConfig(
90
89
  );
91
90
  }
92
91
 
93
- merged[name] = { target: "node" } as SdBuildPackageConfig;
92
+ merged[name] = { target: "node" };
94
93
  pathMap.set(name, relPath);
95
94
  }
96
95
 
@@ -1,6 +1,6 @@
1
1
  import path from "path";
2
2
  import fs from "node:fs";
3
- import { createWorker, FsWatcher } from "@simplysm/core-node";
3
+ import { createWorker, FsWatcher, pathx } from "@simplysm/core-node";
4
4
  import { err as errNs } from "@simplysm/core-common";
5
5
  import { setupWorkerLifecycle } from "./shared-worker-lifecycle";
6
6
  import {
@@ -208,6 +208,11 @@ function createSourceFileCachePlugin(): esbuild.Plugin {
208
208
  ...typeScriptFileCache.keys(),
209
209
  ];
210
210
  const changedFiles = mtimeTracker.detectChanges(watchTargets);
211
+ const normalizedChangedFiles = new Set<string>();
212
+ for (const file of changedFiles) {
213
+ normalizedChangedFiles.add(pathx.posix(file));
214
+ }
215
+ esbuildResult.sourceFileCache.cycleModifiedFiles = normalizedChangedFiles;
211
216
  if (changedFiles.size > 0) {
212
217
  esbuildResult.sourceFileCache.invalidate(changedFiles);
213
218
  }
@@ -48,7 +48,7 @@ function setupPlugin(plugin: esbuild.Plugin) {
48
48
  outputFiles: [],
49
49
  metafile: { inputs: {}, outputs: {} },
50
50
  ...result,
51
- } as esbuild.BuildResult);
51
+ });
52
52
  },
53
53
  invokeOnDispose() { onDisposeCb?.(); },
54
54
  get onStartCb() { return onStartCb; },
@@ -32,7 +32,7 @@ function setupPlugin(plugin: esbuild.Plugin) {
32
32
  outputFiles: [],
33
33
  metafile: { inputs: {}, outputs: {} },
34
34
  ...result,
35
- } as esbuild.BuildResult)) ?? null
35
+ })) ?? null
36
36
  );
37
37
  },
38
38
  };