@simplysm/sd-cli 13.0.76 → 13.0.77

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 (154) hide show
  1. package/README.md +341 -16
  2. package/dist/builders/DtsBuilder.js +2 -2
  3. package/dist/builders/DtsBuilder.js.map +1 -1
  4. package/dist/builders/LibraryBuilder.d.ts +3 -3
  5. package/dist/builders/LibraryBuilder.d.ts.map +1 -1
  6. package/dist/builders/LibraryBuilder.js +2 -2
  7. package/dist/builders/LibraryBuilder.js.map +1 -1
  8. package/dist/builders/types.d.ts +7 -1
  9. package/dist/builders/types.d.ts.map +1 -1
  10. package/dist/capacitor/capacitor.d.ts +5 -0
  11. package/dist/capacitor/capacitor.d.ts.map +1 -1
  12. package/dist/capacitor/capacitor.js +59 -59
  13. package/dist/capacitor/capacitor.js.map +1 -1
  14. package/dist/commands/check.js +4 -4
  15. package/dist/commands/check.js.map +1 -1
  16. package/dist/commands/device.js +3 -3
  17. package/dist/commands/device.js.map +1 -1
  18. package/dist/commands/lint.js +4 -4
  19. package/dist/commands/lint.js.map +1 -1
  20. package/dist/commands/publish.js +20 -20
  21. package/dist/commands/publish.js.map +1 -1
  22. package/dist/commands/replace-deps.js +1 -1
  23. package/dist/commands/replace-deps.js.map +1 -1
  24. package/dist/commands/typecheck.js +9 -9
  25. package/dist/commands/typecheck.js.map +1 -1
  26. package/dist/electron/electron.js +16 -16
  27. package/dist/electron/electron.js.map +1 -1
  28. package/dist/orchestrators/BuildOrchestrator.js +6 -6
  29. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  30. package/dist/orchestrators/DevOrchestrator.d.ts +7 -6
  31. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  32. package/dist/orchestrators/DevOrchestrator.js +157 -203
  33. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  34. package/dist/orchestrators/WatchOrchestrator.d.ts.map +1 -1
  35. package/dist/orchestrators/WatchOrchestrator.js +3 -4
  36. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  37. package/dist/sd-cli.js +1 -1
  38. package/dist/sd-cli.js.map +1 -1
  39. package/dist/sd-config.types.d.ts +9 -3
  40. package/dist/sd-config.types.d.ts.map +1 -1
  41. package/dist/utils/copy-public.d.ts.map +1 -1
  42. package/dist/utils/copy-public.js +23 -27
  43. package/dist/utils/copy-public.js.map +1 -1
  44. package/dist/utils/copy-src.d.ts.map +1 -1
  45. package/dist/utils/copy-src.js +7 -7
  46. package/dist/utils/copy-src.js.map +1 -1
  47. package/dist/utils/esbuild-config.d.ts.map +1 -1
  48. package/dist/utils/esbuild-config.js +36 -42
  49. package/dist/utils/esbuild-config.js.map +1 -1
  50. package/dist/utils/replace-deps.js +7 -7
  51. package/dist/utils/replace-deps.js.map +1 -1
  52. package/dist/utils/sd-config.js +2 -2
  53. package/dist/utils/sd-config.js.map +1 -1
  54. package/dist/utils/template.js +7 -7
  55. package/dist/utils/template.js.map +1 -1
  56. package/dist/utils/tsconfig.d.ts +1 -2
  57. package/dist/utils/tsconfig.d.ts.map +1 -1
  58. package/dist/utils/tsconfig.js +5 -8
  59. package/dist/utils/tsconfig.js.map +1 -1
  60. package/dist/utils/typecheck-serialization.js +2 -2
  61. package/dist/utils/typecheck-serialization.js.map +1 -1
  62. package/dist/utils/vite-config.d.ts +2 -0
  63. package/dist/utils/vite-config.d.ts.map +1 -1
  64. package/dist/utils/vite-config.js +36 -3
  65. package/dist/utils/vite-config.js.map +1 -1
  66. package/dist/utils/worker-events.d.ts +11 -1
  67. package/dist/utils/worker-events.d.ts.map +1 -1
  68. package/dist/utils/worker-events.js +3 -5
  69. package/dist/utils/worker-events.js.map +1 -1
  70. package/dist/utils/worker-utils.d.ts +2 -2
  71. package/dist/utils/worker-utils.d.ts.map +1 -1
  72. package/dist/utils/worker-utils.js +1 -1
  73. package/dist/utils/worker-utils.js.map +1 -1
  74. package/dist/workers/client.worker.d.ts +1 -1
  75. package/dist/workers/client.worker.js +3 -3
  76. package/dist/workers/client.worker.js.map +1 -1
  77. package/dist/workers/dts.worker.d.ts +1 -1
  78. package/dist/workers/dts.worker.d.ts.map +1 -1
  79. package/dist/workers/dts.worker.js +13 -28
  80. package/dist/workers/dts.worker.js.map +1 -1
  81. package/dist/workers/library.worker.d.ts +1 -1
  82. package/dist/workers/library.worker.js +4 -4
  83. package/dist/workers/library.worker.js.map +1 -1
  84. package/dist/workers/lint.worker.d.ts +1 -1
  85. package/dist/workers/server-runtime.worker.d.ts +1 -1
  86. package/dist/workers/server-runtime.worker.js +4 -4
  87. package/dist/workers/server-runtime.worker.js.map +1 -1
  88. package/dist/workers/server.worker.d.ts +1 -1
  89. package/dist/workers/server.worker.js +6 -6
  90. package/dist/workers/server.worker.js.map +1 -1
  91. package/package.json +4 -4
  92. package/src/builders/DtsBuilder.ts +2 -2
  93. package/src/builders/LibraryBuilder.ts +7 -10
  94. package/src/builders/types.ts +6 -1
  95. package/src/capacitor/capacitor.ts +61 -60
  96. package/src/commands/check.ts +4 -4
  97. package/src/commands/device.ts +3 -3
  98. package/src/commands/lint.ts +4 -4
  99. package/src/commands/publish.ts +20 -20
  100. package/src/commands/replace-deps.ts +1 -1
  101. package/src/commands/typecheck.ts +9 -9
  102. package/src/electron/electron.ts +16 -16
  103. package/src/orchestrators/BuildOrchestrator.ts +6 -6
  104. package/src/orchestrators/DevOrchestrator.ts +210 -256
  105. package/src/orchestrators/WatchOrchestrator.ts +8 -10
  106. package/src/sd-cli.ts +1 -1
  107. package/src/sd-config.types.ts +10 -3
  108. package/src/utils/copy-public.ts +22 -26
  109. package/src/utils/copy-src.ts +7 -7
  110. package/src/utils/esbuild-config.ts +51 -63
  111. package/src/utils/replace-deps.ts +7 -7
  112. package/src/utils/sd-config.ts +2 -2
  113. package/src/utils/template.ts +7 -7
  114. package/src/utils/tsconfig.ts +6 -10
  115. package/src/utils/typecheck-serialization.ts +2 -2
  116. package/src/utils/vite-config.ts +376 -341
  117. package/src/utils/worker-events.ts +13 -10
  118. package/src/utils/worker-utils.ts +45 -45
  119. package/src/workers/client.worker.ts +3 -3
  120. package/src/workers/dts.worker.ts +451 -467
  121. package/src/workers/library.worker.ts +4 -4
  122. package/src/workers/server-runtime.worker.ts +4 -4
  123. package/src/workers/server.worker.ts +572 -572
  124. package/templates/init/package.json.hbs +2 -2
  125. package/templates/init/packages/client-admin/package.json.hbs +5 -5
  126. package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +2 -2
  127. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +1 -1
  128. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +1 -1
  129. package/templates/init/packages/db-main/package.json.hbs +2 -2
  130. package/templates/init/packages/server/package.json.hbs +4 -4
  131. package/templates/init/tests/e2e/package.json.hbs +1 -1
  132. package/tests/get-compiler-options-for-package.spec.ts +13 -27
  133. package/tests/get-types-from-package-json.spec.ts +15 -11
  134. package/tests/load-ignore-patterns.spec.ts +15 -11
  135. package/tests/load-sd-config.spec.ts +16 -14
  136. package/tests/publish-config-narrowing.spec.ts +20 -0
  137. package/tests/run-lint.spec.ts +38 -34
  138. package/tests/run-typecheck.spec.ts +194 -135
  139. package/tests/sd-public-dev-plugin-mime.spec.ts +19 -0
  140. package/dist/builders/index.d.ts +0 -5
  141. package/dist/builders/index.d.ts.map +0 -1
  142. package/dist/builders/index.js +0 -5
  143. package/dist/builders/index.js.map +0 -6
  144. package/dist/infra/index.d.ts +0 -4
  145. package/dist/infra/index.d.ts.map +0 -1
  146. package/dist/infra/index.js +0 -4
  147. package/dist/infra/index.js.map +0 -6
  148. package/dist/orchestrators/index.d.ts +0 -4
  149. package/dist/orchestrators/index.d.ts.map +0 -1
  150. package/dist/orchestrators/index.js +0 -4
  151. package/dist/orchestrators/index.js.map +0 -6
  152. package/src/builders/index.ts +0 -4
  153. package/src/infra/index.ts +0 -3
  154. package/src/orchestrators/index.ts +0 -3
