@simplysm/sd-cli 14.0.46 → 14.0.48

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 (134) hide show
  1. package/README.md +782 -0
  2. package/dist/angular/ngtsc-build-core.js +2 -2
  3. package/dist/angular/ngtsc-build-core.js.map +1 -1
  4. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  5. package/dist/angular/vite-angular-plugin.js +3 -2
  6. package/dist/angular/vite-angular-plugin.js.map +1 -1
  7. package/dist/capacitor/capacitor-android.js +2 -2
  8. package/dist/capacitor/capacitor-android.js.map +1 -1
  9. package/dist/capacitor/capacitor-build.d.ts.map +1 -1
  10. package/dist/capacitor/capacitor-build.js +2 -1
  11. package/dist/capacitor/capacitor-build.js.map +1 -1
  12. package/dist/capacitor/capacitor-icon.d.ts.map +1 -1
  13. package/dist/capacitor/capacitor-icon.js +2 -1
  14. package/dist/capacitor/capacitor-icon.js.map +1 -1
  15. package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
  16. package/dist/capacitor/capacitor-npm-config.js +2 -1
  17. package/dist/capacitor/capacitor-npm-config.js.map +1 -1
  18. package/dist/capacitor/capacitor.d.ts.map +1 -1
  19. package/dist/capacitor/capacitor.js +2 -1
  20. package/dist/capacitor/capacitor.js.map +1 -1
  21. package/dist/commands/device.js +2 -2
  22. package/dist/commands/device.js.map +1 -1
  23. package/dist/commands/publish/git-phase.js +1 -1
  24. package/dist/commands/publish/git-phase.js.map +1 -1
  25. package/dist/commands/replace-deps.js +2 -2
  26. package/dist/commands/replace-deps.js.map +1 -1
  27. package/dist/deps/replace-deps/collect-deps.js +2 -2
  28. package/dist/deps/replace-deps/collect-deps.js.map +1 -1
  29. package/dist/deps/replace-deps/replace-deps.d.ts.map +1 -1
  30. package/dist/deps/replace-deps/replace-deps.js +108 -81
  31. package/dist/deps/replace-deps/replace-deps.js.map +1 -1
  32. package/dist/deps/server-externals/server-production-files.js +2 -2
  33. package/dist/deps/server-externals/server-production-files.js.map +1 -1
  34. package/dist/electron/electron.d.ts.map +1 -1
  35. package/dist/electron/electron.js +2 -1
  36. package/dist/electron/electron.js.map +1 -1
  37. package/dist/engines/BaseEngine.d.ts.map +1 -1
  38. package/dist/engines/BaseEngine.js +2 -2
  39. package/dist/engines/BaseEngine.js.map +1 -1
  40. package/dist/engines/EsbuildClientEngine.d.ts.map +1 -1
  41. package/dist/engines/EsbuildClientEngine.js +2 -2
  42. package/dist/engines/EsbuildClientEngine.js.map +1 -1
  43. package/dist/engines/NgtscEngine.js +2 -2
  44. package/dist/engines/NgtscEngine.js.map +1 -1
  45. package/dist/engines/ServerEsbuildEngine.js +2 -2
  46. package/dist/engines/ServerEsbuildEngine.js.map +1 -1
  47. package/dist/engines/TscEngine.js +2 -2
  48. package/dist/engines/TscEngine.js.map +1 -1
  49. package/dist/engines/engine-factory.d.ts.map +1 -1
  50. package/dist/engines/engine-factory.js +2 -2
  51. package/dist/engines/engine-factory.js.map +1 -1
  52. package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts.map +1 -1
  53. package/dist/esbuild/esbuild-angular-compiler-plugin.js +52 -20
  54. package/dist/esbuild/esbuild-angular-compiler-plugin.js.map +1 -1
  55. package/dist/esbuild/esbuild-config.js +2 -2
  56. package/dist/esbuild/esbuild-config.js.map +1 -1
  57. package/dist/esbuild/esbuild-worker-plugin.d.ts +14 -2
  58. package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -1
  59. package/dist/esbuild/esbuild-worker-plugin.js +13 -5
  60. package/dist/esbuild/esbuild-worker-plugin.js.map +1 -1
  61. package/dist/lint/lint-with-program.js +2 -2
  62. package/dist/lint/lint-with-program.js.map +1 -1
  63. package/dist/runtime/lazy-logger.d.ts +14 -0
  64. package/dist/runtime/lazy-logger.d.ts.map +1 -0
  65. package/dist/runtime/lazy-logger.js +23 -0
  66. package/dist/runtime/lazy-logger.js.map +1 -0
  67. package/dist/sd-cli-entry.js +2 -2
  68. package/dist/sd-cli-entry.js.map +1 -1
  69. package/dist/sd-cli.js +2 -2
  70. package/dist/sd-cli.js.map +1 -1
  71. package/dist/ts-compiler/SdTsCompiler.d.ts +11 -0
  72. package/dist/ts-compiler/SdTsCompiler.d.ts.map +1 -1
  73. package/dist/ts-compiler/SdTsCompiler.js +223 -116
  74. package/dist/ts-compiler/SdTsCompiler.js.map +1 -1
  75. package/dist/typecheck/typecheck-non-package.js +2 -2
  76. package/dist/typecheck/typecheck-non-package.js.map +1 -1
  77. package/dist/typecheck/typecheck-serialization.d.ts +31 -9
  78. package/dist/typecheck/typecheck-serialization.d.ts.map +1 -1
  79. package/dist/typecheck/typecheck-serialization.js +62 -22
  80. package/dist/typecheck/typecheck-serialization.js.map +1 -1
  81. package/dist/utils/output-utils.js +2 -2
  82. package/dist/utils/output-utils.js.map +1 -1
  83. package/dist/utils/package-classify.js +2 -2
  84. package/dist/utils/package-classify.js.map +1 -1
  85. package/dist/utils/package-utils.js +2 -2
  86. package/dist/utils/package-utils.js.map +1 -1
  87. package/dist/utils/sd-config.js +2 -2
  88. package/dist/utils/sd-config.js.map +1 -1
  89. package/dist/utils/tsconfig.d.ts.map +1 -1
  90. package/dist/utils/tsconfig.js +3 -5
  91. package/dist/utils/tsconfig.js.map +1 -1
  92. package/package.json +8 -8
  93. package/src/angular/ngtsc-build-core.ts +3 -3
  94. package/src/angular/vite-angular-plugin.ts +3 -2
  95. package/src/capacitor/capacitor-android.ts +2 -2
  96. package/src/capacitor/capacitor-build.ts +2 -1
  97. package/src/capacitor/capacitor-icon.ts +2 -1
  98. package/src/capacitor/capacitor-npm-config.ts +2 -1
  99. package/src/capacitor/capacitor.ts +2 -1
  100. package/src/commands/device.ts +2 -2
  101. package/src/commands/publish/git-phase.ts +1 -1
  102. package/src/commands/replace-deps.ts +2 -2
  103. package/src/deps/replace-deps/collect-deps.ts +2 -2
  104. package/src/deps/replace-deps/replace-deps.ts +119 -85
  105. package/src/deps/server-externals/server-production-files.ts +2 -2
  106. package/src/electron/electron.ts +2 -1
  107. package/src/engines/BaseEngine.ts +2 -2
  108. package/src/engines/EsbuildClientEngine.ts +2 -2
  109. package/src/engines/NgtscEngine.ts +2 -2
  110. package/src/engines/ServerEsbuildEngine.ts +2 -2
  111. package/src/engines/TscEngine.ts +2 -2
  112. package/src/engines/engine-factory.ts +2 -2
  113. package/src/esbuild/esbuild-angular-compiler-plugin.ts +66 -21
  114. package/src/esbuild/esbuild-config.ts +2 -2
  115. package/src/esbuild/esbuild-worker-plugin.ts +24 -4
  116. package/src/lint/lint-with-program.ts +2 -2
  117. package/src/runtime/lazy-logger.ts +23 -0
  118. package/src/sd-cli-entry.ts +2 -2
  119. package/src/sd-cli.ts +2 -2
  120. package/src/ts-compiler/SdTsCompiler.ts +280 -138
  121. package/src/typecheck/typecheck-non-package.ts +2 -2
  122. package/src/typecheck/typecheck-serialization.ts +100 -26
  123. package/src/utils/output-utils.ts +2 -2
  124. package/src/utils/package-classify.ts +2 -2
  125. package/src/utils/package-utils.ts +2 -2
  126. package/src/utils/sd-config.ts +2 -2
  127. package/src/utils/tsconfig.ts +3 -4
  128. package/tests/esbuild/esbuild-worker-plugin.spec.ts +245 -0
  129. package/tests/utils/replace-deps-watch.acc.spec.ts +85 -0
  130. package/tests/utils/replace-deps-watch.spec.ts +198 -1
  131. package/dist/sd-cli/tests/angular/fixtures/packages/basic-app/tests/test.fixture.d.ts +0 -3
  132. package/dist/sd-cli/tests/angular/fixtures/packages/basic-app/tests/test.fixture.d.ts.map +0 -1
  133. package/dist/sd-cli/tests/angular/fixtures/packages/basic-app/tests/test.fixture.js +0 -9
  134. package/dist/sd-cli/tests/angular/fixtures/packages/basic-app/tests/test.fixture.js.map +0 -1
