@simplysm/sd-cli 13.0.11 → 13.0.13

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 (160) hide show
  1. package/README.md +42 -2
  2. package/dist/builders/BaseBuilder.d.ts +15 -4
  3. package/dist/builders/BaseBuilder.d.ts.map +1 -1
  4. package/dist/builders/BaseBuilder.js +50 -0
  5. package/dist/builders/BaseBuilder.js.map +1 -1
  6. package/dist/builders/DtsBuilder.d.ts.map +1 -1
  7. package/dist/builders/DtsBuilder.js +2 -39
  8. package/dist/builders/DtsBuilder.js.map +1 -1
  9. package/dist/builders/LibraryBuilder.d.ts.map +1 -1
  10. package/dist/builders/LibraryBuilder.js +2 -39
  11. package/dist/builders/LibraryBuilder.js.map +1 -1
  12. package/dist/builders/types.d.ts +2 -2
  13. package/dist/builders/types.d.ts.map +1 -1
  14. package/dist/capacitor/capacitor.d.ts.map +1 -1
  15. package/dist/capacitor/capacitor.js +2 -1
  16. package/dist/capacitor/capacitor.js.map +1 -1
  17. package/dist/commands/add-client.d.ts.map +1 -1
  18. package/dist/commands/add-client.js +1 -9
  19. package/dist/commands/add-client.js.map +1 -1
  20. package/dist/commands/add-server.d.ts.map +1 -1
  21. package/dist/commands/add-server.js +1 -9
  22. package/dist/commands/add-server.js.map +1 -1
  23. package/dist/commands/build.d.ts +1 -2
  24. package/dist/commands/build.d.ts.map +1 -1
  25. package/dist/commands/build.js +12 -311
  26. package/dist/commands/build.js.map +1 -1
  27. package/dist/commands/dev.d.ts +1 -1
  28. package/dist/commands/dev.d.ts.map +1 -1
  29. package/dist/commands/dev.js +11 -432
  30. package/dist/commands/dev.js.map +1 -1
  31. package/dist/commands/device.d.ts.map +1 -1
  32. package/dist/commands/device.js +17 -32
  33. package/dist/commands/device.js.map +1 -1
  34. package/dist/commands/init.d.ts.map +1 -1
  35. package/dist/commands/init.js +1 -9
  36. package/dist/commands/init.js.map +1 -1
  37. package/dist/commands/lint.d.ts +1 -1
  38. package/dist/commands/lint.d.ts.map +1 -1
  39. package/dist/commands/lint.js +71 -118
  40. package/dist/commands/lint.js.map +2 -2
  41. package/dist/commands/publish.d.ts.map +1 -1
  42. package/dist/commands/publish.js +47 -69
  43. package/dist/commands/publish.js.map +1 -1
  44. package/dist/commands/typecheck.d.ts +1 -1
  45. package/dist/commands/typecheck.d.ts.map +1 -1
  46. package/dist/commands/typecheck.js +11 -24
  47. package/dist/commands/typecheck.js.map +1 -1
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +4 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/infra/ResultCollector.d.ts +1 -1
  53. package/dist/infra/ResultCollector.d.ts.map +1 -1
  54. package/dist/infra/ResultCollector.js +1 -1
  55. package/dist/infra/ResultCollector.js.map +1 -1
  56. package/dist/orchestrators/BuildOrchestrator.d.ts +53 -0
  57. package/dist/orchestrators/BuildOrchestrator.d.ts.map +1 -0
  58. package/dist/orchestrators/BuildOrchestrator.js +338 -0
  59. package/dist/orchestrators/BuildOrchestrator.js.map +6 -0
  60. package/dist/orchestrators/DevOrchestrator.d.ts +64 -0
  61. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -0
  62. package/dist/orchestrators/DevOrchestrator.js +524 -0
  63. package/dist/orchestrators/DevOrchestrator.js.map +6 -0
  64. package/dist/orchestrators/WatchOrchestrator.d.ts +1 -1
  65. package/dist/orchestrators/WatchOrchestrator.d.ts.map +1 -1
  66. package/dist/orchestrators/WatchOrchestrator.js +30 -21
  67. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  68. package/dist/orchestrators/index.d.ts +2 -0
  69. package/dist/orchestrators/index.d.ts.map +1 -1
  70. package/dist/orchestrators/index.js +4 -0
  71. package/dist/orchestrators/index.js.map +1 -1
  72. package/dist/sd-cli-entry.js +14 -14
  73. package/dist/sd-cli-entry.js.map +1 -1
  74. package/dist/sd-cli.js +6 -4
  75. package/dist/sd-cli.js.map +1 -1
  76. package/dist/utils/esbuild-config.d.ts +8 -0
  77. package/dist/utils/esbuild-config.d.ts.map +1 -1
  78. package/dist/utils/esbuild-config.js +23 -28
  79. package/dist/utils/esbuild-config.js.map +1 -1
  80. package/dist/utils/output-utils.d.ts +0 -1
  81. package/dist/utils/output-utils.d.ts.map +1 -1
  82. package/dist/utils/output-utils.js +1 -1
  83. package/dist/utils/output-utils.js.map +1 -1
  84. package/dist/utils/package-utils.d.ts +5 -1
  85. package/dist/utils/package-utils.d.ts.map +1 -1
  86. package/dist/utils/package-utils.js +12 -0
  87. package/dist/utils/package-utils.js.map +1 -1
  88. package/dist/utils/rebuild-manager.d.ts +15 -0
  89. package/dist/utils/rebuild-manager.d.ts.map +1 -0
  90. package/dist/utils/rebuild-manager.js +50 -0
  91. package/dist/utils/rebuild-manager.js.map +6 -0
  92. package/dist/utils/replace-deps.d.ts.map +1 -1
  93. package/dist/utils/replace-deps.js +7 -13
  94. package/dist/utils/replace-deps.js.map +1 -1
  95. package/dist/utils/vite-config.d.ts.map +1 -1
  96. package/dist/utils/vite-config.js +61 -7
  97. package/dist/utils/vite-config.js.map +1 -1
  98. package/dist/utils/worker-events.d.ts +5 -4
  99. package/dist/utils/worker-events.d.ts.map +1 -1
  100. package/dist/utils/worker-events.js +4 -0
  101. package/dist/utils/worker-events.js.map +1 -1
  102. package/dist/utils/worker-utils.d.ts +13 -0
  103. package/dist/utils/worker-utils.d.ts.map +1 -0
  104. package/dist/utils/worker-utils.js +15 -0
  105. package/dist/utils/worker-utils.js.map +6 -0
  106. package/dist/workers/client.worker.d.ts.map +1 -1
  107. package/dist/workers/client.worker.js +2 -14
  108. package/dist/workers/client.worker.js.map +1 -1
  109. package/dist/workers/library.worker.d.ts.map +1 -1
  110. package/dist/workers/library.worker.js +14 -16
  111. package/dist/workers/library.worker.js.map +1 -1
  112. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  113. package/dist/workers/server-runtime.worker.js +11 -11
  114. package/dist/workers/server-runtime.worker.js.map +1 -1
  115. package/dist/workers/server.worker.d.ts.map +1 -1
  116. package/dist/workers/server.worker.js +2 -14
  117. package/dist/workers/server.worker.js.map +1 -1
  118. package/package.json +4 -5
  119. package/src/builders/BaseBuilder.ts +71 -4
  120. package/src/builders/DtsBuilder.ts +2 -49
  121. package/src/builders/LibraryBuilder.ts +2 -51
  122. package/src/builders/types.ts +2 -2
  123. package/src/capacitor/capacitor.ts +2 -1
  124. package/src/commands/add-client.ts +1 -14
  125. package/src/commands/add-server.ts +1 -13
  126. package/src/commands/build.ts +13 -443
  127. package/src/commands/dev.ts +12 -582
  128. package/src/commands/device.ts +17 -34
  129. package/src/commands/init.ts +1 -13
  130. package/src/commands/lint.ts +85 -146
  131. package/src/commands/publish.ts +58 -76
  132. package/src/commands/typecheck.ts +13 -46
  133. package/src/index.ts +1 -0
  134. package/src/infra/ResultCollector.ts +2 -2
  135. package/src/orchestrators/BuildOrchestrator.ts +499 -0
  136. package/src/orchestrators/DevOrchestrator.ts +703 -0
  137. package/src/orchestrators/WatchOrchestrator.ts +42 -25
  138. package/src/orchestrators/index.ts +2 -0
  139. package/src/sd-cli-entry.ts +14 -14
  140. package/src/sd-cli.ts +6 -4
  141. package/src/utils/esbuild-config.ts +31 -33
  142. package/src/utils/output-utils.ts +1 -2
  143. package/src/utils/package-utils.ts +16 -1
  144. package/src/utils/rebuild-manager.ts +65 -0
  145. package/src/utils/replace-deps.ts +25 -23
  146. package/src/utils/vite-config.ts +115 -9
  147. package/src/utils/worker-events.ts +13 -5
  148. package/src/utils/worker-utils.ts +26 -0
  149. package/src/workers/client.worker.ts +3 -19
  150. package/src/workers/library.worker.ts +16 -22
  151. package/src/workers/server-runtime.worker.ts +16 -17
  152. package/src/workers/server.worker.ts +2 -19
  153. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  154. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  155. package/templates/init/package.json.hbs +3 -3
  156. package/dist/utils/listr-manager.d.ts +0 -37
  157. package/dist/utils/listr-manager.d.ts.map +0 -1
  158. package/dist/utils/listr-manager.js +0 -59
  159. package/dist/utils/listr-manager.js.map +0 -6
  160. package/src/utils/listr-manager.ts +0 -89
