@donkeylabs/adapter-sveltekit 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 DonkeyLabs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,181 @@
1
+ # @donkeylabs/adapter-sveltekit
2
+
3
+ SvelteKit adapter for `@donkeylabs/server`. Enables seamless integration between SvelteKit and your backend API with:
4
+
5
+ - **Single Bun process** serves both SvelteKit pages and API routes
6
+ - **Direct service calls during SSR** (no HTTP overhead)
7
+ - **Unified API client** works identically in SSR and browser
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ bun add @donkeylabs/adapter-sveltekit
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ### 1. Configure the Adapter
18
+
19
+ ```ts
20
+ // svelte.config.js
21
+ import adapter from "@donkeylabs/adapter-sveltekit";
22
+
23
+ export default {
24
+ kit: {
25
+ adapter: adapter({
26
+ serverEntry: "./src/server/index.ts",
27
+ }),
28
+ },
29
+ };
30
+ ```
31
+
32
+ ### 2. Add Vite Plugin for Development
33
+
34
+ ```ts
35
+ // vite.config.ts
36
+ import { sveltekit } from "@sveltejs/kit/vite";
37
+ import { donkeylabsDev } from "@donkeylabs/adapter-sveltekit/vite";
38
+ import { defineConfig } from "vite";
39
+
40
+ export default defineConfig({
41
+ plugins: [
42
+ donkeylabsDev({ serverEntry: "./src/server/index.ts" }),
43
+ sveltekit(),
44
+ ],
45
+ });
46
+ ```
47
+
48
+ ### 3. Set Up Server Hooks
49
+
50
+ ```ts
51
+ // src/hooks.server.ts
52
+ import { createHandle } from "@donkeylabs/adapter-sveltekit/hooks";
53
+
54
+ export const handle = createHandle();
55
+ ```
56
+
57
+ ### 4. Generate API Client
58
+
59
+ ```ts
60
+ // donkeylabs.config.ts
61
+ import { defineConfig } from "@donkeylabs/server";
62
+ import { SvelteKitClientGenerator } from "@donkeylabs/adapter-sveltekit/generator";
63
+
64
+ export default defineConfig({
65
+ plugins: ["./src/server/plugins/**/index.ts"],
66
+ client: {
67
+ output: "./src/lib/api.ts",
68
+ generator: SvelteKitClientGenerator,
69
+ },
70
+ });
71
+ ```
72
+
73
+ ### 5. Use the API Client
74
+
75
+ ```svelte
76
+ <!-- src/routes/+page.svelte -->
77
+ <script lang="ts">
78
+ import { ApiClient } from "$lib/api";
79
+
80
+ const api = new ApiClient();
81
+
82
+ async function greet() {
83
+ const result = await api.greet({ name: "World" });
84
+ console.log(result.message);
85
+ }
86
+ </script>
87
+ ```
88
+
89
+ In SSR (`+page.server.ts`), pass `locals` for direct service calls:
90
+
91
+ ```ts
92
+ // src/routes/+page.server.ts
93
+ import { ApiClient } from "$lib/api";
94
+
95
+ export async function load({ locals, fetch }) {
96
+ const api = new ApiClient({ locals, fetch });
97
+ const data = await api.getData({}); // Direct call, no HTTP!
98
+ return { data };
99
+ }
100
+ ```
101
+
102
+ ## Development Modes
103
+
104
+ ### Recommended: In-Process Mode
105
+
106
+ Run with `bun --bun` for single-process development:
107
+
108
+ ```bash
109
+ bun --bun run dev
110
+ ```
111
+
112
+ - Single port (5173)
113
+ - Direct service calls during SSR
114
+ - Hot reload for both frontend and backend
115
+
116
+ ### Fallback: Subprocess Mode
117
+
118
+ Run without `--bun` flag:
119
+
120
+ ```bash
121
+ bun run dev
122
+ ```
123
+
124
+ - Two processes (Vite on 5173, backend on 3001)
125
+ - API requests proxied to backend
126
+ - Use when in-process mode has compatibility issues
127
+
128
+ ## Production Build
129
+
130
+ ```bash
131
+ bun run build
132
+ bun build/server/entry.js
133
+ ```
134
+
135
+ ## Package Exports
136
+
137
+ | Export | Description |
138
+ |--------|-------------|
139
+ | `@donkeylabs/adapter-sveltekit` | Main adapter function |
140
+ | `@donkeylabs/adapter-sveltekit/client` | Unified API client base |
141
+ | `@donkeylabs/adapter-sveltekit/hooks` | SvelteKit hooks helpers |
142
+ | `@donkeylabs/adapter-sveltekit/generator` | Client code generator |
143
+ | `@donkeylabs/adapter-sveltekit/vite` | Vite dev plugin |
144
+
145
+ ## Type Definitions
146
+
147
+ Add to your `app.d.ts`:
148
+
149
+ ```ts
150
+ // src/app.d.ts
151
+ import type { DonkeylabsLocals } from "@donkeylabs/adapter-sveltekit/hooks";
152
+
153
+ declare global {
154
+ namespace App {
155
+ interface Locals extends DonkeylabsLocals {}
156
+ }
157
+ }
158
+
159
+ export {};
160
+ ```
161
+
162
+ ## SSE (Server-Sent Events)
163
+
164
+ Subscribe to real-time events in the browser:
165
+
166
+ ```ts
167
+ const api = new ApiClient();
168
+
169
+ const unsubscribe = api.sse.subscribe(
170
+ ["notifications", "updates"],
171
+ (event, data) => {
172
+ console.log("Event:", event, data);
173
+ }
174
+ );
175
+
176
+ // Later: unsubscribe();
177
+ ```
178
+
179
+ ## License
180
+
181
+ MIT
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@donkeylabs/adapter-sveltekit",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "SvelteKit adapter for @donkeylabs/server - seamless SSR/browser API integration",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts"
12
+ },
13
+ "./client": {
14
+ "types": "./src/client/index.ts",
15
+ "import": "./src/client/index.ts"
16
+ },
17
+ "./hooks": {
18
+ "types": "./src/hooks/index.ts",
19
+ "import": "./src/hooks/index.ts"
20
+ },
21
+ "./generator": {
22
+ "types": "./src/generator/index.ts",
23
+ "import": "./src/generator/index.ts"
24
+ },
25
+ "./vite": {
26
+ "types": "./src/vite.ts",
27
+ "import": "./src/vite.ts"
28
+ }
29
+ },
30
+ "files": [
31
+ "src",
32
+ "LICENSE",
33
+ "README.md"
34
+ ],
35
+ "scripts": {
36
+ "typecheck": "bun --bun tsc --noEmit"
37
+ },
38
+ "peerDependencies": {
39
+ "@sveltejs/kit": "^2.0.0",
40
+ "@donkeylabs/server": "*"
41
+ },
42
+ "keywords": [
43
+ "sveltekit",
44
+ "svelte",
45
+ "adapter",
46
+ "api",
47
+ "rpc",
48
+ "ssr",
49
+ "bun",
50
+ "typescript"
51
+ ],
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "https://github.com/donkeylabs/server",
55
+ "directory": "packages/adapter-sveltekit"
56
+ },
57
+ "license": "MIT"
58
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Unified API client for @donkeylabs/adapter-sveltekit
3
+ *
4
+ * Auto-detects environment:
5
+ * - SSR: Direct service calls through locals (no HTTP)
6
+ * - Browser: HTTP calls to API routes
7
+ */
8
+
9
+ export interface RequestOptions {
10
+ headers?: Record<string, string>;
11
+ signal?: AbortSignal;
12
+ }
13
+
14
+ export interface ClientOptions {
15
+ /** Base URL for HTTP calls. Defaults to empty string (relative URLs). */
16
+ baseUrl?: string;
17
+ /** SvelteKit locals object for SSR direct calls. */
18
+ locals?: any;
19
+ /** Custom fetch function. In SSR, pass event.fetch to handle relative URLs. */
20
+ fetch?: typeof fetch;
21
+ }
22
+
23
+ export interface SSESubscription {
24
+ unsubscribe: () => void;
25
+ }
26
+
27
+ /**
28
+ * Base class for unified API clients.
29
+ * Extend this class with your generated route methods.
30
+ */
31
+ export class UnifiedApiClientBase {
32
+ protected baseUrl: string;
33
+ protected locals?: any;
34
+ protected isSSR: boolean;
35
+ protected customFetch?: typeof fetch;
36
+
37
+ constructor(options?: ClientOptions) {
38
+ this.baseUrl = options?.baseUrl ?? "";
39
+ this.locals = options?.locals;
40
+ this.isSSR = typeof window === "undefined";
41
+ this.customFetch = options?.fetch;
42
+ }
43
+
44
+ /**
45
+ * Make a request to an API route.
46
+ * Automatically uses direct calls in SSR (when locals.handleRoute is available), HTTP otherwise.
47
+ */
48
+ protected async request<TInput, TOutput>(
49
+ route: string,
50
+ input: TInput,
51
+ options?: RequestOptions
52
+ ): Promise<TOutput> {
53
+ // Use direct route handler if available (SSR with locals)
54
+ if (this.locals?.handleRoute) {
55
+ return this.locals.handleRoute(route, input);
56
+ }
57
+ // Fall back to HTTP (browser or SSR without locals)
58
+ return this.httpCall<TInput, TOutput>(route, input, options);
59
+ }
60
+
61
+ /**
62
+ * HTTP call to API endpoint (browser or SSR with event.fetch).
63
+ */
64
+ private async httpCall<TInput, TOutput>(
65
+ route: string,
66
+ input: TInput,
67
+ options?: RequestOptions
68
+ ): Promise<TOutput> {
69
+ const url = `${this.baseUrl}/${route}`;
70
+ const fetchFn = this.customFetch ?? fetch;
71
+
72
+ const response = await fetchFn(url, {
73
+ method: "POST",
74
+ headers: {
75
+ "Content-Type": "application/json",
76
+ ...options?.headers,
77
+ },
78
+ body: JSON.stringify(input),
79
+ signal: options?.signal,
80
+ });
81
+
82
+ if (!response.ok) {
83
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
84
+ throw new Error(error.message || error.error || `HTTP ${response.status}`);
85
+ }
86
+
87
+ return response.json();
88
+ }
89
+
90
+ /**
91
+ * SSE (Server-Sent Events) subscription.
92
+ * Only works in the browser.
93
+ */
94
+ sse = {
95
+ /**
96
+ * Subscribe to SSE channels.
97
+ * Returns a function to unsubscribe.
98
+ *
99
+ * @example
100
+ * const unsub = api.sse.subscribe(["notifications"], (event, data) => {
101
+ * console.log(event, data);
102
+ * });
103
+ * // Later: unsub();
104
+ */
105
+ subscribe: (
106
+ channels: string[],
107
+ callback: (event: string, data: any) => void,
108
+ options?: { reconnect?: boolean }
109
+ ): (() => void) => {
110
+ if (typeof window === "undefined") {
111
+ // SSR - return no-op
112
+ return () => {};
113
+ }
114
+
115
+ const url = `${this.baseUrl}/sse?channels=${channels.join(",")}`;
116
+ let eventSource: EventSource | null = new EventSource(url);
117
+ let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
118
+
119
+ // Known event types from the server
120
+ const eventTypes = ['cron-event', 'job-completed', 'internal-event', 'manual', 'message'];
121
+
122
+ const handleMessage = (e: MessageEvent) => {
123
+ try {
124
+ const data = JSON.parse(e.data);
125
+ callback(e.type || "message", data);
126
+ } catch {
127
+ callback(e.type || "message", e.data);
128
+ }
129
+ };
130
+
131
+ const handleError = () => {
132
+ if (options?.reconnect !== false && eventSource) {
133
+ eventSource.close();
134
+ reconnectTimeout = setTimeout(() => {
135
+ eventSource = new EventSource(url);
136
+ // Re-attach all listeners on reconnect
137
+ eventSource.onmessage = handleMessage;
138
+ eventSource.onerror = handleError;
139
+ for (const type of eventTypes) {
140
+ eventSource.addEventListener(type, handleMessage);
141
+ }
142
+ }, 1000);
143
+ }
144
+ };
145
+
146
+ // Listen for unnamed messages
147
+ eventSource.onmessage = handleMessage;
148
+ eventSource.onerror = handleError;
149
+
150
+ // Listen for named event types (SSE sends "event: type-name")
151
+ for (const type of eventTypes) {
152
+ eventSource.addEventListener(type, handleMessage);
153
+ }
154
+
155
+ // Return unsubscribe function
156
+ return () => {
157
+ if (reconnectTimeout) {
158
+ clearTimeout(reconnectTimeout);
159
+ }
160
+ if (eventSource) {
161
+ eventSource.close();
162
+ eventSource = null;
163
+ }
164
+ };
165
+ },
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Create an API client instance.
171
+ * Call with locals and fetch in SSR, without in browser.
172
+ *
173
+ * @example
174
+ * // +page.server.ts (SSR)
175
+ * const api = createApiClient({ locals, fetch });
176
+ *
177
+ * // +page.svelte (browser)
178
+ * const api = createApiClient();
179
+ */
180
+ export function createApiClient<T extends UnifiedApiClientBase>(
181
+ ClientClass: new (options?: ClientOptions) => T,
182
+ options?: ClientOptions
183
+ ): T {
184
+ return new ClientClass(options);
185
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * SvelteKit-specific client generator
3
+ *
4
+ * This generator extends the core @donkeylabs/server generator
5
+ * to produce clients that work with both SSR (direct calls) and browser (HTTP).
6
+ */
7
+
8
+ import { mkdir, writeFile } from "node:fs/promises";
9
+ import { dirname } from "node:path";
10
+ import {
11
+ generateClientFromRoutes,
12
+ type ExtractedRoute,
13
+ type ClientGeneratorOptions,
14
+ } from "@donkeylabs/server/generator";
15
+
16
+ /** SvelteKit-specific generator options */
17
+ export const svelteKitGeneratorOptions: ClientGeneratorOptions = {
18
+ baseImport:
19
+ 'import { UnifiedApiClientBase, type ApiClientOptions } from "@donkeylabs/adapter-sveltekit/client";',
20
+ baseClass: "UnifiedApiClientBase",
21
+ constructorSignature: "options?: ApiClientOptions",
22
+ constructorBody: "super(options);",
23
+ factoryFunction: `/**
24
+ * Create an API client instance
25
+ *
26
+ * @param options.locals - Pass SvelteKit locals for SSR direct calls (no HTTP overhead)
27
+ * @param options.baseUrl - Override the base URL for HTTP calls
28
+ *
29
+ * @example SSR usage in +page.server.ts:
30
+ * \`\`\`ts
31
+ * export const load = async ({ locals }) => {
32
+ * const api = createApi({ locals });
33
+ * const data = await api.myRoute.get({}); // Direct call, no HTTP!
34
+ * return { data };
35
+ * };
36
+ * \`\`\`
37
+ *
38
+ * @example Browser usage in +page.svelte:
39
+ * \`\`\`svelte
40
+ * <script>
41
+ * import { createApi } from '$lib/api';
42
+ * const api = createApi(); // HTTP calls
43
+ * let data = $state(null);
44
+ * async function load() {
45
+ * data = await api.myRoute.get({});
46
+ * }
47
+ * </script>
48
+ * \`\`\`
49
+ */
50
+ export function createApi(options?: ApiClientOptions) {
51
+ return new ApiClient(options);
52
+ }`,
53
+ };
54
+
55
+ /**
56
+ * Generate a SvelteKit-compatible API client
57
+ *
58
+ * This is called by the donkeylabs CLI when adapter is set to "@donkeylabs/adapter-sveltekit"
59
+ */
60
+ export async function generateClient(
61
+ _config: Record<string, unknown>,
62
+ routes: ExtractedRoute[],
63
+ outputPath: string
64
+ ): Promise<void> {
65
+ const code = generateClientFromRoutes(routes, svelteKitGeneratorOptions);
66
+
67
+ // Ensure output directory exists
68
+ const outputDir = dirname(outputPath);
69
+ await mkdir(outputDir, { recursive: true });
70
+
71
+ // Write the generated client
72
+ await writeFile(outputPath, code);
73
+ }
74
+
75
+ // Re-export building blocks for advanced usage
76
+ export {
77
+ generateClientFromRoutes,
78
+ type ExtractedRoute,
79
+ type ClientGeneratorOptions,
80
+ } from "@donkeylabs/server/generator";
@@ -0,0 +1,124 @@
1
+ /**
2
+ * SvelteKit hooks helper for @donkeylabs/adapter-sveltekit
3
+ */
4
+
5
+ import type { Handle } from "@sveltejs/kit";
6
+
7
+ // Try to import dev server reference (only available in dev mode)
8
+ let getDevServer: (() => any) | undefined;
9
+ try {
10
+ // Dynamic import to avoid bundling vite.ts in production
11
+ const viteModule = await import("../vite.js");
12
+ getDevServer = viteModule.getDevServer;
13
+ } catch {
14
+ // Not in dev mode or vite not available
15
+ }
16
+
17
+ export interface DonkeylabsPlatform {
18
+ donkeylabs?: {
19
+ services: Record<string, any>;
20
+ core: {
21
+ logger: any;
22
+ cache: any;
23
+ events: any;
24
+ cron: any;
25
+ jobs: any;
26
+ sse: any;
27
+ rateLimiter: any;
28
+ db: any;
29
+ };
30
+ /** Direct route handler for SSR (no HTTP!) */
31
+ handleRoute: (routeName: string, input: any) => Promise<any>;
32
+ };
33
+ }
34
+
35
+ export interface DonkeylabsLocals {
36
+ plugins: Record<string, any>;
37
+ core: {
38
+ logger: any;
39
+ cache: any;
40
+ events: any;
41
+ sse: any;
42
+ };
43
+ db: any;
44
+ ip: string;
45
+ /** Direct route handler for SSR API calls */
46
+ handleRoute?: (routeName: string, input: any) => Promise<any>;
47
+ }
48
+
49
+ /**
50
+ * Create a SvelteKit handle function that populates event.locals
51
+ * with @donkeylabs/server context.
52
+ *
53
+ * @example
54
+ * // src/hooks.server.ts
55
+ * import { createHandle } from "@donkeylabs/adapter-sveltekit/hooks";
56
+ * export const handle = createHandle();
57
+ */
58
+ export function createHandle(): Handle {
59
+ return async ({ event, resolve }) => {
60
+ const platform = event.platform as DonkeylabsPlatform | undefined;
61
+
62
+ if (platform?.donkeylabs) {
63
+ // Production mode: use platform.donkeylabs from adapter
64
+ const { services, core, handleRoute } = platform.donkeylabs;
65
+
66
+ // Populate locals with server context
67
+ (event.locals as DonkeylabsLocals).plugins = services;
68
+ (event.locals as DonkeylabsLocals).core = {
69
+ logger: core.logger,
70
+ cache: core.cache,
71
+ events: core.events,
72
+ sse: core.sse,
73
+ };
74
+ (event.locals as DonkeylabsLocals).db = core.db;
75
+ (event.locals as DonkeylabsLocals).ip = event.getClientAddress();
76
+ // Expose the direct route handler for SSR API calls
77
+ (event.locals as DonkeylabsLocals).handleRoute = handleRoute;
78
+ } else if (getDevServer) {
79
+ // Dev mode: use global dev server from vite plugin
80
+ const devServer = getDevServer();
81
+ if (devServer) {
82
+ const core = devServer.getCore();
83
+ const plugins = devServer.getServices();
84
+
85
+ (event.locals as DonkeylabsLocals).plugins = plugins;
86
+ (event.locals as DonkeylabsLocals).core = {
87
+ logger: core.logger,
88
+ cache: core.cache,
89
+ events: core.events,
90
+ sse: core.sse,
91
+ };
92
+ (event.locals as DonkeylabsLocals).db = core.db;
93
+ (event.locals as DonkeylabsLocals).ip = event.getClientAddress();
94
+ // Direct route handler for SSR
95
+ (event.locals as DonkeylabsLocals).handleRoute = async (routeName: string, input: any) => {
96
+ return devServer.callRoute(routeName, input, event.getClientAddress());
97
+ };
98
+ }
99
+ }
100
+
101
+ return resolve(event);
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Sequence multiple handle functions together.
107
+ *
108
+ * @example
109
+ * import { sequence, createHandle } from "@donkeylabs/adapter-sveltekit/hooks";
110
+ * export const handle = sequence(createHandle(), myOtherHandle);
111
+ */
112
+ export function sequence(...handlers: Handle[]): Handle {
113
+ return async ({ event, resolve }) => {
114
+ let resolveChain = resolve;
115
+
116
+ for (let i = handlers.length - 1; i >= 0; i--) {
117
+ const handler = handlers[i];
118
+ const next = resolveChain;
119
+ resolveChain = (event) => handler({ event, resolve: next });
120
+ }
121
+
122
+ return resolveChain(event);
123
+ };
124
+ }
package/src/index.ts ADDED
@@ -0,0 +1,395 @@
1
+ /**
2
+ * @donkeylabs/adapter-sveltekit
3
+ *
4
+ * SvelteKit adapter that integrates with @donkeylabs/server.
5
+ * - Single Bun process serves both SvelteKit pages and API routes
6
+ * - Direct service calls during SSR (no HTTP overhead)
7
+ * - Unified API client works in both SSR and browser
8
+ */
9
+
10
+ import type { Adapter, Builder } from "@sveltejs/kit";
11
+ import { fileURLToPath } from "node:url";
12
+ import { dirname, join, relative, resolve } from "node:path";
13
+ import { writeFileSync, mkdirSync } from "node:fs";
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+
18
+ export interface AdapterOptions {
19
+ /**
20
+ * Output directory for the built app.
21
+ * @default "build"
22
+ */
23
+ out?: string;
24
+
25
+ /**
26
+ * Path to your @donkeylabs/server entry file.
27
+ * This file should export a configured AppServer instance.
28
+ *
29
+ * @example "./src/server/index.ts"
30
+ */
31
+ serverEntry: string;
32
+
33
+ /**
34
+ * Whether to precompress static assets with gzip/brotli.
35
+ * @default true
36
+ */
37
+ precompress?: boolean;
38
+
39
+ /**
40
+ * Environment variable prefix for PORT and HOST.
41
+ * @default ""
42
+ */
43
+ envPrefix?: string;
44
+
45
+ /**
46
+ * Enable development mode features.
47
+ * @default false
48
+ */
49
+ development?: boolean;
50
+ }
51
+
52
+ export default function adapter(options: AdapterOptions): Adapter {
53
+ const {
54
+ out = "build",
55
+ serverEntry,
56
+ precompress = true,
57
+ envPrefix = "",
58
+ development = false,
59
+ } = options;
60
+
61
+ if (!serverEntry) {
62
+ throw new Error("@donkeylabs/adapter-sveltekit: serverEntry option is required");
63
+ }
64
+
65
+ return {
66
+ name: "@donkeylabs/adapter-sveltekit",
67
+
68
+ async adapt(builder: Builder) {
69
+ const serverDir = join(out, "server");
70
+ const clientDir = join(out, "client");
71
+ const prerenderedDir = join(out, "prerendered");
72
+
73
+ // 1. Clean and create output directories
74
+ builder.rimraf(out);
75
+ mkdirSync(serverDir, { recursive: true });
76
+
77
+ builder.log.minor("Writing SvelteKit server files...");
78
+
79
+ // 2. Write SvelteKit server files
80
+ builder.writeServer(serverDir);
81
+
82
+ // 3. Copy static assets and prerendered pages
83
+ const clientFiles = builder.writeClient(clientDir);
84
+ const prerenderedFiles = builder.writePrerendered(prerenderedDir);
85
+
86
+ // 4. Generate the manifest
87
+ const relativePath = relative(serverDir, ".");
88
+ builder.generateManifest({ relativePath });
89
+
90
+ // 5. Generate the unified runtime entry point
91
+ const serverEntryResolved = resolve(serverEntry);
92
+ const serverEntryRelative = relative(serverDir, serverEntryResolved);
93
+
94
+ const entryCode = generateEntryPoint({
95
+ serverEntryRelative,
96
+ envPrefix,
97
+ development,
98
+ clientDir: relative(serverDir, clientDir),
99
+ prerenderedDir: relative(serverDir, prerenderedDir),
100
+ });
101
+
102
+ writeFileSync(join(serverDir, "entry.js"), entryCode);
103
+
104
+ // 6. Write runtime handler (inline the full implementation)
105
+ writeFileSync(join(serverDir, "handler.js"), generateRuntimeHandler());
106
+
107
+ // 7. Precompress if enabled
108
+ if (precompress) {
109
+ builder.log.minor("Compressing assets...");
110
+ await builder.compress(clientDir);
111
+ if (prerenderedFiles.length > 0) {
112
+ await builder.compress(prerenderedDir);
113
+ }
114
+ }
115
+
116
+ builder.log.success(`Adapter output written to ${out}`);
117
+ builder.log.minor(`Run with: bun ${out}/server/entry.js`);
118
+ },
119
+ };
120
+ }
121
+
122
+ function generateEntryPoint(config: {
123
+ serverEntryRelative: string;
124
+ envPrefix: string;
125
+ development: boolean;
126
+ clientDir: string;
127
+ prerenderedDir: string;
128
+ }): string {
129
+ const { serverEntryRelative, envPrefix, development, clientDir, prerenderedDir } = config;
130
+
131
+ return `// Generated by @donkeylabs/adapter-sveltekit
132
+ import { Server } from "./index.js";
133
+ import { manifest } from "./manifest.js";
134
+ import { createUnifiedServer } from "./handler.js";
135
+
136
+ // Import user's @donkeylabs/server setup
137
+ const serverModule = await import("${serverEntryRelative}");
138
+ const donkeylabsServer = serverModule.server || serverModule.default;
139
+
140
+ if (!donkeylabsServer) {
141
+ throw new Error(
142
+ "@donkeylabs/adapter-sveltekit: Could not find server export. " +
143
+ "Make sure your server entry file exports 'server' or uses default export."
144
+ );
145
+ }
146
+
147
+ // Initialize @donkeylabs/server (migrations, plugins, routes)
148
+ await donkeylabsServer.initialize();
149
+
150
+ // Create SvelteKit server
151
+ const svelteServer = new Server(manifest);
152
+ await svelteServer.init({ env: process.env });
153
+
154
+ // Configuration
155
+ const port = Number(process.env.${envPrefix}PORT) || 3000;
156
+ const host = process.env.${envPrefix}HOST || "0.0.0.0";
157
+ const development = ${development} || process.env.NODE_ENV === "development";
158
+
159
+ // Start unified server
160
+ createUnifiedServer({
161
+ svelteServer,
162
+ donkeylabsServer,
163
+ port,
164
+ host,
165
+ clientDir: "${clientDir}",
166
+ prerenderedDir: "${prerenderedDir}",
167
+ development,
168
+ });
169
+
170
+ console.log(\`Server running at http://\${host}:\${port}\`);
171
+ `;
172
+ }
173
+
174
+ function generateRuntimeHandler(): string {
175
+ // Inline the full runtime handler implementation
176
+ return `// Generated runtime handler by @donkeylabs/adapter-sveltekit
177
+ import { resolve, dirname } from "node:path";
178
+ import { fileURLToPath } from "node:url";
179
+
180
+ const __dirname = dirname(fileURLToPath(import.meta.url));
181
+
182
+ /**
183
+ * Create a unified Bun server that handles both SvelteKit and @donkeylabs/server requests.
184
+ */
185
+ export function createUnifiedServer(config) {
186
+ const {
187
+ svelteServer,
188
+ donkeylabsServer,
189
+ port,
190
+ host,
191
+ clientDir: clientDirRelative,
192
+ prerenderedDir: prerenderedDirRelative,
193
+ development = false,
194
+ } = config;
195
+
196
+ // Resolve paths relative to this script's directory
197
+ const clientDir = resolve(__dirname, clientDirRelative);
198
+ const prerenderedDir = resolve(__dirname, prerenderedDirRelative);
199
+
200
+ // Get services and core from @donkeylabs/server for injection into SvelteKit
201
+ const services = donkeylabsServer.getServices();
202
+ const core = donkeylabsServer.getCore();
203
+
204
+ Bun.serve({
205
+ port,
206
+ hostname: host,
207
+
208
+ async fetch(req, server) {
209
+ const url = new URL(req.url);
210
+ const pathname = url.pathname;
211
+ const ip = extractClientIP(req, server.requestIP(req)?.address);
212
+
213
+ // 1. Handle CORS preflight for API routes
214
+ if (req.method === "OPTIONS") {
215
+ return new Response(null, {
216
+ status: 204,
217
+ headers: getCorsHeaders(),
218
+ });
219
+ }
220
+
221
+ // 2. API routes (POST /route.name)
222
+ if (req.method === "POST") {
223
+ const routeName = pathname.slice(1); // Remove leading /
224
+
225
+ // Check if this is a registered API route
226
+ if (donkeylabsServer.hasRoute(routeName)) {
227
+ const response = await donkeylabsServer.handleRequest(req, routeName, ip, {
228
+ corsHeaders: getCorsHeaders(),
229
+ });
230
+ if (response) return response;
231
+ }
232
+ }
233
+
234
+ // 3. SSE endpoint
235
+ if (pathname === "/sse" && req.method === "GET") {
236
+ return handleSSE(req, core, ip);
237
+ }
238
+
239
+ // 4. Static assets (/_app/*, etc.)
240
+ if (isStaticAsset(pathname)) {
241
+ const staticResponse = await serveStatic(clientDir, pathname);
242
+ if (staticResponse) return staticResponse;
243
+ }
244
+
245
+ // 5. Prerendered pages
246
+ const prerenderedResponse = await servePrerendered(prerenderedDir, pathname);
247
+ if (prerenderedResponse) return prerenderedResponse;
248
+
249
+ // 6. SvelteKit pages
250
+ return handleSvelteKit(req, svelteServer, {
251
+ services,
252
+ core,
253
+ ip,
254
+ donkeylabsServer,
255
+ });
256
+ },
257
+ });
258
+ }
259
+
260
+ function isStaticAsset(pathname) {
261
+ return (
262
+ pathname.startsWith("/_app/") ||
263
+ pathname.startsWith("/static/") ||
264
+ pathname === "/favicon.ico" ||
265
+ pathname === "/robots.txt" ||
266
+ pathname === "/sitemap.xml" ||
267
+ /\\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot|webp|avif|json|webmanifest)$/.test(pathname)
268
+ );
269
+ }
270
+
271
+ async function serveStatic(clientDir, pathname) {
272
+ const filePath = clientDir + pathname;
273
+ const file = Bun.file(filePath);
274
+
275
+ if (await file.exists()) {
276
+ // Check for precompressed versions
277
+ const brFile = Bun.file(filePath + ".br");
278
+ const gzFile = Bun.file(filePath + ".gz");
279
+
280
+ if (await brFile.exists()) {
281
+ return new Response(brFile, {
282
+ headers: {
283
+ "Content-Type": file.type || "application/octet-stream",
284
+ "Content-Encoding": "br",
285
+ "Cache-Control": "public, max-age=31536000, immutable",
286
+ },
287
+ });
288
+ }
289
+
290
+ if (await gzFile.exists()) {
291
+ return new Response(gzFile, {
292
+ headers: {
293
+ "Content-Type": file.type || "application/octet-stream",
294
+ "Content-Encoding": "gzip",
295
+ "Cache-Control": "public, max-age=31536000, immutable",
296
+ },
297
+ });
298
+ }
299
+
300
+ return new Response(file, {
301
+ headers: {
302
+ "Content-Type": file.type || "application/octet-stream",
303
+ "Cache-Control": "public, max-age=31536000, immutable",
304
+ },
305
+ });
306
+ }
307
+
308
+ return null;
309
+ }
310
+
311
+ async function servePrerendered(prerenderedDir, pathname) {
312
+ let filePath = prerenderedDir + pathname;
313
+ if (!pathname.endsWith(".html") && !pathname.includes(".")) {
314
+ filePath = prerenderedDir + (pathname === "/" ? "/index" : pathname) + ".html";
315
+ }
316
+
317
+ const file = Bun.file(filePath);
318
+ if (await file.exists()) {
319
+ return new Response(file, {
320
+ headers: {
321
+ "Content-Type": "text/html",
322
+ "Cache-Control": "public, max-age=0, must-revalidate",
323
+ },
324
+ });
325
+ }
326
+
327
+ return null;
328
+ }
329
+
330
+ async function handleSvelteKit(req, svelteServer, context) {
331
+ return svelteServer.respond(req, {
332
+ getClientAddress: () => context.ip,
333
+ platform: {
334
+ donkeylabs: {
335
+ services: context.services,
336
+ core: context.core,
337
+ // Direct route handler for SSR API calls (no HTTP!)
338
+ handleRoute: async (routeName, input) => {
339
+ return context.donkeylabsServer.callRoute(routeName, input, context.ip);
340
+ },
341
+ },
342
+ },
343
+ });
344
+ }
345
+
346
+ function handleSSE(req, core, ip) {
347
+ const url = new URL(req.url);
348
+ const channels = url.searchParams.get("channels")?.split(",") || [];
349
+
350
+ if (channels.length === 0) {
351
+ return new Response("Missing channels parameter", { status: 400 });
352
+ }
353
+
354
+ const lastEventId = req.headers.get("Last-Event-ID") || undefined;
355
+ const { client, response } = core.sse.addClient({ lastEventId });
356
+
357
+ for (const channel of channels) {
358
+ core.sse.subscribe(client.id, channel);
359
+ }
360
+
361
+ req.signal.addEventListener("abort", () => {
362
+ core.sse.removeClient(client.id);
363
+ });
364
+
365
+ const headers = new Headers(response.headers);
366
+ const corsHeaders = getCorsHeaders();
367
+ Object.entries(corsHeaders).forEach(([k, v]) => headers.set(k, v));
368
+
369
+ return new Response(response.body, {
370
+ status: response.status,
371
+ headers,
372
+ });
373
+ }
374
+
375
+ function extractClientIP(req, socketIP) {
376
+ return (
377
+ req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
378
+ req.headers.get("x-real-ip") ||
379
+ socketIP ||
380
+ "127.0.0.1"
381
+ );
382
+ }
383
+
384
+ function getCorsHeaders() {
385
+ return {
386
+ "Access-Control-Allow-Origin": "*",
387
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
388
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
389
+ };
390
+ }
391
+ `;
392
+ }
393
+
394
+ // Re-export types
395
+ export type { AdapterOptions as Options };
package/src/vite.ts ADDED
@@ -0,0 +1,359 @@
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
+
9
+ import type { Plugin, ViteDevServer } from "vite";
10
+ import { spawn, type ChildProcess } from "node:child_process";
11
+ import { resolve } from "node:path";
12
+ import http from "node:http";
13
+
14
+ export interface DevPluginOptions {
15
+ /**
16
+ * Path to your @donkeylabs/server entry file.
17
+ * This file should export a configured AppServer instance.
18
+ *
19
+ * @example "./src/server/index.ts"
20
+ */
21
+ serverEntry: string;
22
+
23
+ /**
24
+ * Port for the backend server (subprocess mode only).
25
+ * @default 3001
26
+ */
27
+ backendPort?: number;
28
+ }
29
+
30
+ // Check if running with Bun runtime (bun --bun)
31
+ const isBunRuntime = typeof globalThis.Bun !== "undefined";
32
+
33
+ // Global reference to the app server for SSR direct calls
34
+ let globalAppServer: any = null;
35
+
36
+ /**
37
+ * Get the global app server instance for SSR direct calls.
38
+ * This allows hooks to access the server without HTTP.
39
+ */
40
+ export function getDevServer(): any {
41
+ return globalAppServer;
42
+ }
43
+
44
+ /**
45
+ * Vite plugin that integrates @donkeylabs/server with the dev server.
46
+ *
47
+ * - With `bun --bun run dev`: Runs in-process (single port, recommended)
48
+ * - With `bun run dev`: Spawns subprocess (two ports, fallback)
49
+ *
50
+ * @example
51
+ * // vite.config.ts
52
+ * import { donkeylabsDev } from "@donkeylabs/adapter-sveltekit/vite";
53
+ *
54
+ * export default defineConfig({
55
+ * plugins: [
56
+ * donkeylabsDev({ serverEntry: "./src/server/index.ts" }),
57
+ * sveltekit()
58
+ * ]
59
+ * });
60
+ */
61
+ export function donkeylabsDev(options: DevPluginOptions): Plugin {
62
+ const { serverEntry, backendPort = 3001 } = options;
63
+
64
+ if (!serverEntry) {
65
+ throw new Error("donkeylabsDev: serverEntry option is required");
66
+ }
67
+
68
+ // State for subprocess mode
69
+ let backendProcess: ChildProcess | null = null;
70
+ let backendReady = false;
71
+
72
+ // State for in-process mode
73
+ let appServer: any = null;
74
+ let serverReady = false;
75
+
76
+ return {
77
+ name: "donkeylabs-dev",
78
+ enforce: "pre",
79
+
80
+ async configureServer(server: ViteDevServer) {
81
+ const serverEntryResolved = resolve(process.cwd(), serverEntry);
82
+
83
+ if (isBunRuntime) {
84
+ // ========== IN-PROCESS MODE (bun --bun run dev) ==========
85
+ // Import and initialize server directly - no subprocess, no proxy
86
+ console.log("[donkeylabs-dev] Starting in-process mode (Bun runtime detected)");
87
+
88
+ try {
89
+ const serverModule = await import(serverEntryResolved);
90
+ appServer = serverModule.server || serverModule.default;
91
+
92
+ if (!appServer) {
93
+ throw new Error("No server export found in " + serverEntry);
94
+ }
95
+
96
+ // Initialize without starting HTTP server
97
+ await appServer.initialize();
98
+ serverReady = true;
99
+ // Set global reference for SSR direct calls
100
+ globalAppServer = appServer;
101
+ console.log("[donkeylabs-dev] Server initialized (in-process mode)");
102
+ } catch (err) {
103
+ console.error("[donkeylabs-dev] Failed to initialize server:", err);
104
+ throw err;
105
+ }
106
+
107
+ // Return middleware setup function
108
+ return () => {
109
+ // In-process request handler
110
+ const inProcessMiddleware = async (req: any, res: any, next: any) => {
111
+ const url = req.url || "/";
112
+
113
+ // Handle SSE
114
+ if (req.method === "GET" && url.startsWith("/sse")) {
115
+ if (!serverReady || !appServer) return next();
116
+
117
+ const fullUrl = new URL(url, "http://localhost");
118
+ const channels = fullUrl.searchParams.get("channels")?.split(",").filter(Boolean) || [];
119
+ const lastEventId = req.headers["last-event-id"] || undefined;
120
+
121
+ const { client, response } = appServer.getCore().sse.addClient({ lastEventId });
122
+
123
+ for (const channel of channels) {
124
+ appServer.getCore().sse.subscribe(client.id, channel);
125
+ }
126
+
127
+ // Set SSE headers
128
+ res.writeHead(200, {
129
+ "Content-Type": "text/event-stream",
130
+ "Cache-Control": "no-cache",
131
+ "Connection": "keep-alive",
132
+ "Access-Control-Allow-Origin": "*",
133
+ });
134
+
135
+ // Stream SSE data
136
+ const reader = response.body?.getReader();
137
+ if (reader) {
138
+ const pump = async () => {
139
+ try {
140
+ while (true) {
141
+ const { done, value } = await reader.read();
142
+ if (done) break;
143
+ res.write(value);
144
+ }
145
+ } catch {
146
+ // Connection closed
147
+ }
148
+ };
149
+ pump();
150
+ }
151
+
152
+ req.on("close", () => {
153
+ appServer.getCore().sse.removeClient(client.id);
154
+ });
155
+
156
+ return; // Don't call next()
157
+ }
158
+
159
+ // Handle API routes (POST)
160
+ if (req.method === "POST" && /^\/[a-zA-Z][a-zA-Z0-9_.]*$/.test(url)) {
161
+ if (!serverReady || !appServer) return next();
162
+
163
+ const routeName = url.slice(1);
164
+ if (!appServer.hasRoute(routeName)) return next();
165
+
166
+ // Collect body
167
+ let body = "";
168
+ req.on("data", (chunk: any) => (body += chunk));
169
+ req.on("end", async () => {
170
+ try {
171
+ const input = body ? JSON.parse(body) : {};
172
+ const ip = req.socket?.remoteAddress || "127.0.0.1";
173
+
174
+ const result = await appServer.callRoute(routeName, input, ip);
175
+
176
+ res.setHeader("Content-Type", "application/json");
177
+ res.setHeader("Access-Control-Allow-Origin", "*");
178
+ res.end(JSON.stringify(result));
179
+ } catch (err: any) {
180
+ res.statusCode = err.status || 500;
181
+ res.setHeader("Content-Type", "application/json");
182
+ res.end(JSON.stringify({ error: err.message || "Internal error" }));
183
+ }
184
+ });
185
+
186
+ return; // Don't call next()
187
+ }
188
+
189
+ next();
190
+ };
191
+
192
+ // CORS preflight
193
+ const corsMiddleware = (req: any, res: any, next: any) => {
194
+ if (req.method === "OPTIONS") {
195
+ res.setHeader("Access-Control-Allow-Origin", "*");
196
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
197
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
198
+ res.statusCode = 204;
199
+ res.end();
200
+ return;
201
+ }
202
+ next();
203
+ };
204
+
205
+ // Add to front of middleware stack
206
+ const stack = (server.middlewares as any).stack;
207
+ if (stack && Array.isArray(stack)) {
208
+ stack.unshift({ route: "", handle: corsMiddleware });
209
+ stack.unshift({ route: "", handle: inProcessMiddleware });
210
+ } else {
211
+ server.middlewares.use(inProcessMiddleware);
212
+ server.middlewares.use(corsMiddleware);
213
+ }
214
+ };
215
+ } else {
216
+ // ========== SUBPROCESS MODE (bun run dev) ==========
217
+ // Spawn backend as separate process and proxy requests
218
+ console.log(`[donkeylabs-dev] Starting subprocess mode (backend on port ${backendPort})`);
219
+
220
+ const bootstrapCode = `
221
+ const serverModule = await import("${serverEntryResolved}");
222
+ const server = serverModule.server || serverModule.default;
223
+
224
+ if (!server) {
225
+ console.error("[donkeylabs-backend] No server export found");
226
+ process.exit(1);
227
+ }
228
+
229
+ server.port = ${backendPort};
230
+ await server.start();
231
+ console.log("[donkeylabs-backend] Server ready on port ${backendPort}");
232
+ `;
233
+
234
+ backendProcess = spawn("bun", ["--eval", bootstrapCode], {
235
+ stdio: ["pipe", "pipe", "pipe"],
236
+ env: { ...process.env, NODE_ENV: "development" },
237
+ });
238
+
239
+ backendProcess.stdout?.on("data", (data: Buffer) => {
240
+ const msg = data.toString().trim();
241
+ if (msg) {
242
+ console.log(msg);
243
+ if (msg.includes("Server ready") || msg.includes("Server running")) {
244
+ backendReady = true;
245
+ }
246
+ }
247
+ });
248
+
249
+ backendProcess.stderr?.on("data", (data: Buffer) => {
250
+ const msg = data.toString().trim();
251
+ if (msg) console.error(msg);
252
+ });
253
+
254
+ backendProcess.on("error", (err) => {
255
+ console.error("[donkeylabs-dev] Failed to start backend:", err);
256
+ });
257
+
258
+ backendProcess.on("exit", (code) => {
259
+ if (code !== 0 && code !== null) {
260
+ console.error(`[donkeylabs-dev] Backend exited with code ${code}`);
261
+ }
262
+ backendProcess = null;
263
+ backendReady = false;
264
+ });
265
+
266
+ server.httpServer?.on("close", () => {
267
+ if (backendProcess) {
268
+ backendProcess.kill();
269
+ backendProcess = null;
270
+ }
271
+ });
272
+
273
+ // Return middleware setup function
274
+ return () => {
275
+ const waitForBackend = new Promise<void>((resolve) => {
276
+ const check = () => (backendReady ? resolve() : setTimeout(check, 100));
277
+ setTimeout(check, 500);
278
+ setTimeout(() => {
279
+ if (!backendReady) {
280
+ console.warn("[donkeylabs-dev] Backend startup timeout");
281
+ resolve();
282
+ }
283
+ }, 10000);
284
+ });
285
+
286
+ // Proxy middleware
287
+ const proxyMiddleware = (req: any, res: any, next: any) => {
288
+ const url = req.url || "/";
289
+ const isApiRoute = req.method === "POST" && /^\/[a-zA-Z][a-zA-Z0-9_.]*$/.test(url);
290
+
291
+ if (!isApiRoute) return next();
292
+
293
+ waitForBackend.then(() => {
294
+ const proxyReq = http.request(
295
+ {
296
+ hostname: "localhost",
297
+ port: backendPort,
298
+ path: url,
299
+ method: req.method,
300
+ headers: { ...req.headers, host: `localhost:${backendPort}` },
301
+ },
302
+ (proxyRes) => {
303
+ res.setHeader("Access-Control-Allow-Origin", "*");
304
+ res.statusCode = proxyRes.statusCode || 200;
305
+ for (const [k, v] of Object.entries(proxyRes.headers)) {
306
+ if (v) res.setHeader(k, v);
307
+ }
308
+ proxyRes.pipe(res);
309
+ }
310
+ );
311
+
312
+ proxyReq.on("error", (err) => {
313
+ console.error(`[donkeylabs-dev] Proxy error:`, err.message);
314
+ res.statusCode = 502;
315
+ res.end(JSON.stringify({ error: "Backend unavailable" }));
316
+ });
317
+
318
+ req.pipe(proxyReq);
319
+ });
320
+ };
321
+
322
+ const corsMiddleware = (req: any, res: any, next: any) => {
323
+ if (req.method === "OPTIONS") {
324
+ res.setHeader("Access-Control-Allow-Origin", "*");
325
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
326
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
327
+ res.statusCode = 204;
328
+ res.end();
329
+ return;
330
+ }
331
+ next();
332
+ };
333
+
334
+ const stack = (server.middlewares as any).stack;
335
+ if (stack && Array.isArray(stack)) {
336
+ stack.unshift({ route: "", handle: corsMiddleware });
337
+ stack.unshift({ route: "", handle: proxyMiddleware });
338
+ } else {
339
+ server.middlewares.use(proxyMiddleware);
340
+ server.middlewares.use(corsMiddleware);
341
+ }
342
+ };
343
+ }
344
+ },
345
+
346
+ async closeBundle() {
347
+ if (backendProcess) {
348
+ backendProcess.kill();
349
+ backendProcess = null;
350
+ }
351
+ if (appServer) {
352
+ await appServer.shutdown?.();
353
+ appServer = null;
354
+ }
355
+ },
356
+ };
357
+ }
358
+
359
+ export default donkeylabsDev;