@donkeylabs/adapter-sveltekit 2.0.14 → 2.0.16
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.
- package/dist/client/index.d.ts +231 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +501 -0
- package/dist/generator/index.d.ts +17 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +330 -0
- package/dist/hooks/index.d.ts +53 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +96 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.js} +51 -120
- package/dist/vite.d.ts +71 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +611 -0
- package/package.json +16 -14
- package/src/client/index.ts +0 -659
- package/src/generator/index.ts +0 -401
- package/src/hooks/index.ts +0 -124
- package/src/vite.ts +0 -729
package/dist/vite.js
ADDED
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vite plugin for @donkeylabs/adapter-sveltekit dev server integration
|
|
3
|
+
*
|
|
4
|
+
* Supports two modes:
|
|
5
|
+
* - `bun --bun run dev`: Single-process mode (in-process, one port)
|
|
6
|
+
* - `bun run dev`: Subprocess mode (two processes, proxy)
|
|
7
|
+
*/
|
|
8
|
+
import { spawn, exec } from "node:child_process";
|
|
9
|
+
import { resolve, join } from "node:path";
|
|
10
|
+
import { watch, existsSync } from "node:fs";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
import http from "node:http";
|
|
14
|
+
// Check if running with Bun runtime (bun --bun)
|
|
15
|
+
const isBunRuntime = typeof globalThis.Bun !== "undefined";
|
|
16
|
+
/**
|
|
17
|
+
* Get the global app server instance for SSR direct calls.
|
|
18
|
+
* This allows hooks to access the server without HTTP.
|
|
19
|
+
*/
|
|
20
|
+
export function getDevServer() {
|
|
21
|
+
return globalThis.__donkeylabs_dev_server__;
|
|
22
|
+
}
|
|
23
|
+
function setDevServer(server) {
|
|
24
|
+
globalThis.__donkeylabs_dev_server__ = server;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Vite plugin that integrates @donkeylabs/server with the dev server.
|
|
28
|
+
*
|
|
29
|
+
* - With `bun --bun run dev`: Runs in-process (single port, recommended)
|
|
30
|
+
* - With `bun run dev`: Spawns subprocess (two ports, fallback)
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // vite.config.ts
|
|
34
|
+
* import { donkeylabsDev } from "@donkeylabs/adapter-sveltekit/vite";
|
|
35
|
+
*
|
|
36
|
+
* export default defineConfig({
|
|
37
|
+
* plugins: [
|
|
38
|
+
* donkeylabsDev({ serverEntry: "./src/server/index.ts" }),
|
|
39
|
+
* sveltekit()
|
|
40
|
+
* ]
|
|
41
|
+
* });
|
|
42
|
+
*/
|
|
43
|
+
export function donkeylabsDev(options = {}) {
|
|
44
|
+
const { serverEntry = "./src/server/index.ts", backendPort = 3001, watchTypes = true, watchDir = "./src/server", hotReloadRoutes = true, routePatterns = ["**/routes/**/*.ts", "**/routes/**/*.js"], } = options;
|
|
45
|
+
// State for subprocess mode
|
|
46
|
+
let backendProcess = null;
|
|
47
|
+
let backendReady = false;
|
|
48
|
+
// State for in-process mode
|
|
49
|
+
let appServer = null;
|
|
50
|
+
let serverReady = false;
|
|
51
|
+
let viteServer = null;
|
|
52
|
+
// State for file watcher
|
|
53
|
+
let fileWatcher = null;
|
|
54
|
+
let isGenerating = false;
|
|
55
|
+
let lastGenerationTime = 0;
|
|
56
|
+
let debounceTimer = null;
|
|
57
|
+
// State for hot reload
|
|
58
|
+
let hotReloadTimer = null;
|
|
59
|
+
const HOT_RELOAD_DEBOUNCE_MS = 100;
|
|
60
|
+
const COOLDOWN_MS = 2000;
|
|
61
|
+
const DEBOUNCE_MS = 500;
|
|
62
|
+
// Patterns to ignore (generated files)
|
|
63
|
+
const IGNORED_PATTERNS = [/schema\.ts$/, /\.d\.ts$/, /api\.ts$/];
|
|
64
|
+
// Check if a file matches route patterns
|
|
65
|
+
function isRouteFile(filename) {
|
|
66
|
+
return routePatterns.some((pattern) => {
|
|
67
|
+
// Simple glob matching for common patterns
|
|
68
|
+
const regexPattern = pattern
|
|
69
|
+
.replace(/\*\*/g, ".*")
|
|
70
|
+
.replace(/\*/g, "[^/]*")
|
|
71
|
+
.replace(/\./g, "\\.");
|
|
72
|
+
return new RegExp(regexPattern).test(filename);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
// Hot reload a route file
|
|
76
|
+
async function hotReloadRoute(filepath) {
|
|
77
|
+
if (!appServer || !serverReady || !viteServer || !hotReloadRoutes)
|
|
78
|
+
return;
|
|
79
|
+
console.log("\x1b[36m[donkeylabs-dev]\x1b[0m Hot reloading route:", filepath);
|
|
80
|
+
try {
|
|
81
|
+
// Invalidate the module in Vite's cache
|
|
82
|
+
const mod = viteServer.moduleGraph.getModuleById(filepath);
|
|
83
|
+
if (mod) {
|
|
84
|
+
viteServer.moduleGraph.invalidateModule(mod);
|
|
85
|
+
}
|
|
86
|
+
// Re-import the module with cache busting
|
|
87
|
+
const timestamp = Date.now();
|
|
88
|
+
const moduleUrl = `${filepath}?t=${timestamp}`;
|
|
89
|
+
// Import the fresh module
|
|
90
|
+
const freshModule = await viteServer.ssrLoadModule(moduleUrl);
|
|
91
|
+
// Find the router export (could be named 'router', 'default', or end with 'Router')
|
|
92
|
+
let newRouter = freshModule.router || freshModule.default;
|
|
93
|
+
if (!newRouter) {
|
|
94
|
+
for (const key of Object.keys(freshModule)) {
|
|
95
|
+
if (key.endsWith("Router") && typeof freshModule[key]?.getRoutes === "function") {
|
|
96
|
+
newRouter = freshModule[key];
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (newRouter && typeof newRouter.getRoutes === "function") {
|
|
102
|
+
// Get the router prefix
|
|
103
|
+
const prefix = newRouter.getPrefix?.() || "";
|
|
104
|
+
if (prefix) {
|
|
105
|
+
appServer.reloadRouter(prefix, newRouter);
|
|
106
|
+
console.log("\x1b[32m[donkeylabs-dev]\x1b[0m Route hot reload complete:", prefix);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
// If no prefix, rebuild all routes
|
|
110
|
+
appServer.rebuildRouteMap();
|
|
111
|
+
console.log("\x1b[32m[donkeylabs-dev]\x1b[0m Route map rebuilt");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
console.warn("\x1b[33m[donkeylabs-dev]\x1b[0m No router export found in:", filepath);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
console.error("\x1b[31m[donkeylabs-dev]\x1b[0m Hot reload error:", err.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function debouncedHotReload(filepath) {
|
|
123
|
+
if (hotReloadTimer)
|
|
124
|
+
clearTimeout(hotReloadTimer);
|
|
125
|
+
hotReloadTimer = setTimeout(() => hotReloadRoute(filepath), HOT_RELOAD_DEBOUNCE_MS);
|
|
126
|
+
}
|
|
127
|
+
function shouldIgnoreFile(filename) {
|
|
128
|
+
return IGNORED_PATTERNS.some((pattern) => pattern.test(filename));
|
|
129
|
+
}
|
|
130
|
+
async function regenerateTypes() {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
if (now - lastGenerationTime < COOLDOWN_MS || isGenerating)
|
|
133
|
+
return;
|
|
134
|
+
isGenerating = true;
|
|
135
|
+
lastGenerationTime = now;
|
|
136
|
+
console.log("\x1b[36m[donkeylabs-dev]\x1b[0m Server files changed, regenerating types...");
|
|
137
|
+
try {
|
|
138
|
+
await execAsync("bun run gen:types");
|
|
139
|
+
console.log("\x1b[32m[donkeylabs-dev]\x1b[0m Types regenerated successfully");
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
console.error("\x1b[31m[donkeylabs-dev]\x1b[0m Error regenerating types:", e.message);
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
isGenerating = false;
|
|
146
|
+
lastGenerationTime = Date.now();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function ensureTypesGenerated() {
|
|
150
|
+
// Check if the client file exists (common locations)
|
|
151
|
+
const clientPaths = [
|
|
152
|
+
resolve(process.cwd(), "src/lib/api.ts"),
|
|
153
|
+
resolve(process.cwd(), "src/api.ts"),
|
|
154
|
+
];
|
|
155
|
+
const clientExists = clientPaths.some((p) => existsSync(p));
|
|
156
|
+
if (clientExists)
|
|
157
|
+
return;
|
|
158
|
+
console.log("\x1b[36m[donkeylabs-dev]\x1b[0m Generated client not found, running initial type generation...");
|
|
159
|
+
isGenerating = true;
|
|
160
|
+
try {
|
|
161
|
+
await execAsync("bun run gen:types");
|
|
162
|
+
console.log("\x1b[32m[donkeylabs-dev]\x1b[0m Initial types generated successfully");
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.warn("\x1b[33m[donkeylabs-dev]\x1b[0m Initial type generation failed:", e.message);
|
|
166
|
+
console.warn("\x1b[33m[donkeylabs-dev]\x1b[0m Run 'bun run gen:types' manually to generate types");
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
isGenerating = false;
|
|
170
|
+
lastGenerationTime = Date.now();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function debouncedRegenerate() {
|
|
174
|
+
if (debounceTimer)
|
|
175
|
+
clearTimeout(debounceTimer);
|
|
176
|
+
debounceTimer = setTimeout(regenerateTypes, DEBOUNCE_MS);
|
|
177
|
+
}
|
|
178
|
+
function startFileWatcher() {
|
|
179
|
+
if (fileWatcher)
|
|
180
|
+
return;
|
|
181
|
+
if (!watchTypes && !hotReloadRoutes)
|
|
182
|
+
return;
|
|
183
|
+
const watchPath = resolve(process.cwd(), watchDir);
|
|
184
|
+
try {
|
|
185
|
+
fileWatcher = watch(watchPath, { recursive: true }, (_eventType, filename) => {
|
|
186
|
+
if (!filename)
|
|
187
|
+
return;
|
|
188
|
+
if (!filename.endsWith(".ts") && !filename.endsWith(".js"))
|
|
189
|
+
return;
|
|
190
|
+
if (shouldIgnoreFile(filename))
|
|
191
|
+
return;
|
|
192
|
+
const fullPath = join(watchPath, filename);
|
|
193
|
+
// Check if this is a route file for hot reload
|
|
194
|
+
if (hotReloadRoutes && isRouteFile(filename)) {
|
|
195
|
+
debouncedHotReload(fullPath);
|
|
196
|
+
}
|
|
197
|
+
// Also trigger type regeneration
|
|
198
|
+
if (watchTypes) {
|
|
199
|
+
debouncedRegenerate();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
const features = [
|
|
203
|
+
watchTypes ? "type generation" : null,
|
|
204
|
+
hotReloadRoutes ? "hot reload" : null,
|
|
205
|
+
].filter(Boolean).join(", ");
|
|
206
|
+
console.log(`\x1b[36m[donkeylabs-dev]\x1b[0m Watching ${watchDir} for ${features}...`);
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
console.warn(`\x1b[33m[donkeylabs-dev]\x1b[0m Could not watch ${watchDir}:`, err);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function stopFileWatcher() {
|
|
213
|
+
if (debounceTimer)
|
|
214
|
+
clearTimeout(debounceTimer);
|
|
215
|
+
if (hotReloadTimer)
|
|
216
|
+
clearTimeout(hotReloadTimer);
|
|
217
|
+
if (fileWatcher) {
|
|
218
|
+
fileWatcher.close();
|
|
219
|
+
fileWatcher = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
name: "donkeylabs-dev",
|
|
224
|
+
enforce: "pre",
|
|
225
|
+
// Read PORT env variable and configure Vite's server port
|
|
226
|
+
config(config) {
|
|
227
|
+
const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : undefined;
|
|
228
|
+
if (envPort && !isNaN(envPort)) {
|
|
229
|
+
console.log(`[donkeylabs-dev] Using PORT=${envPort} from environment`);
|
|
230
|
+
return {
|
|
231
|
+
server: {
|
|
232
|
+
...config.server,
|
|
233
|
+
port: envPort,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
async configureServer(server) {
|
|
239
|
+
const serverEntryResolved = resolve(process.cwd(), serverEntry);
|
|
240
|
+
// Store vite server reference for hot reload
|
|
241
|
+
viteServer = server;
|
|
242
|
+
// Ensure types are generated on first start (if client file doesn't exist)
|
|
243
|
+
await ensureTypesGenerated();
|
|
244
|
+
// Start file watcher for auto type regeneration and hot reload
|
|
245
|
+
startFileWatcher();
|
|
246
|
+
if (isBunRuntime) {
|
|
247
|
+
// ========== IN-PROCESS MODE (bun --bun run dev) ==========
|
|
248
|
+
// Import and initialize server directly - no subprocess, no proxy
|
|
249
|
+
console.log("[donkeylabs-dev] Starting in-process mode (Bun runtime detected)");
|
|
250
|
+
// Log the actual URL once Vite's server starts listening
|
|
251
|
+
server.httpServer?.on("listening", () => {
|
|
252
|
+
const address = server.httpServer?.address();
|
|
253
|
+
if (address && typeof address === "object") {
|
|
254
|
+
const host = address.address === "::" || address.address === "0.0.0.0" ? "localhost" : address.address;
|
|
255
|
+
console.log(`[donkeylabs-dev] Ready at http://${host}:${address.port}`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
try {
|
|
259
|
+
const serverModule = await import(/* @vite-ignore */ serverEntryResolved);
|
|
260
|
+
appServer = serverModule.server || serverModule.default;
|
|
261
|
+
if (!appServer) {
|
|
262
|
+
throw new Error("No server export found in " + serverEntry);
|
|
263
|
+
}
|
|
264
|
+
// Initialize without starting HTTP server
|
|
265
|
+
await appServer.initialize();
|
|
266
|
+
serverReady = true;
|
|
267
|
+
// Set global reference for SSR direct calls (uses globalThis for cross-module sharing)
|
|
268
|
+
setDevServer(appServer);
|
|
269
|
+
console.log("[donkeylabs-dev] Server initialized (in-process mode)");
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.error("[donkeylabs-dev] Failed to initialize server:", err);
|
|
273
|
+
throw err;
|
|
274
|
+
}
|
|
275
|
+
// Return middleware setup function
|
|
276
|
+
return () => {
|
|
277
|
+
// In-process request handler
|
|
278
|
+
const inProcessMiddleware = async (req, res, next) => {
|
|
279
|
+
const url = req.url || "/";
|
|
280
|
+
const urlObj = new URL(url, "http://localhost");
|
|
281
|
+
const pathname = urlObj.pathname;
|
|
282
|
+
// Handle SSE endpoint
|
|
283
|
+
if (req.method === "GET" && pathname === "/sse") {
|
|
284
|
+
if (!serverReady || !appServer)
|
|
285
|
+
return next();
|
|
286
|
+
const channels = urlObj.searchParams.get("channels")?.split(",").filter(Boolean) || [];
|
|
287
|
+
const lastEventId = req.headers["last-event-id"] || undefined;
|
|
288
|
+
const { client, response } = appServer.getCore().sse.addClient({ lastEventId });
|
|
289
|
+
for (const channel of channels) {
|
|
290
|
+
appServer.getCore().sse.subscribe(client.id, channel);
|
|
291
|
+
}
|
|
292
|
+
// Set SSE headers
|
|
293
|
+
res.writeHead(200, {
|
|
294
|
+
"Content-Type": "text/event-stream",
|
|
295
|
+
"Cache-Control": "no-cache",
|
|
296
|
+
"Connection": "keep-alive",
|
|
297
|
+
"Access-Control-Allow-Origin": "*",
|
|
298
|
+
});
|
|
299
|
+
// Stream SSE data
|
|
300
|
+
const reader = response.body?.getReader();
|
|
301
|
+
let sseClosed = false;
|
|
302
|
+
req.on("close", () => {
|
|
303
|
+
sseClosed = true;
|
|
304
|
+
reader?.cancel().catch(() => { });
|
|
305
|
+
appServer.getCore().sse.removeClient(client.id);
|
|
306
|
+
});
|
|
307
|
+
if (reader) {
|
|
308
|
+
const pump = async () => {
|
|
309
|
+
try {
|
|
310
|
+
while (!sseClosed) {
|
|
311
|
+
const { done, value } = await reader.read();
|
|
312
|
+
if (done || sseClosed)
|
|
313
|
+
break;
|
|
314
|
+
res.write(value);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Connection closed
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
pump();
|
|
322
|
+
}
|
|
323
|
+
return; // Don't call next()
|
|
324
|
+
}
|
|
325
|
+
// Handle API routes (GET or POST for route names like /routeName.action)
|
|
326
|
+
if ((req.method === "GET" || req.method === "POST") && /^\/[a-zA-Z][a-zA-Z0-9_.]*$/.test(pathname)) {
|
|
327
|
+
if (!serverReady || !appServer)
|
|
328
|
+
return next();
|
|
329
|
+
const routeName = pathname.slice(1);
|
|
330
|
+
if (!appServer.hasRoute(routeName))
|
|
331
|
+
return next();
|
|
332
|
+
// Build a proper Request object to pass to handleRequest
|
|
333
|
+
const buildRequest = async () => {
|
|
334
|
+
const fullUrl = `http://localhost${url}`;
|
|
335
|
+
const headers = new Headers();
|
|
336
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
337
|
+
if (typeof value === "string") {
|
|
338
|
+
headers.set(key, value);
|
|
339
|
+
}
|
|
340
|
+
else if (Array.isArray(value)) {
|
|
341
|
+
for (const v of value)
|
|
342
|
+
headers.append(key, v);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (req.method === "POST") {
|
|
346
|
+
// Collect body for POST
|
|
347
|
+
const chunks = [];
|
|
348
|
+
for await (const chunk of req) {
|
|
349
|
+
chunks.push(chunk);
|
|
350
|
+
}
|
|
351
|
+
const body = Buffer.concat(chunks);
|
|
352
|
+
return new Request(fullUrl, {
|
|
353
|
+
method: "POST",
|
|
354
|
+
headers,
|
|
355
|
+
body,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return new Request(fullUrl, { method: "GET", headers });
|
|
359
|
+
};
|
|
360
|
+
try {
|
|
361
|
+
const request = await buildRequest();
|
|
362
|
+
const ip = req.socket?.remoteAddress || "127.0.0.1";
|
|
363
|
+
// Use handleRequest which properly handles all handler types (typed, raw, stream, sse, html)
|
|
364
|
+
const response = await appServer.handleRequest(request, routeName, ip, { corsHeaders: { "Access-Control-Allow-Origin": "*" } });
|
|
365
|
+
if (!response) {
|
|
366
|
+
return next();
|
|
367
|
+
}
|
|
368
|
+
// Stream the response back
|
|
369
|
+
res.statusCode = response.status;
|
|
370
|
+
for (const [key, value] of response.headers) {
|
|
371
|
+
res.setHeader(key, value);
|
|
372
|
+
}
|
|
373
|
+
// Flush headers immediately for streaming responses
|
|
374
|
+
if (typeof res.flushHeaders === "function") {
|
|
375
|
+
res.flushHeaders();
|
|
376
|
+
}
|
|
377
|
+
// Handle body streaming (non-blocking for continuous streams like MJPEG)
|
|
378
|
+
if (response.body) {
|
|
379
|
+
const reader = response.body.getReader();
|
|
380
|
+
let closed = false;
|
|
381
|
+
// Handle client disconnect
|
|
382
|
+
req.on("close", () => {
|
|
383
|
+
closed = true;
|
|
384
|
+
reader.cancel().catch(() => { });
|
|
385
|
+
});
|
|
386
|
+
// Pump without awaiting - allows continuous streams
|
|
387
|
+
const pump = async () => {
|
|
388
|
+
try {
|
|
389
|
+
while (!closed) {
|
|
390
|
+
const { done, value } = await reader.read();
|
|
391
|
+
if (done || closed) {
|
|
392
|
+
if (!closed)
|
|
393
|
+
res.end();
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
// Write and check if client is still connected
|
|
397
|
+
const canContinue = res.write(value);
|
|
398
|
+
if (!canContinue && !closed) {
|
|
399
|
+
// Backpressure - wait for drain
|
|
400
|
+
await new Promise(resolve => res.once("drain", resolve));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
if (!closed)
|
|
406
|
+
res.end();
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
pump(); // Don't await - let it run in background
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
res.end();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
console.error("[donkeylabs-dev] Request error:", err);
|
|
417
|
+
res.statusCode = err.status || 500;
|
|
418
|
+
res.setHeader("Content-Type", "application/json");
|
|
419
|
+
res.end(JSON.stringify({ error: err.message || "Internal error" }));
|
|
420
|
+
}
|
|
421
|
+
return; // Don't call next()
|
|
422
|
+
}
|
|
423
|
+
next();
|
|
424
|
+
};
|
|
425
|
+
// CORS preflight
|
|
426
|
+
const corsMiddleware = (req, res, next) => {
|
|
427
|
+
if (req.method === "OPTIONS") {
|
|
428
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
429
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
430
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
431
|
+
res.statusCode = 204;
|
|
432
|
+
res.end();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
next();
|
|
436
|
+
};
|
|
437
|
+
// Add to front of middleware stack
|
|
438
|
+
const stack = server.middlewares.stack;
|
|
439
|
+
if (stack && Array.isArray(stack)) {
|
|
440
|
+
stack.unshift({ route: "", handle: corsMiddleware });
|
|
441
|
+
stack.unshift({ route: "", handle: inProcessMiddleware });
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
server.middlewares.use(inProcessMiddleware);
|
|
445
|
+
server.middlewares.use(corsMiddleware);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// ========== SUBPROCESS MODE (bun run dev) ==========
|
|
451
|
+
// Spawn backend as separate process and proxy requests
|
|
452
|
+
console.log(`[donkeylabs-dev] Starting subprocess mode (backend on port ${backendPort})`);
|
|
453
|
+
const bootstrapCode = `
|
|
454
|
+
const serverModule = await import("${serverEntryResolved}");
|
|
455
|
+
const server = serverModule.server || serverModule.default;
|
|
456
|
+
|
|
457
|
+
if (!server) {
|
|
458
|
+
console.error("[donkeylabs-backend] No server export found");
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
server.port = ${backendPort};
|
|
463
|
+
await server.start();
|
|
464
|
+
console.log("[donkeylabs-backend] Server ready on port ${backendPort}");
|
|
465
|
+
`;
|
|
466
|
+
backendProcess = spawn("bun", ["--eval", bootstrapCode], {
|
|
467
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
468
|
+
env: { ...process.env, NODE_ENV: "development" },
|
|
469
|
+
});
|
|
470
|
+
backendProcess.stdout?.on("data", (data) => {
|
|
471
|
+
const msg = data.toString().trim();
|
|
472
|
+
if (msg) {
|
|
473
|
+
console.log(msg);
|
|
474
|
+
if (msg.includes("Server ready") || msg.includes("Server running")) {
|
|
475
|
+
backendReady = true;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
backendProcess.stderr?.on("data", (data) => {
|
|
480
|
+
const msg = data.toString().trim();
|
|
481
|
+
if (msg)
|
|
482
|
+
console.error(msg);
|
|
483
|
+
});
|
|
484
|
+
backendProcess.on("error", (err) => {
|
|
485
|
+
console.error("[donkeylabs-dev] Failed to start backend:", err);
|
|
486
|
+
});
|
|
487
|
+
backendProcess.on("exit", (code) => {
|
|
488
|
+
if (code !== 0 && code !== null) {
|
|
489
|
+
console.error(`[donkeylabs-dev] Backend exited with code ${code}`);
|
|
490
|
+
}
|
|
491
|
+
backendProcess = null;
|
|
492
|
+
backendReady = false;
|
|
493
|
+
});
|
|
494
|
+
server.httpServer?.on("close", () => {
|
|
495
|
+
if (backendProcess) {
|
|
496
|
+
backendProcess.kill();
|
|
497
|
+
backendProcess = null;
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
// Return middleware setup function
|
|
501
|
+
return () => {
|
|
502
|
+
const waitForBackend = new Promise((resolve) => {
|
|
503
|
+
const check = () => (backendReady ? resolve() : setTimeout(check, 100));
|
|
504
|
+
setTimeout(check, 500);
|
|
505
|
+
setTimeout(() => {
|
|
506
|
+
if (!backendReady) {
|
|
507
|
+
console.warn("[donkeylabs-dev] Backend startup timeout");
|
|
508
|
+
resolve();
|
|
509
|
+
}
|
|
510
|
+
}, 10000);
|
|
511
|
+
});
|
|
512
|
+
// Proxy middleware - handles GET and POST for API routes
|
|
513
|
+
const proxyMiddleware = (req, res, next) => {
|
|
514
|
+
const url = req.url || "/";
|
|
515
|
+
const urlObj = new URL(url, "http://localhost");
|
|
516
|
+
const pathname = urlObj.pathname;
|
|
517
|
+
// API routes are GET or POST to paths like /routeName.action
|
|
518
|
+
const isApiRoute = (req.method === "GET" || req.method === "POST") && /^\/[a-zA-Z][a-zA-Z0-9_.]*$/.test(pathname);
|
|
519
|
+
if (!isApiRoute)
|
|
520
|
+
return next();
|
|
521
|
+
waitForBackend.then(() => {
|
|
522
|
+
let proxyAborted = false;
|
|
523
|
+
const proxyReq = http.request({
|
|
524
|
+
hostname: "localhost",
|
|
525
|
+
port: backendPort,
|
|
526
|
+
path: url, // Include query string
|
|
527
|
+
method: req.method,
|
|
528
|
+
headers: { ...req.headers, host: `localhost:${backendPort}` },
|
|
529
|
+
}, (proxyRes) => {
|
|
530
|
+
if (proxyAborted)
|
|
531
|
+
return;
|
|
532
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
533
|
+
res.statusCode = proxyRes.statusCode || 200;
|
|
534
|
+
for (const [k, v] of Object.entries(proxyRes.headers)) {
|
|
535
|
+
if (v)
|
|
536
|
+
res.setHeader(k, v);
|
|
537
|
+
}
|
|
538
|
+
// Flush headers for streaming responses
|
|
539
|
+
if (typeof res.flushHeaders === "function") {
|
|
540
|
+
res.flushHeaders();
|
|
541
|
+
}
|
|
542
|
+
// Stream response back (works for binary/streaming responses)
|
|
543
|
+
proxyRes.pipe(res);
|
|
544
|
+
// Clean up on proxy response end
|
|
545
|
+
proxyRes.on("end", () => {
|
|
546
|
+
if (!proxyAborted)
|
|
547
|
+
res.end();
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
// Handle client disconnect - abort proxy request
|
|
551
|
+
req.on("close", () => {
|
|
552
|
+
if (!proxyAborted) {
|
|
553
|
+
proxyAborted = true;
|
|
554
|
+
proxyReq.destroy();
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
proxyReq.on("error", (err) => {
|
|
558
|
+
if (proxyAborted)
|
|
559
|
+
return; // Ignore errors after abort
|
|
560
|
+
console.error(`[donkeylabs-dev] Proxy error:`, err.message);
|
|
561
|
+
res.statusCode = 502;
|
|
562
|
+
res.end(JSON.stringify({ error: "Backend unavailable" }));
|
|
563
|
+
});
|
|
564
|
+
// For POST, pipe the body; for GET, just end
|
|
565
|
+
if (req.method === "POST") {
|
|
566
|
+
req.pipe(proxyReq);
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
proxyReq.end();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
};
|
|
573
|
+
const corsMiddleware = (req, res, next) => {
|
|
574
|
+
if (req.method === "OPTIONS") {
|
|
575
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
576
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
577
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
578
|
+
res.statusCode = 204;
|
|
579
|
+
res.end();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
next();
|
|
583
|
+
};
|
|
584
|
+
const stack = server.middlewares.stack;
|
|
585
|
+
if (stack && Array.isArray(stack)) {
|
|
586
|
+
stack.unshift({ route: "", handle: corsMiddleware });
|
|
587
|
+
stack.unshift({ route: "", handle: proxyMiddleware });
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
server.middlewares.use(proxyMiddleware);
|
|
591
|
+
server.middlewares.use(corsMiddleware);
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
async closeBundle() {
|
|
597
|
+
stopFileWatcher();
|
|
598
|
+
viteServer = null;
|
|
599
|
+
if (backendProcess) {
|
|
600
|
+
backendProcess.kill();
|
|
601
|
+
backendProcess = null;
|
|
602
|
+
}
|
|
603
|
+
if (appServer) {
|
|
604
|
+
await appServer.shutdown?.();
|
|
605
|
+
appServer = null;
|
|
606
|
+
setDevServer(null);
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
export default donkeylabsDev;
|
package/package.json
CHANGED
|
@@ -1,38 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@donkeylabs/adapter-sveltekit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SvelteKit adapter for @donkeylabs/server - seamless SSR/browser API integration",
|
|
6
|
-
"main": "./
|
|
7
|
-
"types": "./
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"types": "./
|
|
11
|
-
"import": "./
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
12
|
},
|
|
13
13
|
"./client": {
|
|
14
|
-
"types": "./
|
|
15
|
-
"import": "./
|
|
14
|
+
"types": "./dist/client/index.d.ts",
|
|
15
|
+
"import": "./dist/client/index.js"
|
|
16
16
|
},
|
|
17
17
|
"./hooks": {
|
|
18
|
-
"types": "./
|
|
19
|
-
"import": "./
|
|
18
|
+
"types": "./dist/hooks/index.d.ts",
|
|
19
|
+
"import": "./dist/hooks/index.js"
|
|
20
20
|
},
|
|
21
21
|
"./generator": {
|
|
22
|
-
"types": "./
|
|
23
|
-
"import": "./
|
|
22
|
+
"types": "./dist/generator/index.d.ts",
|
|
23
|
+
"import": "./dist/generator/index.js"
|
|
24
24
|
},
|
|
25
25
|
"./vite": {
|
|
26
|
-
"types": "./
|
|
27
|
-
"import": "./
|
|
26
|
+
"types": "./dist/vite.d.ts",
|
|
27
|
+
"import": "./dist/vite.js"
|
|
28
28
|
}
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
|
-
"
|
|
31
|
+
"dist",
|
|
32
32
|
"LICENSE",
|
|
33
33
|
"README.md"
|
|
34
34
|
],
|
|
35
35
|
"scripts": {
|
|
36
|
+
"build": "tsc -p tsconfig.build.json",
|
|
37
|
+
"prepublishOnly": "npm run build",
|
|
36
38
|
"typecheck": "bun --bun tsc --noEmit"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|