@simplysm/sd-cli 14.0.11 → 14.0.12

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 (263) hide show
  1. package/README.md +58 -253
  2. package/dist/angular/client-transform-stylesheet.js +1 -1
  3. package/dist/angular/client-transform-stylesheet.js.map +1 -1
  4. package/dist/angular/vite-angular-plugin.d.ts +1 -1
  5. package/dist/angular/vite-angular-plugin.d.ts.map +1 -1
  6. package/dist/angular/vite-angular-plugin.js +60 -34
  7. package/dist/angular/vite-angular-plugin.js.map +1 -1
  8. package/dist/angular/vite-postcss-inline-plugin.d.ts +1 -1
  9. package/dist/angular/vite-postcss-inline-plugin.js +1 -1
  10. package/dist/capacitor/capacitor.d.ts +14 -2
  11. package/dist/capacitor/capacitor.d.ts.map +1 -1
  12. package/dist/capacitor/capacitor.js +131 -17
  13. package/dist/capacitor/capacitor.js.map +1 -1
  14. package/dist/commands/build.d.ts +3 -10
  15. package/dist/commands/build.d.ts.map +1 -1
  16. package/dist/commands/build.js +3 -10
  17. package/dist/commands/build.js.map +1 -1
  18. package/dist/commands/check.js +3 -3
  19. package/dist/commands/check.js.map +1 -1
  20. package/dist/commands/dev.d.ts +3 -9
  21. package/dist/commands/dev.d.ts.map +1 -1
  22. package/dist/commands/dev.js +3 -9
  23. package/dist/commands/dev.js.map +1 -1
  24. package/dist/commands/device.d.ts +3 -3
  25. package/dist/commands/device.js +5 -5
  26. package/dist/commands/device.js.map +1 -1
  27. package/dist/commands/publish.d.ts +1 -1
  28. package/dist/commands/publish.d.ts.map +1 -1
  29. package/dist/commands/publish.js +18 -26
  30. package/dist/commands/publish.js.map +1 -1
  31. package/dist/commands/replace-deps.d.ts +3 -3
  32. package/dist/commands/replace-deps.d.ts.map +1 -1
  33. package/dist/commands/replace-deps.js +1 -1
  34. package/dist/commands/typecheck.d.ts +4 -3
  35. package/dist/commands/typecheck.d.ts.map +1 -1
  36. package/dist/commands/typecheck.js +5 -11
  37. package/dist/commands/typecheck.js.map +1 -1
  38. package/dist/commands/watch.d.ts +9 -9
  39. package/dist/commands/watch.js +9 -9
  40. package/dist/electron/electron.d.ts.map +1 -1
  41. package/dist/electron/electron.js +42 -3
  42. package/dist/electron/electron.js.map +1 -1
  43. package/dist/engines/BaseEngine.d.ts +1 -1
  44. package/dist/engines/BaseEngine.d.ts.map +1 -1
  45. package/dist/engines/BaseEngine.js +3 -1
  46. package/dist/engines/BaseEngine.js.map +1 -1
  47. package/dist/engines/NgtscEngine.d.ts +7 -7
  48. package/dist/engines/NgtscEngine.d.ts.map +1 -1
  49. package/dist/engines/NgtscEngine.js +3 -3
  50. package/dist/engines/ServerEsbuildEngine.d.ts +7 -7
  51. package/dist/engines/ServerEsbuildEngine.d.ts.map +1 -1
  52. package/dist/engines/ServerEsbuildEngine.js +3 -3
  53. package/dist/engines/TscEngine.d.ts +7 -7
  54. package/dist/engines/TscEngine.d.ts.map +1 -1
  55. package/dist/engines/TscEngine.js +3 -3
  56. package/dist/engines/ViteEngine.d.ts +1 -1
  57. package/dist/engines/ViteEngine.d.ts.map +1 -1
  58. package/dist/engines/ViteEngine.js +7 -12
  59. package/dist/engines/ViteEngine.js.map +1 -1
  60. package/dist/engines/index.d.ts +5 -5
  61. package/dist/engines/index.js +5 -5
  62. package/dist/engines/types.d.ts +20 -20
  63. package/dist/engines/types.d.ts.map +1 -1
  64. package/dist/infra/ResultCollector.d.ts +9 -9
  65. package/dist/infra/ResultCollector.js +8 -8
  66. package/dist/infra/SignalHandler.d.ts +7 -7
  67. package/dist/infra/SignalHandler.js +7 -7
  68. package/dist/infra/WorkerManager.d.ts +14 -14
  69. package/dist/infra/WorkerManager.js +14 -14
  70. package/dist/orchestrators/BuildOrchestrator.d.ts +25 -25
  71. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  72. package/dist/orchestrators/BuildOrchestrator.js +29 -29
  73. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  74. package/dist/orchestrators/DevWatchOrchestrator.d.ts +7 -7
  75. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  76. package/dist/orchestrators/DevWatchOrchestrator.js +34 -34
  77. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  78. package/dist/sd-cli-entry.d.ts +2 -2
  79. package/dist/sd-cli-entry.d.ts.map +1 -1
  80. package/dist/sd-cli-entry.js +15 -8
  81. package/dist/sd-cli-entry.js.map +1 -1
  82. package/dist/sd-cli.d.ts +3 -3
  83. package/dist/sd-cli.js +16 -16
  84. package/dist/sd-cli.js.map +1 -1
  85. package/dist/sd-config.types.d.ts +105 -105
  86. package/dist/sd-config.types.d.ts.map +1 -1
  87. package/dist/utils/angular-compiler.js +5 -5
  88. package/dist/utils/angular-compiler.js.map +1 -1
  89. package/dist/utils/build-env.d.ts +1 -1
  90. package/dist/utils/build-env.js +1 -1
  91. package/dist/utils/concurrency.d.ts +7 -7
  92. package/dist/utils/concurrency.js +7 -7
  93. package/dist/utils/copy-public.d.ts +9 -9
  94. package/dist/utils/copy-public.js +17 -17
  95. package/dist/utils/copy-public.js.map +1 -1
  96. package/dist/utils/copy-src.d.ts +9 -9
  97. package/dist/utils/copy-src.js +11 -11
  98. package/dist/utils/copy-src.js.map +1 -1
  99. package/dist/utils/engine-stop.d.ts +8 -9
  100. package/dist/utils/engine-stop.d.ts.map +1 -1
  101. package/dist/utils/engine-stop.js +9 -10
  102. package/dist/utils/engine-stop.js.map +1 -1
  103. package/dist/utils/esbuild-config.d.ts +23 -23
  104. package/dist/utils/esbuild-config.d.ts.map +1 -1
  105. package/dist/utils/esbuild-config.js +25 -25
  106. package/dist/utils/esbuild-config.js.map +1 -1
  107. package/dist/utils/lint-with-program.d.ts +15 -15
  108. package/dist/utils/lint-with-program.d.ts.map +1 -1
  109. package/dist/utils/lint-with-program.js +29 -29
  110. package/dist/utils/lint-with-program.js.map +1 -1
  111. package/dist/utils/ngtsc-build-core.d.ts +8 -8
  112. package/dist/utils/ngtsc-build-core.d.ts.map +1 -1
  113. package/dist/utils/ngtsc-build-core.js +14 -14
  114. package/dist/utils/ngtsc-build-core.js.map +1 -1
  115. package/dist/utils/output-path-rewriter.d.ts +14 -14
  116. package/dist/utils/output-path-rewriter.js +18 -18
  117. package/dist/utils/output-path-rewriter.js.map +1 -1
  118. package/dist/utils/output-utils.d.ts +6 -6
  119. package/dist/utils/output-utils.js +11 -11
  120. package/dist/utils/output-utils.js.map +1 -1
  121. package/dist/utils/package-utils.d.ts +21 -21
  122. package/dist/utils/package-utils.d.ts.map +1 -1
  123. package/dist/utils/package-utils.js +56 -45
  124. package/dist/utils/package-utils.js.map +1 -1
  125. package/dist/utils/replace-deps.d.ts +25 -25
  126. package/dist/utils/replace-deps.d.ts.map +1 -1
  127. package/dist/utils/replace-deps.js +84 -65
  128. package/dist/utils/replace-deps.js.map +1 -1
  129. package/dist/utils/sd-config.d.ts +3 -3
  130. package/dist/utils/sd-config.js +3 -3
  131. package/dist/utils/tsc-build.d.ts +13 -13
  132. package/dist/utils/tsc-build.d.ts.map +1 -1
  133. package/dist/utils/tsc-build.js +9 -9
  134. package/dist/utils/tsc-build.js.map +1 -1
  135. package/dist/utils/tsconfig.d.ts +11 -9
  136. package/dist/utils/tsconfig.d.ts.map +1 -1
  137. package/dist/utils/tsconfig.js +11 -9
  138. package/dist/utils/tsconfig.js.map +1 -1
  139. package/dist/utils/typecheck-non-package.d.ts +5 -6
  140. package/dist/utils/typecheck-non-package.d.ts.map +1 -1
  141. package/dist/utils/typecheck-non-package.js +7 -8
  142. package/dist/utils/typecheck-non-package.js.map +1 -1
  143. package/dist/utils/typecheck-serialization.d.ts +8 -8
  144. package/dist/utils/typecheck-serialization.d.ts.map +1 -1
  145. package/dist/utils/typecheck-serialization.js +12 -16
  146. package/dist/utils/typecheck-serialization.js.map +1 -1
  147. package/dist/utils/vite-config.d.ts +8 -5
  148. package/dist/utils/vite-config.d.ts.map +1 -1
  149. package/dist/utils/vite-config.js +36 -29
  150. package/dist/utils/vite-config.js.map +1 -1
  151. package/dist/utils/vite-scope-watch-plugin.d.ts.map +1 -1
  152. package/dist/utils/vite-scope-watch-plugin.js +1 -1
  153. package/dist/utils/vite-scope-watch-plugin.js.map +1 -1
  154. package/dist/utils/worker-events.d.ts +12 -12
  155. package/dist/utils/worker-events.d.ts.map +1 -1
  156. package/dist/utils/worker-events.js +10 -10
  157. package/dist/utils/worker-events.js.map +1 -1
  158. package/dist/utils/worker-utils.d.ts +12 -13
  159. package/dist/utils/worker-utils.d.ts.map +1 -1
  160. package/dist/utils/worker-utils.js +12 -13
  161. package/dist/utils/worker-utils.js.map +1 -1
  162. package/dist/vitest-plugin.d.ts.map +1 -1
  163. package/dist/vitest-plugin.js +5 -7
  164. package/dist/vitest-plugin.js.map +1 -1
  165. package/dist/workers/client.worker.d.ts +4 -2
  166. package/dist/workers/client.worker.d.ts.map +1 -1
  167. package/dist/workers/client.worker.js +209 -1
  168. package/dist/workers/client.worker.js.map +1 -1
  169. package/dist/workers/library-build.worker.d.ts +1 -1
  170. package/dist/workers/library-build.worker.d.ts.map +1 -1
  171. package/dist/workers/library-build.worker.js +7 -7
  172. package/dist/workers/library-build.worker.js.map +1 -1
  173. package/dist/workers/lint.worker.d.ts +2 -2
  174. package/dist/workers/lint.worker.js +2 -2
  175. package/dist/workers/ngtsc-build.worker.js +30 -30
  176. package/dist/workers/ngtsc-build.worker.js.map +1 -1
  177. package/dist/workers/server-build.worker.d.ts +17 -17
  178. package/dist/workers/server-build.worker.d.ts.map +1 -1
  179. package/dist/workers/server-build.worker.js +46 -46
  180. package/dist/workers/server-build.worker.js.map +1 -1
  181. package/dist/workers/server-runtime.worker.d.ts +7 -7
  182. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  183. package/dist/workers/server-runtime.worker.js +17 -17
  184. package/dist/workers/server-runtime.worker.js.map +1 -1
  185. package/docs/config.md +340 -0
  186. package/docs/publish-configuration-types.md +87 -0
  187. package/docs/pwa-configuration-types.md +55 -0
  188. package/docs/vitest-plugin.md +47 -0
  189. package/package.json +9 -7
  190. package/src/angular/client-transform-stylesheet.ts +1 -1
  191. package/src/angular/vite-angular-plugin.ts +70 -37
  192. package/src/angular/vite-postcss-inline-plugin.ts +1 -1
  193. package/src/capacitor/capacitor.ts +159 -23
  194. package/src/commands/build.ts +3 -10
  195. package/src/commands/check.ts +3 -3
  196. package/src/commands/dev.ts +3 -9
  197. package/src/commands/device.ts +5 -5
  198. package/src/commands/publish.ts +30 -26
  199. package/src/commands/replace-deps.ts +3 -3
  200. package/src/commands/typecheck.ts +7 -13
  201. package/src/commands/watch.ts +9 -9
  202. package/src/electron/electron.ts +49 -4
  203. package/src/engines/BaseEngine.ts +4 -1
  204. package/src/engines/NgtscEngine.ts +7 -7
  205. package/src/engines/ServerEsbuildEngine.ts +7 -7
  206. package/src/engines/TscEngine.ts +7 -7
  207. package/src/engines/ViteEngine.ts +8 -13
  208. package/src/engines/index.ts +5 -5
  209. package/src/engines/types.ts +20 -20
  210. package/src/infra/ResultCollector.ts +9 -9
  211. package/src/infra/SignalHandler.ts +7 -7
  212. package/src/infra/WorkerManager.ts +14 -14
  213. package/src/orchestrators/BuildOrchestrator.ts +37 -37
  214. package/src/orchestrators/DevWatchOrchestrator.ts +36 -36
  215. package/src/sd-cli-entry.ts +15 -8
  216. package/src/sd-cli.ts +16 -16
  217. package/src/sd-config.types.ts +107 -107
  218. package/src/utils/angular-compiler.ts +5 -5
  219. package/src/utils/build-env.ts +1 -1
  220. package/src/utils/concurrency.ts +7 -7
  221. package/src/utils/copy-public.ts +17 -17
  222. package/src/utils/copy-src.ts +11 -11
  223. package/src/utils/engine-stop.ts +9 -10
  224. package/src/utils/esbuild-config.ts +29 -29
  225. package/src/utils/lint-with-program.ts +34 -34
  226. package/src/utils/ngtsc-build-core.ts +17 -17
  227. package/src/utils/output-path-rewriter.ts +18 -18
  228. package/src/utils/output-utils.ts +11 -11
  229. package/src/utils/package-utils.ts +57 -45
  230. package/src/utils/replace-deps.ts +92 -67
  231. package/src/utils/sd-config.ts +3 -3
  232. package/src/utils/tsc-build.ts +18 -18
  233. package/src/utils/tsconfig.ts +11 -9
  234. package/src/utils/typecheck-non-package.ts +7 -8
  235. package/src/utils/typecheck-serialization.ts +13 -15
  236. package/src/utils/vite-config.ts +45 -35
  237. package/src/utils/vite-scope-watch-plugin.ts +6 -1
  238. package/src/utils/worker-events.ts +16 -16
  239. package/src/utils/worker-utils.ts +12 -13
  240. package/src/vitest-plugin.ts +5 -8
  241. package/src/workers/client.worker.ts +236 -2
  242. package/src/workers/library-build.worker.ts +8 -8
  243. package/src/workers/lint.worker.ts +2 -2
  244. package/src/workers/ngtsc-build.worker.ts +31 -31
  245. package/src/workers/server-build.worker.ts +60 -60
  246. package/src/workers/server-runtime.worker.ts +22 -22
  247. package/tests/angular/vite-angular-plugin-hmr-fallback.spec.ts +1 -0
  248. package/tests/angular/vite-angular-plugin-hmr.spec.ts +78 -0
  249. package/tests/angular/vite-angular-plugin.spec.ts +67 -0
  250. package/tests/capacitor/capacitor-build.spec.ts +6 -4
  251. package/tests/capacitor/capacitor-icon.spec.ts +7 -5
  252. package/tests/capacitor/capacitor-init.spec.ts +120 -10
  253. package/tests/capacitor/capacitor-run.spec.ts +14 -17
  254. package/tests/capacitor/capacitor-workspace.spec.ts +5 -3
  255. package/tests/commands/check.spec.ts +2 -2
  256. package/tests/commands/publish.spec.ts +2 -2
  257. package/tests/commands/typecheck.spec.ts +8 -0
  258. package/tests/electron/electron.spec.ts +12 -10
  259. package/tests/engines/base-engine.spec.ts +37 -0
  260. package/tests/engines/vite-engine.spec.ts +115 -3
  261. package/tests/utils/vite-config.spec.ts +144 -90
  262. package/tests/workers/client-worker.spec.ts +690 -0
  263. package/tests/workers/server-build-worker.spec.ts +3 -3