@@ -50,14 +50,27 @@ async function copyWithUnlink(
50
50
  if (stats.isDirectory()) {
51
51
  await fsx.mkdir(targetPath);
52
52
  const names = await fs.promises.readdir(sourcePath);
53
+ const allowedChildren = names
54
+ .map((name) => path.resolve(sourcePath, name))
55
+ .filter((child) => filter == null || filter(child));
56
+ const allowedBasenames = new Set(allowedChildren.map((c) => path.basename(c)));
57
+
58
+ // 고아 엔트리 정리: filter 범위 내이면서 소스에 없는 타겟 엔트리 삭제
59
+ const targetNames = await fs.promises.readdir(targetPath).catch(() => [] as string[]);
60
+ await Promise.all(
61
+ targetNames.map(async (name) => {
62
+ const targetChild = path.join(targetPath, name);
63
+ if (filter != null && !filter(targetChild)) return;
64
+ if (allowedBasenames.has(name)) return;
65
+ await fs.promises.rm(targetChild, { recursive: true, force: true });
66
+ }),
67
+ );
68
+
53
69
  await Promise.all(
54
- names
55
- .map((name) => path.resolve(sourcePath, name))
56
- .filter((child) => filter == null || filter(child))
57
- .map((child) => copyWithUnlink(
58
- child,
59
- path.join(targetPath, path.basename(child)),
60
- )),
70
+ allowedChildren.map((child) => copyWithUnlink(
71
+ child,
72
+ path.join(targetPath, path.basename(child)),
73
+ )),
61
74
  );
62
75
  } else {
63
76
  if (await isFileContentSame(sourcePath, targetPath)) return;
@@ -201,115 +214,136 @@ export async function watchReplaceDeps(
201
214
 
202
215
  const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
203
216
 
204
- // 소스 디렉토리 감시자 설정
205
- const watchers: FsWatcher[] = [];
206
- const watchedSources = new Set<string>();
207
-
208
217
  logger.start(`replace-deps 워치 시작 중... (${entries.length}개 대상)`);
209
218
 
219
+ // resolvedSourcePath(posix) → entries 그룹화
220
+ // resolvedSourcePath는 replace-deps-resolve의 pathx.posixResolve 결과로 이미 POSIX이다.
221
+ const sourceMap = new Map<string, ReplaceDepEntry[]>();
210
222
  for (const entry of entries) {
211
- if (watchedSources.has(entry.resolvedSourcePath)) continue;
212
- watchedSources.add(entry.resolvedSourcePath);
223
+ const key = entry.resolvedSourcePath;
224
+ const arr = sourceMap.get(key) ?? [];
225
+ arr.push(entry);
226
+ sourceMap.set(key, arr);
227
+ }
213
228
 
214
- try {
215
- // 소스 패키지의 files 필드를 읽어 감시 대상 경로 구성
216
- const files = await loadFilesField(entry.resolvedSourcePath);
217
- if (files == null) {
218
- logger.warn(`[${entry.targetName}] package.json에 files 필드가 없어 감시 건너뜀`);
219
- continue;
220
- }
229
+ // 각 source의 watchPaths 수집 (files 필드 없는 source는 경고 후 제외). source 단위 병렬.
230
+ const allWatchPaths = new Set<string>();
231
+ await Promise.all(
232
+ [...sourceMap].map(async ([sourcePath, sourceEntries]) => {
233
+ try {
234
+ const files = await loadFilesField(sourcePath);
235
+
236
+ if (files == null) {
237
+ logger.warn(
238
+ `[${sourceEntries[0].targetName}] package.json에 files 필드가 없어 감시 건너뜀`,
239
+ );
240
+ sourceMap.delete(sourcePath);
241
+ return;
242
+ }
221
243
 
222
- // files 항목 경로 + npm 기본 파일 경로
223
- const watchPaths = files.map((f) =>
224
- pathx.posix(path.join(entry.resolvedSourcePath, f)),
225
- );
244
+ for (const f of files) {
245
+ allWatchPaths.add(pathx.posix(path.join(sourcePath, f)));
246
+ }
226
247
 
227
- // 소스 루트에서 npm 기본 파일 패턴에 매칭되는 파일 추가
228
- try {
229
- const rootEntries = await fs.promises.readdir(entry.resolvedSourcePath);
248
+ const rootEntries = await fs.promises
249
+ .readdir(sourcePath)
250
+ .catch(() => [] as string[]);
230
251
  for (const name of rootEntries) {
231
252
  if (NPM_DEFAULT_FILE_PATTERN.test(name)) {
232
- watchPaths.push(pathx.posix(path.join(entry.resolvedSourcePath, name)));
253
+ allWatchPaths.add(pathx.posix(path.join(sourcePath, name)));
233
254
  }
234
255
  }
235
- } catch {
236
- // readdir 실패 시 npm 기본 파일 감시 생략
256
+ } catch (err) {
257
+ logger.error(
258
+ `[${sourceEntries[0].targetName}] 감시 설정 실패: ${err instanceof Error ? err.message : err}`,
259
+ );
260
+ sourceMap.delete(sourcePath);
237
261
  }
262
+ }),
263
+ );
264
+
265
+ if (allWatchPaths.size === 0) {
266
+ if (entries.length > 0) {
267
+ logger.warn("감시 대상이 없어 워치가 시작되지 않음");
268
+ } else {
269
+ logger.success("replace-deps 워치 준비 완료");
270
+ }
271
+ return { entries, dispose: () => {} };
272
+ }
238
273
 
239
- // 소스 경로에 해당하는 entries만 사전 필터링하여 캡처
240
- const sourceEntries = entries.filter(
241
- (e) => e.resolvedSourcePath === entry.resolvedSourcePath,
242
- );
274
+ // longest-prefix 매칭을 위해 경로 우선 정렬
275
+ const sortedSources = [...sourceMap.keys()].sort((a, b) => b.length - a.length);
276
+
277
+ const findSource = (changedPath: string): string | undefined => {
278
+ for (const src of sortedSources) {
279
+ if (changedPath === src || changedPath.startsWith(src + "/")) return src;
280
+ }
281
+ return undefined;
282
+ };
283
+
284
+ const watcher = await FsWatcher.watch([...allWatchPaths], {
285
+ followSymlinks: false,
286
+ });
243
287
 
244
- const watcher = await FsWatcher.watch(watchPaths, {
245
- followSymlinks: false,
246
- });
247
- watcher.onChange({ delay: 300 }, async (changeInfos) => {
248
- let hasActualCopy = false;
288
+ watcher.onChange({ delay: 300 }, async (changeInfos) => {
289
+ const flags = await Promise.all(
290
+ changeInfos.map(async ({ path: changedPath }) => {
291
+ const src = findSource(changedPath);
292
+ if (src == null) return false;
293
+ const sourceEntries = sourceMap.get(src)!;
249
294
 
250
- for (const { path: changedPath } of changeInfos) {
251
- // 사전 필터링된 항목만 순회
252
- for (const e of sourceEntries) {
253
- // 소스로부터의 상대 경로 계산
254
- const relativePath = pathx.posix(path.relative(e.resolvedSourcePath, changedPath));
255
- const destPath = pathx.posix(path.join(e.actualTargetPath, relativePath));
295
+ let localActualCopy = false;
256
296
 
297
+ // 동일 source의 복수 target 복사는 순차로 유지한다 (destination 중복 시 race 방지).
298
+ for (const e of sourceEntries) {
299
+ const relativePath = pathx.posix(path.relative(e.resolvedSourcePath, changedPath));
300
+ const destPath = pathx.posix(path.join(e.actualTargetPath, relativePath));
301
+
302
+ try {
303
+ let sourceExists = false;
257
304
  try {
258
- // 소스 존재 여부 확인
259
- let sourceExists = false;
260
- try {
261
- await fs.promises.access(changedPath);
262
- sourceExists = true;
263
- } catch {
264
- // 소스가 삭제됨
265
- }
305
+ await fs.promises.access(changedPath);
306
+ sourceExists = true;
307
+ } catch {
308
+ // 소스가 삭제됨
309
+ }
266
310
 
267
- if (sourceExists) {
268
- // 소스가 디렉토리인지 파일인지 확인
269
- const stat = await fs.promises.stat(changedPath);
270
- if (stat.isDirectory()) {
271
- await fsx.mkdir(destPath);
272
- } else {
273
- // 파일 내용이 동일하면 복사 건너뜀 (불필요한 리빌드 방지)
274
- if (await isFileContentSame(changedPath, destPath)) continue;
275
- await fsx.mkdir(pathx.posix(path.dirname(destPath)));
276
- await fsx.copy(changedPath, destPath);
277
- hasActualCopy = true;
278
- }
311
+ if (sourceExists) {
312
+ const stat = await fs.promises.stat(changedPath);
313
+ if (stat.isDirectory()) {
314
+ await fsx.mkdir(destPath);
279
315
  } else {
280
- // 소스가 삭제됨 대상도 삭제
281
- await fsx.rm(destPath);
282
- hasActualCopy = true;
316
+ if (await isFileContentSame(changedPath, destPath)) continue;
317
+ await fsx.mkdir(pathx.posix(path.dirname(destPath)));
318
+ await fsx.copy(changedPath, destPath);
319
+ localActualCopy = true;
283
320
  }
284
- } catch (err) {
285
- logger.error(
286
- `[${e.targetName}] 복사 실패 (${relativePath}): ${err instanceof Error ? err.message : err}`,
287
- );
321
+ } else {
322
+ await fsx.rm(destPath);
323
+ localActualCopy = true;
288
324
  }
325
+ } catch (err) {
326
+ logger.error(
327
+ `[${e.targetName}] 복사 실패 (${relativePath}): ${err instanceof Error ? err.message : err}`,
328
+ );
289
329
  }
290
330
  }
291
331
 
292
- if (hasActualCopy) {
293
- options?.onChanged?.();
294
- }
295
- });
332
+ return localActualCopy;
333
+ }),
334
+ );
296
335
 
297
- watchers.push(watcher);
298
- } catch (err) {
299
- logger.error(
300
- `[${entry.targetName}] 감시 설정 실패: ${err instanceof Error ? err.message : err}`,
301
- );
336
+ if (flags.some((f) => f)) {
337
+ options?.onChanged?.();
302
338
  }
303
- }
339
+ });
304
340
 
305
341
  logger.success("replace-deps 워치 준비 완료");
306
342
 
307
343
  return {
308
344
  entries,
309
345
  dispose: () => {
310
- for (const watcher of watchers) {
311
- void watcher.close();
312
- }
346
+ void watcher.close();
313
347
  },
314
348
  };
315
349
  }
@@ -4,10 +4,10 @@ import fs from "fs";
4
4
  import YAML from "yaml";
5
5
  import TOML from "smol-toml";
6
6
  import { cpx } from "@simplysm/core-node";
7
- import { consola } from "consola";
7
+ import { createLazyLogger } from "../../runtime/lazy-logger";
8
8
  import { collectAllDependencyExternals } from "../../esbuild/esbuild-config";
9
9
 
10
- const logger = consola.withTag("sd:cli:server-production-files");
10
+ const logger = createLazyLogger("sd:cli:server-production-files");
11
11
 
12
12
  /**
13
13
  * 외부 모듈을 두 용도로 분리하여 수집한다.
@@ -3,11 +3,12 @@ import fs from "fs";
3
3
  import module from "module";
4
4
  import { cpx, fsx, pathx } from "@simplysm/core-node";
5
5
  import { consola, LogLevels } from "consola";
6
+ import { createLazyLogger } from "../runtime/lazy-logger";
6
7
  import type { NpmConfig, SdElectronConfig } from "../sd-config.types.js";
7
8
  import { createEnvBanner } from "../esbuild/esbuild-config.js";
8
9
 
9
10
  export class Electron {
10
- private static readonly _logger = consola.withTag("sd:cli:electron");
11
+ private static readonly _logger = createLazyLogger("sd:cli:electron");
11
12
 
12
13
  private readonly _electronPath: string;
13
14
  private readonly _srcPath: string;
@@ -1,15 +1,15 @@
1
1
  import { Worker, type WorkerProxy } from "@simplysm/core-node";
2
2
  import { err as errNs } from "@simplysm/core-common";
3
- import { consola } from "consola";
4
3
  import type { BuildResult, ResultCollector } from "../runtime/ResultCollector";
5
4
  import { stopEngineWorker } from "../runtime/engine-stop";
6
5
  import { setupWatchEvents } from "../runtime/engine-watch-events";
6
+ import { createLazyLogger } from "../runtime/lazy-logger";
7
7
  import type { LintWithProgramResult } from "../lint/lint-with-program";
8
8
  import type { RebuildManager } from "../runtime/rebuild-manager";
9
9
  import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
10
10
  import type { BuildEngine, BuildOutput, EngineResult, PackageInfo } from "./types";
11
11
 
12
- const logger = consola.withTag("sd:cli:engine");
12
+ const logger = createLazyLogger("sd:cli:engine");
13
13
 
14
14
  /**
15
15
  * 모든 빌드 워커가 공유하는 공통 빌드 워커 이벤트
@@ -1,15 +1,15 @@
1
1
  import fs from "node:fs";
2
2
  import path from "path";
3
3
  import { Worker, type WorkerProxy } from "@simplysm/core-node";
4
- import { consola } from "consola";
5
4
  import type * as ClientWorkerModule from "../workers/client.worker";
6
5
  import type { ResultCollector } from "../runtime/ResultCollector";
7
6
  import { stopEngineWorker } from "../runtime/engine-stop";
8
7
  import { setupWatchEvents, type NormalizedBuildInfo } from "../runtime/engine-watch-events";
8
+ import { createLazyLogger } from "../runtime/lazy-logger";
9
9
  import type { RebuildManager } from "../runtime/rebuild-manager";
10
10
  import type { BuildEngine, BuildOutput, ClientPackageInfo, EngineResult } from "./types";
11
11
 
12
- const logger = consola.withTag("sd:cli:engine:esbuild-client");
12
+ const logger = createLazyLogger("sd:cli:engine:esbuild-client");
13
13
 
14
14
  /**
15
15
  * EsbuildClientEngine 옵션
@@ -3,9 +3,9 @@ import type { ResultCollector } from "../runtime/ResultCollector";
3
3
  import type { RebuildManager } from "../runtime/rebuild-manager";
4
4
  import type { BuildOutput, BuildPackageInfo, EngineResult } from "./types";
5
5
  import { BaseEngine, type CommonBuildWorkerModule } from "./BaseEngine";
6
- import { consola } from "consola";
6
+ import { createLazyLogger } from "../runtime/lazy-logger";
7
7
 
8
- const logger = consola.withTag("sd:cli:engine:ngtsc");
8
+ const logger = createLazyLogger("sd:cli:engine:ngtsc");
9
9
 
10
10
  /**
11
11
  * NgtscEngine 옵션
@@ -3,9 +3,9 @@ import type { ResultCollector } from "../runtime/ResultCollector";
3
3
  import type { RebuildManager } from "../runtime/rebuild-manager";
4
4
  import type { BuildOutput, EngineResult, ServerPackageInfo } from "./types";
5
5
  import { BaseEngine, type CommonBuildWorkerModule } from "./BaseEngine";
6
- import { consola } from "consola";
6
+ import { createLazyLogger } from "../runtime/lazy-logger";
7
7
 
8
- const logger = consola.withTag("sd:cli:engine:server");
8
+ const logger = createLazyLogger("sd:cli:engine:server");
9
9
 
10
10
  /**
11
11
  * ServerEsbuildEngine 옵션
@@ -3,9 +3,9 @@ import type { ResultCollector } from "../runtime/ResultCollector";
3
3
  import type { RebuildManager } from "../runtime/rebuild-manager";
4
4
  import type { BuildOutput, BuildPackageInfo, EngineResult } from "./types";
5
5
  import { BaseEngine, type CommonBuildWorkerModule } from "./BaseEngine";
6
- import { consola } from "consola";
6
+ import { createLazyLogger } from "../runtime/lazy-logger";
7
7
 
8
- const logger = consola.withTag("sd:cli:engine:tsc");
8
+ const logger = createLazyLogger("sd:cli:engine:tsc");
9
9
 
10
10
  /**
11
11
  * TscEngine 옵션
@@ -1,5 +1,5 @@
1
- import { consola } from "consola";
2
1
  import type { ResultCollector } from "../runtime/ResultCollector";
2
+ import { createLazyLogger } from "../runtime/lazy-logger";
3
3
  import type { RebuildManager } from "../runtime/rebuild-manager";
4
4
  import { hasAngularCoreDependency } from "../utils/package-utils";
5
5
  import { NgtscEngine } from "./NgtscEngine";
@@ -8,7 +8,7 @@ import { TscEngine } from "./TscEngine";
8
8
  import { EsbuildClientEngine } from "./EsbuildClientEngine";
9
9
  import type { BuildEngine, BuildPackageInfo, ClientPackageInfo, ServerPackageInfo } from "./types";
10
10
 
11
- const logger = consola.withTag("sd:cli:engine");
11
+ const logger = createLazyLogger("sd:cli:engine");
12
12
 
13
13
 
14
14
  /**
@@ -3,10 +3,13 @@ import fs from "fs";
3
3
  import os from "os";
4
4
  import ts from "typescript";
5
5
  import type esbuild from "esbuild";
6
- import { consola } from "consola";
7
6
  import { JavaScriptTransformer, Cache as AngularCache } from "@angular/build/private";
7
+ import { createLazyLogger } from "../runtime/lazy-logger";
8
8
  import type { AngularSourceFileCache } from "../angular/angular-compiler";
9
- import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
9
+ import type {
10
+ SerializedDiagnostic,
11
+ SerializedMessageChain,
12
+ } from "../typecheck/typecheck-serialization";
10
13
  import { SdTsCompiler } from "../ts-compiler/SdTsCompiler";
11
14
  import type { ISdTsCompilerResult } from "../ts-compiler/sd-ts-compiler-result";
12
15
  import { FileReferenceTracker } from "./file-reference-tracker";
@@ -15,7 +18,7 @@ import { createCachedLoad, type LoadResultCache } from "./load-result-cache";
15
18
  import { collectHmrCandidates, HMR_MODIFIED_FILE_LIMIT } from "../angular/hmr-candidates";
16
19
  import { transformWorkerPatterns } from "./esbuild-worker-plugin";
17
20
 
18
- const logger = consola.withTag("sd:cli:angular-plugin");
21
+ const logger = createLazyLogger("sd:cli:angular-plugin");
19
22
 
20
23
  //#region Types
21
24
 
@@ -125,29 +128,67 @@ export function convertDiagnostic(
125
128
  return { text, location };
126
129
  }
127
130
 
131
+ function flattenSerializedMessage(
132
+ messageText: string | SerializedMessageChain,
133
+ indent = 0,
134
+ ): string {
135
+ if (typeof messageText === "string") return messageText;
136
+ const prefix = " ".repeat(indent);
137
+ let text = prefix + messageText.messageText;
138
+ if (messageText.next != null) {
139
+ for (const n of messageText.next) {
140
+ text += "\n" + flattenSerializedMessage(n, indent + 1);
141
+ }
142
+ }
143
+ return text;
144
+ }
145
+
146
+ function locationOf(
147
+ fileInfo: { fileName: string } | undefined,
148
+ start: number | undefined,
149
+ length: number | undefined,
150
+ program: ts.Program,
151
+ cwd: string,
152
+ ): esbuild.PartialMessage["location"] {
153
+ if (fileInfo == null || start == null) return null;
154
+ const sf = program.getSourceFile(fileInfo.fileName);
155
+ if (sf == null) return null;
156
+ const pos = sf.getLineAndCharacterOfPosition(start);
157
+ const lineStart = sf.getLineStarts()[pos.line];
158
+ const lineEnd = sf.getLineStarts()[pos.line + 1] ?? sf.text.length;
159
+ const lineText = sf.text.slice(lineStart, lineEnd).replace(/\r?\n$/, "");
160
+ return {
161
+ file: path.relative(cwd, fileInfo.fileName),
162
+ line: pos.line + 1,
163
+ column: pos.character,
164
+ lineText,
165
+ length: length ?? 0,
166
+ };
167
+ }
168
+
128
169
  export function convertSerializedDiagnosticToEsbuild(
129
170
  d: SerializedDiagnostic,
130
171
  program: ts.Program,
131
172
  cwd: string,
132
173
  ): esbuild.PartialMessage {
133
- let location: esbuild.PartialMessage["location"] = null;
134
- if (d.file != null && d.start != null) {
135
- const sf = program.getSourceFile(d.file.fileName);
136
- if (sf != null) {
137
- const pos = sf.getLineAndCharacterOfPosition(d.start);
138
- const lineStart = sf.getLineStarts()[pos.line];
139
- const lineEnd = sf.getLineStarts()[pos.line + 1] ?? sf.text.length;
140
- const lineText = sf.text.slice(lineStart, lineEnd).replace(/\r?\n$/, "");
141
- location = {
142
- file: path.relative(cwd, d.file.fileName),
143
- line: pos.line + 1,
144
- column: pos.character,
145
- lineText,
146
- length: d.length ?? 0,
147
- };
174
+ const location = locationOf(d.file, d.start, d.length, program, cwd);
175
+ const text = flattenSerializedMessage(d.messageText);
176
+
177
+ const notes: esbuild.PartialNote[] = [];
178
+ if (d.relatedInformation != null) {
179
+ for (const ri of d.relatedInformation) {
180
+ notes.push({
181
+ text: flattenSerializedMessage(ri.messageText),
182
+ location: locationOf(ri.file, ri.start, ri.length, program, cwd),
183
+ });
148
184
  }
149
185
  }
150
- return { text: d.messageText, location };
186
+
187
+ return {
188
+ text,
189
+ location,
190
+ notes: notes.length > 0 ? notes : undefined,
191
+ };
151
192
  }
152
193
 
153
194
  //#endregion
@@ -340,7 +381,11 @@ export function createAngularCompilerPlugin(
340
381
  // ── emitResults → typeScriptFileCache (Worker 패턴 처리 포함) ──
341
382
  for (const { contents, sourceFileName } of compileResult.emitResults ?? []) {
342
383
  const normalized = path.normalize(sourceFileName);
343
- const workerResult = transformWorkerPatterns(contents, normalized, build);
384
+ // emitResults.contents는 ngtsc가 이미 JS로 방출한 결과이므로 TS 재변환 스킵.
385
+ // sourceFileName은 원본 .ts 경로이지만 content는 JS이다.
386
+ const workerResult = transformWorkerPatterns(contents, normalized, build, {
387
+ skipTsTransform: true,
388
+ });
344
389
  if (workerResult != null) {
345
390
  typeScriptFileCache.set(normalized, workerResult.contents);
346
391
  errors.push(...workerResult.errors);
@@ -541,7 +586,7 @@ export function createAngularCompilerPlugin(
541
586
  sideEffects,
542
587
  );
543
588
 
544
- // Worker 패턴 처리 (D2)
589
+ // Worker 패턴 처리
545
590
  const textContents = new TextDecoder().decode(contents);
546
591
  const workerResult = transformWorkerPatterns(textContents, request, build);
547
592
  if (workerResult != null) {
@@ -3,10 +3,10 @@ import { readFileSync, existsSync } from "fs";
3
3
  import fs from "fs/promises";
4
4
  import { createRequire } from "module";
5
5
  import type esbuild from "esbuild";
6
- import { consola } from "consola";
6
+ import { createLazyLogger } from "../runtime/lazy-logger";
7
7
  import { addJsExtensionToImports } from "../utils/output-path-rewriter";
8
8
 
9
- const logger = consola.withTag("sd:cli:esbuild-config");
9
+ const logger = createLazyLogger("sd:cli:esbuild-config");
10
10
 
11
11
  /**
12
12
  * esbuild outputFiles에서 변경된 파일만 디스크에 쓴다.
@@ -41,6 +41,8 @@ function isImportMetaUrl(node: any): boolean {
41
41
  *
42
42
  * 정규식과 달리 주석, 문자열 리터럴 내부의 패턴을 오탐하지 않는다.
43
43
  * 파싱 실패 시 빈 배열을 반환한다.
44
+ *
45
+ * @param content - JavaScript 소스 코드. TypeScript는 상위 `transformWorkerPatterns()`가 사전 변환한다.
44
46
  */
45
47
  export function findWorkerPatterns(content: string): WorkerMatch[] {
46
48
  let ast: acorn.Node;
@@ -153,6 +155,17 @@ export interface TransformWorkerResult {
153
155
  workerMetafile?: esbuild.Metafile;
154
156
  }
155
157
 
158
+ /**
159
+ * transformWorkerPatterns 옵션.
160
+ */
161
+ export interface TransformWorkerOptions {
162
+ /**
163
+ * true이면 .ts/.cts/.mts 확장자여도 esbuild.transformSync(loader: "ts")를 스킵한다.
164
+ * 호출자가 content가 이미 JS임을 보장하는 경우에만 사용한다 (예: ngtsc emit 결과).
165
+ */
166
+ skipTsTransform?: boolean;
167
+ }
168
+
156
169
  /**
157
170
  * Worker 파일을 esbuild.buildSync()로 별도 ESM 번들로 빌드한다.
158
171
  * esbuild-angular-compiler-plugin.ts의 bundleWebWorker를 기반으로 작성.
@@ -202,7 +215,7 @@ function bundleWorker(
202
215
  * 파일 내용에서 Worker/SharedWorker 패턴을 감지하여 Worker 파일을 번들링하고
203
216
  * URL 경로를 번들된 파일 경로로 치환한다.
204
217
  *
205
- * Angular 플러그인 등 외부에서 직접 호출할 수 있도록 export한다 (D2).
218
+ * Angular 컴파일러 플러그인 등 외부에서 직접 호출한다.
206
219
  *
207
220
  * @returns 변환 결과. 패턴이 없으면 undefined.
208
221
  */
@@ -210,9 +223,14 @@ export function transformWorkerPatterns(
210
223
  content: string,
211
224
  filePath: string,
212
225
  build: esbuild.PluginBuild,
226
+ options?: TransformWorkerOptions,
213
227
  ): TransformWorkerResult | undefined {
214
- // 빠른 사전 필터 — AST 파싱 전에 키워드 존재 여부로 걸러냄 (원본 TS content 기준)
215
- if (!content.includes("Worker") && !content.includes("import.meta.resolve")) {
228
+ // 빠른 사전 필터 — new Worker(), new SharedWorker(), import.meta.resolve()의 단어 경계
229
+ // 호출 형태만 통과시킨다. 식별자·타입·interface로만 등장하는 Worker 키워드는 차단하여
230
+ // TS transformSync 오버헤드를 줄인다. 정확성은 후속 AST 판별(findWorkerPatterns)이 담당한다.
231
+ // new 와 Worker/SharedWorker 사이에 주석이 낀 호출(예: `new /* c */ Worker`)은 의도된
232
+ // 트레이드오프로 탈락한다. 실발생 가능성이 희박하여 현행을 유지한다.
233
+ if (!/\b(new\s+Worker|new\s+SharedWorker|import\.meta\.resolve)\b/.test(content)) {
216
234
  return undefined;
217
235
  }
218
236
 
@@ -221,8 +239,10 @@ export function transformWorkerPatterns(
221
239
 
222
240
  // TS(.ts/.cts/.mts)는 JS로 변환한 후 AST 파싱. acorn은 TS 구문을 처리하지 못하므로
223
241
  // import type, 타입 어노테이션 등이 있으면 파싱 실패로 Worker 패턴이 조용히 누락된다.
242
+ // skipTsTransform이 true이면 호출자가 content가 이미 JS임을 보장하므로 변환을 스킵한다
243
+ // (예: Angular 컴파일러 플러그인이 ngtsc emit 결과를 전달하는 경로).
224
244
  let effectiveContent = content;
225
- if (/\.[cm]?ts$/.test(filePath)) {
245
+ if (options?.skipTsTransform !== true && /\.[cm]?ts$/.test(filePath)) {
226
246
  try {
227
247
  const transformed = build.esbuild.transformSync(content, {
228
248
  loader: "ts",
@@ -1,9 +1,9 @@
1
1
  import type ts from "typescript";
2
2
  import { ESLint } from "eslint";
3
3
  import { pathx } from "@simplysm/core-node";
4
- import { consola } from "consola";
4
+ import { createLazyLogger } from "../runtime/lazy-logger";
5
5
 
6
- const logger = consola.withTag("sd:cli:lint-with-program");
6
+ const logger = createLazyLogger("sd:cli:lint-with-program");
7
7
 
8
8
  /**
9
9
  * LintWithProgramRunner.lint()가 반환하는 lint 결과
@@ -0,0 +1,23 @@
1
+ import { consola, type ConsolaInstance } from "consola";
2
+
3
+ /**
4
+ * 모듈 레벨에서 선언해도 안전한 lazy logger 프록시.
5
+ *
6
+ * consola.withTag()는 호출 시점의 consola.options(level/reporters)를
7
+ * 스냅샷 복사한 새 인스턴스를 반환한다. 모듈 import 시점에 호출하면
8
+ * setupConsola 이전의 기본 level(info) 상태가 고정되어 이후 setupConsola가
9
+ * level을 debug로 올려도 반영되지 않는다.
10
+ *
11
+ * createLazyLogger는 실제 ConsolaInstance 생성을 첫 접근 시점까지 미뤄
12
+ * setupConsola 이후의 설정이 반영된 스냅샷을 갖도록 한다.
13
+ */
14
+ export function createLazyLogger(tag: string): ConsolaInstance {
15
+ let cached: ConsolaInstance | undefined;
16
+ return new Proxy({} as ConsolaInstance, {
17
+ get(_target, prop) {
18
+ cached ??= consola.withTag(tag);
19
+ const value: unknown = Reflect.get(cached, prop);
20
+ return typeof value === "function" ? value.bind(cached) : value;
21
+ },
22
+ });
23
+ }
@@ -15,10 +15,10 @@ import path from "path";
15
15
  import fs from "fs";
16
16
  import { fileURLToPath } from "url";
17
17
  import { EventEmitter } from "node:events";
18
- import { consola } from "consola";
19
18
  import { setupConsola } from "@simplysm/core-node";
19
+ import { createLazyLogger } from "./runtime/lazy-logger";
20
20
 
21
- const logger = consola.withTag("sd:cli:entry");
21
+ const logger = createLazyLogger("sd:cli:entry");
22
22
 
23
23
  Error.stackTraceLimit = Infinity;
24
24
  EventEmitter.defaultMaxListeners = 100;
package/src/sd-cli.ts CHANGED
@@ -8,15 +8,15 @@
8
8
  */
9
9
 
10
10
  import { cpx, setupConsola } from "@simplysm/core-node";
11
- import { consola } from "consola";
12
11
  import os from "os";
13
12
  import path from "path";
14
13
  import { fileURLToPath } from "url";
14
+ import { createLazyLogger } from "./runtime/lazy-logger";
15
15
 
16
16
  const __filename = fileURLToPath(import.meta.url);
17
17
  const __dirname = path.dirname(__filename);
18
18
  const isDev = path.extname(__filename) === ".ts";
19
- const logger = consola.withTag("sd:cli");
19
+ const logger = createLazyLogger("sd:cli");
20
20
 
21
21
  if (isDev) {
22
22
  // 개발 모드 (.ts): affinity 적용 후 직접 실행