@@ -1,12 +1,11 @@
1
1
  import path from "path";
2
- import { Listr } from "listr2";
3
2
  import { consola } from "consola";
4
3
  import type { BuildTarget, SdConfig, SdPackageConfig } from "../sd-config.types";
5
4
  import { loadSdConfig } from "../utils/sd-config";
6
5
  import { filterPackagesByTargets } from "../utils/package-utils";
7
6
  import { setupReplaceDeps, watchReplaceDeps, type WatchReplaceDepResult } from "../utils/replace-deps";
8
7
  import { printErrors } from "../utils/output-utils";
9
- import { RebuildListrManager } from "../utils/listr-manager";
8
+ import { RebuildManager } from "../utils/rebuild-manager";
10
9
  import { ResultCollector } from "../infra/ResultCollector";
11
10
  import { SignalHandler } from "../infra/SignalHandler";
12
11
  import { LibraryBuilder } from "../builders/LibraryBuilder";
@@ -38,7 +37,7 @@ export class WatchOrchestrator {
38
37
 
39
38
  private _resultCollector!: ResultCollector;
40
39
  private _signalHandler!: SignalHandler;
41
- private _rebuildManager!: RebuildListrManager;
40
+ private _rebuildManager!: RebuildManager;
42
41
  private _libraryBuilder!: LibraryBuilder;
43
42
  private _dtsBuilder!: DtsBuilder;
44
43
  private _packages: PackageInfo[] = [];
@@ -69,7 +68,7 @@ export class WatchOrchestrator {
69
68
  });