@@ -34,7 +34,7 @@ export interface SdAngularPluginOptions {
34
34
  warnings?: string[];
35
35
  lint?: LintWithProgramResult;
36
36
  }) => void;
37
- /** Enable lint using ts.Program from compilation */
37
+ /** 컴파일의 ts.Program 사용하여 lint 실행 */
38
38
  enableLint?: boolean;
39
39
  /** browserslist 타겟 (정규화된 배열) */
40
40
  browserslist?: string[];
@@ -68,7 +68,7 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
68
68
  ) as { name?: string };
69
69
  pkgName = pkgJson.name ?? "unknown";
70
70
  } catch {
71
- // ignore
71
+ // 무시
72
72
  }
73
73
  lintRunner = new LintWithProgramRunner({
74
74
  cwd: process.cwd(),
@@ -83,6 +83,12 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
83
83
  let hmrLock: Promise<void> = Promise.resolve();
84
84
  const scssDependencies = new Map<string, Set<string>>();
85
85
 
86
+ // Pre-bundle transformer: optimizeDeps의 esbuild 단계에서 Angular Linker 실행
87
+ const prebundleTransformer = new JavaScriptTransformer(
88
+ { sourcemap: options.dev, jit: false, thirdPartySourcemaps: options.dev },
89
+ 1,
90
+ );
91
+
86
92
  function createJsTransformer(): JavaScriptTransformer {
87
93
  const maxThreads = Math.max(1, Math.floor((os.cpus().length * 2) / 3));
88
94
  return new JavaScriptTransformer(
@@ -107,6 +113,24 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
107
113
  ngJitMode: "false",
108
114
  ngHmrMode: options.dev ? undefined : "false",
109
115
  },
116
+ optimizeDeps: {
117
+ esbuildOptions: {
118
+ plugins: [
119
+ {
120
+ name: "angular-vite-optimize-deps",
121
+ setup(build: { onLoad: Function }) {
122
+ build.onLoad(
123
+ { filter: /\.[cm]?js$/ },
124
+ async (args: { path: string }) => ({
125
+ contents: await prebundleTransformer.transformFile(args.path),
126
+ loader: "js" as const,
127
+ }),
128
+ );
129
+ },
130
+ },
131
+ ],
132
+ },
133
+ },
110
134
  };
111
135
  },
112
136
 
@@ -188,7 +212,7 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
188
212
  logger.error(err);
189
213
  }
