@simplysm/sd-cli 13.0.68 → 13.0.70

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 (201) hide show
  1. package/README.md +10 -957
  2. package/dist/builders/BaseBuilder.d.ts +23 -23
  3. package/dist/builders/BaseBuilder.d.ts.map +1 -1
  4. package/dist/builders/BaseBuilder.js +15 -15
  5. package/dist/builders/DtsBuilder.d.ts +4 -4
  6. package/dist/builders/DtsBuilder.js +1 -1
  7. package/dist/builders/LibraryBuilder.d.ts +3 -3
  8. package/dist/builders/types.d.ts +10 -10
  9. package/dist/capacitor/capacitor.d.ts +36 -36
  10. package/dist/capacitor/capacitor.js +63 -63
  11. package/dist/capacitor/capacitor.js.map +1 -1
  12. package/dist/commands/add-client.d.ts +8 -8
  13. package/dist/commands/add-client.js +15 -15
  14. package/dist/commands/add-client.js.map +1 -1
  15. package/dist/commands/add-server.d.ts +9 -9
  16. package/dist/commands/add-server.js +13 -13
  17. package/dist/commands/add-server.js.map +1 -1
  18. package/dist/commands/build.d.ts +9 -9
  19. package/dist/commands/check.js +3 -3
  20. package/dist/commands/check.js.map +1 -1
  21. package/dist/commands/dev.d.ts +9 -9
  22. package/dist/commands/device.d.ts +9 -9
  23. package/dist/commands/device.d.ts.map +1 -1
  24. package/dist/commands/device.js +17 -17
  25. package/dist/commands/device.js.map +1 -1
  26. package/dist/commands/init.d.ts +6 -6
  27. package/dist/commands/init.js +12 -12
  28. package/dist/commands/init.js.map +1 -1
  29. package/dist/commands/lint.d.ts +23 -23
  30. package/dist/commands/lint.d.ts.map +1 -1
  31. package/dist/commands/lint.js +25 -25
  32. package/dist/commands/lint.js.map +1 -1
  33. package/dist/commands/publish.d.ts +13 -13
  34. package/dist/commands/publish.d.ts.map +1 -1
  35. package/dist/commands/publish.js +61 -61
  36. package/dist/commands/publish.js.map +1 -1
  37. package/dist/commands/replace-deps.d.ts +3 -3
  38. package/dist/commands/replace-deps.d.ts.map +1 -1
  39. package/dist/commands/replace-deps.js +1 -1
  40. package/dist/commands/replace-deps.js.map +1 -1
  41. package/dist/commands/typecheck.d.ts +20 -20
  42. package/dist/commands/typecheck.d.ts.map +1 -1
  43. package/dist/commands/typecheck.js +20 -20
  44. package/dist/commands/typecheck.js.map +1 -1
  45. package/dist/commands/watch.d.ts +7 -7
  46. package/dist/electron/electron.d.ts +27 -27
  47. package/dist/electron/electron.js +32 -32
  48. package/dist/electron/electron.js.map +1 -1
  49. package/dist/infra/ResultCollector.d.ts +9 -9
  50. package/dist/infra/ResultCollector.js +5 -5
  51. package/dist/infra/SignalHandler.d.ts +7 -7
  52. package/dist/infra/SignalHandler.js +4 -4
  53. package/dist/infra/WorkerManager.d.ts +14 -14
  54. package/dist/infra/WorkerManager.js +11 -11
  55. package/dist/orchestrators/BuildOrchestrator.d.ts +19 -19
  56. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -1
  57. package/dist/orchestrators/BuildOrchestrator.js +26 -26
  58. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  59. package/dist/orchestrators/DevOrchestrator.d.ts +25 -25
  60. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  61. package/dist/orchestrators/DevOrchestrator.js +30 -30
  62. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  63. package/dist/orchestrators/WatchOrchestrator.d.ts +13 -13
  64. package/dist/orchestrators/WatchOrchestrator.js +17 -17
  65. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  66. package/dist/sd-cli-entry.d.ts +2 -2
  67. package/dist/sd-cli-entry.js +38 -38
  68. package/dist/sd-cli-entry.js.map +1 -1
  69. package/dist/sd-cli.d.ts +2 -2
  70. package/dist/sd-cli.js +1 -1
  71. package/dist/sd-cli.js.map +1 -1
  72. package/dist/sd-config.types.d.ts +84 -84
  73. package/dist/sd-config.types.d.ts.map +1 -1
  74. package/dist/utils/build-env.d.ts +1 -1
  75. package/dist/utils/config-editor.d.ts +5 -5
  76. package/dist/utils/config-editor.js +2 -2
  77. package/dist/utils/config-editor.js.map +1 -1
  78. package/dist/utils/copy-public.d.ts +9 -9
  79. package/dist/utils/copy-src.d.ts +9 -9
  80. package/dist/utils/esbuild-config.d.ts +30 -30
  81. package/dist/utils/esbuild-config.d.ts.map +1 -1
  82. package/dist/utils/output-utils.d.ts +6 -6
  83. package/dist/utils/package-utils.d.ts +6 -6
  84. package/dist/utils/package-utils.js +1 -1
  85. package/dist/utils/package-utils.js.map +1 -1
  86. package/dist/utils/rebuild-manager.js +3 -3
  87. package/dist/utils/rebuild-manager.js.map +1 -1
  88. package/dist/utils/replace-deps.d.ts +25 -25
  89. package/dist/utils/replace-deps.js +3 -3
  90. package/dist/utils/replace-deps.js.map +1 -1
  91. package/dist/utils/sd-config.d.ts +3 -3
  92. package/dist/utils/sd-config.js +3 -3
  93. package/dist/utils/sd-config.js.map +1 -1
  94. package/dist/utils/tailwind-config-deps.d.ts +3 -3
  95. package/dist/utils/template.d.ts +8 -8
  96. package/dist/utils/tsconfig.d.ts +16 -16
  97. package/dist/utils/tsconfig.js +2 -2
  98. package/dist/utils/tsconfig.js.map +1 -1
  99. package/dist/utils/typecheck-serialization.d.ts +8 -8
  100. package/dist/utils/vite-config.d.ts +8 -8
  101. package/dist/utils/vite-config.d.ts.map +1 -1
  102. package/dist/utils/vite-config.js +3 -3
  103. package/dist/utils/worker-events.d.ts +12 -12
  104. package/dist/utils/worker-events.d.ts.map +1 -1
  105. package/dist/utils/worker-utils.d.ts +3 -3
  106. package/dist/utils/worker-utils.js +2 -2
  107. package/dist/utils/worker-utils.js.map +1 -1
  108. package/dist/workers/client.worker.d.ts +14 -14
  109. package/dist/workers/client.worker.d.ts.map +1 -1
  110. package/dist/workers/client.worker.js +1 -1
  111. package/dist/workers/client.worker.js.map +1 -1
  112. package/dist/workers/dts.worker.d.ts +13 -13
  113. package/dist/workers/dts.worker.d.ts.map +1 -1
  114. package/dist/workers/dts.worker.js +3 -3
  115. package/dist/workers/dts.worker.js.map +1 -1
  116. package/dist/workers/library.worker.d.ts +12 -12
  117. package/dist/workers/library.worker.js +1 -1
  118. package/dist/workers/library.worker.js.map +1 -1
  119. package/dist/workers/lint.worker.d.ts +1 -1
  120. package/dist/workers/server-runtime.worker.d.ts +6 -6
  121. package/dist/workers/server-runtime.worker.js +6 -6
  122. package/dist/workers/server-runtime.worker.js.map +1 -1
  123. package/dist/workers/server.worker.d.ts +20 -20
  124. package/dist/workers/server.worker.d.ts.map +1 -1
  125. package/dist/workers/server.worker.js +6 -6
  126. package/dist/workers/server.worker.js.map +1 -1
  127. package/package.json +8 -7
  128. package/src/builders/BaseBuilder.ts +33 -33
  129. package/src/builders/DtsBuilder.ts +5 -5
  130. package/src/builders/LibraryBuilder.ts +9 -9
  131. package/src/builders/types.ts +10 -10
  132. package/src/capacitor/capacitor.ts +119 -119
  133. package/src/commands/add-client.ts +31 -31
  134. package/src/commands/add-server.ts +34 -34
  135. package/src/commands/build.ts +9 -9
  136. package/src/commands/check.ts +5 -5
  137. package/src/commands/dev.ts +9 -9
  138. package/src/commands/device.ts +30 -30
  139. package/src/commands/init.ts +25 -25
  140. package/src/commands/lint.ts +64 -64
  141. package/src/commands/publish.ts +139 -139
  142. package/src/commands/replace-deps.ts +4 -4
  143. package/src/commands/typecheck.ts +74 -74
  144. package/src/commands/watch.ts +7 -7
  145. package/src/electron/electron.ts +51 -51
  146. package/src/infra/ResultCollector.ts +9 -9
  147. package/src/infra/SignalHandler.ts +7 -7
  148. package/src/infra/WorkerManager.ts +14 -14
  149. package/src/orchestrators/BuildOrchestrator.ts +76 -76
  150. package/src/orchestrators/DevOrchestrator.ts +88 -88
  151. package/src/orchestrators/WatchOrchestrator.ts +39 -39
  152. package/src/sd-cli-entry.ts +43 -43
  153. package/src/sd-cli.ts +15 -15
  154. package/src/sd-config.types.ts +85 -85
  155. package/src/utils/build-env.ts +1 -1
  156. package/src/utils/config-editor.ts +19 -19
  157. package/src/utils/copy-public.ts +17 -17
  158. package/src/utils/copy-src.ts +11 -11
  159. package/src/utils/esbuild-config.ts +33 -33
  160. package/src/utils/output-utils.ts +11 -11
  161. package/src/utils/package-utils.ts +12 -12
  162. package/src/utils/rebuild-manager.ts +3 -3
  163. package/src/utils/replace-deps.ts +361 -361
  164. package/src/utils/sd-config.ts +44 -44
  165. package/src/utils/tailwind-config-deps.ts +98 -98
  166. package/src/utils/template.ts +56 -56
  167. package/src/utils/tsconfig.ts +127 -127
  168. package/src/utils/typecheck-serialization.ts +86 -86
  169. package/src/utils/vite-config.ts +341 -341
  170. package/src/utils/worker-events.ts +16 -16
  171. package/src/utils/worker-utils.ts +45 -45
  172. package/src/workers/client.worker.ts +34 -34
  173. package/src/workers/dts.worker.ts +467 -467
  174. package/src/workers/library.worker.ts +314 -314
  175. package/src/workers/lint.worker.ts +16 -16
  176. package/src/workers/server-runtime.worker.ts +157 -157
  177. package/src/workers/server.worker.ts +572 -572
  178. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  179. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  180. package/templates/init/package.json.hbs +3 -3
  181. package/tests/config-editor.spec.ts +160 -0
  182. package/tests/copy-src.spec.ts +50 -0
  183. package/tests/get-compiler-options-for-package.spec.ts +139 -0
  184. package/tests/get-package-source-files.spec.ts +181 -0
  185. package/tests/get-types-from-package-json.spec.ts +107 -0
  186. package/tests/infra/ResultCollector.spec.ts +39 -0
  187. package/tests/infra/SignalHandler.spec.ts +38 -0
  188. package/tests/infra/WorkerManager.spec.ts +97 -0
  189. package/tests/load-ignore-patterns.spec.ts +188 -0
  190. package/tests/load-sd-config.spec.ts +137 -0
  191. package/tests/package-utils.spec.ts +188 -0
  192. package/tests/parse-root-tsconfig.spec.ts +89 -0
  193. package/tests/replace-deps.spec.ts +308 -0
  194. package/tests/run-lint.spec.ts +415 -0
  195. package/tests/run-typecheck.spec.ts +653 -0
  196. package/tests/run-watch.spec.ts +75 -0
  197. package/tests/sd-cli.spec.ts +330 -0
  198. package/tests/tailwind-config-deps.spec.ts +30 -0
  199. package/tests/template.spec.ts +70 -0
  200. package/tests/utils/rebuild-manager.spec.ts +43 -0
  201. package/tests/write-changed-output-files.spec.ts +97 -0