70
69
  this._logger.debug("sd.config.ts 로드 완료");
71
70
  } catch (err) {
72
- consola.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
71
+ this._logger.error(`sd.config.ts 로드 실패: ${err instanceof Error ? err.message : err}`);
73
72
  process.exitCode = 1;
74
73
  throw err;
75
74
  }
@@ -109,7 +108,7 @@ export class WatchOrchestrator {
109
108
  // 인프라 초기화
110
109
  this._resultCollector = new ResultCollector();
111
110
  this._signalHandler = new SignalHandler();
112
- this._rebuildManager = new RebuildListrManager(this._logger);
111
+ this._rebuildManager = new RebuildManager(this._logger);
113
112
 
114
113
  // 배치 완료 시 에러 출력
115
114
  this._rebuildManager.on("batchComplete", () => {
@@ -133,7 +132,7 @@ export class WatchOrchestrator {
133
132
 
134
133
  /**
135
134
  * Watch 모드 시작
136
- * - 초기 빌드 Listr 실행
135
+ * - 초기 빌드 실행
137
136
  * - 결과 출력
138
137
  */
139
138
  async start(): Promise<void> {
@@ -141,26 +140,10 @@ export class WatchOrchestrator {
141
140
  return;
142
141
  }
143
142
 
144
- // 초기 빌드 Listr 구성
143
+ // 초기 빌드 Promise 구성
145
144
  const buildPromises = this._libraryBuilder.getInitialBuildPromises();
146
145
  const dtsPromises = this._dtsBuilder.getInitialBuildPromises();
147
146
 
148
- const initialListr = new Listr(
149
- [
150
- // Library 빌드 태스크
151
- ...this._packages.map((pkg) => ({
152
- title: `${pkg.name} (${pkg.config.target})`,
153
- task: () => buildPromises.get(`${pkg.name}:build`) ?? Promise.resolve(),
154
- })),
155
- // DTS 태스크
156
- ...this._packages.map((pkg) => ({
157
- title: `${pkg.name} (dts)`,
158
- task: () => dtsPromises.get(`${pkg.name}:dts`) ?? Promise.resolve(),
159
- })),
160
- ],
161
- { concurrent: true },
162
- );
163
-
164
147
  // copySrc watch 시작
165
148
  for (const pkg of this._packages) {
166
149
  const buildConfig = pkg.config as SdBuildPackageConfig;
@@ -174,8 +157,42 @@ export class WatchOrchestrator {
174
157
  void this._libraryBuilder.startWatch();
175
158
  void this._dtsBuilder.startWatch();
176
159
 
177
- // Listr 실행 (초기 빌드 완료까지 대기)
178
- await initialListr.run();
160
+ // 초기 빌드 시작
161
+ this._logger.start("초기 빌드 진행 중...");
162
+
163
+ // Library 빌드 및 DTS 빌드 전체 Promise 배열 구성
164
+ const allBuildTasks: Array<{ name: string; promise: Promise<void> }> = [];
165
+
166
+ // Library 빌드 태스크
167
+ for (const pkg of this._packages) {
168
+ const promise = buildPromises.get(`${pkg.name}:build`) ?? Promise.resolve();
169
+ allBuildTasks.push({
170
+ name: `${pkg.name}:build`,
171
+ promise: (async () => {
172
+ this._logger.debug(`${pkg.name} (${pkg.config.target}) 빌드 시작`);
173
+ await promise;
174
+ this._logger.debug(`${pkg.name} (${pkg.config.target}) 빌드 완료`);
175
+ })(),
176
+ });
177
+ }
178
+
179
+ // DTS 태스크
180
+ for (const pkg of this._packages) {
181
+ const promise = dtsPromises.get(`${pkg.name}:dts`) ?? Promise.resolve();
182
+ allBuildTasks.push({
183
+ name: `${pkg.name}:dts`,
184
+ promise: (async () => {
185
+ this._logger.debug(`${pkg.name} (dts) 시작`);
186
+ await promise;
187
+ this._logger.debug(`${pkg.name} (dts) 완료`);
188
+ })(),
189
+ });
190
+ }
191
+
192
+ // 모든 빌드 작업 동시 실행 (초기 빌드 완료까지 대기)
193
+ await Promise.allSettled(allBuildTasks.map((task) => task.promise));
194
+
195
+ this._logger.success("초기 빌드 완료");
179
196
 
180
197
  // 초기 빌드 결과 출력
181
198
  printErrors(this._resultCollector.toMap());
@@ -1 +1,3 @@
1
+ export { BuildOrchestrator, type BuildOrchestratorOptions } from "./BuildOrchestrator";
2
+ export { DevOrchestrator, type DevOrchestratorOptions } from "./DevOrchestrator";
1
3
  export { WatchOrchestrator, type WatchOrchestratorOptions } from "./WatchOrchestrator";
@@ -85,7 +85,7 @@ export function createCliParser(argv: string[]): Argv {
85
85
  default: [],
86
86
  })
87
87
  .options({
88
- options: {
88
+ configOpt: {
89
89
  type: "string",
90
90
  array: true,
91
91
  alias: "o",
@@ -96,7 +96,7 @@ export function createCliParser(argv: string[]): Argv {
96
96
  async (args) => {
97
97
  await runTypecheck({
98
98
  targets: args.targets,
99
- options: args.options,
99
+ options: args.configOpt,
100
100
  });
101
101
  },
102
102
  )
@@ -114,7 +114,7 @@ export function createCliParser(argv: string[]): Argv {
114
114
  default: [],
115
115
  })
116
116
  .options({
117
- options: {
117
+ configOpt: {
118
118
  type: "string",
119
119
  array: true,
120
120
  alias: "o",
@@ -125,7 +125,7 @@ export function createCliParser(argv: string[]): Argv {
125
125
  async (args) => {
126
126
  await runWatch({
127
127
  targets: args.targets,
128
- options: args.options,
128
+ options: args.configOpt,
129
129
  });
130
130
  },
131
131
  )
@@ -143,7 +143,7 @@ export function createCliParser(argv: string[]): Argv {
143
143
  default: [],
144
144
  })
145
145
  .options({
146
- options: {
146
+ configOpt: {
147
147
  type: "string",
148
148
  array: true,
149
149
  alias: "o",
@@ -154,7 +154,7 @@ export function createCliParser(argv: string[]): Argv {
154
154
  async (args) => {
155
155
  await runDev({
156
156
  targets: args.targets,
157
- options: args.options,
157
+ options: args.configOpt,
158
158
  });
159
159
  },
160
160
  )
@@ -172,7 +172,7 @@ export function createCliParser(argv: string[]): Argv {
172
172
  default: [],
173
173
  })
174
174
  .options({
175
- options: {
175
+ configOpt: {
176
176
  type: "string",
177
177
  array: true,
178
178
  alias: "o",
@@ -183,7 +183,7 @@ export function createCliParser(argv: string[]): Argv {
183
183
  async (args) => {
184
184
  await runBuild({
185
185
  targets: args.targets,
186
- options: args.options,
186
+ options: args.configOpt,
187
187
  });
188
188
  },
189
189
  )
@@ -206,7 +206,7 @@ export function createCliParser(argv: string[]): Argv {
206
206
  alias: "u",
207
207
  describe: "개발 서버 URL (미지정 시 sd.config.ts의 server 설정 사용)",
208
208
  },
209
- options: {
209
+ configOpt: {
210
210
  type: "string",
211
211
  array: true,
212
212
  alias: "o",
@@ -218,7 +218,7 @@ export function createCliParser(argv: string[]): Argv {
218
218
  await runDevice({
219
219
  package: args.package,
220
220
  url: args.url,
221
- options: args.options,
221
+ options: args.configOpt,
222
222
  });
223
223
  },
224
224
  )
@@ -279,7 +279,7 @@ export function createCliParser(argv: string[]): Argv {
279
279
  describe: "실제 배포 없이 시뮬레이션",
280
280
  default: false,
281
281
  },
282
- "options": {
282
+ "configOpt": {
283
283
  type: "string",
284
284
  array: true,
285
285
  alias: "o",
@@ -292,7 +292,7 @@ export function createCliParser(argv: string[]): Argv {
292
292
  targets: args.targets,
293
293
  noBuild: !args.build,
294
294
  dryRun: args.dryRun,
295
- options: args.options,
295
+ options: args.configOpt,
296
296
  });
297
297
  },
298
298
  )
@@ -304,7 +304,7 @@ export function createCliParser(argv: string[]): Argv {
304
304
  .version(false)
305
305
  .hide("help")
306
306
  .options({
307
- options: {
307
+ configOpt: {
308
308
  type: "string",
309
309
  array: true,
310
310
  alias: "o",
@@ -314,7 +314,7 @@ export function createCliParser(argv: string[]): Argv {
314
314
  }),
315
315
  async (args) => {
316
316
  await runReplaceDeps({
317
- options: args.options,
317
+ options: args.configOpt,
318
318
  });
319
319
  },
320
320
  )
package/src/sd-cli.ts CHANGED
@@ -12,15 +12,16 @@ import os from "os";
12
12
  import path from "path";
13
13
  import { fileURLToPath } from "url";
14
14
 
15
- const cliEntryUrl = import.meta.resolve("./sd-cli-entry");
16
- const cliEntryFilePath = fileURLToPath(cliEntryUrl);
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+ const isDev = path.extname(__filename) === ".ts";
17
18
 
18
- if (path.extname(cliEntryFilePath) === ".ts") {
19
+ if (isDev) {
19
20
  // 개발 모드 (.ts): affinity 적용 후 직접 실행
20
21
  // import만으로는 메인 모듈 감지가 실패하므로 (process.argv[1] ≠ sd-cli-entry)
21
22
  // createCliParser를 명시적으로 호출한다.
22
23
  configureAffinityAndPriority(process.pid);
23
- const { createCliParser } = await import(cliEntryUrl);
24
+ const { createCliParser } = await import("./sd-cli-entry.js");
24
25
  await createCliParser(process.argv.slice(2)).parse();
25
26
  } else {
26
27
  // 배포 모드 (.js): 2단계 실행
@@ -38,6 +39,7 @@ if (path.extname(cliEntryFilePath) === ".ts") {
38
39
  }
39
40
 
40
41
  // Phase 2: 새 프로세스로 실제 CLI 실행 (모듈 캐시 초기화)
42
+ const cliEntryFilePath = path.join(__dirname, "sd-cli-entry.js");
41
43
  const child = spawn(
42
44
  "node",
43
45
  ["--max-old-space-size=8192", "--max-semi-space-size=16", cliEntryFilePath, ...process.argv.slice(2)],
@@ -2,45 +2,42 @@ import path from "path";
2
2
  import { readFileSync, existsSync } from "fs";
3
3
  import fs from "fs/promises";
4
4
  import { createRequire } from "module";
5
- import { glob } from "glob";
6
5
  import type esbuild from "esbuild";
7
6
  import { solidPlugin } from "esbuild-plugin-solid";
8
7
  import type { TypecheckEnv } from "./tsconfig";
9
8
 
10
9
  /**
11
- * ESM 상대 import 경로에 .js 확장자를 추가하는 esbuild 플러그인.
10
+ * esbuild outputFiles 실제로 변경된 파일만 디스크에 쓴다.
12
11
  *
13
- * bundle: false 모드에서 esbuild는 import 경로를 그대로 유지하므로,
14
- * Node.js ESM에서 직접 실행 확장자 누락으로 모듈을 찾지 못하는 문제를 해결한다.
12
+ * - .js 파일: ESM 상대 import 경로에 .js 확장자를 추가한 후 비교
13
+ * - 파일(.js.map 등): 원본 그대로 비교
14
+ * - 기존 파일과 내용이 동일하면 쓰기를 건너뛰어 타임스탬프를 유지한다.
15
15
  */
16
- function esmRelativeImportPlugin(outdir: string): esbuild.Plugin {
17
- return {
18
- name: "esm-relative-import",
19
- setup(build) {
20
- build.onEnd(async () => {
21
- const files = await glob("**/*.js", { cwd: outdir });
22
-
23
- await Promise.all(
24
- files.map(async (file) => {
25
- const filePath = path.join(outdir, file);
26
- const content = await fs.readFile(filePath, "utf-8");
27
-
28
- const rewritten = content.replace(
29
- /((?:from|import)\s*["'])(\.\.?\/[^"']*?)(["'])/g,
30
- (_match, prefix: string, importPath: string, suffix: string) => {
31
- if (/\.(js|mjs|cjs|json|css|wasm|node)$/i.test(importPath)) return _match;
32
- return `${prefix}${importPath}.js${suffix}`;
33
- },
34
- );
35
-
36
- if (rewritten !== content) {
37
- await fs.writeFile(filePath, rewritten);
38
- }
39
- }),
40
- );
41
- });
42
- },
43
- };
16
+ export async function writeChangedOutputFiles(outputFiles: esbuild.OutputFile[]): Promise<void> {
17
+ await Promise.all(
18
+ outputFiles.map(async (file) => {
19
+ const finalText = file.path.endsWith(".js")
20
+ ? file.text.replace(
21
+ /((?:from|import)\s*["'])(\.\.?\/[^"']*?)(["'])/g,
22
+ (_match, prefix: string, importPath: string, suffix: string) => {
23
+ if (/\.(js|mjs|cjs|json|css|wasm|node)$/i.test(importPath)) return _match;
24
+ return `${prefix}${importPath}.js${suffix}`;
25
+ },
26
+ )
27
+ : file.text;
28
+
29
+ // Compare with existing file — skip write if unchanged
30
+ try {
31
+ const existing = await fs.readFile(file.path, "utf-8");
32
+ if (existing === finalText) return;
33
+ } catch {
34
+ // File doesn't exist yet
35
+ }
36
+
37
+ await fs.mkdir(path.dirname(file.path), { recursive: true });
38
+ await fs.writeFile(file.path, finalText);
39
+ }),
40
+ );
44
41
  }
45
42
 
46
43
  /**
@@ -86,7 +83,7 @@ function hasSolidDependency(pkgDir: string): boolean {
86
83
  * - target: node면 node20, 그 외는 chrome84
87
84
  */
88
85
  export function createLibraryEsbuildOptions(options: LibraryEsbuildOptions): esbuild.BuildOptions {
89
- const plugins: esbuild.Plugin[] = [esmRelativeImportPlugin(path.join(options.pkgDir, "dist"))];
86
+ const plugins: esbuild.Plugin[] = [];
90
87
 
91
88
  if (hasSolidDependency(options.pkgDir)) {
92
89
  plugins.unshift(solidPlugin());
@@ -101,6 +98,7 @@ export function createLibraryEsbuildOptions(options: LibraryEsbuildOptions): esb
101
98
  platform: options.target === "node" ? "node" : "browser",
102
99
  target: options.target === "node" ? "node20" : "chrome84",
103
100
  bundle: false,
101
+ write: false,
104
102
  tsconfigRaw: { compilerOptions: options.compilerOptions as esbuild.TsconfigRaw["compilerOptions"] },
105
103
  plugins,
106
104
  };
@@ -10,7 +10,6 @@ type ErrorResult = PackageResult | BuildResult;
10
10
 
11
11
  /**
12
12
  * 에러만 출력한다.
13
- * 성공한 빌드는 listr의 체크마크로 이미 표시되므로 별도 출력하지 않음.
14
13
  * @param results 패키지별 빌드 결과 상태
15
14
  */
16
15
  export function printErrors(results: Map<string, ErrorResult>): void {
@@ -35,7 +34,7 @@ export function printErrors(results: Map<string, ErrorResult>): void {
35
34
  */
36
35
  export function printServers(results: Map<string, PackageResult>, serverClientsMap?: Map<string, string[]>): void {
37
36
  // 서버 정보 수집
38
- const servers = [...results.values()].filter((r) => r.status === "server" && r.port != null);
37
+ const servers = [...results.values()].filter((r) => r.status === "running" && r.port != null);
39
38
 
40
39
  // 서버 정보 출력 (있으면 앞에 빈 줄 추가)
41
40
  if (servers.length > 0) {
@@ -1,5 +1,20 @@
1
+ import path from "path";
2
+ import fs from "fs";
1
3
  import type { SdPackageConfig } from "../sd-config.types";
2
4
 
5
+ /**
6
+ * import.meta.dirname에서 상위로 올라가며 package.json을 찾아 패키지 루트를 반환한다.
7
+ */
8
+ export function findPackageRoot(startDir: string): string {
9
+ let dir = startDir;
10
+ while (!fs.existsSync(path.join(dir, "package.json"))) {
11
+ const parent = path.dirname(dir);
12
+ if (parent === dir) throw new Error("package.json을 찾을 수 없습니다.");
13
+ dir = parent;
14
+ }
15
+ return dir;
16
+ }
17
+
3
18
  /**
4
19
  * 패키지명에서 watch scope 목록을 생성한다.
5
20
  * - 패키지명의 scope (예: "@myapp/root" → "@myapp")
@@ -23,7 +38,7 @@ export interface PackageResult {
23
38
  name: string;
24
39
  target: string;
25
40
  type: "build" | "dts" | "server" | "capacitor";
26
- status: "success" | "error" | "server";
41
+ status: "success" | "error" | "running";
27
42
  message?: string;
28
43
  port?: number;
29
44
  }
@@ -0,0 +1,65 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { consola } from "consola";
3
+
4
+ interface RebuildManagerEvents {
5
+ batchComplete: [];
6
+ }
7
+
8
+ export class RebuildManager extends EventEmitter<RebuildManagerEvents> {
9
+ private _isRunning = false;
10
+ private readonly _pendingBuilds = new Map<string, { title: string; promise: Promise<void>; resolver: () => void }>();
11
+ private readonly _logger: ReturnType<typeof consola.withTag>;
12
+
13
+ constructor(logger: ReturnType<typeof consola.withTag>) {
14
+ super();
15
+ this._logger = logger;
16
+ }
17
+
18
+ registerBuild(key: string, title: string): () => void {
19
+ let resolver!: () => void;
20
+ const promise = new Promise<void>((resolve) => {
21
+ resolver = resolve;
22
+ });
23
+
24
+ this._pendingBuilds.set(key, { title, promise, resolver });
25
+
26
+ if (!this._isRunning) {
27
+ void Promise.resolve().then(() => void this._runBatch());
28
+ }
29
+
30
+ return resolver;
31
+ }
32
+
33
+ private async _runBatch(): Promise<void> {
34
+ if (this._isRunning || this._pendingBuilds.size === 0) {
35
+ return;
36
+ }
37
+
38
+ this._isRunning = true;
39
+
40
+ const batchBuilds = new Map(this._pendingBuilds);
41
+ this._pendingBuilds.clear();
42
+
43
+ const tasks = Array.from(batchBuilds.entries());
44
+ for (const [, { title }] of tasks) {
45
+ this._logger.debug(`리빌드 시작: ${title}`);
46
+ }
47
+
48
+ const results = await Promise.allSettled(tasks.map(([, { promise }]) => promise));
49
+
50
+ const failed = results.filter((r): r is PromiseRejectedResult => r.status === "rejected");
51
+ if (failed.length > 0) {
52
+ for (const result of failed) {
53
+ this._logger.error("리빌드 중 오류 발생", { error: String(result.reason) });
54
+ }
55
+ }
56
+
57
+ this.emit("batchComplete");
58
+
59
+ this._isRunning = false;
60
+
61
+ if (this._pendingBuilds.size > 0) {
62
+ void this._runBatch();
63
+ }
64
+ }
65
+ }
@@ -120,19 +120,15 @@ export interface WatchReplaceDepResult {
120
120
  }
121
121
 
122
122
  /**
123
- * replaceDeps 설정에 따라 node_modules 패키지를 소스 디렉토리로 복사 교체한다.
123
+ * 프로젝트 루트와 workspace 패키지 경로 목록을 수집한다.
124
124
  *
125
- * 1. pnpm-workspace.yaml 파싱 workspace 패키지 경로 목록
126
- * 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
127
- * 3. 기존 symlink/디렉토리 제거 → 소스 경로를 복사 (node_modules, package.json, .cache, tests 제외)
125
+ * pnpm-workspace.yaml 파싱하여 workspace 패키지들의 절대 경로를 수집한다.
126
+ * 파일이 없거나 파싱 실패 루트 경로만 반환한다.
128
127
  *
129
128
  * @param projectRoot - 프로젝트 루트 경로
130
- * @param replaceDeps - sd.config.ts의 replaceDeps 설정
129
+ * @returns [루트, ...workspace 패키지 경로] 배열
131
130
  */
132
- export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<string, string>): Promise<void> {
133
- const logger = consola.withTag("sd:cli:replace-deps");
134
-
135
- // 1. Workspace 패키지 경로 목록 수집
131
+ async function collectSearchRoots(projectRoot: string): Promise<string[]> {
136
132
  const searchRoots = [projectRoot];
137
133
 
138
134
  const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
@@ -148,6 +144,25 @@ export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<
148
144
  // pnpm-workspace.yaml가 없으면 루트만 처리
149
145
  }
150
146
 
147
+ return searchRoots;
148
+ }
149
+
150
+ /**
151
+ * replaceDeps 설정에 따라 node_modules 내 패키지를 소스 디렉토리로 복사 교체한다.
152
+ *
153
+ * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
154
+ * 2. [루트, ...workspace 패키지]의 node_modules에서 매칭되는 패키지 찾기
155
+ * 3. 기존 symlink/디렉토리 제거 → 소스 경로를 복사 (node_modules, package.json, .cache, tests 제외)
156
+ *
157
+ * @param projectRoot - 프로젝트 루트 경로
158
+ * @param replaceDeps - sd.config.ts의 replaceDeps 설정
159
+ */
160
+ export async function setupReplaceDeps(projectRoot: string, replaceDeps: Record<string, string>): Promise<void> {
161
+ const logger = consola.withTag("sd:cli:replace-deps");
162
+
163
+ // 1. Workspace 패키지 경로 목록 수집
164
+ const searchRoots = await collectSearchRoots(projectRoot);
165
+
151
166
  // 2. 각 searchRoot의 node_modules에서 매칭되는 패키지 찾기
152
167
  for (const searchRoot of searchRoots) {
153
168
  const nodeModulesDir = path.join(searchRoot, "node_modules");
@@ -226,20 +241,7 @@ export async function watchReplaceDeps(
226
241
  const entries: ReplaceDepEntry[] = [];
227
242
 
228
243
  // 1. Workspace 패키지 경로 목록 수집
229
- const searchRoots = [projectRoot];
230
-
231
- const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
232
- try {
233
- const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf-8");
234
- const workspaceGlobs = parseWorkspaceGlobs(yamlContent);
235
-
236
- for (const pattern of workspaceGlobs) {
237
- const dirs = await glob(pattern, { cwd: projectRoot, absolute: true });
238
- searchRoots.push(...dirs);
239
- }
240
- } catch {
241
- // pnpm-workspace.yaml가 없으면 루트만 처리
242
- }
244
+ const searchRoots = await collectSearchRoots(projectRoot);
243
245
 
244
246
  // 2. 각 searchRoot의 node_modules에서 매칭되는 패키지 찾기
245
247
  for (const searchRoot of searchRoots) {