190
214
 
191
- // Lint execution (if enabled)
215
+ // lint 실행 (활성화된 경우)
192
216
  let initialLintResult: LintWithProgramResult | undefined;
193
217
  if (options.enableLint === true) {
194
218
  initialLintResult = await getOrCreateLintRunner().lint({
@@ -196,7 +220,7 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
196
220
  });
197
221
  }
198
222
 
199
- // Report initial build results (both dev and prod)
223
+ // 초기 빌드 결과 보고 (dev, prod 공통)
200
224
  options.onBuild?.({
201
225
  success: diagnosticResult.errors.length === 0,
202
226
  errors: diagnosticResult.errors.map((e) => e.message),
@@ -218,14 +242,13 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
218
242
  if (compiler == null || !options.dev) return;
219
243
  if (
220
244
  !file.endsWith(".ts") &&
221
- !file.endsWith(".tsx") &&
222
245
  !file.endsWith(".html") &&
223
246
  !file.endsWith(".scss")
224
247
  ) {
225
248
  return;
226
249
  }
227
250
 
228
- // Dependency filter: skip if file is not in the TypeScript program
251
+ // 의존성 필터: TypeScript program에 포함되지 않은 파일은 건너뜀
229
252
  const normalizedFile = pathx.posix(file);
230
253
  const programFiles = compiler.getTsProgram().getSourceFiles();
231
254
  const isInProgram = programFiles.some(
@@ -269,13 +292,13 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
269
292
  const diagnosticResult = collectAndFormatDiagnostics(compiler, process.cwd());
270
293
  reportDiagnostics(diagnosticResult);
271
294
 
272
- // Convert affected ts.SourceFile set to file name strings for incremental lint
295
+ // 영향받은 ts.SourceFile 집합을 파일명 문자열로 변환 (incremental lint용)
273
296
  const affectedFileNames = new Set<string>();
274
297
  for (const sf of updateResult.affectedFiles) {
275
298
  affectedFileNames.add(pathx.posix(sf.fileName));
276
299
  }
277
300
 
278
- // Lint execution (if enabled)
301
+ // lint 실행 (활성화된 경우)
279
302
  let lintResult: LintWithProgramResult | undefined;
280
303
  if (options.enableLint === true) {
281
304
  lintResult = await getOrCreateLintRunner().lint({
@@ -306,21 +329,23 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
306
329
  },
307
330
 
308
331
  async transform(_code, id) {
309
- if (!id.endsWith(".ts") && !id.endsWith(".tsx")) return;
332
+ if (jsTransformer == null) return;
310
333
 
311
- const normalizedId = pathx.posix(id);
312
- const emittedContent = emittedFiles.get(normalizedId);
313
- if (emittedContent == null) return;
334
+ let code = _code;
314
335
 
315
- if (jsTransformer == null) {
316
- throw new Error("JavaScriptTransformer가 초기화되지 않았습니다");
336
+ // Phase 1: TS 컴파일 — .ts 파일은 AngularCompiler가 emit한 JS로 교체
337
+ if (id.endsWith(".ts")) {
338
+ const normalizedId = pathx.posix(id);
339
+ const emittedContent = emittedFiles.get(normalizedId);
340
+ if (emittedContent == null) return;
341
+ code = emittedContent;
342
+ } else if (!id.endsWith(".mjs") && !id.endsWith(".js")) {
343
+ return;
317
344
  }
318
345
 
319
- // AOT 메타데이터 제거 + 최적화
320
- const transformed = await jsTransformer.transformData(normalizedId, emittedContent, false);
321
- const code = new TextDecoder().decode(transformed);
322
-
323
- return { code };
346
+ // Phase 2: JS 변환 — Angular Linker로 partial → full AOT 링킹 + 최적화
347
+ const transformed = await jsTransformer.transformData(pathx.posix(id), code, false);
348
+ return { code: new TextDecoder().decode(transformed) };
324
349
  },
325
350
 
326
351
  async buildEnd() {
@@ -330,6 +355,7 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
330
355
  await jsTransformer.close();
331
356
  jsTransformer = undefined;
332
357
  }
358
+ await prebundleTransformer.close();
333
359
  compiler = undefined;
334
360
  emittedFiles.clear();
335
361
  }
@@ -337,24 +363,24 @@ export function sdAngularPlugin(options: SdAngularPluginOptions): Plugin {
337
363
 
338
364
  configureServer(server: ViteDevServer) {
339
365
  // component-middleware 등록 (HMR template updates 서빙)
340
- server.middlewares.use(angularComponentMiddleware(templateUpdates));
366
+ server.middlewares.use(angularComponentMiddleware(templateUpdates, server.config.base));
341
367
 
342
368
  // dev server 종료 시 리소스 정리
343
369
  server.httpServer?.on("close", () => {
344
- if (jsTransformer != null) {
345
- void jsTransformer
346
- .close()
347
- .then(() => {
348
- jsTransformer = undefined;
349
- compiler = undefined;
350
- emittedFiles.clear();
351
- })
352
- .catch((err: unknown) => {
353
- logger.error(
354
- `Resource dispose failed: ${err instanceof Error ? err.message : String(err)}`,
355
- );
356
- });
357
- }
370
+ void Promise.all([
371
+ jsTransformer?.close(),
372
+ prebundleTransformer.close(),
373
+ ])
374
+ .catch((err: unknown) => {
375
+ logger.error(
376
+ `Resource dispose failed: ${err instanceof Error ? err.message : String(err)}`,
377
+ );
378
+ })
379
+ .finally(() => {
380
+ jsTransformer = undefined;
381
+ compiler = undefined;
382
+ emittedFiles.clear();
383
+ });
358
384
  });
359
385
  },
360
386
  };
@@ -401,15 +427,22 @@ function collectAndFormatDiagnostics(compiler: AngularCompiler, cwd: string): Di
401
427
 
402
428
  function angularComponentMiddleware(
403
429
  templateUpdates: Map<string, string>,
430
+ basePath: string,
404
431
  ): (req: IncomingMessage, res: ServerResponse, next: () => void) => void {
405
432
  return (req, res, next) => {
406
- if (req.url == null || !req.url.startsWith("/@ng/component")) {
433
+ const rawUrl = req.url ?? "";
434
+ const parsedUrl = new URL(rawUrl, "http://localhost");
435
+ const pathname = decodeURIComponent(parsedUrl.pathname);
436
+ const strippedPathname =
437
+ basePath !== "/" && pathname.startsWith(basePath)
438
+ ? pathname.slice(basePath.length - 1)
439
+ : pathname;
440
+ if (!strippedPathname.includes("/@ng/component")) {
407
441
  next();
408
442
  return;
409
443
  }
410
444
 
411
- const url = new URL(req.url, "http://localhost");
412
- const componentId = url.searchParams.get("c") ?? "";
445
+ const componentId = parsedUrl.searchParams.get("c") ?? "";
413
446
  const body = templateUpdates.get(encodeURIComponent(componentId)) ?? "";
414
447
 
415
448
  res.writeHead(200, {
@@ -9,7 +9,7 @@ export interface SdPostCssInlinePluginOptions {
9
9
  }
10
10
 
11
11
  /**
12
- * 라이브러리 JS 파일의 Angular @Component 인라인 CSS에 PostCSS를 적용하는 Vite 플러그인.
12
+ * JS 파일 Angular @Component 인라인 스타일에 PostCSS를 적용하는 Vite 플러그인.
13
13
  *
14
14
  * TS AST로 @Component decorator의 `styles` 배열에서 CSS 문자열을 추출하고,
15
15
  * PostCSS를 적용한 후 교체한다.
@@ -5,7 +5,7 @@ import { symlink } from "fs/promises";
5
5
  import { createRequire } from "module";
6
6
  import { cpx, fsx, pathx } from "@simplysm/core-node";
7
7
  import { env } from "@simplysm/core-common";
8
- import { consola } from "consola";
8
+ import { consola, LogLevels } from "consola";
9
9
  import type { SdCapacitorConfig } from "../sd-config.types.js";
10
10
 
11
11
  /**
@@ -91,9 +91,7 @@ export class Capacitor {
91
91
  const platforms = Object.keys(config.platform);
92
92
  for (const p of platforms) {
93
93
  if (p !== "android") {
94
- throw new CapacitorConfigError(
95
- `지원하지 않는 플랫폼입니다: ${p} (현재 android만 지원)`,
96
- );
94
+ throw new CapacitorConfigError(`지원하지 않는 플랫폼입니다: ${p} (현재 android만 지원)`);
97
95
  }
98
96
  }
99
97
  }
@@ -109,47 +107,65 @@ export class Capacitor {
109
107
  * 5. cap sync 또는 cap copy 실행
110
108
  */
111
109
  async initialize(): Promise<void> {
110
+ Capacitor._logger.debug("initialize 시작");
112
111
  await this._acquireLock();
113
112
 
114
113
  try {
115
114
  // 외부 도구 검증
115
+ Capacitor._logger.debug("외부 도구 검증 시작");
116
116
  await this._validateTools();
117
+ Capacitor._logger.debug("외부 도구 검증 완료");
117
118
 
118
119
  // 1. Capacitor 프로젝트 초기화
120
+ Capacitor._logger.debug("Capacitor 프로젝트 초기화 시작");
119
121
  const changed = await this._initCap();
122
+ Capacitor._logger.debug(`Capacitor 프로젝트 초기화 완료 (changed: ${changed})`);
120
123
 
121
124
  // 2. Capacitor 설정 파일 생성
125
+ Capacitor._logger.debug("Capacitor 설정 파일 생성 시작");
122
126
  await this._writeCapConf();
127
+ Capacitor._logger.debug("Capacitor 설정 파일 생성 완료");
123
128
 
124
129
  // 3. 플랫폼 관리 (멱등성: 이미 존재하면 스킵)
130
+ Capacitor._logger.debug("플랫폼 추가 시작");
125
131
  await this._addPlatforms();
132
+ Capacitor._logger.debug("플랫폼 추가 완료");
126
133
 
127
134
  // 4. 아이콘 처리
135
+ Capacitor._logger.debug("아이콘 처리 시작");
128
136
  await this._setupIcon();
137
+ Capacitor._logger.debug("아이콘 처리 완료");
129
138
 
130
139
  // 5. Android 네이티브 설정 구성
131
140
  if (this._platforms.includes("android")) {
141
+ Capacitor._logger.debug("Android 네이티브 설정 시작");
132
142
  await this._configureAndroid();
143
+ Capacitor._logger.debug("Android 네이티브 설정 완료");
133
144
  }
134
145
 
135
146
  // 6. 웹 에셋 동기화
136
147
  if (changed) {
148
+ Capacitor._logger.debug("cap sync 시작 (의존성 변경됨)");
137
149
  await this._execCap(["sync"]);
150
+ Capacitor._logger.debug("cap sync 완료");
138
151
  } else {
152
+ Capacitor._logger.debug("cap copy 시작");
139
153
  await this._execCap(["copy"]);
154
+ Capacitor._logger.debug("cap copy 완료");
140
155
  }
141
156
  } finally {
142
157
  await this._releaseLock();
158
+ Capacitor._logger.debug("initialize 완료");
143
159
  }
144
160
  }
145
161
 
146
162
  //#region Private - 명령어 실행
147
163
 
148
164
  /**
149
- * Capacitor CLI 명령어를 npx로 실행
165
+ * Capacitor CLI 명령어를 pnpm exec로 실행
150
166
  */
151
167
  private async _execCap(args: string[]): Promise<string> {
152
- return this._exec("npx", ["cap", ...args], this._capPath);
168
+ return this._exec("pnpm", ["exec", "cap", ...args], this._capPath);
153
169
  }
154
170
 
155
171
  /**
@@ -157,7 +173,11 @@ export class Capacitor {
157
173
  */
158
174
  private async _exec(command: string, args: string[], cwd: string): Promise<string> {
159
175
  Capacitor._logger.debug(`명령어 실행: ${command} ${args.join(" ")}`);
160
- const { stdout } = await cpx.exec(command, args, { cwd });
176
+ const isDebug = consola.level >= LogLevels.debug;
177
+ const { stdout } = await cpx.spawn(command, args, {
178
+ cwd,
179
+ ...(isDebug ? { stdio: ["ignore", "inherit", "inherit"] } : {}),
180
+ });
161
181
  Capacitor._logger.debug(`실행 결과: ${stdout}`);
162
182
  return stdout;
163
183
  }
@@ -215,6 +235,7 @@ export class Capacitor {
215
235
  "2. ANDROID_HOME 또는 ANDROID_SDK_ROOT 환경 변수를 설정하세요.",
216
236
  );
217
237
  }
238
+ Capacitor._logger.debug(`Android SDK 경로: ${sdkPath}`);
218
239
 
219
240
  // Java 확인 (android 플랫폼인 경우에만)
220
241
  if (this._platforms.includes("android")) {
@@ -223,6 +244,8 @@ export class Capacitor {
223
244
  Capacitor._logger.warn(
224
245
  "Java 21을 찾을 수 없습니다. Gradle이 내장 JDK를 사용하거나 빌드가 실패할 수 있습니다.",
225
246
  );
247
+ } else {
248
+ Capacitor._logger.debug(`Java 21 경로: ${javaPath}`);
226
249
  }
227
250
  }
228
251
  }
@@ -235,26 +258,45 @@ export class Capacitor {
235
258
  * Capacitor 프로젝트 기본 초기화 (package.json, npm install, cap init)
236
259
  */
237
260
  private async _initCap(): Promise<boolean> {
261
+ Capacitor._logger.debug("package.json 설정 시작");
238
262
  const { depChanged, workspacePlugins } = await this._setupNpmConf();
239
263
  const nodeModulesExists = await fsx.exists(pathx.posixResolve(this._capPath, "node_modules"));
264
+ Capacitor._logger.debug(`depChanged: ${depChanged}, nodeModulesExists: ${nodeModulesExists}`);
240
265
 
241
266
  if (!depChanged && nodeModulesExists) {
242
267
  // 의존성 미변경이어도 workspace 플러그인 symlink는 항상 갱신
268
+ Capacitor._logger.debug("의존성 변경 없음, workspace 플러그인 symlink만 갱신");
243
269
  await this._linkWorkspacePlugins(workspacePlugins);
244
270
  return false;
245
271
  }
246
272
 
247
- // npm install
248
- const installResult = await this._exec("npm", ["install"], this._capPath);
249
- Capacitor._logger.debug(`npm install 완료: ${installResult}`);
273
+ // pnpm-workspace.yaml 생성 (상위 workspace 탐색 차단)
274
+ const workspaceYamlPath = pathx.posixResolve(this._capPath, "pnpm-workspace.yaml");
275
+ if (!(await fsx.exists(workspaceYamlPath))) {
276
+ await fsx.write(workspaceYamlPath, "");
277
+ }
278
+ const lockfilePath = pathx.posixResolve(this._capPath, "pnpm-lock.yaml");
279
+ if (!(await fsx.exists(lockfilePath))) {
280
+ await fsx.write(lockfilePath, "");
281
+ }
282
+
283
+ // pnpm install + 빌드 스크립트 승인
284
+ Capacitor._logger.debug("pnpm install 시작");
285
+ await this._exec("pnpm", ["install"], this._capPath);
286
+ await this._exec("pnpm", ["approve-builds", "--all"], this._capPath);
287
+ Capacitor._logger.debug("pnpm install 완료");
250
288
 
251
289
  // workspace 플러그인 symlink
290
+ Capacitor._logger.debug("workspace 플러그인 symlink 시작");
252
291
  await this._linkWorkspacePlugins(workspacePlugins);
292
+ Capacitor._logger.debug("workspace 플러그인 symlink 완료");
253
293
 
254
294
  // 멱등성: capacitor.config.ts가 없을 때만 cap init 실행
255
295
  const configPath = pathx.posixResolve(this._capPath, "capacitor.config.ts");
256
296
  if (!(await fsx.exists(configPath))) {
297
+ Capacitor._logger.debug("cap init 시작");
257
298
  await this._execCap(["init", this._config.appId, this._config.appId]);
299
+ Capacitor._logger.debug("cap init 완료");
258
300
  }
259
301
 
260
302
  // 기본 www/index.html 생성
@@ -264,6 +306,7 @@ export class Capacitor {
264
306
  pathx.posixResolve(wwwPath, "index.html"),
265
307
  "<!DOCTYPE html><html><head></head><body></body></html>",
266
308
  );
309
+ Capacitor._logger.debug("www/index.html 생성 완료");
267
310
 
268
311
  return true;
269
312
  }
@@ -465,8 +508,9 @@ export default config;
465
508
 
466
509
  // capacitor-assets로 모든 해상도 아이콘/스플래시 생성
467
510
  await this._exec(
468
- "npx",
511
+ "pnpm",
469
512
  [
513
+ "exec",
470
514
  "capacitor-assets",
471
515
  "generate",
472
516
  "--iconBackgroundColor",
@@ -500,10 +544,29 @@ export default config;
500
544
  throw new Error(`Android 프로젝트 디렉토리를 찾을 수 없습니다: ${androidPath}`);
501
545
  }
502
546
 
547
+ Capacitor._logger.debug("JAVA_HOME 설정 시작");
503
548
  await this._configureAndroidJavaHomePath(androidPath);
549
+ Capacitor._logger.debug("JAVA_HOME 설정 완료");
550
+
551
+ Capacitor._logger.debug("Android SDK 경로 설정 시작");
504
552
  await this._configureAndroidSdkPath(androidPath);
553
+ Capacitor._logger.debug("Android SDK 경로 설정 완료");
554
+
555
+ Capacitor._logger.debug("AndroidManifest.xml 설정 시작");
505
556
  await this._configureAndroidManifest(androidPath);
557
+ Capacitor._logger.debug("AndroidManifest.xml 설정 완료");
558
+
559
+ Capacitor._logger.debug("루트 build.gradle Kotlin 플러그인 설정 시작");
560
+ await this._configureAndroidRootBuildGradle(androidPath);
561
+ Capacitor._logger.debug("루트 build.gradle Kotlin 플러그인 설정 완료");
562
+
563
+ Capacitor._logger.debug("build.gradle 설정 시작");
506
564
  await this._configureAndroidBuildGradle(androidPath);
565
+ Capacitor._logger.debug("build.gradle 설정 완료");
566
+
567
+ Capacitor._logger.debug("styles.xml 설정 시작");
568
+ await this._configureAndroidStyles(androidPath);
569
+ Capacitor._logger.debug("styles.xml 설정 완료");
507
570
  }
508
571
 
509
572
  /**
@@ -574,7 +637,9 @@ export default config;
574
637
  * Android SDK 경로 탐색
575
638
  */
576
639
  private async _findAndroidSdk(): Promise<string | undefined> {
577
- const androidHome = (env["ANDROID_HOME"] as string | undefined) ?? (env["ANDROID_SDK_ROOT"] as string | undefined);
640
+ const androidHome =
641
+ (env["ANDROID_HOME"] as string | undefined) ??
642
+ (env["ANDROID_SDK_ROOT"] as string | undefined);
578
643
  if (androidHome != null && (await fsx.exists(androidHome))) {
579
644
  return androidHome;
580
645
  }
@@ -670,6 +735,28 @@ export default config;
670
735
  await fsx.write(manifestPath, content);
671
736
  }
672
737
 
738
+ /**
739
+ * 루트 build.gradle에 Kotlin Gradle 플러그인 classpath 추가
740
+ */
741
+ private async _configureAndroidRootBuildGradle(androidPath: string): Promise<void> {
742
+ const rootBuildGradlePath = pathx.posixResolve(androidPath, "build.gradle");
743
+
744
+ if (!(await fsx.exists(rootBuildGradlePath))) {
745
+ Capacitor._logger.warn(`루트 build.gradle 파일을 찾을 수 없습니다: ${rootBuildGradlePath}`);
746
+ return;
747
+ }
748
+
749
+ let content = await fsx.read(rootBuildGradlePath);
750
+
751
+ if (!content.includes("kotlin-gradle-plugin")) {
752
+ content = content.replace(
753
+ /classpath 'com\.android\.tools\.build:gradle:[^']+'/,
754
+ `$&\n classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20'`,
755
+ );
756
+ await fsx.write(rootBuildGradlePath, content);
757
+ }
758
+ }
759
+
673
760
  /**
674
761
  * build.gradle 수정 (서명 설정 제외)
675
762
  */
@@ -710,6 +797,35 @@ export default config;
710
797
  await fsx.write(buildGradlePath, content);
711
798
  }
712
799
 
800
+ /**
801
+ * styles.xml의 스플래시 테마 parent 변경
802
+ *
803
+ * Theme.SplashScreen은 android:windowBackground에 compat_splash_screen을 설정하여
804
+ * android:background(@drawable/splash)와 이중 표시를 발생시킨다.
805
+ * installSplashScreen()을 호출하지 않으므로 Theme.SplashScreen 기능이 불필요하다.
806
+ */
807
+ private async _configureAndroidStyles(androidPath: string): Promise<void> {
808
+ const stylesPath = pathx.posixResolve(androidPath, "app/src/main/res/values/styles.xml");
809
+
810
+ if (!(await fsx.exists(stylesPath))) {
811
+ Capacitor._logger.warn(`styles.xml 파일을 찾을 수 없습니다: ${stylesPath}`);
812
+ return;
813
+ }
814
+
815
+ let content = await fsx.read(stylesPath);
816
+
817
+ if (!content.includes('parent="Theme.SplashScreen"')) {
818
+ return;
819
+ }
820
+
821
+ content = content.replace(
822
+ 'parent="Theme.SplashScreen"',
823
+ 'parent="Theme.AppCompat.DayNight.NoActionBar"',
824
+ );
825
+
826
+ await fsx.write(stylesPath, content);
827
+ }
828
+
713
829
  //#endregion
714
830
 
715
831
  //#region Public — 기기 실행
@@ -719,28 +835,35 @@ export default config;
719
835
  *
720
836
  * 1. capacitor.config.ts에 server.url 설정 (Hot Reload용)
721
837
  * 2. cap copy — 웹 에셋 동기화
722
- * 3. cap run — 기기에서 앱 실행 (실패 시 adb kill-server 후 1회 재시도)
838
+ * 3. cap run — 기기에서 앱 실행
723
839
  */
724
840
  async run(url: string): Promise<void> {
841
+ Capacitor._logger.debug(`server.url 설정: ${url}`);
725
842
  await this._updateServerUrl(url);
726
843
 
727
844
  for (const platform of this._platforms) {
845
+ Capacitor._logger.debug(`[${platform}] cap copy 시작`);
728
846
  await this._execCap(["copy", platform]);
847
+ Capacitor._logger.debug(`[${platform}] cap copy 완료`);
729
848
 
730
849
  try {
850
+ Capacitor._logger.debug(`[${platform}] cap run 시작`);
731
851
  await this._execCap(["run", platform]);
852
+ Capacitor._logger.debug(`[${platform}] cap run 완료`);
732
853
  } catch (err) {
733
854
  if (platform === "android") {
734
- Capacitor._logger.warn("cap run 실패. adb kill-server 후 재시도합니다.");
855
+ Capacitor._logger.debug(`[${platform}] adb kill-server 시작`);
735
856
  try {
736
857
  await this._exec("adb", ["kill-server"], this._capPath);
737
- } catch {
738
- // adb kill-server 실패는 무시
858
+ Capacitor._logger.debug(`[${platform}] adb kill-server 완료`);
859
+ } catch (adbErr) {
860
+ const adbErrMsg = adbErr instanceof Error ? adbErr.message : String(adbErr);
861
+ Capacitor._logger.debug(
862
+ `[${platform}] adb kill-server 실패 (무시): ${adbErrMsg}`,
863
+ );
739
864
  }
740
- await this._execCap(["run", platform]);
741
- } else {
742
- throw err;
743
865
  }
866
+ throw err;
744
867
  }
745
868
  }
746
869
  }
@@ -779,30 +902,40 @@ export default config;
779
902
  * 4. 빌드 산출물 복사
780
903
  */
781
904
  async build(outPath: string): Promise<void> {
905
+ Capacitor._logger.debug("build 시작");
906
+
782
907
  // 1. 웹 에셋 동기화
908
+ Capacitor._logger.debug("cap copy 시작");
783
909
  await this._execCap(["copy"]);
910
+ Capacitor._logger.debug("cap copy 완료");
784
911
 
785
912
  // 2. 빌드 타입 결정
786
913
  const isDebug = this._config.debug === true;
787
914
  const isBundle = this._config.platform?.android?.bundle === true;
788
915
  const buildType = isDebug ? "debug" : "release";
916
+ Capacitor._logger.debug(`빌드 타입: ${buildType}, bundle: ${isBundle}`);
789
917
 
790
918
  // 3. 서명 설정
791
919
  const signConfig = this._config.platform?.android?.sign;
792
920
  if (!isDebug && signConfig != null) {
793
- await this._configureSigningConfig(
794
- pathx.posixResolve(this._capPath, "android"),
795
- signConfig,
796
- );
921
+ Capacitor._logger.debug("서명 설정 시작");
922
+ await this._configureSigningConfig(pathx.posixResolve(this._capPath, "android"), signConfig);
923
+ Capacitor._logger.debug("서명 설정 완료");
797
924
  } else if (!isDebug) {
798
925
  Capacitor._logger.warn("서명 설정이 없어 unsigned 빌드가 생성됩니다.");
799
926
  }
800
927
 
801
928
  // 4. Gradle 빌드
929
+ Capacitor._logger.debug("Gradle 빌드 시작");
802
930
  await this._buildAndroid(buildType, isBundle);
931
+ Capacitor._logger.debug("Gradle 빌드 완료");
803
932
 
804
933
  // 5. 빌드 산출물 복사
934
+ Capacitor._logger.debug("빌드 산출물 복사 시작");
805
935
  await this._copyBuildOutput(outPath, buildType, isBundle);
936
+ Capacitor._logger.debug("빌드 산출물 복사 완료");
937
+
938
+ Capacitor._logger.debug("build 완료");
806
939
  }
807
940
 
808
941
  //#endregion
@@ -878,6 +1011,7 @@ export default config;
878
1011
  ? pathx.posixResolve(androidPath, "gradlew.bat")
879
1012
  : pathx.posixResolve(androidPath, "gradlew");
880
1013
 
1014
+ Capacitor._logger.debug(`Gradle 실행: ${gradlew} ${gradleTask}`);
881
1015
  await this._exec(gradlew, [gradleTask, "--no-daemon"], androidPath);
882
1016
  }
883
1017
 
@@ -899,11 +1033,13 @@ export default config;
899
1033
  );
900
1034
 
901
1035
  // 빌드 산출물 찾기
1036
+ Capacitor._logger.debug(`빌드 산출물 탐색: ${androidBuildPath}`);
902
1037
  const candidates = await fsx.glob(pathx.posixResolve(androidBuildPath, `app-*.${ext}`));
903
1038
  if (candidates.length === 0) {
904
1039
  throw new Error(`빌드 산출물을 찾을 수 없습니다: ${androidBuildPath}`);
905
1040
  }
906
1041
  const builtFile = candidates[0];
1042
+ Capacitor._logger.debug(`빌드 산출물: ${builtFile}`);
907
1043
  const isUnsigned = builtFile.includes("unsigned");
908
1044
 
909
1045
  // 출력 디렉토리 생성
@@ -4,17 +4,10 @@ import {
4
4
  } from "../orchestrators/BuildOrchestrator";
5
5
 
6
6
  /**
7
- * Run production build.
7
+ * BuildOrchestrator를 통해 프로덕션 빌드를 실행한다.
8
8
  *
9
- * - Load `sd.config.ts` to check build target info per package (required)
10
- * - Run lint
11
- * - Clean dist folder (clean build)
12
- * - `node`/`browser`/`neutral` target: esbuild JS build + dts generation (with type check)
13
- * - `client` target: Vite production build + typecheck (dts not needed)
14
- * - Set `process.exitCode = 1` if any step fails
15
- *
16
- * @param options - build execution options
17
- * @returns resolves on completion
9
+ * @param options - 빌드 실행 옵션
10
+ * @returns 완료 시 resolve
18
11
  */
19
12
  export async function runBuild(options: BuildOrchestratorOptions): Promise<void> {
20
13
  const orchestrator = new BuildOrchestrator(options);
@@ -34,7 +34,7 @@ async function spawnVitest(targets: string[]): Promise<CheckResult> {
34
34
  const args = ["vitest", ...targets, "--run"];
35
35
  logger.debug("vitest 실행", { args });
36
36
  logger.start("테스트 실행 중...");
37
- const result = await cpx.exec("pnpm", args, { cwd: process.cwd(), reject: false });
37
+ const result = await cpx.spawn("pnpm", args, { cwd: process.cwd(), reject: false });
38
38
  const output = result.stdout + result.stderr;
39
39
  const code = result.exitCode;
40
40
 
@@ -188,7 +188,7 @@ export async function runCheck(options: CheckOptions): Promise<void> {
188
188
  const results = await Promise.allSettled(tasks);
189
189
  logger.success("체크 실행 완료");
190
190
 
191
- // 결과 수집 (flatten arrays from executeTypecheck)
191
+ // 결과 수집 (executeTypecheck 배열 평탄화)
192
192
  const checkResults: CheckResult[] = results.flatMap((r) => {
193
193
  if (r.status === "fulfilled") {
194
194
  const value = r.value;
@@ -211,7 +211,7 @@ export async function runCheck(options: CheckOptions): Promise<void> {
211
211
  process.stdout.write(formatSection(result));
212
212
  }
213
213
 
214
- // SUMMARY
214
+ // 요약
215
215
  const failed = checkResults.filter((r) => !r.success);
216
216
  const totalErrors = checkResults.reduce((sum, r) => sum + r.errorCount, 0);
217
217
  const totalWarnings = checkResults.reduce((sum, r) => sum + r.warningCount, 0);