@@ -1,467 +1,467 @@
1
- import path from "path";
2
- import ts from "typescript";
3
- import { createWorker, pathIsChildPath, pathNorm } from "@simplysm/core-node";
4
- import { errorMessage } from "@simplysm/core-common";
5
- import { consola } from "consola";
6
- import {
7
- getCompilerOptionsForPackage,
8
- getPackageFiles,
9
- getPackageSourceFiles,
10
- parseRootTsconfig,
11
- type TypecheckEnv,
12
- } from "../utils/tsconfig";
13
- import { serializeDiagnostic, type SerializedDiagnostic } from "../utils/typecheck-serialization";
14
- import { createOnceGuard } from "../utils/worker-utils";
15
-
16
- //#region Types
17
-
18
- /**
19
- * DTS watch 시작 정보
20
- */
21
- export interface DtsWatchInfo {
22
- name: string;
23
- cwd: string;
24
- pkgDir: string;
25
- env: TypecheckEnv;
26
- }
27
-
28
- /**
29
- * DTS 일회성 빌드 정보
30
- */
31
- export interface DtsBuildInfo {
32
- name: string;
33
- cwd: string;
34
- /** 패키지 디렉토리. 미지정 non-package 모드 (packages/ 제외 전체 타입체크) */
35
- pkgDir?: string;
36
- /** 타입체크 환경. pkgDir과 함께 사용 */
37
- env?: TypecheckEnv;
38
- /** true .d.ts 생성 + 타입체크, false 타입체크만 (기본값: true) */
39
- emit?: boolean;
40
- }
41
-
42
- /**
43
- * DTS 일회성 빌드 결과
44
- */
45
- export interface DtsBuildResult {
46
- success: boolean;
47
- errors?: string[];
48
- diagnostics: SerializedDiagnostic[];
49
- errorCount: number;
50
- warningCount: number;
51
- }
52
-
53
- /**
54
- * 빌드 이벤트
55
- */
56
- export interface DtsBuildEvent {
57
- success: boolean;
58
- errors?: string[];
59
- }
60
-
61
- /**
62
- * 에러 이벤트
63
- */
64
- export interface DtsErrorEvent {
65
- message: string;
66
- }
67
-
68
- /**
69
- * Worker 이벤트 타입
70
- */
71
- export interface DtsWorkerEvents extends Record<string, unknown> {
72
- buildStart: Record<string, never>;
73
- build: DtsBuildEvent;
74
- error: DtsErrorEvent;
75
- }
76
-
77
- //#endregion
78
-
79
- //#region 리소스 관리
80
-
81
- const logger = consola.withTag("sd:cli:dts:worker");
82
-
83
- /** tsc watch program (정리 대상) */
84
- let tscWatchProgram:
85
- | ts.WatchOfFilesAndCompilerOptions<ts.EmitAndSemanticDiagnosticsBuilderProgram>
86
- | undefined;
87
-
88
- /**
89
- * 리소스 정리
90
- */
91
- function cleanup(): void {
92
- if (tscWatchProgram != null) {
93
- tscWatchProgram.close();
94
- tscWatchProgram = undefined;
95
- }
96
- }
97
-
98
- process.on("SIGTERM", () => {
99
- try {
100
- cleanup();
101
- } catch (err) {
102
- logger.error("cleanup 실패", err);
103
- }
104
- process.exit(0);
105
- });
106
-
107
- process.on("SIGINT", () => {
108
- try {
109
- cleanup();
110
- } catch (err) {
111
- logger.error("cleanup 실패", err);
112
- }
113
- process.exit(0);
114
- });
115
-
116
- //#endregion
117
-
118
- //#region DTS 출력 경로 재작성
119
-
120
- /**
121
- * .d.ts.map 파일의 sources 경로를 새 위치 기준으로 조정
122
- */
123
- function adjustDtsMapSources(content: string, originalDir: string, newDir: string): string {
124
- if (originalDir === newDir) return content;
125
- try {
126
- const map = JSON.parse(content) as { sources?: string[] };
127
- if (Array.isArray(map.sources)) {
128
- map.sources = map.sources.map((source) => {
129
- const absoluteSource = path.resolve(originalDir, source);
130
- return path.relative(newDir, absoluteSource);
131
- });
132
- }
133
- return JSON.stringify(map);
134
- } catch {
135
- return content;
136
- }
137
- }
138
-
139
- /**
140
- * DTS writeFile용 경로 재작성 함수 생성
141
- *
142
- * TypeScript path alias(@simplysm/*)로 참조된 다른 패키지 소스까지 rootDir 계산에
143
- * 포함하므로, 출력이 dist/{pkgName}/src/... 형태의 중첩 구조로 생성된다.
144
- * 반환된 함수는 현재 패키지의 .d.ts flat 구조(dist/...)로 재작성하고,
145
- * 다른 패키지의 .d.ts 무시한다.
146
- *
147
- * @returns (fileName, content) => [newPath, newContent] | null (null이면 쓰기 무시)
148
- */
149
- function createDtsPathRewriter(
150
- pkgDir: string,
151
- ): (fileName: string, content: string) => [string, string] | null {
152
- const pkgName = path.basename(pkgDir);
153
- const distDir = pathNorm(path.join(pkgDir, "dist"));
154
- const distPrefix = distDir + path.sep;
155
- // 중첩 구조에서 현재 패키지의 접두사: dist/{pkgName}/src/
156
- const ownNestedPrefix = pathNorm(path.join(distDir, pkgName, "src")) + path.sep;
157
-
158
- return (fileName, content) => {
159
- fileName = pathNorm(fileName);
160
-
161
- if (!fileName.startsWith(distPrefix)) return null;
162
-
163
- if (fileName.startsWith(ownNestedPrefix)) {
164
- // 중첩 경로를 flat으로 재작성: dist/{pkgName}/src/... → dist/...
165
- const flatPath = path.join(distDir, fileName.slice(ownNestedPrefix.length));
166
- if (fileName.endsWith(".d.ts.map")) {
167
- content = adjustDtsMapSources(content, path.dirname(fileName), path.dirname(flatPath));
168
- }
169
- return [flatPath, content];
170
- }
171
-
172
- // 다른 패키지의 중첩 출력 (dist/{otherPkg}/src/...) → 무시
173
- const relFromDist = fileName.slice(distPrefix.length);
174
- const segments = relFromDist.split(path.sep);
175
- if (segments.length >= 3 && segments[1] === "src") {
176
- return null;
177
- }
178
-
179
- // 이미 flat 구조 (의존성 없는 패키지) → 그대로 출력
180
- return [fileName, content];
181
- };
182
- }
183
-
184
- //#endregion
185
-
186
- //#region build (일회성 빌드)
187
-
188
- /**
189
- * DTS 일회성 빌드 (타입체크 + dts 생성)
190
- */
191
- async function build(info: DtsBuildInfo): Promise<DtsBuildResult> {
192
- try {
193
- const parsedConfig = parseRootTsconfig(info.cwd);
194
-
195
- let rootFiles: string[];
196
- let baseOptions: ts.CompilerOptions;
197
- let diagnosticFilter: (d: ts.Diagnostic) => boolean;
198
- let tsBuildInfoFile: string;
199
-
200
- if (info.pkgDir != null && info.env != null) {
201
- // 패키지 모드
202
- baseOptions = await getCompilerOptionsForPackage(parsedConfig.options, info.env, info.pkgDir);
203
-
204
- const shouldEmit = info.emit !== false;
205
- if (shouldEmit) {
206
- // emit 모드: src 대상 (d.ts 생성)
207
- rootFiles = getPackageSourceFiles(info.pkgDir, parsedConfig);
208
- const pkgSrcDir = path.join(info.pkgDir, "src");
209
- diagnosticFilter = (d) => d.file == null || pathIsChildPath(d.file.fileName, pkgSrcDir);
210
- } else {
211
- // 타입체크 모드: 패키지 전체 파일 대상 (src + tests)
212
- rootFiles = getPackageFiles(info.pkgDir, parsedConfig);
213
- const pkgDir = info.pkgDir;
214
- diagnosticFilter = (d) => d.file == null || pathIsChildPath(d.file.fileName, pkgDir);
215
- }
216
-
217
- tsBuildInfoFile = path.join(
218
- info.pkgDir,
219
- ".cache",
220
- shouldEmit ? "dts.tsbuildinfo" : `typecheck-${info.env}.tsbuildinfo`,
221
- );
222
- } else {
223
- // non-package 모드: 프로젝트 루트 파일 + 패키지 루트 설정 파일 타입체크
224
- const packagesDir = path.join(info.cwd, "packages");
225
- const isNonPackageFile = (fileName: string): boolean => {
226
- if (!pathIsChildPath(fileName, packagesDir)) return true;
227
- // 패키지 루트 직속 파일(설정 파일) 포함: packages/{pkg}/file.ts
228
- const relative = path.relative(packagesDir, fileName);
229
- return relative.split(path.sep).length === 2;
230
- };
231
- rootFiles = parsedConfig.fileNames.filter(isNonPackageFile);
232
- baseOptions = parsedConfig.options;
233
- diagnosticFilter = (d) => d.file == null || isNonPackageFile(d.file.fileName);
234
- tsBuildInfoFile = path.join(info.cwd, ".cache", "typecheck-root.tsbuildinfo");
235
- }
236
-
237
- // emit 여부 결정 (기본값: true)
238
- const shouldEmit = info.emit !== false;
239
-
240
- const options: ts.CompilerOptions = {
241
- ...baseOptions,
242
- sourceMap: false,
243
- incremental: true,
244
- tsBuildInfoFile,
245
- };
246
-
247
- // emit 여부에 따라 관련 옵션 설정
248
- if (shouldEmit && info.pkgDir != null) {
249
- // dts 생성 + 타입체크 (패키지 모드에서만)
250
- options.noEmit = false;
251
- options.emitDeclarationOnly = true;
252
- options.declaration = true;
253
- options.declarationMap = true;
254
- options.outDir = path.join(info.pkgDir, "dist");
255
- options.declarationDir = path.join(info.pkgDir, "dist");
256
- } else {
257
- // 타입체크만 수행 (dts 생성 안 함)
258
- options.noEmit = true;
259
- options.emitDeclarationOnly = false;
260
- options.declaration = false;
261
- options.declarationMap = false;
262
- // emit outDir/declarationDir 불필요
263
- }
264
-
265
- // incremental program 생성
266
- const host = ts.createIncrementalCompilerHost(options);
267
-
268
- // 현재 패키지의 .d.ts flat 경로로 출력 (다른 패키지 .d.ts 생성 방지 + 중첩 경로 재작성)
269
- if (shouldEmit && info.pkgDir != null) {
270
- const rewritePath = createDtsPathRewriter(info.pkgDir);
271
- const originalWriteFile = host.writeFile;
272
- host.writeFile = (fileName, content, writeByteOrderMark, onError, sourceFiles, data) => {
273
- const result = rewritePath(fileName, content);
274
- if (result != null) {
275
- originalWriteFile(result[0], result[1], writeByteOrderMark, onError, sourceFiles, data);
276
- }
277
- };
278
- }
279
-
280
- const program = ts.createIncrementalProgram({
281
- rootNames: rootFiles,
282
- options,
283
- host,
284
- });
285
-
286
- // emit (noEmit 경우에도 호출해야 diagnostics가 수집됨)
287
- const emitResult = program.emit();
288
-
289
- // diagnostics 수집
290
- const allDiagnostics = [
291
- ...program.getConfigFileParsingDiagnostics(),
292
- ...program.getSyntacticDiagnostics(),
293
- ...program.getOptionsDiagnostics(),
294
- ...program.getGlobalDiagnostics(),
295
- ...program.getSemanticDiagnostics(),
296
- ...emitResult.diagnostics,
297
- ];
298
-
299
- // 해당 패키지 src 폴더 파일만 에러 수집 (다른 패키지 에러 무시)
300
- const filteredDiagnostics = allDiagnostics.filter(diagnosticFilter);
301
-
302
- const serializedDiagnostics = filteredDiagnostics.map(serializeDiagnostic);
303
- const errorCount = filteredDiagnostics.filter(
304
- (d) => d.category === ts.DiagnosticCategory.Error,
305
- ).length;
306
- const warningCount = filteredDiagnostics.filter(
307
- (d) => d.category === ts.DiagnosticCategory.Warning,
308
- ).length;
309
-
310
- // 에러 메시지 문자열 배열 (하위 호환용)
311
- const errors = filteredDiagnostics
312
- .filter((d) => d.category === ts.DiagnosticCategory.Error)
313
- .map((d) => {
314
- const message = ts.flattenDiagnosticMessageText(d.messageText, "\n");
315
- if (d.file != null && d.start != null) {
316
- const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
317
- return `${d.file.fileName}:${line + 1}:${character + 1}: TS${d.code}: ${message}`;
318
- }
319
- return `TS${d.code}: ${message}`;
320
- });
321
-
322
- return {
323
- success: errorCount === 0,
324
- errors: errors.length > 0 ? errors : undefined,
325
- diagnostics: serializedDiagnostics,
326
- errorCount,
327
- warningCount,
328
- };
329
- } catch (err) {
330
- return {
331
- success: false,
332
- errors: [errorMessage(err)],
333
- diagnostics: [],
334
- errorCount: 1,
335
- warningCount: 0,
336
- };
337
- }
338
- }
339
-
340
- //#endregion
341
-
342
- //#region startWatch (watch 모드)
343
-
344
- const guardStartWatch = createOnceGuard("startWatch");
345
-
346
- /**
347
- * DTS watch 시작
348
- * @remarks 함수는 Worker당 번만 호출되어야 합니다.
349
- * @throws 이미 watch 시작된 경우
350
- */
351
- async function startWatch(info: DtsWatchInfo): Promise<void> {
352
- guardStartWatch();
353
-
354
- try {
355
- const parsedConfig = parseRootTsconfig(info.cwd);
356
- const rootFiles = getPackageSourceFiles(info.pkgDir, parsedConfig);
357
- const baseOptions = await getCompilerOptionsForPackage(
358
- parsedConfig.options,
359
- info.env,
360
- info.pkgDir,
361
- );
362
-
363
- // 해당 패키지 경로 (필터링용)
364
- const pkgSrcDir = path.join(info.pkgDir, "src");
365
-
366
- const options: ts.CompilerOptions = {
367
- ...baseOptions,
368
- emitDeclarationOnly: true,
369
- declaration: true,
370
- declarationMap: true,
371
- outDir: path.join(info.pkgDir, "dist"),
372
- declarationDir: path.join(info.pkgDir, "dist"),
373
- sourceMap: false,
374
- noEmit: false,
375
- incremental: true,
376
- tsBuildInfoFile: path.join(info.pkgDir, ".cache", "dts.tsbuildinfo"),
377
- };
378
-
379
- let isFirstBuild = true;
380
- const collectedErrors: string[] = [];
381
-
382
- const reportDiagnostic: ts.DiagnosticReporter = (diagnostic) => {
383
- if (diagnostic.category === ts.DiagnosticCategory.Error) {
384
- // 해당 패키지 src 폴더 파일만 에러 수집 (다른 패키지 에러 무시)
385
- if (diagnostic.file != null && !pathIsChildPath(diagnostic.file.fileName, pkgSrcDir)) {
386
- return;
387
- }
388
-
389
- const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
390
-
391
- // 파일 위치 정보가 있으면 포함 (절대경로:라인:컬럼 형식 - IDE 링크 지원)
392
- if (diagnostic.file != null && diagnostic.start != null) {
393
- const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
394
- diagnostic.start,
395
- );
396
- collectedErrors.push(
397
- `${diagnostic.file.fileName}:${line + 1}:${character + 1}: TS${diagnostic.code}: ${message}`,
398
- );
399
- } else {
400
- collectedErrors.push(`TS${diagnostic.code}: ${message}`);
401
- }
402
- }
403
- };
404
-
405
- // 현재 패키지의 .d.ts flat 경로로 출력 (다른 패키지 .d.ts 생성 방지 + 중첩 경로 재작성)
406
- // TypeScript watch 모드는 import된 모든 모듈의 .d.ts 생성하려고 시도함.
407
- // 모노레포에서 다른 패키지의 .d.ts까지 덮어쓰는 것을 방지하고,
408
- // 중첩 경로(dist/{pkgName}/src/...) flat 경로(dist/...)로 재작성한다.
409
- const rewritePath = createDtsPathRewriter(info.pkgDir);
410
- const originalWriteFile = ts.sys.writeFile;
411
- const customSys: ts.System = {
412
- ...ts.sys,
413
- writeFile: (filePath, content, writeByteOrderMark) => {
414
- const result = rewritePath(filePath, content);
415
- if (result != null) {
416
- originalWriteFile(result[0], result[1], writeByteOrderMark);
417
- }
418
- },
419
- };
420
-
421
- const host = ts.createWatchCompilerHost(
422
- rootFiles,
423
- options,
424
- customSys,
425
- ts.createEmitAndSemanticDiagnosticsBuilderProgram,
426
- reportDiagnostic,
427
- () => {}, // watchStatusReporter - 사용하지 않음
428
- );
429
-
430
- const originalAfterProgramCreate = host.afterProgramCreate;
431
- host.afterProgramCreate = (program) => {
432
- originalAfterProgramCreate?.(program);
433
-
434
- if (!isFirstBuild) {
435
- sender.send("buildStart", {});
436
- }
437
-
438
- program.emit();
439
-
440
- sender.send("build", {
441
- success: collectedErrors.length === 0,
442
- errors: collectedErrors.length > 0 ? [...collectedErrors] : undefined,
443
- });
444
-
445
- collectedErrors.length = 0;
446
- isFirstBuild = false;
447
- };
448
-
449
- tscWatchProgram = ts.createWatchProgram(host);
450
- } catch (err) {
451
- sender.send("error", {
452
- message: errorMessage(err),
453
- });
454
- }
455
- }
456
-
457
- const sender = createWorker<
458
- { startWatch: typeof startWatch; build: typeof build },
459
- DtsWorkerEvents
460
- >({
461
- startWatch,
462
- build,
463
- });
464
-
465
- export default sender;
466
-
467
- //#endregion
1
+ import path from "path";
2
+ import ts from "typescript";
3
+ import { createWorker, pathIsChildPath, pathNorm } from "@simplysm/core-node";
4
+ import { errorMessage } from "@simplysm/core-common";
5
+ import { consola } from "consola";
6
+ import {
7
+ getCompilerOptionsForPackage,
8
+ getPackageFiles,
9
+ getPackageSourceFiles,
10
+ parseRootTsconfig,
11
+ type TypecheckEnv,
12
+ } from "../utils/tsconfig";
13
+ import { serializeDiagnostic, type SerializedDiagnostic } from "../utils/typecheck-serialization";
14
+ import { createOnceGuard } from "../utils/worker-utils";
15
+
16
+ //#region Types
17
+
18
+ /**
19
+ * DTS watch start info
20
+ */
21
+ export interface DtsWatchInfo {
22
+ name: string;
23
+ cwd: string;
24
+ pkgDir: string;
25
+ env: TypecheckEnv;
26
+ }
27
+
28
+ /**
29
+ * DTS one-time build info
30
+ */
31
+ export interface DtsBuildInfo {
32
+ name: string;
33
+ cwd: string;
34
+ /** Package directory. If unspecified, non-package mode (typecheck all except packages/) */
35
+ pkgDir?: string;
36
+ /** Typecheck environment. Used together with pkgDir */
37
+ env?: TypecheckEnv;
38
+ /** true to generate .d.ts + typecheck, false to typecheck only (default: true) */
39
+ emit?: boolean;
40
+ }
41
+
42
+ /**
43
+ * DTS one-time build result
44
+ */
45
+ export interface DtsBuildResult {
46
+ success: boolean;
47
+ errors?: string[];
48
+ diagnostics: SerializedDiagnostic[];
49
+ errorCount: number;
50
+ warningCount: number;
51
+ }
52
+
53
+ /**
54
+ * Build event
55
+ */
56
+ export interface DtsBuildEvent {
57
+ success: boolean;
58
+ errors?: string[];
59
+ }
60
+
61
+ /**
62
+ * Error event
63
+ */
64
+ export interface DtsErrorEvent {
65
+ message: string;
66
+ }
67
+
68
+ /**
69
+ * Worker event types
70
+ */
71
+ export interface DtsWorkerEvents extends Record<string, unknown> {
72
+ buildStart: Record<string, never>;
73
+ build: DtsBuildEvent;
74
+ error: DtsErrorEvent;
75
+ }
76
+
77
+ //#endregion
78
+
79
+ //#region Resource Management
80
+
81
+ const logger = consola.withTag("sd:cli:dts:worker");
82
+
83
+ /** tsc watch program (to be cleaned up) */
84
+ let tscWatchProgram:
85
+ | ts.WatchOfFilesAndCompilerOptions<ts.EmitAndSemanticDiagnosticsBuilderProgram>
86
+ | undefined;
87
+
88
+ /**
89
+ * Clean up resources
90
+ */
91
+ function cleanup(): void {
92
+ if (tscWatchProgram != null) {
93
+ tscWatchProgram.close();
94
+ tscWatchProgram = undefined;
95
+ }
96
+ }
97
+
98
+ process.on("SIGTERM", () => {
99
+ try {
100
+ cleanup();
101
+ } catch (err) {
102
+ logger.error("Cleanup failed", err);
103
+ }
104
+ process.exit(0);
105
+ });
106
+
107
+ process.on("SIGINT", () => {
108
+ try {
109
+ cleanup();
110
+ } catch (err) {
111
+ logger.error("Cleanup failed", err);
112
+ }
113
+ process.exit(0);
114
+ });
115
+
116
+ //#endregion
117
+
118
+ //#region DTS Output Path Rewriting
119
+
120
+ /**
121
+ * Adjust sources path in .d.ts.map file to new location
122
+ */
123
+ function adjustDtsMapSources(content: string, originalDir: string, newDir: string): string {
124
+ if (originalDir === newDir) return content;
125
+ try {
126
+ const map = JSON.parse(content) as { sources?: string[] };
127
+ if (Array.isArray(map.sources)) {
128
+ map.sources = map.sources.map((source) => {
129
+ const absoluteSource = path.resolve(originalDir, source);
130
+ return path.relative(newDir, absoluteSource);
131
+ });
132
+ }
133
+ return JSON.stringify(map);
134
+ } catch {
135
+ return content;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Create path rewriter function for DTS writeFile
141
+ *
142
+ * TypeScript includes other package sources referenced via path alias (@simplysm/*)
143
+ * in rootDir calculation, so output is generated as nested structure dist/{pkgName}/src/...
144
+ * The returned function rewrites only this package's .d.ts to flat structure (dist/...)
145
+ * and ignores .d.ts from other packages.
146
+ *
147
+ * @returns (fileName, content) => [newPath, newContent] | null (null to skip writing)
148
+ */
149
+ function createDtsPathRewriter(
150
+ pkgDir: string,
151
+ ): (fileName: string, content: string) => [string, string] | null {
152
+ const pkgName = path.basename(pkgDir);
153
+ const distDir = pathNorm(path.join(pkgDir, "dist"));
154
+ const distPrefix = distDir + path.sep;
155
+ // Nested structure prefix for this package: dist/{pkgName}/src/
156
+ const ownNestedPrefix = pathNorm(path.join(distDir, pkgName, "src")) + path.sep;
157
+
158
+ return (fileName, content) => {
159
+ fileName = pathNorm(fileName);
160
+
161
+ if (!fileName.startsWith(distPrefix)) return null;
162
+
163
+ if (fileName.startsWith(ownNestedPrefix)) {
164
+ // Rewrite nested path to flat: dist/{pkgName}/src/... → dist/...
165
+ const flatPath = path.join(distDir, fileName.slice(ownNestedPrefix.length));
166
+ if (fileName.endsWith(".d.ts.map")) {
167
+ content = adjustDtsMapSources(content, path.dirname(fileName), path.dirname(flatPath));
168
+ }
169
+ return [flatPath, content];
170
+ }
171
+
172
+ // Nested output from other packages (dist/{otherPkg}/src/...) → ignore
173
+ const relFromDist = fileName.slice(distPrefix.length);
174
+ const segments = relFromDist.split(path.sep);
175
+ if (segments.length >= 3 && segments[1] === "src") {
176
+ return null;
177
+ }
178
+
179
+ // Already flat structure (package with no dependencies) → output as is
180
+ return [fileName, content];
181
+ };
182
+ }
183
+
184
+ //#endregion
185
+
186
+ //#region build (one-time build)
187
+
188
+ /**
189
+ * DTS one-time build (typecheck + dts generation)
190
+ */
191
+ async function build(info: DtsBuildInfo): Promise<DtsBuildResult> {
192
+ try {
193
+ const parsedConfig = parseRootTsconfig(info.cwd);
194
+
195
+ let rootFiles: string[];
196
+ let baseOptions: ts.CompilerOptions;
197
+ let diagnosticFilter: (d: ts.Diagnostic) => boolean;
198
+ let tsBuildInfoFile: string;
199
+
200
+ if (info.pkgDir != null && info.env != null) {
201
+ // Package mode
202
+ baseOptions = await getCompilerOptionsForPackage(parsedConfig.options, info.env, info.pkgDir);
203
+
204
+ const shouldEmit = info.emit !== false;
205
+ if (shouldEmit) {
206
+ // Emit mode: only src (generate d.ts)
207
+ rootFiles = getPackageSourceFiles(info.pkgDir, parsedConfig);
208
+ const pkgSrcDir = path.join(info.pkgDir, "src");
209
+ diagnosticFilter = (d) => d.file == null || pathIsChildPath(d.file.fileName, pkgSrcDir);
210
+ } else {
211
+ // Typecheck mode: all package files (src + tests)
212
+ rootFiles = getPackageFiles(info.pkgDir, parsedConfig);
213
+ const pkgDir = info.pkgDir;
214
+ diagnosticFilter = (d) => d.file == null || pathIsChildPath(d.file.fileName, pkgDir);
215
+ }
216
+
217
+ tsBuildInfoFile = path.join(
218
+ info.pkgDir,
219
+ ".cache",
220
+ shouldEmit ? "dts.tsbuildinfo" : `typecheck-${info.env}.tsbuildinfo`,
221
+ );
222
+ } else {
223
+ // Non-package mode: root project files + package root config files typecheck
224
+ const packagesDir = path.join(info.cwd, "packages");
225
+ const isNonPackageFile = (fileName: string): boolean => {
226
+ if (!pathIsChildPath(fileName, packagesDir)) return true;
227
+ // Include files directly in package root (config files): packages/{pkg}/file.ts
228
+ const relative = path.relative(packagesDir, fileName);
229
+ return relative.split(path.sep).length === 2;
230
+ };
231
+ rootFiles = parsedConfig.fileNames.filter(isNonPackageFile);
232
+ baseOptions = parsedConfig.options;
233
+ diagnosticFilter = (d) => d.file == null || isNonPackageFile(d.file.fileName);
234
+ tsBuildInfoFile = path.join(info.cwd, ".cache", "typecheck-root.tsbuildinfo");
235
+ }
236
+
237
+ // Determine emit (default: true)
238
+ const shouldEmit = info.emit !== false;
239
+
240
+ const options: ts.CompilerOptions = {
241
+ ...baseOptions,
242
+ sourceMap: false,
243
+ incremental: true,
244
+ tsBuildInfoFile,
245
+ };
246
+
247
+ // Set related options based on emit
248
+ if (shouldEmit && info.pkgDir != null) {
249
+ // Generate dts + typecheck (package mode only)
250
+ options.noEmit = false;
251
+ options.emitDeclarationOnly = true;
252
+ options.declaration = true;
253
+ options.declarationMap = true;
254
+ options.outDir = path.join(info.pkgDir, "dist");
255
+ options.declarationDir = path.join(info.pkgDir, "dist");
256
+ } else {
257
+ // Typecheck only (no dts generation)
258
+ options.noEmit = true;
259
+ options.emitDeclarationOnly = false;
260
+ options.declaration = false;
261
+ options.declarationMap = false;
262
+ // outDir/declarationDir not needed when not emitting
263
+ }
264
+
265
+ // Create incremental program
266
+ const host = ts.createIncrementalCompilerHost(options);
267
+
268
+ // Output only this package's .d.ts in flat path (prevent other packages' .d.ts generation + rewrite nested paths)
269
+ if (shouldEmit && info.pkgDir != null) {
270
+ const rewritePath = createDtsPathRewriter(info.pkgDir);
271
+ const originalWriteFile = host.writeFile;
272
+ host.writeFile = (fileName, content, writeByteOrderMark, onError, sourceFiles, data) => {
273
+ const result = rewritePath(fileName, content);
274
+ if (result != null) {
275
+ originalWriteFile(result[0], result[1], writeByteOrderMark, onError, sourceFiles, data);
276
+ }
277
+ };
278
+ }
279
+
280
+ const program = ts.createIncrementalProgram({
281
+ rootNames: rootFiles,
282
+ options,
283
+ host,
284
+ });
285
+
286
+ // Emit (must call even with noEmit to collect diagnostics)
287
+ const emitResult = program.emit();
288
+
289
+ // Collect diagnostics
290
+ const allDiagnostics = [
291
+ ...program.getConfigFileParsingDiagnostics(),
292
+ ...program.getSyntacticDiagnostics(),
293
+ ...program.getOptionsDiagnostics(),
294
+ ...program.getGlobalDiagnostics(),
295
+ ...program.getSemanticDiagnostics(),
296
+ ...emitResult.diagnostics,
297
+ ];
298
+
299
+ // Collect errors only from this package's src folder (ignore other packages' errors)
300
+ const filteredDiagnostics = allDiagnostics.filter(diagnosticFilter);
301
+
302
+ const serializedDiagnostics = filteredDiagnostics.map(serializeDiagnostic);
303
+ const errorCount = filteredDiagnostics.filter(
304
+ (d) => d.category === ts.DiagnosticCategory.Error,
305
+ ).length;
306
+ const warningCount = filteredDiagnostics.filter(
307
+ (d) => d.category === ts.DiagnosticCategory.Warning,
308
+ ).length;
309
+
310
+ // Error message string array (for backward compatibility)
311
+ const errors = filteredDiagnostics
312
+ .filter((d) => d.category === ts.DiagnosticCategory.Error)
313
+ .map((d) => {
314
+ const message = ts.flattenDiagnosticMessageText(d.messageText, "\n");
315
+ if (d.file != null && d.start != null) {
316
+ const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
317
+ return `${d.file.fileName}:${line + 1}:${character + 1}: TS${d.code}: ${message}`;
318
+ }
319
+ return `TS${d.code}: ${message}`;
320
+ });
321
+
322
+ return {
323
+ success: errorCount === 0,
324
+ errors: errors.length > 0 ? errors : undefined,
325
+ diagnostics: serializedDiagnostics,
326
+ errorCount,
327
+ warningCount,
328
+ };
329
+ } catch (err) {
330
+ return {
331
+ success: false,
332
+ errors: [errorMessage(err)],
333
+ diagnostics: [],
334
+ errorCount: 1,
335
+ warningCount: 0,
336
+ };
337
+ }
338
+ }
339
+
340
+ //#endregion
341
+
342
+ //#region startWatch (watch mode)
343
+
344
+ const guardStartWatch = createOnceGuard("startWatch");
345
+
346
+ /**
347
+ * Start DTS watch
348
+ * @remarks This function should be called only once per Worker.
349
+ * @throws If watch has already been started
350
+ */
351
+ async function startWatch(info: DtsWatchInfo): Promise<void> {
352
+ guardStartWatch();
353
+
354
+ try {
355
+ const parsedConfig = parseRootTsconfig(info.cwd);
356
+ const rootFiles = getPackageSourceFiles(info.pkgDir, parsedConfig);
357
+ const baseOptions = await getCompilerOptionsForPackage(
358
+ parsedConfig.options,
359
+ info.env,
360
+ info.pkgDir,
361
+ );
362
+
363
+ // This package path (for filtering)
364
+ const pkgSrcDir = path.join(info.pkgDir, "src");
365
+
366
+ const options: ts.CompilerOptions = {
367
+ ...baseOptions,
368
+ emitDeclarationOnly: true,
369
+ declaration: true,
370
+ declarationMap: true,
371
+ outDir: path.join(info.pkgDir, "dist"),
372
+ declarationDir: path.join(info.pkgDir, "dist"),
373
+ sourceMap: false,
374
+ noEmit: false,
375
+ incremental: true,
376
+ tsBuildInfoFile: path.join(info.pkgDir, ".cache", "dts.tsbuildinfo"),
377
+ };
378
+
379
+ let isFirstBuild = true;
380
+ const collectedErrors: string[] = [];
381
+
382
+ const reportDiagnostic: ts.DiagnosticReporter = (diagnostic) => {
383
+ if (diagnostic.category === ts.DiagnosticCategory.Error) {
384
+ // Collect errors only from this package's src folder (ignore other packages' errors)
385
+ if (diagnostic.file != null && !pathIsChildPath(diagnostic.file.fileName, pkgSrcDir)) {
386
+ return;
387
+ }
388
+
389
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
390
+
391
+ // Include file location info if available (absolute path:line:column format - supports IDE links)
392
+ if (diagnostic.file != null && diagnostic.start != null) {
393
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
394
+ diagnostic.start,
395
+ );
396
+ collectedErrors.push(
397
+ `${diagnostic.file.fileName}:${line + 1}:${character + 1}: TS${diagnostic.code}: ${message}`,
398
+ );
399
+ } else {
400
+ collectedErrors.push(`TS${diagnostic.code}: ${message}`);
401
+ }
402
+ }
403
+ };
404
+
405
+ // Output only this package's .d.ts in flat path (prevent other packages' .d.ts generation + rewrite nested paths)
406
+ // TypeScript watch mode attempts to generate .d.ts for all imported modules.
407
+ // In a monorepo, we prevent overwriting .d.ts from other packages and
408
+ // rewrite nested paths (dist/{pkgName}/src/...) to flat paths (dist/...).
409
+ const rewritePath = createDtsPathRewriter(info.pkgDir);
410
+ const originalWriteFile = ts.sys.writeFile;
411
+ const customSys: ts.System = {
412
+ ...ts.sys,
413
+ writeFile: (filePath, content, writeByteOrderMark) => {
414
+ const result = rewritePath(filePath, content);
415
+ if (result != null) {
416
+ originalWriteFile(result[0], result[1], writeByteOrderMark);
417
+ }
418
+ },
419
+ };
420
+
421
+ const host = ts.createWatchCompilerHost(
422
+ rootFiles,
423
+ options,
424
+ customSys,
425
+ ts.createEmitAndSemanticDiagnosticsBuilderProgram,
426
+ reportDiagnostic,
427
+ () => {}, // watchStatusReporter - not used
428
+ );
429
+
430
+ const originalAfterProgramCreate = host.afterProgramCreate;
431
+ host.afterProgramCreate = (program) => {
432
+ originalAfterProgramCreate?.(program);
433
+
434
+ if (!isFirstBuild) {
435
+ sender.send("buildStart", {});
436
+ }
437
+
438
+ program.emit();
439
+
440
+ sender.send("build", {
441
+ success: collectedErrors.length === 0,
442
+ errors: collectedErrors.length > 0 ? [...collectedErrors] : undefined,
443
+ });
444
+
445
+ collectedErrors.length = 0;
446
+ isFirstBuild = false;
447
+ };
448
+
449
+ tscWatchProgram = ts.createWatchProgram(host);
450
+ } catch (err) {
451
+ sender.send("error", {
452
+ message: errorMessage(err),
453
+ });
454
+ }
455
+ }
456
+
457
+ const sender = createWorker<
458
+ { startWatch: typeof startWatch; build: typeof build },
459
+ DtsWorkerEvents
460
+ >({
461
+ startWatch,
462
+ build,
463
+ });
464
+
465
+ export default sender;
466
+
467
+ //#endregion