@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,361 +1,361 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import { glob } from "glob";
4
- import { consola } from "consola";
5
- import { fsCopy, fsMkdir, fsRm, FsWatcher, pathIsChildPath } from "@simplysm/core-node";
6
-
7
- /**
8
- * replaceDeps 설정의 glob 패턴과 대상 패키지 목록을 매칭하여
9
- * { targetName, sourcePath } 쌍을 반환한다.
10
- *
11
- * @param replaceDeps - sd.config.ts replaceDeps 설정 (키: glob 패턴, 값: 소스 경로)
12
- * @param targetNames - node_modules에서 찾은 패키지 이름 목록 (예: ["@simplysm/solid", ...])
13
- * @returns 매칭된 { targetName, sourcePath } 배열
14
- */
15
- export function resolveReplaceDepEntries(
16
- replaceDeps: Record<string, string>,
17
- targetNames: string[],
18
- ): Array<{ targetName: string; sourcePath: string }> {
19
- const results: Array<{ targetName: string; sourcePath: string }> = [];
20
-
21
- for (const [pattern, sourceTemplate] of Object.entries(replaceDeps)) {
22
- // glob 패턴을 정규식으로 변환: * → (.*), . → \., / → [\\/]
23
- const regexpText = pattern.replace(/[\\/.+*]/g, (ch) => {
24
- if (ch === "*") return "(.*)";
25
- if (ch === ".") return "\\.";
26
- if (ch === "/" || ch === "\\") return "[\\\\/]";
27
- if (ch === "+") return "\\+";
28
- return ch;
29
- });
30
- const regex = new RegExp(`^${regexpText}$`);
31
- const hasWildcard = pattern.includes("*");
32
-
33
- for (const targetName of targetNames) {
34
- const match = regex.exec(targetName);
35
- if (match == null) continue;
36
-
37
- // 캡처 그룹이 있으면 소스 경로의 *에 치환
38
- const sourcePath = hasWildcard ? sourceTemplate.replace(/\*/g, match[1]) : sourceTemplate;
39
-
40
- results.push({ targetName, sourcePath });
41
- }
42
- }
43
-
44
- return results;
45
- }
46
-
47
- /**
48
- * pnpm-workspace.yaml 내용을 파싱하여 workspace packages glob 배열을 반환한다.
49
- * 별도 YAML 라이브러리 없이 간단한 라인 파싱으로 처리한다.
50
- *
51
- * @param content - pnpm-workspace.yaml 파일 내용
52
- * @returns glob 패턴 배열 (예: ["packages/*", "tools/*"])
53
- */
54
- export function parseWorkspaceGlobs(content: string): string[] {
55
- const lines = content.split("\n");
56
- const globs: string[] = [];
57
- let inPackages = false;
58
-
59
- for (const line of lines) {
60
- const trimmed = line.trim();
61
-
62
- if (trimmed === "packages:") {
63
- inPackages = true;
64
- continue;
65
- }
66
-
67
- // packages 섹션 내의 리스트 항목
68
- if (inPackages && trimmed.startsWith("- ")) {
69
- const value = trimmed
70
- .slice(2)
71
- .trim()
72
- .replace(/^["']|["']$/g, "");
73
- globs.push(value);
74
- continue;
75
- }
76
-
77
- // 다른 섹션이 시작되면 종료
78
- if (inPackages && trimmed !== "" && !trimmed.startsWith("#")) {
79
- break;
80
- }
81
- }
82
-
83
- return globs;
84
- }
85
-
86
- /**
87
- * 복사 제외할 항목 이름들
88
- */
89
- const EXCLUDED_NAMES = new Set(["node_modules", "package.json", ".cache", "tests"]);
90
-
91
- /**
92
- * replaceDeps 복사 사용할 필터 함수
93
- * node_modules, package.json, .cache, tests를 제외한다.
94
- *
95
- * @param itemPath - 복사할 항목의 절대 경로
96
- * @returns 복사 대상이면 true, 제외하면 false
97
- */
98
- function replaceDepsCopyFilter(itemPath: string): boolean {
99
- const basename = path.basename(itemPath);
100
- return !EXCLUDED_NAMES.has(basename);
101
- }
102
-
103
- /**
104
- * replaceDeps 복사 교체 항목
105
- */
106
- export interface ReplaceDepEntry {
107
- targetName: string;
108
- sourcePath: string;
109
- targetPath: string;
110
- resolvedSourcePath: string;
111
- actualTargetPath: string;
112
- }
113
-
114
- /**
115
- * watchReplaceDeps 반환 타입
116
- */
117
- export interface WatchReplaceDepResult {
118
- entries: ReplaceDepEntry[];
119
- dispose: () => void;
120
- }
121
-
122
- /**
123
- * 프로젝트 루트와 workspace 패키지 경로 목록을 수집한다.
124
- *
125
- * pnpm-workspace.yaml 파싱하여 workspace 패키지들의 절대 경로를 수집한다.
126
- * 파일이 없거나 파싱 실패 루트 경로만 반환한다.
127
- *
128
- * @param projectRoot - 프로젝트 루트 경로
129
- * @returns [루트, ...workspace 패키지 경로] 배열
130
- */
131
- async function collectSearchRoots(projectRoot: string): Promise<string[]> {
132
- const searchRoots = [projectRoot];
133
-
134
- const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
135
- try {
136
- const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf-8");
137
- const workspaceGlobs = parseWorkspaceGlobs(yamlContent);
138
-
139
- for (const pattern of workspaceGlobs) {
140
- const dirs = await glob(pattern, { cwd: projectRoot, absolute: true });
141
- searchRoots.push(...dirs);
142
- }
143
- } catch {
144
- // pnpm-workspace.yaml 없으면 루트만 처리
145
- }
146
-
147
- return searchRoots;
148
- }
149
-
150
- /**
151
- * replaceDeps 설정에서 모든 교체 대상 항목을 해석한다.
152
- *
153
- * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
154
- * 2. [루트, ...workspace 패키지] node_modules에서 매칭되는 패키지 찾기
155
- * 3. 패턴 매칭 + 소스 경로 존재 확인 + symlink 해석
156
- *
157
- * @param projectRoot - 프로젝트 루트 경로
158
- * @param replaceDeps - sd.config.ts의 replaceDeps 설정
159
- * @param logger - consola 로거
160
- * @returns 해석된 교체 대상 항목 배열
161
- */
162
- async function resolveAllReplaceDepEntries(
163
- projectRoot: string,
164
- replaceDeps: Record<string, string>,
165
- logger: ReturnType<typeof consola.withTag>,
166
- ): Promise<ReplaceDepEntry[]> {
167
- const entries: ReplaceDepEntry[] = [];
168
-
169
- const searchRoots = await collectSearchRoots(projectRoot);
170
-
171
- for (const searchRoot of searchRoots) {
172
- const nodeModulesDir = path.join(searchRoot, "node_modules");
173
-
174
- try {
175
- await fs.promises.access(nodeModulesDir);
176
- } catch {
177
- continue; // node_modules 없으면 스킵
178
- }
179
-
180
- // replaceDeps의 glob 패턴으로 node_modules 디렉토리 탐색
181
- const targetNames: string[] = [];
182
- for (const pattern of Object.keys(replaceDeps)) {
183
- const matches = await glob(pattern, { cwd: nodeModulesDir });
184
- targetNames.push(...matches);
185
- }
186
-
187
- if (targetNames.length === 0) continue;
188
-
189
- // 패턴 매칭 경로 해석
190
- const matchedEntries = resolveReplaceDepEntries(replaceDeps, targetNames);
191
-
192
- for (const { targetName, sourcePath } of matchedEntries) {
193
- const targetPath = path.join(nodeModulesDir, targetName);
194
- const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
195
-
196
- // 소스 경로 존재 확인
197
- try {
198
- await fs.promises.access(resolvedSourcePath);
199
- } catch {
200
- logger.warn(`소스 경로가 존재하지 않아 스킵합니다: ${resolvedSourcePath}`);
201
- continue;
202
- }
203
-
204
- // targetPath symlink realpath로 해석하여 실제 .pnpm 스토어 경로 얻기
205
- let actualTargetPath = targetPath;
206
- try {
207
- const stat = await fs.promises.lstat(targetPath);
208
- if (stat.isSymbolicLink()) {
209
- actualTargetPath = await fs.promises.realpath(targetPath);
210
- }
211
- } catch {
212
- // targetPath 존재하지 않으면 그대로 사용
213
- }
214
-
215
- entries.push({
216
- targetName,
217
- sourcePath,
218
- targetPath,
219
- resolvedSourcePath,
220
- actualTargetPath,
221
- });
222
- }
223
- }
224
-
225
- return entries;
226
- }
227
-
228
- /**
229
- * replaceDeps 설정에 따라 node_modules 패키지를 소스 디렉토리로 복사 교체한다.
230
- *
231
- * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
232
- * 2. [루트, ...workspace 패키지] node_modules에서 매칭되는 패키지 찾기
233
- * 3. 기존 symlink/디렉토리 제거소스 경로를 복사 (node_modules, package.json, .cache, tests 제외)
234
- *
235
- * @param projectRoot - 프로젝트 루트 경로
236
- * @param replaceDeps - sd.config.ts의 replaceDeps 설정
237
- */
238
- export async function setupReplaceDeps(
239
- projectRoot: string,
240
- replaceDeps: Record<string, string>,
241
- ): Promise<void> {
242
- const logger = consola.withTag("sd:cli:replace-deps");
243
- let setupCount = 0;
244
-
245
- logger.start("Setting up replace-deps");
246
-
247
- const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
248
-
249
- for (const { targetName, resolvedSourcePath, actualTargetPath } of entries) {
250
- try {
251
- // 소스 파일을 actualTargetPath에 덮어쓰기 복사 (기존 디렉토리 유지, symlink 보존)
252
- await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
253
-
254
- setupCount += 1;
255
- } catch (err) {
256
- logger.error(`복사 교체 실패 (${targetName}): ${err instanceof Error ? err.message : err}`);
257
- }
258
- }
259
-
260
- logger.success(`Replaced ${setupCount} dependencies`);
261
- }
262
-
263
- /**
264
- * replaceDeps 설정에 따라 소스 디렉토리를 watch하여 변경 대상 경로로 복사한다.
265
- *
266
- * 1. pnpm-workspace.yaml 파싱 → workspace 패키지 경로 목록
267
- * 2. [루트, ...workspace 패키지] node_modules에서 매칭되는 패키지 찾기
268
- * 3. 소스 디렉토리를 FsWatcher watch (300ms delay)
269
- * 4. 변경 대상 경로로 복사 (node_modules, package.json, .cache, tests 제외)
270
- *
271
- * @param projectRoot - 프로젝트 루트 경로
272
- * @param replaceDeps - sd.config.ts의 replaceDeps 설정
273
- * @returns entries dispose 함수
274
- */
275
- export async function watchReplaceDeps(
276
- projectRoot: string,
277
- replaceDeps: Record<string, string>,
278
- ): Promise<WatchReplaceDepResult> {
279
- const logger = consola.withTag("sd:cli:replace-deps:watch");
280
-
281
- const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
282
-
283
- // 소스 디렉토리 watch 설정
284
- const watchers: FsWatcher[] = [];
285
- const watchedSources = new Set<string>();
286
-
287
- logger.start(`Watching ${entries.length} replace-deps target(s)`);
288
-
289
- for (const entry of entries) {
290
- if (watchedSources.has(entry.resolvedSourcePath)) continue;
291
- watchedSources.add(entry.resolvedSourcePath);
292
-
293
- const excludedPaths = [...EXCLUDED_NAMES].map((name) =>
294
- path.join(entry.resolvedSourcePath, name),
295
- );
296
-
297
- const watcher = await FsWatcher.watch([entry.resolvedSourcePath], { followSymlinks: false });
298
- watcher.onChange({ delay: 300 }, async (changeInfos) => {
299
- for (const { path: changedPath } of changeInfos) {
300
- // 제외 항목 필터링: basename 일치 또는 제외 디렉토리 하위 경로
301
- if (
302
- EXCLUDED_NAMES.has(path.basename(changedPath)) ||
303
- excludedPaths.some((ep) => pathIsChildPath(changedPath, ep))
304
- ) {
305
- continue;
306
- }
307
-
308
- // 소스 경로를 사용하는 모든 entry에 대해 복사
309
- for (const e of entries) {
310
- if (e.resolvedSourcePath !== entry.resolvedSourcePath) continue;
311
-
312
- // 소스 경로 기준 상대 경로 계산
313
- const relativePath = path.relative(e.resolvedSourcePath, changedPath);
314
- const destPath = path.join(e.actualTargetPath, relativePath);
315
-
316
- try {
317
- // 소스가 존재하는지 확인
318
- let sourceExists = false;
319
- try {
320
- await fs.promises.access(changedPath);
321
- sourceExists = true;
322
- } catch {
323
- // 소스가 삭제됨
324
- }
325
-
326
- if (sourceExists) {
327
- // 소스가 디렉토리인지 파일인지 확인
328
- const stat = await fs.promises.stat(changedPath);
329
- if (stat.isDirectory()) {
330
- await fsMkdir(destPath);
331
- } else {
332
- await fsMkdir(path.dirname(destPath));
333
- await fsCopy(changedPath, destPath, replaceDepsCopyFilter);
334
- }
335
- } else {
336
- // 소스가 삭제됨대상도 삭제
337
- await fsRm(destPath);
338
- }
339
- } catch (err) {
340
- logger.error(
341
- `복사 실패 (${e.targetName}/${relativePath}): ${err instanceof Error ? err.message : err}`,
342
- );
343
- }
344
- }
345
- }
346
- });
347
-
348
- watchers.push(watcher);
349
- }
350
-
351
- logger.success(`Replace-deps watch ready`);
352
-
353
- return {
354
- entries,
355
- dispose: () => {
356
- for (const watcher of watchers) {
357
- void watcher.close();
358
- }
359
- },
360
- };
361
- }
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { glob } from "glob";
4
+ import { consola } from "consola";
5
+ import { fsCopy, fsMkdir, fsRm, FsWatcher, pathIsChildPath } from "@simplysm/core-node";
6
+
7
+ /**
8
+ * Match glob patterns from replaceDeps config with target package list
9
+ * and return { targetName, sourcePath } pairs
10
+ *
11
+ * @param replaceDeps - replaceDeps config from sd.config.ts (key: glob pattern, value: source path)
12
+ * @param targetNames - List of package names found in node_modules (e.g., ["@simplysm/solid", ...])
13
+ * @returns Array of matched { targetName, sourcePath }
14
+ */
15
+ export function resolveReplaceDepEntries(
16
+ replaceDeps: Record<string, string>,
17
+ targetNames: string[],
18
+ ): Array<{ targetName: string; sourcePath: string }> {
19
+ const results: Array<{ targetName: string; sourcePath: string }> = [];
20
+
21
+ for (const [pattern, sourceTemplate] of Object.entries(replaceDeps)) {
22
+ // Convert glob pattern to regex: * → (.*), . → \., / → [\\/]
23
+ const regexpText = pattern.replace(/[\\/.+*]/g, (ch) => {
24
+ if (ch === "*") return "(.*)";
25
+ if (ch === ".") return "\\.";
26
+ if (ch === "/" || ch === "\\") return "[\\\\/]";
27
+ if (ch === "+") return "\\+";
28
+ return ch;
29
+ });
30
+ const regex = new RegExp(`^${regexpText}$`);
31
+ const hasWildcard = pattern.includes("*");
32
+
33
+ for (const targetName of targetNames) {
34
+ const match = regex.exec(targetName);
35
+ if (match == null) continue;
36
+
37
+ // If capture group exists, substitute * in source path with captured value
38
+ const sourcePath = hasWildcard ? sourceTemplate.replace(/\*/g, match[1]) : sourceTemplate;
39
+
40
+ results.push({ targetName, sourcePath });
41
+ }
42
+ }
43
+
44
+ return results;
45
+ }
46
+
47
+ /**
48
+ * Parse pnpm-workspace.yaml content and return array of workspace packages globs
49
+ * Simple line parsing without separate YAML library
50
+ *
51
+ * @param content - Content of pnpm-workspace.yaml file
52
+ * @returns Array of glob patterns (e.g., ["packages/*", "tools/*"])
53
+ */
54
+ export function parseWorkspaceGlobs(content: string): string[] {
55
+ const lines = content.split("\n");
56
+ const globs: string[] = [];
57
+ let inPackages = false;
58
+
59
+ for (const line of lines) {
60
+ const trimmed = line.trim();
61
+
62
+ if (trimmed === "packages:") {
63
+ inPackages = true;
64
+ continue;
65
+ }
66
+
67
+ // List items in packages section
68
+ if (inPackages && trimmed.startsWith("- ")) {
69
+ const value = trimmed
70
+ .slice(2)
71
+ .trim()
72
+ .replace(/^["']|["']$/g, "");
73
+ globs.push(value);
74
+ continue;
75
+ }
76
+
77
+ // End when other section starts
78
+ if (inPackages && trimmed !== "" && !trimmed.startsWith("#")) {
79
+ break;
80
+ }
81
+ }
82
+
83
+ return globs;
84
+ }
85
+
86
+ /**
87
+ * Names to exclude during copy
88
+ */
89
+ const EXCLUDED_NAMES = new Set(["node_modules", "package.json", ".cache", "tests"]);
90
+
91
+ /**
92
+ * Filter function for replaceDeps copy
93
+ * Excludes node_modules, package.json, .cache, tests
94
+ *
95
+ * @param itemPath - Absolute path of item to copy
96
+ * @returns true if copy target, false if excluded
97
+ */
98
+ function replaceDepsCopyFilter(itemPath: string): boolean {
99
+ const basename = path.basename(itemPath);
100
+ return !EXCLUDED_NAMES.has(basename);
101
+ }
102
+
103
+ /**
104
+ * replaceDeps copy/replace item
105
+ */
106
+ export interface ReplaceDepEntry {
107
+ targetName: string;
108
+ sourcePath: string;
109
+ targetPath: string;
110
+ resolvedSourcePath: string;
111
+ actualTargetPath: string;
112
+ }
113
+
114
+ /**
115
+ * Return type of watchReplaceDeps
116
+ */
117
+ export interface WatchReplaceDepResult {
118
+ entries: ReplaceDepEntry[];
119
+ dispose: () => void;
120
+ }
121
+
122
+ /**
123
+ * Collect project root and workspace package paths.
124
+ *
125
+ * Parse pnpm-workspace.yaml to collect absolute paths of workspace packages.
126
+ * If file is missing or parsing fails, return only root path.
127
+ *
128
+ * @param projectRoot - Project root path
129
+ * @returns [root, ...workspace package paths] array
130
+ */
131
+ async function collectSearchRoots(projectRoot: string): Promise<string[]> {
132
+ const searchRoots = [projectRoot];
133
+
134
+ const workspaceYamlPath = path.join(projectRoot, "pnpm-workspace.yaml");
135
+ try {
136
+ const yamlContent = await fs.promises.readFile(workspaceYamlPath, "utf-8");
137
+ const workspaceGlobs = parseWorkspaceGlobs(yamlContent);
138
+
139
+ for (const pattern of workspaceGlobs) {
140
+ const dirs = await glob(pattern, { cwd: projectRoot, absolute: true });
141
+ searchRoots.push(...dirs);
142
+ }
143
+ } catch {
144
+ // If pnpm-workspace.yaml doesn't exist, only process root
145
+ }
146
+
147
+ return searchRoots;
148
+ }
149
+
150
+ /**
151
+ * Resolve all replacement target items from replaceDeps config.
152
+ *
153
+ * 1. Parse pnpm-workspace.yaml → workspace package paths
154
+ * 2. Find matching packages in [root, ...workspace packages] node_modules
155
+ * 3. Pattern matching + verify source path exists + resolve symlinks
156
+ *
157
+ * @param projectRoot - Project root path
158
+ * @param replaceDeps - replaceDeps config from sd.config.ts
159
+ * @param logger - consola logger
160
+ * @returns Array of resolved replacement target items
161
+ */
162
+ async function resolveAllReplaceDepEntries(
163
+ projectRoot: string,
164
+ replaceDeps: Record<string, string>,
165
+ logger: ReturnType<typeof consola.withTag>,
166
+ ): Promise<ReplaceDepEntry[]> {
167
+ const entries: ReplaceDepEntry[] = [];
168
+
169
+ const searchRoots = await collectSearchRoots(projectRoot);
170
+
171
+ for (const searchRoot of searchRoots) {
172
+ const nodeModulesDir = path.join(searchRoot, "node_modules");
173
+
174
+ try {
175
+ await fs.promises.access(nodeModulesDir);
176
+ } catch {
177
+ continue; // Skip if node_modules doesn't exist
178
+ }
179
+
180
+ // Search node_modules directories using each glob pattern from replaceDeps
181
+ const targetNames: string[] = [];
182
+ for (const pattern of Object.keys(replaceDeps)) {
183
+ const matches = await glob(pattern, { cwd: nodeModulesDir });
184
+ targetNames.push(...matches);
185
+ }
186
+
187
+ if (targetNames.length === 0) continue;
188
+
189
+ // Pattern matching and path resolution
190
+ const matchedEntries = resolveReplaceDepEntries(replaceDeps, targetNames);
191
+
192
+ for (const { targetName, sourcePath } of matchedEntries) {
193
+ const targetPath = path.join(nodeModulesDir, targetName);
194
+ const resolvedSourcePath = path.resolve(projectRoot, sourcePath);
195
+
196
+ // Verify source path exists
197
+ try {
198
+ await fs.promises.access(resolvedSourcePath);
199
+ } catch {
200
+ logger.warn(`Source path does not exist, skipping: ${resolvedSourcePath}`);
201
+ continue;
202
+ }
203
+
204
+ // If targetPath is symlink, resolve to get actual .pnpm store path
205
+ let actualTargetPath = targetPath;
206
+ try {
207
+ const stat = await fs.promises.lstat(targetPath);
208
+ if (stat.isSymbolicLink()) {
209
+ actualTargetPath = await fs.promises.realpath(targetPath);
210
+ }
211
+ } catch {
212
+ // If targetPath doesn't exist, use as-is
213
+ }
214
+
215
+ entries.push({
216
+ targetName,
217
+ sourcePath,
218
+ targetPath,
219
+ resolvedSourcePath,
220
+ actualTargetPath,
221
+ });
222
+ }
223
+ }
224
+
225
+ return entries;
226
+ }
227
+
228
+ /**
229
+ * Replace packages in node_modules with source directories according to replaceDeps config.
230
+ *
231
+ * 1. Parse pnpm-workspace.yaml → workspace package paths
232
+ * 2. Find matching packages in [root, ...workspace packages] node_modules
233
+ * 3. Remove existing symlinks/directoriescopy source path (excluding node_modules, package.json, .cache, tests)
234
+ *
235
+ * @param projectRoot - Project root path
236
+ * @param replaceDeps - replaceDeps config from sd.config.ts
237
+ */
238
+ export async function setupReplaceDeps(
239
+ projectRoot: string,
240
+ replaceDeps: Record<string, string>,
241
+ ): Promise<void> {
242
+ const logger = consola.withTag("sd:cli:replace-deps");
243
+ let setupCount = 0;
244
+
245
+ logger.start("Setting up replace-deps");
246
+
247
+ const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
248
+
249
+ for (const { targetName, resolvedSourcePath, actualTargetPath } of entries) {
250
+ try {
251
+ // Overwrite-copy source files to actualTargetPath (maintain existing directory, preserve symlinks)
252
+ await fsCopy(resolvedSourcePath, actualTargetPath, replaceDepsCopyFilter);
253
+
254
+ setupCount += 1;
255
+ } catch (err) {
256
+ logger.error(`Copy replace failed (${targetName}): ${err instanceof Error ? err.message : err}`);
257
+ }
258
+ }
259
+
260
+ logger.success(`Replaced ${setupCount} dependencies`);
261
+ }
262
+
263
+ /**
264
+ * Watch source directories according to replaceDeps config and copy changes to target paths.
265
+ *
266
+ * 1. Parse pnpm-workspace.yaml → workspace package paths
267
+ * 2. Find matching packages in [root, ...workspace packages] node_modules
268
+ * 3. Watch source directories with FsWatcher (300ms delay)
269
+ * 4. Copy changes to target paths (excluding node_modules, package.json, .cache, tests)
270
+ *
271
+ * @param projectRoot - Project root path
272
+ * @param replaceDeps - replaceDeps config from sd.config.ts
273
+ * @returns entries and dispose function
274
+ */
275
+ export async function watchReplaceDeps(
276
+ projectRoot: string,
277
+ replaceDeps: Record<string, string>,
278
+ ): Promise<WatchReplaceDepResult> {
279
+ const logger = consola.withTag("sd:cli:replace-deps:watch");
280
+
281
+ const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
282
+
283
+ // Setup source directory watchers
284
+ const watchers: FsWatcher[] = [];
285
+ const watchedSources = new Set<string>();
286
+
287
+ logger.start(`Watching ${entries.length} replace-deps target(s)`);
288
+
289
+ for (const entry of entries) {
290
+ if (watchedSources.has(entry.resolvedSourcePath)) continue;
291
+ watchedSources.add(entry.resolvedSourcePath);
292
+
293
+ const excludedPaths = [...EXCLUDED_NAMES].map((name) =>
294
+ path.join(entry.resolvedSourcePath, name),
295
+ );
296
+
297
+ const watcher = await FsWatcher.watch([entry.resolvedSourcePath], { followSymlinks: false });
298
+ watcher.onChange({ delay: 300 }, async (changeInfos) => {
299
+ for (const { path: changedPath } of changeInfos) {
300
+ // Filter excluded items: basename match or path within excluded directory
301
+ if (
302
+ EXCLUDED_NAMES.has(path.basename(changedPath)) ||
303
+ excludedPaths.some((ep) => pathIsChildPath(changedPath, ep))
304
+ ) {
305
+ continue;
306
+ }
307
+
308
+ // Copy for all entries using this source path
309
+ for (const e of entries) {
310
+ if (e.resolvedSourcePath !== entry.resolvedSourcePath) continue;
311
+
312
+ // Calculate relative path from source
313
+ const relativePath = path.relative(e.resolvedSourcePath, changedPath);
314
+ const destPath = path.join(e.actualTargetPath, relativePath);
315
+
316
+ try {
317
+ // Check if source exists
318
+ let sourceExists = false;
319
+ try {
320
+ await fs.promises.access(changedPath);
321
+ sourceExists = true;
322
+ } catch {
323
+ // Source was deleted
324
+ }
325
+
326
+ if (sourceExists) {
327
+ // Check if source is directory or file
328
+ const stat = await fs.promises.stat(changedPath);
329
+ if (stat.isDirectory()) {
330
+ await fsMkdir(destPath);
331
+ } else {
332
+ await fsMkdir(path.dirname(destPath));
333
+ await fsCopy(changedPath, destPath, replaceDepsCopyFilter);
334
+ }
335
+ } else {
336
+ // Source was deleted delete target
337
+ await fsRm(destPath);
338
+ }
339
+ } catch (err) {
340
+ logger.error(
341
+ `Copy failed (${e.targetName}/${relativePath}): ${err instanceof Error ? err.message : err}`,
342
+ );
343
+ }
344
+ }
345
+ }
346
+ });
347
+
348
+ watchers.push(watcher);
349
+ }
350
+
351
+ logger.success(`Replace-deps watch ready`);
352
+
353
+ return {
354
+ entries,
355
+ dispose: () => {
356
+ for (const watcher of watchers) {
357
+ void watcher.close();
358
+ }
359
+ },
360
+ };
361
+ }