@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.
@@ -1,401 +0,0 @@
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
- generateClientCode,
12
- zodToTypeScript,
13
- toPascalCase,
14
- toCamelCase,
15
- type RouteInfo,
16
- type ExtractedRoute,
17
- type ClientGeneratorOptions,
18
- } from "@donkeylabs/server/generator";
19
-
20
- /**
21
- * Type guard to check if a route is a full RouteInfo (with prefix and routeName)
22
- */
23
- function isRouteInfo(route: RouteInfo | ExtractedRoute): route is RouteInfo {
24
- return (
25
- typeof route === "object" &&
26
- route !== null &&
27
- "prefix" in route &&
28
- "routeName" in route &&
29
- typeof (route as RouteInfo).prefix === "string"
30
- );
31
- }
32
-
33
- /** SvelteKit-specific generator options */
34
- export const svelteKitGeneratorOptions: ClientGeneratorOptions = {
35
- baseImport:
36
- 'import { UnifiedApiClientBase, SSEConnection, type ClientOptions, type RequestOptions, type SSEConnectionOptions } from "@donkeylabs/adapter-sveltekit/client";',
37
- baseClass: "UnifiedApiClientBase",
38
- constructorSignature: "options?: ClientOptions",
39
- constructorBody: "super(options);",
40
- factoryFunction: `/**
41
- * Create an API client instance
42
- *
43
- * @param options.locals - Pass SvelteKit locals for SSR direct calls (no HTTP overhead)
44
- * @param options.baseUrl - Override the base URL for HTTP calls
45
- *
46
- * @example SSR usage in +page.server.ts:
47
- * \`\`\`ts
48
- * export const load = async ({ locals }) => {
49
- * const api = createApi({ locals });
50
- * const data = await api.myRoute.get({}); // Direct call, no HTTP!
51
- * return { data };
52
- * };
53
- * \`\`\`
54
- *
55
- * @example Browser usage in +page.svelte:
56
- * \`\`\`svelte
57
- * <script>
58
- * import { createApi } from '$lib/api';
59
- * const api = createApi(); // HTTP calls
60
- * let data = $state(null);
61
- * async function load() {
62
- * data = await api.myRoute.get({});
63
- * }
64
- * </script>
65
- * \`\`\`
66
- */
67
- export function createApi(options?: ClientOptions) {
68
- return new ApiClient(options);
69
- }`,
70
- };
71
-
72
- /**
73
- * Namespace tree node for building nested client structure
74
- */
75
- interface NamespaceTreeNode {
76
- methods: { methodDef: string; typeDef: string }[];
77
- children: Map<string, NamespaceTreeNode>;
78
- }
79
-
80
- /**
81
- * Build a nested tree structure from routes
82
- * e.g., routes "api.counter.get", "api.cache.set" become:
83
- * api -> { counter -> { get }, cache -> { set } }
84
- */
85
- function buildRouteTree(routes: RouteInfo[], commonPrefix: string): Map<string, NamespaceTreeNode> {
86
- const tree = new Map<string, NamespaceTreeNode>();
87
-
88
- for (const route of routes) {
89
- // Get the path parts for nesting (e.g., "api.counter.get" -> ["api", "counter", "get"])
90
- const parts = route.name.split(".");
91
- const methodName = parts[parts.length - 1]!; // Last part is the method
92
- const namespaceParts = parts.slice(0, -1); // Everything before is namespace path
93
-
94
- if (namespaceParts.length === 0) {
95
- // Root level method
96
- if (!tree.has("_root")) {
97
- tree.set("_root", { methods: [], children: new Map() });
98
- }
99
- tree.get("_root")!.methods.push(generateMethodAndType(route, methodName, "Root", commonPrefix));
100
- continue;
101
- }
102
-
103
- // Navigate/create the tree path
104
- let current = tree;
105
- for (let i = 0; i < namespaceParts.length; i++) {
106
- const part = namespaceParts[i]!;
107
- if (!current.has(part)) {
108
- current.set(part, { methods: [], children: new Map() });
109
- }
110
-
111
- if (i === namespaceParts.length - 1) {
112
- // At the final namespace level - add the method here
113
- const pascalNs = toPascalCase(namespaceParts.join("."));
114
- current.get(part)!.methods.push(generateMethodAndType(route, methodName, pascalNs, commonPrefix));
115
- } else {
116
- // Continue traversing
117
- current = current.get(part)!.children;
118
- }
119
- }
120
- }
121
-
122
- return tree;
123
- }
124
-
125
- /**
126
- * Generate method definition and type definition for a route
127
- */
128
- function generateMethodAndType(
129
- route: RouteInfo,
130
- methodName: string,
131
- pascalNs: string,
132
- commonPrefix: string
133
- ): { methodDef: string; typeDef: string } {
134
- const camelMethod = toCamelCase(methodName);
135
- const pascalRoute = toPascalCase(methodName);
136
- const fullRouteName = route.name; // Already includes full path
137
-
138
- // Generate input type
139
- const inputType = route.inputSource
140
- ? (route.inputSource.trim().startsWith("z.") ? zodToTypeScript(route.inputSource) : route.inputSource)
141
- : "Record<string, never>";
142
-
143
- // Generate type definition
144
- let typeDef = "";
145
- let methodDef = "";
146
-
147
- if (route.handler === "stream" || route.handler === "html") {
148
- typeDef = ` export namespace ${pascalRoute} {
149
- export type Input = Expand<${inputType}>;
150
- }
151
- export type ${pascalRoute} = { Input: ${pascalRoute}.Input };`;
152
-
153
- if (route.handler === "stream") {
154
- methodDef = `${camelMethod}: {
155
- /** POST request with JSON body (programmatic) */
156
- fetch: (input: Routes.${pascalNs}.${pascalRoute}.Input, options?: RequestOptions): Promise<Response> => this.streamRequest("${fullRouteName}", input, options),
157
- /** GET URL for browser src attributes (video, img, download links) */
158
- url: (input: Routes.${pascalNs}.${pascalRoute}.Input): string => this.streamUrl("${fullRouteName}", input),
159
- /** GET request with query params */
160
- get: (input: Routes.${pascalNs}.${pascalRoute}.Input, options?: RequestOptions): Promise<Response> => this.streamGet("${fullRouteName}", input, options),
161
- }`;
162
- } else {
163
- const hasInput = route.inputSource;
164
- methodDef = `${camelMethod}: (${hasInput ? `input: Routes.${pascalNs}.${pascalRoute}.Input` : ""}): Promise<string> => this.htmlRequest("${fullRouteName}"${hasInput ? ", input" : ""})`;
165
- }
166
- } else if (route.handler === "sse") {
167
- const eventsEntries = route.eventsSource
168
- ? Object.entries(route.eventsSource).map(([eventName, eventSchema]) => {
169
- const eventType = eventSchema.trim().startsWith("z.")
170
- ? zodToTypeScript(eventSchema)
171
- : eventSchema;
172
- return ` "${eventName}": Expand<${eventType}>;`;
173
- })
174
- : [];
175
- const eventsType = eventsEntries.length > 0
176
- ? `{\n${eventsEntries.join("\n")}\n }`
177
- : "Record<string, unknown>";
178
-
179
- typeDef = ` export namespace ${pascalRoute} {
180
- export type Input = Expand<${inputType}>;
181
- export type Events = ${eventsType};
182
- }
183
- export type ${pascalRoute} = { Input: ${pascalRoute}.Input; Events: ${pascalRoute}.Events };`;
184
-
185
- const hasInput = route.inputSource;
186
- if (hasInput) {
187
- methodDef = `${camelMethod}: (input: Routes.${pascalNs}.${pascalRoute}.Input, options?: SSEConnectionOptions): SSEConnection<Routes.${pascalNs}.${pascalRoute}.Events> => this.sseConnect("${fullRouteName}", input, options)`;
188
- } else {
189
- methodDef = `${camelMethod}: (options?: SSEConnectionOptions): SSEConnection<Routes.${pascalNs}.${pascalRoute}.Events> => this.sseConnect("${fullRouteName}", undefined, options)`;
190
- }
191
- } else if (route.handler === "raw") {
192
- typeDef = ""; // Raw routes don't have types
193
- methodDef = `${camelMethod}: (init?: RequestInit): Promise<Response> => this.rawRequest("${fullRouteName}", init)`;
194
- } else if (route.handler === "formData") {
195
- const outputType = route.outputSource
196
- ? (route.outputSource.trim().startsWith("z.") ? zodToTypeScript(route.outputSource) : route.outputSource)
197
- : "unknown";
198
-
199
- typeDef = ` export namespace ${pascalRoute} {
200
- export type Input = Expand<${inputType}>;
201
- export type Output = Expand<${outputType}>;
202
- }
203
- export type ${pascalRoute} = { Input: ${pascalRoute}.Input; Output: ${pascalRoute}.Output };`;
204
-
205
- methodDef = `${camelMethod}: (fields: Routes.${pascalNs}.${pascalRoute}.Input, files: File[]): Promise<Routes.${pascalNs}.${pascalRoute}.Output> => this.formDataRequest("${fullRouteName}", fields, files)`;
206
- } else {
207
- // typed handler (default)
208
- const outputType = route.outputSource
209
- ? (route.outputSource.trim().startsWith("z.") ? zodToTypeScript(route.outputSource) : route.outputSource)
210
- : "unknown";
211
-
212
- typeDef = ` export namespace ${pascalRoute} {
213
- export type Input = Expand<${inputType}>;
214
- export type Output = Expand<${outputType}>;
215
- }
216
- export type ${pascalRoute} = { Input: ${pascalRoute}.Input; Output: ${pascalRoute}.Output };`;
217
-
218
- methodDef = `${camelMethod}: (input: Routes.${pascalNs}.${pascalRoute}.Input, options?: RequestOptions): Promise<Routes.${pascalNs}.${pascalRoute}.Output> => this.request("${fullRouteName}", input, options)`;
219
- }
220
-
221
- return { methodDef, typeDef };
222
- }
223
-
224
- /**
225
- * Generate nested object code from a tree node
226
- */
227
- function generateNestedMethods(node: NamespaceTreeNode, indent: string = " "): string {
228
- const parts: string[] = [];
229
-
230
- // Add methods at this level
231
- for (const { methodDef } of node.methods) {
232
- // Indent each line of the method definition
233
- const indented = methodDef.split("\n").map((line, i) =>
234
- i === 0 ? `${indent}${line}` : `${indent}${line}`
235
- ).join("\n");
236
- parts.push(indented);
237
- }
238
-
239
- // Add nested namespaces
240
- for (const [childName, childNode] of node.children) {
241
- const childContent = generateNestedMethods(childNode, indent + " ");
242
- parts.push(`${indent}${childName}: {\n${childContent}\n${indent}}`);
243
- }
244
-
245
- return parts.join(",\n");
246
- }
247
-
248
- /**
249
- * Collect all type definitions from a tree
250
- */
251
- function collectTypeDefs(tree: Map<string, NamespaceTreeNode>, prefix: string = ""): Map<string, string[]> {
252
- const result = new Map<string, string[]>();
253
-
254
- for (const [name, node] of tree) {
255
- const nsPath = prefix ? `${prefix}.${name}` : name;
256
- const pascalNs = name === "_root" ? "Root" : toPascalCase(nsPath);
257
-
258
- // Collect types from this node's methods
259
- const typeDefs = node.methods
260
- .map(m => m.typeDef)
261
- .filter(t => t.length > 0);
262
-
263
- if (typeDefs.length > 0) {
264
- if (!result.has(pascalNs)) {
265
- result.set(pascalNs, []);
266
- }
267
- result.get(pascalNs)!.push(...typeDefs);
268
- }
269
-
270
- // Recursively collect from children
271
- const childTypes = collectTypeDefs(node.children, nsPath);
272
- for (const [childNs, childDefs] of childTypes) {
273
- if (!result.has(childNs)) {
274
- result.set(childNs, []);
275
- }
276
- result.get(childNs)!.push(...childDefs);
277
- }
278
- }
279
-
280
- return result;
281
- }
282
-
283
- /**
284
- * Generate a fully-typed SvelteKit-compatible API client
285
- */
286
- function generateTypedSvelteKitClient(routes: RouteInfo[]): string {
287
- const opts = svelteKitGeneratorOptions;
288
- const commonPrefix = ""; // We don't strip prefixes anymore - nested structure handles it
289
-
290
- // Build nested tree structure from routes
291
- const tree = buildRouteTree(routes, commonPrefix);
292
-
293
- // Collect type definitions from tree
294
- const typesByNamespace = collectTypeDefs(tree);
295
- const typeBlocks: string[] = [];
296
- for (const [nsName, typeDefs] of typesByNamespace) {
297
- if (typeDefs.length > 0) {
298
- typeBlocks.push(` export namespace ${nsName} {\n${typeDefs.join("\n\n")}\n }`);
299
- }
300
- }
301
-
302
- // Generate method blocks from tree
303
- const methodBlocks: string[] = [];
304
- for (const [topLevel, node] of tree) {
305
- if (topLevel === "_root") {
306
- // Root level methods become direct class properties
307
- for (const { methodDef } of node.methods) {
308
- methodBlocks.push(` ${methodDef};`);
309
- }
310
- continue;
311
- }
312
-
313
- const content = generateNestedMethods(node, " ");
314
- methodBlocks.push(` ${topLevel} = {\n${content}\n };`);
315
- }
316
-
317
- return `// Auto-generated by donkeylabs generate
318
- // DO NOT EDIT MANUALLY
319
-
320
- ${opts.baseImport}
321
-
322
- // Utility type that forces TypeScript to expand types on hover
323
- type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
324
-
325
- // ============================================
326
- // Route Types
327
- // ============================================
328
-
329
- export namespace Routes {
330
- ${typeBlocks.join("\n\n") || " // No typed routes found"}
331
- }
332
-
333
- // ============================================
334
- // API Client
335
- // ============================================
336
-
337
- export class ApiClient extends ${opts.baseClass} {
338
- constructor(${opts.constructorSignature}) {
339
- ${opts.constructorBody}
340
- }
341
-
342
- ${methodBlocks.join("\n\n") || " // No routes defined"}
343
- }
344
-
345
- ${opts.factoryFunction}
346
- `;
347
- }
348
-
349
- /**
350
- * Generate a SvelteKit-compatible API client
351
- *
352
- * This is called by the donkeylabs CLI when adapter is set to "@donkeylabs/adapter-sveltekit"
353
- */
354
- export async function generateClient(
355
- _config: Record<string, unknown>,
356
- routes: RouteInfo[] | ExtractedRoute[],
357
- outputPath: string
358
- ): Promise<void> {
359
- let code: string;
360
-
361
- // Always try typed generation if we have routes
362
- if (routes.length > 0 && isRouteInfo(routes[0])) {
363
- // Full RouteInfo - generate typed client
364
- code = generateTypedSvelteKitClient(routes as RouteInfo[]);
365
- } else if (routes.length > 0) {
366
- // Convert ExtractedRoute to RouteInfo for typed generation
367
- const routeInfos: RouteInfo[] = (routes as ExtractedRoute[]).map((r) => {
368
- const parts = r.name.split(".");
369
- return {
370
- name: r.name,
371
- prefix: parts.slice(0, -1).join("."),
372
- routeName: parts[parts.length - 1] || r.name,
373
- handler: (r.handler || "typed") as "typed" | "raw",
374
- inputSource: undefined,
375
- outputSource: undefined,
376
- };
377
- });
378
- code = generateTypedSvelteKitClient(routeInfos);
379
- } else {
380
- // Empty routes - generate minimal client
381
- code = generateTypedSvelteKitClient([]);
382
- }
383
-
384
- // Ensure output directory exists
385
- const outputDir = dirname(outputPath);
386
- await mkdir(outputDir, { recursive: true });
387
-
388
- // Write the generated client
389
- await writeFile(outputPath, code);
390
- }
391
-
392
- // Re-export building blocks for advanced usage
393
- export {
394
- generateClientCode,
395
- zodToTypeScript,
396
- toPascalCase,
397
- toCamelCase,
398
- type RouteInfo,
399
- type ExtractedRoute,
400
- type ClientGeneratorOptions,
401
- } from "@donkeylabs/server/generator";
@@ -1,124 +0,0 @@
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
- }