@donkeylabs/adapter-sveltekit 2.0.12 → 2.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/vite.ts +145 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/adapter-sveltekit",
3
- "version": "2.0.12",
3
+ "version": "2.0.13",
4
4
  "type": "module",
5
5
  "description": "SvelteKit adapter for @donkeylabs/server - seamless SSR/browser API integration",
6
6
  "main": "./src/index.ts",
package/src/vite.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import type { Plugin, ViteDevServer } from "vite";
10
10
  import { spawn, type ChildProcess, exec } from "node:child_process";
11
11
  import { resolve, join } from "node:path";
12
- import { watch, type FSWatcher } from "node:fs";
12
+ import { watch, type FSWatcher, existsSync } from "node:fs";
13
13
  import { promisify } from "node:util";
14
14
 
15
15
  const execAsync = promisify(exec);
@@ -41,6 +41,19 @@ export interface DevPluginOptions {
41
41
  * @default "./src/server"
42
42
  */
43
43
  watchDir?: string;
44
+
45
+ /**
46
+ * Enable hot reload for route files (dev mode only).
47
+ * When a route file changes, the router will be reloaded without server restart.
48
+ * @default true
49
+ */
50
+ hotReloadRoutes?: boolean;
51
+
52
+ /**
53
+ * Glob patterns for route files to watch for hot reload.
54
+ * @default ["**\/routes\/**\/*.ts"]
55
+ */
56
+ routePatterns?: string[];
44
57
  }
45
58
 
46
59
  // Check if running with Bun runtime (bun --bun)
@@ -87,6 +100,8 @@ export function donkeylabsDev(options: DevPluginOptions = {}): Plugin {
87
100
  backendPort = 3001,
88
101
  watchTypes = true,
89
102
  watchDir = "./src/server",
103
+ hotReloadRoutes = true,
104
+ routePatterns = ["**/routes/**/*.ts", "**/routes/**/*.js"],
90
105
  } = options;
91
106
 
92
107
  // State for subprocess mode
