@simplysm/sd-cli 13.0.75 → 13.0.77

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +341 -16
  2. package/dist/builders/DtsBuilder.js +2 -2
  3. package/dist/builders/DtsBuilder.js.map +1 -1
  4. package/dist/builders/LibraryBuilder.d.ts +3 -3
  5. package/dist/builders/LibraryBuilder.d.ts.map +1 -1
  6. package/dist/builders/LibraryBuilder.js +2 -2
  7. package/dist/builders/LibraryBuilder.js.map +1 -1
  8. package/dist/builders/types.d.ts +7 -1
  9. package/dist/builders/types.d.ts.map +1 -1
  10. package/dist/capacitor/capacitor.d.ts +5 -0
  11. package/dist/capacitor/capacitor.d.ts.map +1 -1
  12. package/dist/capacitor/capacitor.js +59 -59
  13. package/dist/capacitor/capacitor.js.map +1 -1
  14. package/dist/commands/check.js +4 -4
  15. package/dist/commands/check.js.map +1 -1
  16. package/dist/commands/device.js +3 -3
  17. package/dist/commands/device.js.map +1 -1
  18. package/dist/commands/lint.d.ts +2 -2
  19. package/dist/commands/lint.d.ts.map +1 -1
  20. package/dist/commands/lint.js +4 -98
  21. package/dist/commands/lint.js.map +1 -1
  22. package/dist/commands/publish.js +20 -20
  23. package/dist/commands/publish.js.map +1 -1
  24. package/dist/commands/replace-deps.js +1 -1
  25. package/dist/commands/replace-deps.js.map +1 -1
  26. package/dist/commands/typecheck.js +9 -9
  27. package/dist/commands/typecheck.js.map +1 -1
  28. package/dist/electron/electron.js +16 -16
  29. package/dist/electron/electron.js.map +1 -1
  30. package/dist/orchestrators/BuildOrchestrator.js +6 -6
  31. package/dist/orchestrators/BuildOrchestrator.js.map +1 -1
  32. package/dist/orchestrators/DevOrchestrator.d.ts +7 -6
  33. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  34. package/dist/orchestrators/DevOrchestrator.js +157 -203
  35. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  36. package/dist/orchestrators/WatchOrchestrator.d.ts.map +1 -1
  37. package/dist/orchestrators/WatchOrchestrator.js +3 -4
  38. package/dist/orchestrators/WatchOrchestrator.js.map +1 -1
  39. package/dist/sd-cli.js +1 -1
  40. package/dist/sd-cli.js.map +1 -1
  41. package/dist/sd-config.types.d.ts +9 -3
  42. package/dist/sd-config.types.d.ts.map +1 -1
  43. package/dist/utils/copy-public.d.ts.map +1 -1
  44. package/dist/utils/copy-public.js +23 -27
  45. package/dist/utils/copy-public.js.map +1 -1
  46. package/dist/utils/copy-src.d.ts.map +1 -1
  47. package/dist/utils/copy-src.js +7 -7
  48. package/dist/utils/copy-src.js.map +1 -1
  49. package/dist/utils/esbuild-config.d.ts.map +1 -1
  50. package/dist/utils/esbuild-config.js +36 -42
  51. package/dist/utils/esbuild-config.js.map +1 -1
  52. package/dist/utils/replace-deps.js +7 -7
  53. package/dist/utils/replace-deps.js.map +1 -1
  54. package/dist/utils/sd-config.js +2 -2
  55. package/dist/utils/sd-config.js.map +1 -1
  56. package/dist/utils/template.js +7 -7
  57. package/dist/utils/template.js.map +1 -1
  58. package/dist/utils/tsconfig.d.ts +1 -2
  59. package/dist/utils/tsconfig.d.ts.map +1 -1
  60. package/dist/utils/tsconfig.js +5 -8
  61. package/dist/utils/tsconfig.js.map +1 -1
  62. package/dist/utils/typecheck-serialization.js +2 -2
  63. package/dist/utils/typecheck-serialization.js.map +1 -1
  64. package/dist/utils/vite-config.d.ts +2 -0
  65. package/dist/utils/vite-config.d.ts.map +1 -1
  66. package/dist/utils/vite-config.js +36 -3
  67. package/dist/utils/vite-config.js.map +1 -1
  68. package/dist/utils/worker-events.d.ts +11 -1
  69. package/dist/utils/worker-events.d.ts.map +1 -1
  70. package/dist/utils/worker-events.js +3 -5
  71. package/dist/utils/worker-events.js.map +1 -1
  72. package/dist/utils/worker-utils.d.ts +2 -2
  73. package/dist/utils/worker-utils.d.ts.map +1 -1
  74. package/dist/utils/worker-utils.js +1 -1
  75. package/dist/utils/worker-utils.js.map +1 -1
  76. package/dist/workers/client.worker.d.ts +1 -1
  77. package/dist/workers/client.worker.js +3 -3
  78. package/dist/workers/client.worker.js.map +1 -1
  79. package/dist/workers/dts.worker.d.ts +1 -1
  80. package/dist/workers/dts.worker.d.ts.map +1 -1
  81. package/dist/workers/dts.worker.js +13 -28
  82. package/dist/workers/dts.worker.js.map +1 -1
  83. package/dist/workers/library.worker.d.ts +1 -1
  84. package/dist/workers/library.worker.js +4 -4
  85. package/dist/workers/library.worker.js.map +1 -1
  86. package/dist/workers/lint.worker.d.ts +1 -1
  87. package/dist/workers/server-runtime.worker.d.ts +1 -1
  88. package/dist/workers/server-runtime.worker.js +4 -4
  89. package/dist/workers/server-runtime.worker.js.map +1 -1
  90. package/dist/workers/server.worker.d.ts +1 -1
  91. package/dist/workers/server.worker.js +6 -6
  92. package/dist/workers/server.worker.js.map +1 -1
  93. package/package.json +4 -5
  94. package/src/builders/DtsBuilder.ts +2 -2
  95. package/src/builders/LibraryBuilder.ts +7 -10
  96. package/src/builders/types.ts +6 -1
  97. package/src/capacitor/capacitor.ts +61 -60
  98. package/src/commands/check.ts +4 -4
  99. package/src/commands/device.ts +3 -3
  100. package/src/commands/lint.ts +6 -117
  101. package/src/commands/publish.ts +20 -20
  102. package/src/commands/replace-deps.ts +1 -1
  103. package/src/commands/typecheck.ts +9 -9
  104. package/src/electron/electron.ts +16 -16
  105. package/src/orchestrators/BuildOrchestrator.ts +6 -6
  106. package/src/orchestrators/DevOrchestrator.ts +210 -256
  107. package/src/orchestrators/WatchOrchestrator.ts +8 -10
  108. package/src/sd-cli.ts +1 -1
  109. package/src/sd-config.types.ts +10 -3
  110. package/src/utils/copy-public.ts +22 -26
  111. package/src/utils/copy-src.ts +7 -7
  112. package/src/utils/esbuild-config.ts +51 -63
  113. package/src/utils/replace-deps.ts +7 -7
  114. package/src/utils/sd-config.ts +2 -2
  115. package/src/utils/template.ts +7 -7
  116. package/src/utils/tsconfig.ts +6 -10
  117. package/src/utils/typecheck-serialization.ts +2 -2
  118. package/src/utils/vite-config.ts +376 -341
  119. package/src/utils/worker-events.ts +13 -10
  120. package/src/utils/worker-utils.ts +45 -45
  121. package/src/workers/client.worker.ts +3 -3
  122. package/src/workers/dts.worker.ts +451 -467
  123. package/src/workers/library.worker.ts +4 -4
  124. package/src/workers/server-runtime.worker.ts +4 -4
  125. package/src/workers/server.worker.ts +572 -572
  126. package/templates/init/package.json.hbs +2 -3
  127. package/templates/init/packages/client-admin/package.json.hbs +5 -5
  128. package/templates/init/packages/client-admin/src/views/auth/LoginView.tsx +2 -2
  129. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeDetail.tsx.hbs +86 -105
  130. package/templates/init/packages/client-admin/src/views/home/base/employee/EmployeeSheet.tsx.hbs +1 -1
  131. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleDetail.tsx.hbs +4 -12
  132. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionDetail.tsx.hbs +0 -2
  133. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RolePermissionView.tsx +1 -1
  134. package/templates/init/packages/client-admin/src/views/home/base/role-permission/RoleSheet.tsx.hbs +1 -1
  135. package/templates/init/packages/client-admin/src/views/home/my-info/MyInfoDetail.tsx.hbs +36 -43
  136. package/templates/init/packages/db-main/package.json.hbs +2 -2
  137. package/templates/init/packages/server/package.json.hbs +4 -4
  138. package/templates/init/tests/e2e/package.json.hbs +1 -1
  139. package/tests/get-compiler-options-for-package.spec.ts +13 -72
  140. package/tests/get-package-source-files.spec.ts +0 -42
  141. package/tests/get-types-from-package-json.spec.ts +15 -30
  142. package/tests/infra/ResultCollector.spec.ts +0 -9
  143. package/tests/infra/WorkerManager.spec.ts +0 -34
  144. package/tests/load-ignore-patterns.spec.ts +15 -40
  145. package/tests/load-sd-config.spec.ts +16 -53
  146. package/tests/publish-config-narrowing.spec.ts +20 -0
  147. package/tests/run-lint.spec.ts +38 -87
  148. package/tests/run-typecheck.spec.ts +194 -303
  149. package/tests/run-watch.spec.ts +0 -34
  150. package/tests/sd-cli.spec.ts +0 -88
  151. package/tests/sd-public-dev-plugin-mime.spec.ts +19 -0
  152. package/dist/builders/index.d.ts +0 -5
  153. package/dist/builders/index.d.ts.map +0 -1
  154. package/dist/builders/index.js +0 -5
  155. package/dist/builders/index.js.map +0 -6
  156. package/dist/infra/index.d.ts +0 -4
  157. package/dist/infra/index.d.ts.map +0 -1
  158. package/dist/infra/index.js +0 -4
  159. package/dist/infra/index.js.map +0 -6
  160. package/dist/orchestrators/index.d.ts +0 -4
  161. package/dist/orchestrators/index.d.ts.map +0 -1
  162. package/dist/orchestrators/index.js +0 -4
  163. package/dist/orchestrators/index.js.map +0 -6
  164. package/src/builders/index.ts +0 -4
  165. package/src/infra/index.ts +0 -3
  166. package/src/orchestrators/index.ts +0 -3
  167. package/templates/init/stylelint.config.ts +0 -1
