@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,341 +1,341 @@
1
- import fs from "fs";
2
- import { createRequire } from "module";
3
- import path from "path";
4
- import type { Plugin, UserConfig as ViteUserConfig } from "vite";
5
- import tsconfigPaths from "vite-tsconfig-paths";
6
- import solidPlugin from "vite-plugin-solid";
7
- import { VitePWA } from "vite-plugin-pwa";
8
- import tailwindcss from "tailwindcss";
9
- import type esbuild from "esbuild";
10
- import { getTailwindConfigDeps } from "./tailwind-config-deps.js";
11
- import { FsWatcher, pathNorm } from "@simplysm/core-node";
12
-
13
- /**
14
- * Tailwind config의 scope 패키지 의존성을 watch하는 Vite 플러그인.
15
- *
16
- * Tailwind CSS 내장 의존성 추적은 상대 경로 import만 처리하므로,
17
- * preset 등으로 참조하는 scope 패키지의 config 변경을 감지하지 못한다.
18
- * 플러그인이 해당 파일들을 watch하고, 변경 Tailwind 캐시를 무효화한다.
19
- */
20
- function sdTailwindConfigDepsPlugin(pkgDir: string, replaceDeps: string[]): Plugin {
21
- return {
22
- name: "sd-tailwind-config-deps",
23
- configureServer(server) {
24
- const configPath = path.join(pkgDir, "tailwind.config.ts");
25
- if (!fs.existsSync(configPath)) return;
26
-
27
- const allDeps = getTailwindConfigDeps(configPath, replaceDeps);
28
- const configAbsolute = path.resolve(configPath);
29
- const externalDeps = allDeps.filter((d) => d !== configAbsolute);
30
- if (externalDeps.length === 0) return;
31
-
32
- for (const dep of externalDeps) {
33
- server.watcher.add(dep);
34
- }
35
-
36
- server.watcher.on("change", (changed) => {
37
- if (externalDeps.some((d) => pathNorm(d) === pathNorm(changed))) {
38
- // jiti (Tailwind config 로더)가 사용하는 require 캐시를 정리하여
39
- // config 재로드 변경된 파일이 새로 읽히도록 한다
40
- const _require = createRequire(import.meta.url);
41
- for (const dep of allDeps) {
42
- delete _require.cache[dep];
43
- }
44
-
45
- // Tailwind 캐시 무효화: config mtime 갱신하여 재로드 유도
46
- const now = new Date();
47
- fs.utimesSync(configPath, now, now);
48
- server.ws.send({ type: "full-reload" });
49
- }
50
- });
51
- },
52
- };
53
- }
54
-
55
- /**
56
- * 패키지가 subpath-only export인지 확인 (exports에 "." 없는 패키지)
57
- *
58
- * 예: @tiptap/pm "./state", "./view" subpath만 export하므로 pre-bundling 불가
59
- * pnpm 구조에서 경로를 시도:
60
- * 1. realpath 따라 .pnpm node_modules에서 찾기
61
- * 2. symlink된 workspace 패키지의 node_modules에서 fallback
62
- */
63
- function isSubpathOnlyPackage(pkgJsonPath: string): boolean {
64
- try {
65
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")) as {
66
- exports?: Record<string, unknown> | string;
67
- main?: string;
68
- module?: string;
69
- };
70
- if (
71
- pkgJson.exports != null &&
72
- typeof pkgJson.exports === "object" &&
73
- !("." in pkgJson.exports) &&
74
- pkgJson.main == null &&
75
- pkgJson.module == null
76
- ) {
77
- return true;
78
- }
79
- } catch {
80
- // 읽기 실패 false 반환 (pre-bundling 포함)
81
- }
82
- return false;
83
- }
84
-
85
- /**
86
- * public-dev/ 디렉토리의 파일을 dev 모드에서 public/보다 우선하여 서빙하는 Vite 플러그인.
87
- * Vite 기본 publicDir(public/) 그대로 유지하면서, public-dev/의 파일이 같은 경로에 있으면 우선한다.
88
- */
89
- function sdPublicDevPlugin(pkgDir: string): Plugin {
90
- const publicDevDir = path.join(pkgDir, "public-dev");
91
-
92
- return {
93
- name: "sd-public-dev",
94
- configureServer(server) {
95
- if (!fs.existsSync(publicDevDir)) return;
96
-
97
- // Vite의 기본 static 서빙보다 먼저 public-dev/ 파일을 체크
98
- server.middlewares.use((req, res, next) => {
99
- if (req.url == null) {
100
- next();
101
- return;
102
- }
103
-
104
- // base path 제거
105
- const base = server.config.base || "/";
106
- let urlPath = req.url.split("?")[0];
107
- if (urlPath.startsWith(base)) {
108
- urlPath = urlPath.slice(base.length);
109
- }
110
- if (urlPath.startsWith("/")) {
111
- urlPath = urlPath.slice(1);
112
- }
113
-
114
- // path traversal 방어: publicDevDir 범위 밖의 파일 접근 차단
115
- const decodedPath = decodeURIComponent(urlPath);
116
- const filePath = path.resolve(publicDevDir, decodedPath);
117
- const normalizedRoot = path.resolve(publicDevDir);
118
- if (!filePath.startsWith(normalizedRoot + path.sep) && filePath !== normalizedRoot) {
119
- next();
120
- return;
121
- }
122
-
123
- if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
124
- // sirv 대신 간단히 파일 스트림으로 응답
125
- const stream = fs.createReadStream(filePath);
126
- stream.pipe(res);
127
- } else {
128
- next();
129
- }
130
- });
131
- },
132
- };
133
- }
134
-
135
- /**
136
- * scope 패키지의 dist 디렉토리 변경을 감지하는 Vite 플러그인.
137
- *
138
- * Vite node_modules 기본적으로 watch에서 제외하므로,
139
- * scope 패키지의 dist 파일이 변경되어도 HMR/리빌드가 트리거되지 않는다.
140
- * 플러그인은 별도의 FsWatcher scope 패키지의 dist 디렉토리를 감시하고,
141
- * 변경 Vite 내부 HMR 파이프라인을 트리거한다.
142
- * optimizeDeps에서 제외하여 pre-bundled 캐시로 인한 변경 무시를 방지한다.
143
- */
144
- function sdScopeWatchPlugin(
145
- pkgDir: string,
146
- replaceDeps: string[],
147
- onScopeRebuild?: () => void,
148
- ): Plugin {
149
- return {
150
- name: "sd-scope-watch",
151
- config() {
152
- const excluded: string[] = [];
153
- const nestedDepsToInclude: string[] = [];
154
-
155
- for (const pkg of replaceDeps) {
156
- excluded.push(pkg);
157
-
158
- const pkgParts = pkg.split("/");
159
- const depPkgJsonPath = path.join(pkgDir, "node_modules", ...pkgParts, "package.json");
160
- try {
161
- const depPkgJson = JSON.parse(fs.readFileSync(depPkgJsonPath, "utf-8")) as {
162
- dependencies?: Record<string, string>;
163
- };
164
- for (const dep of Object.keys(depPkgJson.dependencies ?? {})) {
165
- if (replaceDeps.includes(dep)) continue;
166
- if (dep === "solid-js" || dep.startsWith("@solidjs/") || dep.startsWith("solid-"))
167
- continue;
168
- if (dep === "tailwindcss") continue;
169
-
170
- const realPkgPath = fs.realpathSync(path.join(pkgDir, "node_modules", ...pkgParts));
171
- const pnpmNodeModules = path.resolve(realPkgPath, "../..");
172
- const depPkgJsonResolved = path.join(pnpmNodeModules, dep, "package.json");
173
- if (isSubpathOnlyPackage(depPkgJsonResolved)) continue;
174
-
175
- const depPkgJsonFallback = path.join(
176
- pkgDir,
177
- "node_modules",
178
- ...pkgParts,
179
- "node_modules",
180
- dep,
181
- "package.json",
182
- );
183
- if (isSubpathOnlyPackage(depPkgJsonFallback)) continue;
184
-
185
- nestedDepsToInclude.push(`${pkg} > ${dep}`);
186
- }
187
- } catch {
188
- // package.json 읽기 실패 시 스킵
189
- }
190
- }
191
-
192
- return {
193
- optimizeDeps: {
194
- force: true,
195
- exclude: excluded,
196
- include: [...new Set(nestedDepsToInclude)],
197
- },
198
- };
199
- },
200
- async configureServer(server) {
201
- const watchPaths: string[] = [];
202
-
203
- for (const pkg of replaceDeps) {
204
- const pkgParts = pkg.split("/");
205
- const pkgRoot = path.join(pkgDir, "node_modules", ...pkgParts);
206
- if (!fs.existsSync(pkgRoot)) continue;
207
-
208
- const distDir = path.join(pkgRoot, "dist");
209
- if (fs.existsSync(distDir)) {
210
- watchPaths.push(distDir);
211
- }
212
-
213
- for (const file of fs.readdirSync(pkgRoot)) {
214
- if (
215
- file.endsWith(".css") ||
216
- file === "tailwind.config.ts" ||
217
- file === "tailwind.config.js"
218
- ) {
219
- watchPaths.push(path.join(pkgRoot, file));
220
- }
221
- }
222
- }
223
-
224
- if (watchPaths.length === 0) return;
225
-
226
- const scopeWatcher = await FsWatcher.watch(watchPaths);
227
- scopeWatcher.onChange({ delay: 300 }, (changeInfos) => {
228
- for (const { path: changedPath } of changeInfos) {
229
- let realPath: string;
230
- try {
231
- realPath = fs.realpathSync(changedPath);
232
- } catch {
233
- continue;
234
- }
235
- server.watcher.emit("change", realPath);
236
- }
237
- onScopeRebuild?.();
238
- });
239
-
240
- server.httpServer?.on("close", () => void scopeWatcher.close());
241
- },
242
- };
243
- }
244
-
245
- /**
246
- * Vite 설정 생성 옵션
247
- */
248
- export interface ViteConfigOptions {
249
- pkgDir: string;
250
- name: string;
251
- tsconfigPath: string;
252
- compilerOptions: Record<string, unknown>;
253
- env?: Record<string, string>;
254
- mode: "build" | "dev";
255
- /** dev 모드일 서버 포트 (0이면 자동 할당) */
256
- serverPort?: number;
257
- /** replaceDeps 패키지명 배열 (resolve 완료된 상태) */
258
- replaceDeps?: string[];
259
- /** replaceDeps 패키지 dist 변경 감지 시 콜백 */
260
- onScopeRebuild?: () => void;
261
- }
262
-
263
- /**
264
- * Vite 설정 생성
265
- *
266
- * SolidJS + TailwindCSS 기반의 client 패키지 빌드/개발 서버용 설정입니다.
267
- * - build 모드: production 빌드 (logLevel: silent)
268
- * - dev 모드: dev server (define으로 env 치환, server 설정)
269
- */
270
- export function createViteConfig(options: ViteConfigOptions): ViteUserConfig {
271
- const { pkgDir, name, tsconfigPath, compilerOptions, env, mode, serverPort, replaceDeps } =
272
- options;
273
-
274
- // Read package.json to extract app name for PWA manifest
275
- const pkgJsonPath = path.join(pkgDir, "package.json");
276
- const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")) as { name: string };
277
- const appName = pkgJson.name.replace(/^@[^/]+\//, "");
278
-
279
- // process.env 치환 (build/dev 모두 적용)
280
- const envDefine: Record<string, string> = {};
281
- if (env != null) {
282
- envDefine["process.env"] = JSON.stringify(env);
283
- }
284
-
285
- const config: ViteUserConfig = {
286
- root: pkgDir,
287
- base: `/${name}/`,
288
- plugins: [
289
- tsconfigPaths({ projects: [tsconfigPath] }),
290
- solidPlugin(),
291
- VitePWA({
292
- registerType: "prompt",
293
- injectRegister: "script",
294
- manifest: {
295
- name: appName,
296
- short_name: appName,
297
- display: "standalone",
298
- theme_color: "#ffffff",
299
- background_color: "#ffffff",
300
- },
301
- workbox: {
302
- globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2}"],
303
- },
304
- }),
305
- ...(replaceDeps != null && replaceDeps.length > 0
306
- ? [sdTailwindConfigDepsPlugin(pkgDir, replaceDeps)]
307
- : []),
308
- ...(replaceDeps != null && replaceDeps.length > 0
309
- ? [sdScopeWatchPlugin(pkgDir, replaceDeps, options.onScopeRebuild)]
310
- : []),
311
- ...(mode === "dev" ? [sdPublicDevPlugin(pkgDir)] : []),
312
- ],
313
- css: {
314
- postcss: {
315
- plugins: [tailwindcss({ config: path.join(pkgDir, "tailwind.config.ts") })],
316
- },
317
- },
318
- esbuild: {
319
- tsconfigRaw: { compilerOptions: compilerOptions as esbuild.TsconfigRaw["compilerOptions"] },
320
- },
321
- };
322
-
323
- // process.env 치환 (build/dev 모두 적용)
324
- config.define = envDefine;
325
-
326
- if (mode === "build") {
327
- config.logLevel = "silent";
328
- } else {
329
- // dev 모드
330
- config.server = {
331
- // serverPort === 0: 서버 연결 클라이언트 (proxy 대상)
332
- // → host 127.0.0.1 명시하여 IPv4 바인딩 보장
333
- // (Windows에서 localhost ::1(IPv6) 해석되면 proxy 127.0.0.1 연결 ECONNREFUSED 발생)
334
- host: serverPort === 0 ? "127.0.0.1" : undefined,
335
- port: serverPort === 0 ? undefined : serverPort,
336
- strictPort: serverPort !== 0 && serverPort !== undefined,
337
- };
338
- }
339
-
340
- return config;
341
- }
1
+ import fs from "fs";
2
+ import { createRequire } from "module";
3
+ import path from "path";
4
+ import type { Plugin, UserConfig as ViteUserConfig } from "vite";
5
+ import tsconfigPaths from "vite-tsconfig-paths";
6
+ import solidPlugin from "vite-plugin-solid";
7
+ import { VitePWA } from "vite-plugin-pwa";
8
+ import tailwindcss from "tailwindcss";
9
+ import type esbuild from "esbuild";
10
+ import { getTailwindConfigDeps } from "./tailwind-config-deps.js";
11
+ import { FsWatcher, pathNorm } from "@simplysm/core-node";
12
+
13
+ /**
14
+ * Vite plugin that watches scope package dependencies of a Tailwind config.
15
+ *
16
+ * Tailwind CSS's built-in dependency tracking only handles relative path imports,
17
+ * so it cannot detect config changes in scope packages referenced via presets.
18
+ * This plugin watches those files and invalidates the Tailwind cache on change.
19
+ */
20
+ function sdTailwindConfigDepsPlugin(pkgDir: string, replaceDeps: string[]): Plugin {
21
+ return {
22
+ name: "sd-tailwind-config-deps",
23
+ configureServer(server) {
24
+ const configPath = path.join(pkgDir, "tailwind.config.ts");
25
+ if (!fs.existsSync(configPath)) return;
26
+
27
+ const allDeps = getTailwindConfigDeps(configPath, replaceDeps);
28
+ const configAbsolute = path.resolve(configPath);
29
+ const externalDeps = allDeps.filter((d) => d !== configAbsolute);
30
+ if (externalDeps.length === 0) return;
31
+
32
+ for (const dep of externalDeps) {
33
+ server.watcher.add(dep);
34
+ }
35
+
36
+ server.watcher.on("change", (changed) => {
37
+ if (externalDeps.some((d) => pathNorm(d) === pathNorm(changed))) {
38
+ // Clear require cache used by jiti (Tailwind's config loader)
39
+ // so changed files are re-read on config reload
40
+ const _require = createRequire(import.meta.url);
41
+ for (const dep of allDeps) {
42
+ delete _require.cache[dep];
43
+ }
44
+
45
+ // Invalidate Tailwind cache: update config mtime to trigger reload
46
+ const now = new Date();
47
+ fs.utimesSync(configPath, now, now);
48
+ server.ws.send({ type: "full-reload" });
49
+ }
50
+ });
51
+ },
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Check if a package is subpath-only export (no "." in exports field)
57
+ *
58
+ * e.g., @tiptap/pm only exports "./state", "./view" etc., so pre-bundling is not possible.
59
+ * Tries two paths in pnpm structure:
60
+ * 1. Follow realpath to find in .pnpm node_modules
61
+ * 2. Fallback to symlinked workspace package's node_modules
62
+ */
63
+ function isSubpathOnlyPackage(pkgJsonPath: string): boolean {
64
+ try {
65
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")) as {
66
+ exports?: Record<string, unknown> | string;
67
+ main?: string;
68
+ module?: string;
69
+ };
70
+ if (
71
+ pkgJson.exports != null &&
72
+ typeof pkgJson.exports === "object" &&
73
+ !("." in pkgJson.exports) &&
74
+ pkgJson.main == null &&
75
+ pkgJson.module == null
76
+ ) {
77
+ return true;
78
+ }
79
+ } catch {
80
+ // Return false on read failure (include in pre-bundling)
81
+ }
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Vite plugin that serves files from public-dev/ directory with priority over public/ in dev mode.
87
+ * Keeps Vite's default publicDir (public/) while giving precedence to public-dev/ files at the same path.
88
+ */
89
+ function sdPublicDevPlugin(pkgDir: string): Plugin {
90
+ const publicDevDir = path.join(pkgDir, "public-dev");
91
+
92
+ return {
93
+ name: "sd-public-dev",
94
+ configureServer(server) {
95
+ if (!fs.existsSync(publicDevDir)) return;
96
+
97
+ // Check public-dev/ files before Vite's default static serving
98
+ server.middlewares.use((req, res, next) => {
99
+ if (req.url == null) {
100
+ next();
101
+ return;
102
+ }
103
+
104
+ // Strip base path
105
+ const base = server.config.base || "/";
106
+ let urlPath = req.url.split("?")[0];
107
+ if (urlPath.startsWith(base)) {
108
+ urlPath = urlPath.slice(base.length);
109
+ }
110
+ if (urlPath.startsWith("/")) {
111
+ urlPath = urlPath.slice(1);
112
+ }
113
+
114
+ // Path traversal defense: block access to files outside publicDevDir
115
+ const decodedPath = decodeURIComponent(urlPath);
116
+ const filePath = path.resolve(publicDevDir, decodedPath);
117
+ const normalizedRoot = path.resolve(publicDevDir);
118
+ if (!filePath.startsWith(normalizedRoot + path.sep) && filePath !== normalizedRoot) {
119
+ next();
120
+ return;
121
+ }
122
+
123
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
124
+ // Respond with file stream instead of sirv
125
+ const stream = fs.createReadStream(filePath);
126
+ stream.pipe(res);
127
+ } else {
128
+ next();
129
+ }
130
+ });
131
+ },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Vite plugin that detects changes in scope packages' dist directories.
137
+ *
138
+ * Vite excludes node_modules from watch by default, so dist file changes
139
+ * in scope packages do not trigger HMR/rebuild.
140
+ * This plugin uses a separate FsWatcher to monitor scope packages' dist directories
141
+ * and triggers Vite's internal HMR pipeline on change.
142
+ * Excludes from optimizeDeps to prevent changes being ignored due to pre-bundled cache.
143
+ */
144
+ function sdScopeWatchPlugin(
145
+ pkgDir: string,
146
+ replaceDeps: string[],
147
+ onScopeRebuild?: () => void,
148
+ ): Plugin {
149
+ return {
150
+ name: "sd-scope-watch",
151
+ config() {
152
+ const excluded: string[] = [];
153
+ const nestedDepsToInclude: string[] = [];
154
+
155
+ for (const pkg of replaceDeps) {
156
+ excluded.push(pkg);
157
+
158
+ const pkgParts = pkg.split("/");
159
+ const depPkgJsonPath = path.join(pkgDir, "node_modules", ...pkgParts, "package.json");
160
+ try {
161
+ const depPkgJson = JSON.parse(fs.readFileSync(depPkgJsonPath, "utf-8")) as {
162
+ dependencies?: Record<string, string>;
163
+ };
164
+ for (const dep of Object.keys(depPkgJson.dependencies ?? {})) {
165
+ if (replaceDeps.includes(dep)) continue;
166
+ if (dep === "solid-js" || dep.startsWith("@solidjs/") || dep.startsWith("solid-"))
167
+ continue;
168
+ if (dep === "tailwindcss") continue;
169
+
170
+ const realPkgPath = fs.realpathSync(path.join(pkgDir, "node_modules", ...pkgParts));
171
+ const pnpmNodeModules = path.resolve(realPkgPath, "../..");
172
+ const depPkgJsonResolved = path.join(pnpmNodeModules, dep, "package.json");
173
+ if (isSubpathOnlyPackage(depPkgJsonResolved)) continue;
174
+
175
+ const depPkgJsonFallback = path.join(
176
+ pkgDir,
177
+ "node_modules",
178
+ ...pkgParts,
179
+ "node_modules",
180
+ dep,
181
+ "package.json",
182
+ );
183
+ if (isSubpathOnlyPackage(depPkgJsonFallback)) continue;
184
+
185
+ nestedDepsToInclude.push(`${pkg} > ${dep}`);
186
+ }
187
+ } catch {
188
+ // Skip on package.json read failure
189
+ }
190
+ }
191
+
192
+ return {
193
+ optimizeDeps: {
194
+ force: true,
195
+ exclude: excluded,
196
+ include: [...new Set(nestedDepsToInclude)],
197
+ },
198
+ };
199
+ },
200
+ async configureServer(server) {
201
+ const watchPaths: string[] = [];
202
+
203
+ for (const pkg of replaceDeps) {
204
+ const pkgParts = pkg.split("/");
205
+ const pkgRoot = path.join(pkgDir, "node_modules", ...pkgParts);
206
+ if (!fs.existsSync(pkgRoot)) continue;
207
+
208
+ const distDir = path.join(pkgRoot, "dist");
209
+ if (fs.existsSync(distDir)) {
210
+ watchPaths.push(distDir);
211
+ }
212
+
213
+ for (const file of fs.readdirSync(pkgRoot)) {
214
+ if (
215
+ file.endsWith(".css") ||
216
+ file === "tailwind.config.ts" ||
217
+ file === "tailwind.config.js"
218
+ ) {
219
+ watchPaths.push(path.join(pkgRoot, file));
220
+ }
221
+ }
222
+ }
223
+
224
+ if (watchPaths.length === 0) return;
225
+
226
+ const scopeWatcher = await FsWatcher.watch(watchPaths);
227
+ scopeWatcher.onChange({ delay: 300 }, (changeInfos) => {
228
+ for (const { path: changedPath } of changeInfos) {
229
+ let realPath: string;
230
+ try {
231
+ realPath = fs.realpathSync(changedPath);
232
+ } catch {
233
+ continue;
234
+ }
235
+ server.watcher.emit("change", realPath);
236
+ }
237
+ onScopeRebuild?.();
238
+ });
239
+
240
+ server.httpServer?.on("close", () => void scopeWatcher.close());
241
+ },
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Vite config generation options
247
+ */
248
+ export interface ViteConfigOptions {
249
+ pkgDir: string;
250
+ name: string;
251
+ tsconfigPath: string;
252
+ compilerOptions: Record<string, unknown>;
253
+ env?: Record<string, string>;
254
+ mode: "build" | "dev";
255
+ /** Server port in dev mode (0 for auto-assign) */
256
+ serverPort?: number;
257
+ /** Array of replaceDeps package names (resolved state) */
258
+ replaceDeps?: string[];
259
+ /** Callback when replaceDeps package dist changes */
260
+ onScopeRebuild?: () => void;
261
+ }
262
+
263
+ /**
264
+ * Create Vite config
265
+ *
266
+ * Configuration for building/dev server for client packages based on SolidJS + TailwindCSS.
267
+ * - build mode: production build (logLevel: silent)
268
+ * - dev mode: dev server (env substitution via define, server configuration)
269
+ */
270
+ export function createViteConfig(options: ViteConfigOptions): ViteUserConfig {
271
+ const { pkgDir, name, tsconfigPath, compilerOptions, env, mode, serverPort, replaceDeps } =
272
+ options;
273
+
274
+ // Read package.json to extract app name for PWA manifest
275
+ const pkgJsonPath = path.join(pkgDir, "package.json");
276
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")) as { name: string };
277
+ const appName = pkgJson.name.replace(/^@[^/]+\//, "");
278
+
279
+ // Process.env substitution (applied to both build and dev modes)
280
+ const envDefine: Record<string, string> = {};
281
+ if (env != null) {
282
+ envDefine["process.env"] = JSON.stringify(env);
283
+ }
284
+
285
+ const config: ViteUserConfig = {
286
+ root: pkgDir,
287
+ base: `/${name}/`,
288
+ plugins: [
289
+ tsconfigPaths({ projects: [tsconfigPath] }),
290
+ solidPlugin(),
291
+ VitePWA({
292
+ registerType: "prompt",
293
+ injectRegister: "script",
294
+ manifest: {
295
+ name: appName,
296
+ short_name: appName,
297
+ display: "standalone",
298
+ theme_color: "#ffffff",
299
+ background_color: "#ffffff",
300
+ },
301
+ workbox: {
302
+ globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2}"],
303
+ },
304
+ }),
305
+ ...(replaceDeps != null && replaceDeps.length > 0
306
+ ? [sdTailwindConfigDepsPlugin(pkgDir, replaceDeps)]
307
+ : []),
308
+ ...(replaceDeps != null && replaceDeps.length > 0
309
+ ? [sdScopeWatchPlugin(pkgDir, replaceDeps, options.onScopeRebuild)]
310
+ : []),
311
+ ...(mode === "dev" ? [sdPublicDevPlugin(pkgDir)] : []),
312
+ ],
313
+ css: {
314
+ postcss: {
315
+ plugins: [tailwindcss({ config: path.join(pkgDir, "tailwind.config.ts") })],
316
+ },
317
+ },
318
+ esbuild: {
319
+ tsconfigRaw: { compilerOptions: compilerOptions as esbuild.TsconfigRaw["compilerOptions"] },
320
+ },
321
+ };
322
+
323
+ // Process.env substitution (applied to both build and dev modes)
324
+ config.define = envDefine;
325
+
326
+ if (mode === "build") {
327
+ config.logLevel = "silent";
328
+ } else {
329
+ // Dev mode
330
+ config.server = {
331
+ // serverPort === 0: server-connected client (proxy target)
332
+ // → explicitly set host to 127.0.0.1 to ensure IPv4 binding
333
+ // (On Windows, if localhost resolves to ::1 (IPv6), proxy connection to 127.0.0.1 causes ECONNREFUSED)
334
+ host: serverPort === 0 ? "127.0.0.1" : undefined,
335
+ port: serverPort === 0 ? undefined : serverPort,
336
+ strictPort: serverPort !== 0 && serverPort !== undefined,
337
+ };
338
+ }
339
+
340
+ return config;
341
+ }