@@ -96,6 +111,7 @@ export function donkeylabsDev(options: DevPluginOptions = {}): Plugin {
96
111
  // State for in-process mode
97
112
  let appServer: any = null;
98
113
  let serverReady = false;
114
+ let viteServer: ViteDevServer | null = null;
99
115
 
100
116
  // State for file watcher
101
117
  let fileWatcher: FSWatcher | null = null;
@@ -103,11 +119,82 @@ export function donkeylabsDev(options: DevPluginOptions = {}): Plugin {
103
119
  let lastGenerationTime = 0;
104
120
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
105
121
 
122
+ // State for hot reload
123
+ let hotReloadTimer: ReturnType<typeof setTimeout> | null = null;
124
+ const HOT_RELOAD_DEBOUNCE_MS = 100;
125
+
106
126
  const COOLDOWN_MS = 2000;
107
127
  const DEBOUNCE_MS = 500;
108
128
 
109
129
  // Patterns to ignore (generated files)
110
- const IGNORED_PATTERNS = [/schema\.ts$/, /\.d\.ts$/];
130
+ const IGNORED_PATTERNS = [/schema\.ts$/, /\.d\.ts$/, /api\.ts$/];
131
+
132
+ // Check if a file matches route patterns
133
+ function isRouteFile(filename: string): boolean {
134
+ return routePatterns.some((pattern) => {
135
+ // Simple glob matching for common patterns
136
+ const regexPattern = pattern
137
+ .replace(/\*\*/g, ".*")
138
+ .replace(/\*/g, "[^/]*")
139
+ .replace(/\./g, "\\.");
140
+ return new RegExp(regexPattern).test(filename);
141
+ });
142
+ }
143
+
144
+ // Hot reload a route file
145
+ async function hotReloadRoute(filepath: string) {
146
+ if (!appServer || !serverReady || !viteServer || !hotReloadRoutes) return;
147
+
148
+ console.log("\x1b[36m[donkeylabs-dev]\x1b[0m Hot reloading route:", filepath);
149
+
150
+ try {
151
+ // Invalidate the module in Vite's cache
152
+ const mod = viteServer.moduleGraph.getModuleById(filepath);
153
+ if (mod) {
154
+ viteServer.moduleGraph.invalidateModule(mod);
155
+ }
156
+
157
+ // Re-import the module with cache busting
158
+ const timestamp = Date.now();
159
+ const moduleUrl = `${filepath}?t=${timestamp}`;
160
+
161
+ // Import the fresh module
162
+ const freshModule = await viteServer.ssrLoadModule(moduleUrl);
163
+
164
+ // Find the router export (could be named 'router', 'default', or end with 'Router')
165
+ let newRouter = freshModule.router || freshModule.default;
166
+ if (!newRouter) {
167
+ for (const key of Object.keys(freshModule)) {
168
+ if (key.endsWith("Router") && typeof freshModule[key]?.getRoutes === "function") {
169
+ newRouter = freshModule[key];
170
+ break;
171
+ }
172
+ }
173
+ }
174
+
175
+ if (newRouter && typeof newRouter.getRoutes === "function") {
176
+ // Get the router prefix
177
+ const prefix = newRouter.getPrefix?.() || "";
178
+ if (prefix) {
179
+ appServer.reloadRouter(prefix, newRouter);
180
+ console.log("\x1b[32m[donkeylabs-dev]\x1b[0m Route hot reload complete:", prefix);
181
+ } else {
182
+ // If no prefix, rebuild all routes
183
+ appServer.rebuildRouteMap();
184
+ console.log("\x1b[32m[donkeylabs-dev]\x1b[0m Route map rebuilt");
185
+ }
186
+ } else {
187
+ console.warn("\x1b[33m[donkeylabs-dev]\x1b[0m No router export found in:", filepath);
188
+ }
189
+ } catch (err: any) {
190
+ console.error("\x1b[31m[donkeylabs-dev]\x1b[0m Hot reload error:", err.message);
191
+ }
192
+ }
193
+
194
+ function debouncedHotReload(filepath: string) {
195
+ if (hotReloadTimer) clearTimeout(hotReloadTimer);
196
+ hotReloadTimer = setTimeout(() => hotReloadRoute(filepath), HOT_RELOAD_DEBOUNCE_MS);
197
+ }
111
198
 
112
199
  function shouldIgnoreFile(filename: string): boolean {
113
200
  return IGNORED_PATTERNS.some((pattern) => pattern.test(filename));
@@ -122,7 +209,7 @@ export function donkeylabsDev(options: DevPluginOptions = {}): Plugin {
122
209
  console.log("\x1b[36m[donkeylabs-dev]\x1b[0m Server files changed, regenerating types...");
123
210
 
124
211
  try {
125
- await execAsync("bunx donkeylabs generate");
212
+ await execAsync("bun run gen:types");
126
213
  console.log("\x1b[32m[donkeylabs-dev]\x1b[0m Types regenerated successfully");
127
214
  } catch (e: any) {
128
215
  console.error("\x1b[31m[donkeylabs-dev]\x1b[0m Error regenerating types:", e.message);
@@ -132,23 +219,65 @@ export function donkeylabsDev(options: DevPluginOptions = {}): Plugin {
132
219
  }
133
220
  }
134
221
 
222
+ async function ensureTypesGenerated() {
223
+ // Check if the client file exists (common locations)
224
+ const clientPaths = [
225
+ resolve(process.cwd(), "src/lib/api.ts"),
226
+ resolve(process.cwd(), "src/api.ts"),
227
+ ];
228
+
229
+ const clientExists = clientPaths.some((p) => existsSync(p));
230
+ if (clientExists) return;
231
+
232
+ console.log("\x1b[36m[donkeylabs-dev]\x1b[0m Generated client not found, running initial type generation...");
233
+ isGenerating = true;
234
+
235
+ try {
236
+ await execAsync("bun run gen:types");
237
+ console.log("\x1b[32m[donkeylabs-dev]\x1b[0m Initial types generated successfully");
238
+ } catch (e: any) {
239
+ console.warn("\x1b[33m[donkeylabs-dev]\x1b[0m Initial type generation failed:", e.message);
240
+ console.warn("\x1b[33m[donkeylabs-dev]\x1b[0m Run 'bun run gen:types' manually to generate types");
241
+ } finally {
242
+ isGenerating = false;
243
+ lastGenerationTime = Date.now();
244
+ }
245
+ }
246
+
135
247
  function debouncedRegenerate() {
136
248
  if (debounceTimer) clearTimeout(debounceTimer);
137
249
  debounceTimer = setTimeout(regenerateTypes, DEBOUNCE_MS);
138
250
  }
139
251
 
140
252
  function startFileWatcher() {
141
- if (!watchTypes || fileWatcher) return;
253
+ if (fileWatcher) return;
254
+ if (!watchTypes && !hotReloadRoutes) return;
142
255
 
143
256
  const watchPath = resolve(process.cwd(), watchDir);
144
257
  try {
145
258
  fileWatcher = watch(watchPath, { recursive: true }, (_eventType, filename) => {
146
259
  if (!filename) return;
147
- if (!filename.endsWith(".ts")) return;
260
+ if (!filename.endsWith(".ts") && !filename.endsWith(".js")) return;
148
261
  if (shouldIgnoreFile(filename)) return;
149
- debouncedRegenerate();
262
+
263
+ const fullPath = join(watchPath, filename);
264
+
265
+ // Check if this is a route file for hot reload
266
+ if (hotReloadRoutes && isRouteFile(filename)) {
267
+ debouncedHotReload(fullPath);
268
+ }
269
+
270
+ // Also trigger type regeneration
271
+ if (watchTypes) {
272
+ debouncedRegenerate();
273
+ }
150
274
  });
151
- console.log(`\x1b[36m[donkeylabs-dev]\x1b[0m Watching ${watchDir} for changes...`);
275
+
276
+ const features = [
277
+ watchTypes ? "type generation" : null,
278
+ hotReloadRoutes ? "hot reload" : null,
279
+ ].filter(Boolean).join(", ");
280
+ console.log(`\x1b[36m[donkeylabs-dev]\x1b[0m Watching ${watchDir} for ${features}...`);
152
281
  } catch (err) {
153
282
  console.warn(`\x1b[33m[donkeylabs-dev]\x1b[0m Could not watch ${watchDir}:`, err);
154
283
  }
@@ -156,6 +285,7 @@ export function donkeylabsDev(options: DevPluginOptions = {}): Plugin {
156
285
 
157
286
  function stopFileWatcher() {
158
287
  if (debounceTimer) clearTimeout(debounceTimer);
288
+ if (hotReloadTimer) clearTimeout(hotReloadTimer);
159
289
  if (fileWatcher) {
160
290
  fileWatcher.close();
161
291
  fileWatcher = null;
@@ -183,7 +313,13 @@ export function donkeylabsDev(options: DevPluginOptions = {}): Plugin {
183
313
  async configureServer(server: ViteDevServer) {
184
314
  const serverEntryResolved = resolve(process.cwd(), serverEntry);
185
315
 
186
- // Start file watcher for auto type regeneration
316
+ // Store vite server reference for hot reload
317
+ viteServer = server;
318
+
319
+ // Ensure types are generated on first start (if client file doesn't exist)
320
+ await ensureTypesGenerated();
321
+
322
+ // Start file watcher for auto type regeneration and hot reload
187
323
  startFileWatcher();
188
324
 
189
325
  if (isBunRuntime) {
@@ -576,6 +712,7 @@ export function donkeylabsDev(options: DevPluginOptions = {}): Plugin {
576
712
 
577
713
  async closeBundle() {
578
714
  stopFileWatcher();
715
+ viteServer = null;
579
716
  if (backendProcess) {
580
717
  backendProcess.kill();
581
718
  backendProcess = null;