@@ -1,467 +1,451 @@
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
1
+ import path from "path";
2
+ import ts from "typescript";
3
+ import { createWorker, pathx } from "@simplysm/core-node";
4
+ import { err as errNs } 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, registerCleanupHandlers } 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
+ registerCleanupHandlers(cleanup, logger);
99
+
100
+ //#endregion
101
+
102
+ //#region DTS Output Path Rewriting
103
+
104
+ /**
105
+ * Adjust sources path in .d.ts.map file to new location
106
+ */
107
+ function adjustDtsMapSources(content: string, originalDir: string, newDir: string): string {
108
+ if (originalDir === newDir) return content;
109
+ try {
110
+ const map = JSON.parse(content) as { sources?: string[] };
111
+ if (Array.isArray(map.sources)) {
112
+ map.sources = map.sources.map((source) => {
113
+ const absoluteSource = path.resolve(originalDir, source);
114
+ return path.relative(newDir, absoluteSource);
115
+ });
116
+ }
117
+ return JSON.stringify(map);
118
+ } catch {
119
+ return content;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Create path rewriter function for DTS writeFile
125
+ *
126
+ * TypeScript includes other package sources referenced via path alias (@simplysm/*)
127
+ * in rootDir calculation, so output is generated as nested structure dist/{pkgName}/src/...
128
+ * The returned function rewrites only this package's .d.ts to flat structure (dist/...)
129
+ * and ignores .d.ts from other packages.
130
+ *
131
+ * @returns (fileName, content) => [newPath, newContent] | null (null to skip writing)
132
+ */
133
+ function createDtsPathRewriter(
134
+ pkgDir: string,
135
+ ): (fileName: string, content: string) => [string, string] | null {
136
+ const pkgName = path.basename(pkgDir);
137
+ const distDir = pathx.norm(path.join(pkgDir, "dist"));
138
+ const distPrefix = distDir + path.sep;
139
+ // Nested structure prefix for this package: dist/{pkgName}/src/
140
+ const ownNestedPrefix = pathx.norm(path.join(distDir, pkgName, "src")) + path.sep;
141
+
142
+ return (fileName, content) => {
143
+ fileName = pathx.norm(fileName);
144
+
145
+ if (!fileName.startsWith(distPrefix)) return null;
146
+
147
+ if (fileName.startsWith(ownNestedPrefix)) {
148
+ // Rewrite nested path to flat: dist/{pkgName}/src/... → dist/...
149
+ const flatPath = path.join(distDir, fileName.slice(ownNestedPrefix.length));
150
+ if (fileName.endsWith(".d.ts.map")) {
151
+ content = adjustDtsMapSources(content, path.dirname(fileName), path.dirname(flatPath));
152
+ }
153
+ return [flatPath, content];
154
+ }
155
+
156
+ // Nested output from other packages (dist/{otherPkg}/src/...) ignore
157
+ const relFromDist = fileName.slice(distPrefix.length);
158
+ const segments = relFromDist.split(path.sep);
159
+ if (segments.length >= 3 && segments[1] === "src") {
160
+ return null;
161
+ }
162
+
163
+ // Already flat structure (package with no dependencies) → output as is
164
+ return [fileName, content];
165
+ };
166
+ }
167
+
168
+ //#endregion
169
+
170
+ //#region build (one-time build)
171
+
172
+ /**
173
+ * DTS one-time build (typecheck + dts generation)
174
+ */
175
+ async function build(info: DtsBuildInfo): Promise<DtsBuildResult> {
176
+ try {
177
+ const parsedConfig = parseRootTsconfig(info.cwd);
178
+
179
+ let rootFiles: string[];
180
+ let baseOptions: ts.CompilerOptions;
181
+ let diagnosticFilter: (d: ts.Diagnostic) => boolean;
182
+ let tsBuildInfoFile: string;
183
+
184
+ if (info.pkgDir != null && info.env != null) {
185
+ // Package mode
186
+ baseOptions = await getCompilerOptionsForPackage(parsedConfig.options, info.env, info.pkgDir);
187
+
188
+ const shouldEmit = info.emit !== false;
189
+ if (shouldEmit) {
190
+ // Emit mode: only src (generate d.ts)
191
+ rootFiles = getPackageSourceFiles(info.pkgDir, parsedConfig);
192
+ const pkgSrcDir = path.join(info.pkgDir, "src");
193
+ diagnosticFilter = (d) => d.file == null || pathx.isChildPath(d.file.fileName, pkgSrcDir);
194
+ } else {
195
+ // Typecheck mode: all package files (src + tests)
196
+ rootFiles = getPackageFiles(info.pkgDir, parsedConfig);
197
+ const pkgDir = info.pkgDir;
198
+ diagnosticFilter = (d) => d.file == null || pathx.isChildPath(d.file.fileName, pkgDir);
199
+ }
200
+
201
+ tsBuildInfoFile = path.join(
202
+ info.pkgDir,
203
+ ".cache",
204
+ shouldEmit ? "dts.tsbuildinfo" : `typecheck-${info.env}.tsbuildinfo`,
205
+ );
206
+ } else {
207
+ // Non-package mode: root project files + package root config files typecheck
208
+ const packagesDir = path.join(info.cwd, "packages");
209
+ const isNonPackageFile = (fileName: string): boolean => {
210
+ if (!pathx.isChildPath(fileName, packagesDir)) return true;
211
+ // Include files directly in package root (config files): packages/{pkg}/file.ts
212
+ const relative = path.relative(packagesDir, fileName);
213
+ return relative.split(path.sep).length === 2;
214
+ };
215
+ rootFiles = parsedConfig.fileNames.filter(isNonPackageFile);
216
+ baseOptions = parsedConfig.options;
217
+ diagnosticFilter = (d) => d.file == null || isNonPackageFile(d.file.fileName);
218
+ tsBuildInfoFile = path.join(info.cwd, ".cache", "typecheck-root.tsbuildinfo");
219
+ }
220
+
221
+ // Determine emit (default: true)
222
+ const shouldEmit = info.emit !== false;
223
+
224
+ const options: ts.CompilerOptions = {
225
+ ...baseOptions,
226
+ sourceMap: false,
227
+ incremental: true,
228
+ tsBuildInfoFile,
229
+ };
230
+
231
+ // Set related options based on emit
232
+ if (shouldEmit && info.pkgDir != null) {
233
+ // Generate dts + typecheck (package mode only)
234
+ options.noEmit = false;
235
+ options.emitDeclarationOnly = true;
236
+ options.declaration = true;
237
+ options.declarationMap = true;
238
+ options.outDir = path.join(info.pkgDir, "dist");
239
+ options.declarationDir = path.join(info.pkgDir, "dist");
240
+ } else {
241
+ // Typecheck only (no dts generation)
242
+ options.noEmit = true;
243
+ options.emitDeclarationOnly = false;
244
+ options.declaration = false;
245
+ options.declarationMap = false;
246
+ // outDir/declarationDir not needed when not emitting
247
+ }
248
+
249
+ // Create incremental program
250
+ const host = ts.createIncrementalCompilerHost(options);
251
+
252
+ // Output only this package's .d.ts in flat path (prevent other packages' .d.ts generation + rewrite nested paths)
253
+ if (shouldEmit && info.pkgDir != null) {
254
+ const rewritePath = createDtsPathRewriter(info.pkgDir);
255
+ const originalWriteFile = host.writeFile;
256
+ host.writeFile = (fileName, content, writeByteOrderMark, onError, sourceFiles, data) => {
257
+ const result = rewritePath(fileName, content);
258
+ if (result != null) {
259
+ originalWriteFile(result[0], result[1], writeByteOrderMark, onError, sourceFiles, data);
260
+ }
261
+ };
262
+ }
263
+
264
+ const program = ts.createIncrementalProgram({
265
+ rootNames: rootFiles,
266
+ options,
267
+ host,
268
+ });
269
+
270
+ // Emit (must call even with noEmit to collect diagnostics)
271
+ const emitResult = program.emit();
272
+
273
+ // Collect diagnostics
274
+ const allDiagnostics = [
275
+ ...program.getConfigFileParsingDiagnostics(),
276
+ ...program.getSyntacticDiagnostics(),
277
+ ...program.getOptionsDiagnostics(),
278
+ ...program.getGlobalDiagnostics(),
279
+ ...program.getSemanticDiagnostics(),
280
+ ...emitResult.diagnostics,
281
+ ];
282
+
283
+ // Collect errors only from this package's src folder (ignore other packages' errors)
284
+ const filteredDiagnostics = allDiagnostics.filter(diagnosticFilter);
285
+
286
+ const serializedDiagnostics = filteredDiagnostics.map(serializeDiagnostic);
287
+ const errorCount = filteredDiagnostics.filter(
288
+ (d) => d.category === ts.DiagnosticCategory.Error,
289
+ ).length;
290
+ const warningCount = filteredDiagnostics.filter(
291
+ (d) => d.category === ts.DiagnosticCategory.Warning,
292
+ ).length;
293
+
294
+ // Error message string array (for backward compatibility)
295
+ const errors = filteredDiagnostics
296
+ .filter((d) => d.category === ts.DiagnosticCategory.Error)
297
+ .map((d) => {
298
+ const message = ts.flattenDiagnosticMessageText(d.messageText, "\n");
299
+ if (d.file != null && d.start != null) {
300
+ const { line, character } = d.file.getLineAndCharacterOfPosition(d.start);
301
+ return `${d.file.fileName}:${line + 1}:${character + 1}: TS${d.code}: ${message}`;
302
+ }
303
+ return `TS${d.code}: ${message}`;
304
+ });
305
+
306
+ return {
307
+ success: errorCount === 0,
308
+ errors: errors.length > 0 ? errors : undefined,
309
+ diagnostics: serializedDiagnostics,
310
+ errorCount,
311
+ warningCount,
312
+ };
313
+ } catch (err) {
314
+ return {
315
+ success: false,
316
+ errors: [errNs.message(err)],
317
+ diagnostics: [],
318
+ errorCount: 1,
319
+ warningCount: 0,
320
+ };
321
+ }
322
+ }
323
+
324
+ //#endregion
325
+
326
+ //#region startWatch (watch mode)
327
+
328
+ const guardStartWatch = createOnceGuard("startWatch");
329
+
330
+ /**
331
+ * Start DTS watch
332
+ * @remarks This function should be called only once per Worker.
333
+ * @throws If watch has already been started
334
+ */
335
+ async function startWatch(info: DtsWatchInfo): Promise<void> {
336
+ guardStartWatch();
337
+
338
+ try {
339
+ const parsedConfig = parseRootTsconfig(info.cwd);
340
+ const rootFiles = getPackageSourceFiles(info.pkgDir, parsedConfig);
341
+ const baseOptions = await getCompilerOptionsForPackage(
342
+ parsedConfig.options,
343
+ info.env,
344
+ info.pkgDir,
345
+ );
346
+
347
+ // This package path (for filtering)
348
+ const pkgSrcDir = path.join(info.pkgDir, "src");
349
+
350
+ const options: ts.CompilerOptions = {
351
+ ...baseOptions,
352
+ emitDeclarationOnly: true,
353
+ declaration: true,
354
+ declarationMap: true,
355
+ outDir: path.join(info.pkgDir, "dist"),
356
+ declarationDir: path.join(info.pkgDir, "dist"),
357
+ sourceMap: false,
358
+ noEmit: false,
359
+ incremental: true,
360
+ tsBuildInfoFile: path.join(info.pkgDir, ".cache", "dts.tsbuildinfo"),
361
+ };
362
+
363
+ let isFirstBuild = true;
364
+ const collectedErrors: string[] = [];
365
+
366
+ const reportDiagnostic: ts.DiagnosticReporter = (diagnostic) => {
367
+ if (diagnostic.category === ts.DiagnosticCategory.Error) {
368
+ // Collect errors only from this package's src folder (ignore other packages' errors)
369
+ if (diagnostic.file != null && !pathx.isChildPath(diagnostic.file.fileName, pkgSrcDir)) {
370
+ return;
371
+ }
372
+
373
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
374
+
375
+ // Include file location info if available (absolute path:line:column format - supports IDE links)
376
+ if (diagnostic.file != null && diagnostic.start != null) {
377
+ const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(
378
+ diagnostic.start,
379
+ );
380
+ collectedErrors.push(
381
+ `${diagnostic.file.fileName}:${line + 1}:${character + 1}: TS${diagnostic.code}: ${message}`,
382
+ );
383
+ } else {
384
+ collectedErrors.push(`TS${diagnostic.code}: ${message}`);
385
+ }
386
+ }
387
+ };
388
+
389
+ // Output only this package's .d.ts in flat path (prevent other packages' .d.ts generation + rewrite nested paths)
390
+ // TypeScript watch mode attempts to generate .d.ts for all imported modules.
391
+ // In a monorepo, we prevent overwriting .d.ts from other packages and
392
+ // rewrite nested paths (dist/{pkgName}/src/...) to flat paths (dist/...).
393
+ const rewritePath = createDtsPathRewriter(info.pkgDir);
394
+ const originalWriteFile = ts.sys.writeFile;
395
+ const customSys: ts.System = {
396
+ ...ts.sys,
397
+ writeFile: (filePath, content, writeByteOrderMark) => {
398
+ const result = rewritePath(filePath, content);
399
+ if (result != null) {
400
+ originalWriteFile(result[0], result[1], writeByteOrderMark);
401
+ }
402
+ },
403
+ };
404
+
405
+ const host = ts.createWatchCompilerHost(
406
+ rootFiles,
407
+ options,
408
+ customSys,
409
+ ts.createEmitAndSemanticDiagnosticsBuilderProgram,
410
+ reportDiagnostic,
411
+ () => {}, // watchStatusReporter - not used
412
+ );
413
+
414
+ const originalAfterProgramCreate = host.afterProgramCreate;
415
+ host.afterProgramCreate = (program) => {
416
+ originalAfterProgramCreate?.(program);
417
+
418
+ if (!isFirstBuild) {
419
+ sender.send("buildStart", {});
420
+ }
421
+
422
+ program.emit();
423
+
424
+ sender.send("build", {
425
+ success: collectedErrors.length === 0,
426
+ errors: collectedErrors.length > 0 ? [...collectedErrors] : undefined,
427
+ });
428
+
429
+ collectedErrors.length = 0;
430
+ isFirstBuild = false;
431
+ };
432
+
433
+ tscWatchProgram = ts.createWatchProgram(host);
434
+ } catch (err) {
435
+ sender.send("error", {
436
+ message: errNs.message(err),
437
+ });
438
+ }
439
+ }
440
+
441
+ const sender = createWorker<
442
+ { startWatch: typeof startWatch; build: typeof build },
443
+ DtsWorkerEvents
444
+ >({
445
+ startWatch,
446
+ build,
447
+ });
448
+
449
+ export default sender;
450
+
451
+ //#endregion