@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 +2 -2
- package/src/generator/index.ts +183 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@donkeylabs/adapter-sveltekit",
|
|
3
|
-
"version": "0.
|
|
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",
|
package/src/generator/index.ts
CHANGED
|
@@ -8,11 +8,28 @@
|
|
|
8
8
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
9
9
|
import { dirname } from "node:path";
|
|
10
10
|
import {
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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";
|