@@ -1,341 +1,376 @@
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
- }
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, pathx } 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) => pathx.norm(d) === pathx.norm(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
+ /** Common MIME types for web assets served from public-dev/ */
86
+ const MIME_TYPES: Record<string, string> = {
87
+ ".html": "text/html",
88
+ ".css": "text/css",
89
+ ".js": "text/javascript",
90
+ ".mjs": "text/javascript",
91
+ ".json": "application/json",
92
+ ".png": "image/png",
93
+ ".jpg": "image/jpeg",
94
+ ".jpeg": "image/jpeg",
95
+ ".gif": "image/gif",
96
+ ".svg": "image/svg+xml",
97
+ ".ico": "image/x-icon",
98
+ ".webp": "image/webp",
99
+ ".avif": "image/avif",
100
+ ".woff": "font/woff",
101
+ ".woff2": "font/woff2",
102
+ ".ttf": "font/ttf",
103
+ ".mp4": "video/mp4",
104
+ ".webm": "video/webm",
105
+ };
106
+
107
+ /** @internal Exported for testing */
108
+ export function getMimeType(ext: string): string {
109
+ return MIME_TYPES[ext.toLowerCase()] ?? "application/octet-stream";
110
+ }
111
+
112
+ /**
113
+ * Vite plugin that serves files from public-dev/ directory with priority over public/ in dev mode.
114
+ * Keeps Vite's default publicDir (public/) while giving precedence to public-dev/ files at the same path.
115
+ */
116
+ function sdPublicDevPlugin(pkgDir: string): Plugin {
117
+ const publicDevDir = path.join(pkgDir, "public-dev");
118
+
119
+ return {
120
+ name: "sd-public-dev",
121
+ configureServer(server) {
122
+ if (!fs.existsSync(publicDevDir)) return;
123
+
124
+ // Check public-dev/ files before Vite's default static serving
125
+ server.middlewares.use((req, res, next) => {
126
+ if (req.url == null) {
127
+ next();
128
+ return;
129
+ }
130
+
131
+ // Strip base path
132
+ const base = server.config.base || "/";
133
+ let urlPath = req.url.split("?")[0];
134
+ if (urlPath.startsWith(base)) {
135
+ urlPath = urlPath.slice(base.length);
136
+ }
137
+ if (urlPath.startsWith("/")) {
138
+ urlPath = urlPath.slice(1);
139
+ }
140
+
141
+ // Path traversal defense: block access to files outside publicDevDir
142
+ const decodedPath = decodeURIComponent(urlPath);
143
+ const filePath = path.resolve(publicDevDir, decodedPath);
144
+ const normalizedRoot = path.resolve(publicDevDir);
145
+ if (!filePath.startsWith(normalizedRoot + path.sep) && filePath !== normalizedRoot) {
146
+ next();
147
+ return;
148
+ }
149
+
150
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
151
+ const ext = path.extname(filePath);
152
+ res.setHeader("Content-Type", getMimeType(ext));
153
+ const stream = fs.createReadStream(filePath);
154
+ stream.on("error", () => {
155
+ if (!res.headersSent) {
156
+ next();
157
+ } else {
158
+ res.destroy();
159
+ }
160
+ });
161
+ stream.pipe(res);
162
+ } else {
163
+ next();
164
+ }
165
+ });
166
+ },
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Vite plugin that detects changes in scope packages' dist directories.
172
+ *
173
+ * Vite excludes node_modules from watch by default, so dist file changes
174
+ * in scope packages do not trigger HMR/rebuild.
175
+ * This plugin uses a separate FsWatcher to monitor scope packages' dist directories
176
+ * and triggers Vite's internal HMR pipeline on change.
177
+ * Excludes from optimizeDeps to prevent changes being ignored due to pre-bundled cache.
178
+ */
179
+ function sdScopeWatchPlugin(
180
+ pkgDir: string,
181
+ replaceDeps: string[],
182
+ onScopeRebuild?: () => void,
183
+ ): Plugin {
184
+ return {
185
+ name: "sd-scope-watch",
186
+ config() {
187
+ const excluded: string[] = [];
188
+ const nestedDepsToInclude: string[] = [];
189
+
190
+ for (const pkg of replaceDeps) {
191
+ excluded.push(pkg);
192
+
193
+ const pkgParts = pkg.split("/");
194
+ const depPkgJsonPath = path.join(pkgDir, "node_modules", ...pkgParts, "package.json");
195
+ try {
196
+ const depPkgJson = JSON.parse(fs.readFileSync(depPkgJsonPath, "utf-8")) as {
197
+ dependencies?: Record<string, string>;
198
+ };
199
+ for (const dep of Object.keys(depPkgJson.dependencies ?? {})) {
200
+ if (replaceDeps.includes(dep)) continue;
201
+ if (dep === "solid-js" || dep.startsWith("@solidjs/") || dep.startsWith("solid-"))
202
+ continue;
203
+ if (dep === "tailwindcss") continue;
204
+
205
+ const realPkgPath = fs.realpathSync(path.join(pkgDir, "node_modules", ...pkgParts));
206
+ const pnpmNodeModules = path.resolve(realPkgPath, "../..");
207
+ const depPkgJsonResolved = path.join(pnpmNodeModules, dep, "package.json");
208
+ if (isSubpathOnlyPackage(depPkgJsonResolved)) continue;
209
+
210
+ const depPkgJsonFallback = path.join(
211
+ pkgDir,
212
+ "node_modules",
213
+ ...pkgParts,
214
+ "node_modules",
215
+ dep,
216
+ "package.json",
217
+ );
218
+ if (isSubpathOnlyPackage(depPkgJsonFallback)) continue;
219
+
220
+ nestedDepsToInclude.push(`${pkg} > ${dep}`);
221
+ }
222
+ } catch {
223
+ // Skip on package.json read failure
224
+ }
225
+ }
226
+
227
+ return {
228
+ optimizeDeps: {
229
+ force: true,
230
+ exclude: excluded,
231
+ include: [...new Set(nestedDepsToInclude)],
232
+ },
233
+ };
234
+ },
235
+ async configureServer(server) {
236
+ const watchPaths: string[] = [];
237
+
238
+ for (const pkg of replaceDeps) {
239
+ const pkgParts = pkg.split("/");
240
+ const pkgRoot = path.join(pkgDir, "node_modules", ...pkgParts);
241
+ if (!fs.existsSync(pkgRoot)) continue;
242
+
243
+ const distDir = path.join(pkgRoot, "dist");
244
+ if (fs.existsSync(distDir)) {
245
+ watchPaths.push(distDir);
246
+ }
247
+
248
+ for (const file of fs.readdirSync(pkgRoot)) {
249
+ if (
250
+ file.endsWith(".css") ||
251
+ file === "tailwind.config.ts" ||
252
+ file === "tailwind.config.js"
253
+ ) {
254
+ watchPaths.push(path.join(pkgRoot, file));
255
+ }
256
+ }
257
+ }
258
+
259
+ if (watchPaths.length === 0) return;
260
+
261
+ const scopeWatcher = await FsWatcher.watch(watchPaths);
262
+ scopeWatcher.onChange({ delay: 300 }, (changeInfos) => {
263
+ for (const { path: changedPath } of changeInfos) {
264
+ let realPath: string;
265
+ try {
266
+ realPath = fs.realpathSync(changedPath);
267
+ } catch {
268
+ continue;
269
+ }
270
+ server.watcher.emit("change", realPath);
271
+ }
272
+ onScopeRebuild?.();
273
+ });
274
+
275
+ server.httpServer?.on("close", () => void scopeWatcher.close());
276
+ },
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Vite config generation options
282
+ */
283
+ export interface ViteConfigOptions {
284
+ pkgDir: string;
285
+ name: string;
286
+ tsconfigPath: string;
287
+ compilerOptions: Record<string, unknown>;
288
+ env?: Record<string, string>;
289
+ mode: "build" | "dev";
290
+ /** Server port in dev mode (0 for auto-assign) */
291
+ serverPort?: number;
292
+ /** Array of replaceDeps package names (resolved state) */
293
+ replaceDeps?: string[];
294
+ /** Callback when replaceDeps package dist changes */
295
+ onScopeRebuild?: () => void;
296
+ }
297
+
298
+ /**
299
+ * Create Vite config
300
+ *
301
+ * Configuration for building/dev server for client packages based on SolidJS + TailwindCSS.
302
+ * - build mode: production build (logLevel: silent)
303
+ * - dev mode: dev server (env substitution via define, server configuration)
304
+ */
305
+ export function createViteConfig(options: ViteConfigOptions): ViteUserConfig {
306
+ const { pkgDir, name, tsconfigPath, compilerOptions, env, mode, serverPort, replaceDeps } =
307
+ options;
308
+
309
+ // Read package.json to extract app name for PWA manifest
310
+ const pkgJsonPath = path.join(pkgDir, "package.json");
311
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")) as { name: string };
312
+ const appName = pkgJson.name.replace(/^@[^/]+\//, "");
313
+
314
+ // Process.env substitution (applied to both build and dev modes)
315
+ const envDefine: Record<string, string> = {};
316
+ if (env != null) {
317
+ envDefine["process.env"] = JSON.stringify(env);
318
+ }
319
+
320
+ const config: ViteUserConfig = {
321
+ root: pkgDir,
322
+ base: `/${name}/`,
323
+ plugins: [
324
+ tsconfigPaths({ projects: [tsconfigPath] }),
325
+ solidPlugin(),
326
+ VitePWA({
327
+ registerType: "prompt",
328
+ injectRegister: "script",
329
+ manifest: {
330
+ name: appName,
331
+ short_name: appName,
332
+ display: "standalone",
333
+ theme_color: "#ffffff",
334
+ background_color: "#ffffff",
335
+ },
336
+ workbox: {
337
+ globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2}"],
338
+ },
339
+ }),
340
+ ...(replaceDeps != null && replaceDeps.length > 0
341
+ ? [sdTailwindConfigDepsPlugin(pkgDir, replaceDeps)]
342
+ : []),
343
+ ...(replaceDeps != null && replaceDeps.length > 0
344
+ ? [sdScopeWatchPlugin(pkgDir, replaceDeps, options.onScopeRebuild)]
345
+ : []),
346
+ ...(mode === "dev" ? [sdPublicDevPlugin(pkgDir)] : []),
347
+ ],
348
+ css: {
349
+ postcss: {
350
+ plugins: [tailwindcss({ config: path.join(pkgDir, "tailwind.config.ts") })],
351
+ },
352
+ },
353
+ esbuild: {
354
+ tsconfigRaw: { compilerOptions: compilerOptions as esbuild.TsconfigRaw["compilerOptions"] },
355
+ },
356
+ };
357
+
358
+ // Process.env substitution (applied to both build and dev modes)
359
+ config.define = envDefine;
360
+
361
+ if (mode === "build") {
362
+ config.logLevel = "silent";
363
+ } else {
364
+ // Dev mode
365
+ config.server = {
366
+ // serverPort === 0: server-connected client (proxy target)
367
+ // → explicitly set host to 127.0.0.1 to ensure IPv4 binding
368
+ // (On Windows, if localhost resolves to ::1 (IPv6), proxy connection to 127.0.0.1 causes ECONNREFUSED)
369
+ host: serverPort === 0 ? "127.0.0.1" : undefined,
370
+ port: serverPort === 0 ? undefined : serverPort,
371
+ strictPort: serverPort !== 0 && serverPort !== undefined,
372
+ };
373
+ }
374
+
375
+ return config;
376
+ }