@donkeylabs/adapter-sveltekit 0.1.2 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donkeylabs/adapter-sveltekit",
3
- "version": "0.1.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "SvelteKit adapter for @donkeylabs/server - seamless SSR/browser API integration",
6
6
  "main": "./src/index.ts",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@sveltejs/kit": "^2.0.0",
40
- "@donkeylabs/server": "*"
40
+ "@donkeylabs/server": "^0.4.0"
41
41
  },
42
42
  "keywords": [
43
43
  "sveltekit",
@@ -8,11 +8,28 @@
8
8
  import { mkdir, writeFile } from "node:fs/promises";
9
9
  import { dirname } from "node:path";
10
10
  import {
11
- generateClientFromRoutes,
11
+ generateClientCode,
12
+ zodToTypeScript,
13
+ toPascalCase,
14
+ toCamelCase,
15
+ type RouteInfo,
12
16
  type ExtractedRoute,
13
17
  type ClientGeneratorOptions,
14
18
  } from "@donkeylabs/server/generator";
15
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
+
16
33
  /** SvelteKit-specific generator options */
17
34
  export const svelteKitGeneratorOptions: ClientGeneratorOptions = {
18
35
  baseImport:
@@ -52,6 +69,141 @@ export function createApi(options?: ClientOptions) {
52
69
  }`,
53
70
  };
54
71
 
72
+ /**
73
+ * Generate a fully-typed SvelteKit-compatible API client
74
+ */
75
+ function generateTypedSvelteKitClient(routes: RouteInfo[]): string {
76
+ const opts = svelteKitGeneratorOptions;
77
+
78
+ // Check if all routes share a common prefix (e.g., "api.") - if so, strip it
79
+ let routesToProcess = routes;
80
+ let commonPrefix = "";
81
+ if (routes.length > 0) {
82
+ const firstPart = routes[0]?.name.split(".")[0];
83
+ const allSharePrefix = firstPart && routes.every(r => r.name.startsWith(firstPart + "."));
84
+ if (allSharePrefix && firstPart) {
85
+ commonPrefix = firstPart;
86
+ // Strip the common prefix from route names for client generation
87
+ routesToProcess = routes.map(r => ({
88
+ ...r,
89
+ name: r.name.slice(firstPart.length + 1), // Remove "api." prefix
90
+ prefix: r.prefix === firstPart ? "" : r.prefix.slice(firstPart.length + 1),
91
+ }));
92
+ }
93
+ }
94
+
95
+ // Group routes by namespace
96
+ const groups = new Map<string, RouteInfo[]>();
97
+ for (const route of routesToProcess) {
98
+ const parts = route.name.split(".");
99
+ const namespace = parts.length > 1 ? parts[0]! : "_root";
100
+ if (!groups.has(namespace)) {
101
+ groups.set(namespace, []);
102
+ }
103
+ groups.get(namespace)!.push({
104
+ ...route,
105
+ routeName: parts.length > 1 ? parts.slice(1).join(".") : parts[0]!,
106
+ });
107
+ }
108
+
109
+ // Generate type definitions
110
+ const typeBlocks: string[] = [];
111
+ const methodBlocks: string[] = [];
112
+
113
+ for (const [namespace, nsRoutes] of groups) {
114
+ const pascalNs = namespace === "_root" ? "Root" : toPascalCase(namespace);
115
+ const methodNs = namespace === "_root" ? "_root" : namespace;
116
+
117
+ // Generate types for this namespace
118
+ const typeEntries = nsRoutes
119
+ .filter(r => r.handler === "typed")
120
+ .map(r => {
121
+ const pascalRoute = toPascalCase(r.routeName);
122
+ // If inputSource starts with "z.", it's a Zod source string - convert it
123
+ // Otherwise it's already a TypeScript type string from getTypedMetadata()
124
+ const inputType = r.inputSource
125
+ ? (r.inputSource.trim().startsWith("z.") ? zodToTypeScript(r.inputSource) : r.inputSource)
126
+ : "Record<string, never>";
127
+ const outputType = r.outputSource
128
+ ? (r.outputSource.trim().startsWith("z.") ? zodToTypeScript(r.outputSource) : r.outputSource)
129
+ : "unknown";
130
+ return ` export namespace ${pascalRoute} {
131
+ export type Input = Expand<${inputType}>;
132
+ export type Output = Expand<${outputType}>;
133
+ }
134
+ export type ${pascalRoute} = { Input: ${pascalRoute}.Input; Output: ${pascalRoute}.Output };`;
135
+ });
136
+
137
+ if (typeEntries.length > 0) {
138
+ typeBlocks.push(` export namespace ${pascalNs} {\n${typeEntries.join("\n\n")}\n }`);
139
+ }
140
+
141
+ // Generate methods for this namespace
142
+ const methodEntries = nsRoutes
143
+ .filter(r => r.handler === "typed")
144
+ .map(r => {
145
+ const methodName = toCamelCase(r.routeName);
146
+ const pascalRoute = toPascalCase(r.routeName);
147
+ const inputType = `Routes.${pascalNs}.${pascalRoute}.Input`;
148
+ const outputType = `Routes.${pascalNs}.${pascalRoute}.Output`;
149
+ // Use original route name with prefix for the request
150
+ const fullRouteName = commonPrefix ? `${commonPrefix}.${r.name}` : r.name;
151
+ return ` ${methodName}: (input: ${inputType}): Promise<${outputType}> => this.request("${fullRouteName}", input)`;
152
+ });
153
+
154
+ const rawMethodEntries = nsRoutes
155
+ .filter(r => r.handler === "raw")
156
+ .map(r => {
157
+ const methodName = toCamelCase(r.routeName);
158
+ const fullRouteName = commonPrefix ? `${commonPrefix}.${r.name}` : r.name;
159
+ return ` ${methodName}: (init?: RequestInit): Promise<Response> => this.rawRequest("${fullRouteName}", init)`;
160
+ });
161
+
162
+ const allMethods = [...methodEntries, ...rawMethodEntries];
163
+ if (allMethods.length > 0) {
164
+ if (namespace === "_root") {
165
+ // Root-level methods go directly on the class
166
+ for (const method of allMethods) {
167
+ methodBlocks.push(method.replace(/^ /, " "));
168
+ }
169
+ } else {
170
+ methodBlocks.push(` ${methodNs} = {\n${allMethods.join(",\n")}\n };`);
171
+ }
172
+ }
173
+ }
174
+
175
+ return `// Auto-generated by donkeylabs generate
176
+ // DO NOT EDIT MANUALLY
177
+
178
+ ${opts.baseImport}
179
+
180
+ // Utility type that forces TypeScript to expand types on hover
181
+ type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
182
+
183
+ // ============================================
184
+ // Route Types
185
+ // ============================================
186
+
187
+ export namespace Routes {
188
+ ${typeBlocks.join("\n\n") || " // No typed routes found"}
189
+ }
190
+
191
+ // ============================================
192
+ // API Client
193
+ // ============================================
194
+
195
+ export class ApiClient extends ${opts.baseClass} {
196
+ constructor(${opts.constructorSignature}) {
197
+ ${opts.constructorBody}
198
+ }
199
+
200
+ ${methodBlocks.join("\n\n") || " // No routes defined"}
201
+ }
202
+
203
+ ${opts.factoryFunction}
204
+ `;
205
+ }
206
+
55
207
  /**
56
208
  * Generate a SvelteKit-compatible API client
57
209
  *
@@ -59,10 +211,33 @@ export function createApi(options?: ClientOptions) {
59
211
  */
60
212
  export async function generateClient(
61
213
  _config: Record<string, unknown>,
62
- routes: ExtractedRoute[],
214
+ routes: RouteInfo[] | ExtractedRoute[],
63
215
  outputPath: string
64
216
  ): Promise<void> {
65
- const code = generateClientFromRoutes(routes, svelteKitGeneratorOptions);
217
+ let code: string;
218
+
219
+ // Always try typed generation if we have routes
220
+ if (routes.length > 0 && isRouteInfo(routes[0])) {
221
+ // Full RouteInfo - generate typed client
222
+ code = generateTypedSvelteKitClient(routes as RouteInfo[]);
223
+ } else if (routes.length > 0) {
224
+ // Convert ExtractedRoute to RouteInfo for typed generation
225
+ const routeInfos: RouteInfo[] = (routes as ExtractedRoute[]).map((r) => {
226
+ const parts = r.name.split(".");
227
+ return {
228
+ name: r.name,
229
+ prefix: parts.slice(0, -1).join("."),
230
+ routeName: parts[parts.length - 1] || r.name,
231
+ handler: (r.handler || "typed") as "typed" | "raw",
232
+ inputSource: undefined,
233
+ outputSource: undefined,
234
+ };
235
+ });
236
+ code = generateTypedSvelteKitClient(routeInfos);
237
+ } else {
238
+ // Empty routes - generate minimal client
239
+ code = generateTypedSvelteKitClient([]);
240
+ }
66
241
 
67
242
  // Ensure output directory exists
68
243
  const outputDir = dirname(outputPath);
@@ -74,7 +249,11 @@ export async function generateClient(
74
249
 
75
250
  // Re-export building blocks for advanced usage
76
251
  export {
77
- generateClientFromRoutes,
252
+ generateClientCode,
253
+ zodToTypeScript,
254
+ toPascalCase,
255
+ toCamelCase,
256
+ type RouteInfo,
78
257
  type ExtractedRoute,
79
258
  type ClientGeneratorOptions,
80
259
  } from "@donkeylabs/server/generator";