@donkeylabs/adapter-sveltekit 2.0.13 → 2.0.14
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 +1 -1
- package/src/generator/index.ts +217 -167
package/package.json
CHANGED
package/src/generator/index.ts
CHANGED
|
@@ -70,200 +70,250 @@ export function createApi(options?: ClientOptions) {
|
|
|
70
70
|
};
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Namespace tree node for building nested client structure
|
|
74
74
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
interface NamespaceTreeNode {
|
|
76
|
+
methods: { methodDef: string; typeDef: string }[];
|
|
77
|
+
children: Map<string, NamespaceTreeNode>;
|
|
78
|
+
}
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
}
|
|
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>();
|
|
94
87
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (const route of routesToProcess) {
|
|
88
|
+
for (const route of routes) {
|
|
89
|
+
// Get the path parts for nesting (e.g., "api.counter.get" -> ["api", "counter", "get"])
|
|
98
90
|
const parts = route.name.split(".");
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
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
|
+
}
|
|
102
119
|
}
|
|
103
|
-
groups.get(namespace)!.push({
|
|
104
|
-
...route,
|
|
105
|
-
routeName: parts.length > 1 ? parts.slice(1).join(".") : parts[0]!,
|
|
106
|
-
});
|
|
107
120
|
}
|
|
108
121
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const methodBlocks: string[] = [];
|
|
122
|
+
return tree;
|
|
123
|
+
}
|
|
112
124
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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} {
|
|
131
149
|
export type Input = Expand<${inputType}>;
|
|
132
150
|
}
|
|
133
151
|
export type ${pascalRoute} = { Input: ${pascalRoute}.Input };`;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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} {
|
|
150
180
|
export type Input = Expand<${inputType}>;
|
|
151
181
|
export type Events = ${eventsType};
|
|
152
182
|
}
|
|
153
183
|
export type ${pascalRoute} = { Input: ${pascalRoute}.Input; Events: ${pascalRoute}.Events };`;
|
|
154
|
-
}
|
|
155
184
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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} {
|
|
161
200
|
export type Input = Expand<${inputType}>;
|
|
162
201
|
export type Output = Expand<${outputType}>;
|
|
163
202
|
}
|
|
164
203
|
export type ${pascalRoute} = { Input: ${pascalRoute}.Input; Output: ${pascalRoute}.Output };`;
|
|
165
|
-
});
|
|
166
204
|
|
|
167
|
-
|
|
168
|
-
|
|
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}>;
|
|
169
215
|
}
|
|
216
|
+
export type ${pascalRoute} = { Input: ${pascalRoute}.Input; Output: ${pascalRoute}.Output };`;
|
|
170
217
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
.filter(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return ` ${methodName}: (options?: SSEConnectionOptions): SSEConnection<${eventsType}> => this.sseConnect("${fullRouteName}", undefined, options)`;
|
|
229
|
-
}
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const formDataMethodEntries = nsRoutes
|
|
233
|
-
.filter(r => r.handler === "formData")
|
|
234
|
-
.map(r => {
|
|
235
|
-
const methodName = toCamelCase(r.routeName);
|
|
236
|
-
const pascalRoute = toPascalCase(r.routeName);
|
|
237
|
-
const inputType = r.inputSource ? `Routes.${pascalNs}.${pascalRoute}.Input` : "Record<string, any>";
|
|
238
|
-
const outputType = r.outputSource ? `Routes.${pascalNs}.${pascalRoute}.Output` : "unknown";
|
|
239
|
-
const fullRouteName = commonPrefix ? `${commonPrefix}.${r.name}` : r.name;
|
|
240
|
-
return ` ${methodName}: (fields: ${inputType}, files: File[]): Promise<${outputType}> => this.formDataRequest("${fullRouteName}", fields, files)`;
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
const htmlMethodEntries = nsRoutes
|
|
244
|
-
.filter(r => r.handler === "html")
|
|
245
|
-
.map(r => {
|
|
246
|
-
const methodName = toCamelCase(r.routeName);
|
|
247
|
-
const pascalRoute = toPascalCase(r.routeName);
|
|
248
|
-
const hasInput = r.inputSource;
|
|
249
|
-
const inputType = hasInput ? `Routes.${pascalNs}.${pascalRoute}.Input` : "Record<string, never>";
|
|
250
|
-
const fullRouteName = commonPrefix ? `${commonPrefix}.${r.name}` : r.name;
|
|
251
|
-
return ` ${methodName}: (${hasInput ? `input: ${inputType}` : ""}): Promise<string> => this.htmlRequest("${fullRouteName}"${hasInput ? ", input" : ""})`;
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const allMethods = [...methodEntries, ...rawMethodEntries, ...streamMethodEntries, ...sseMethodEntries, ...formDataMethodEntries, ...htmlMethodEntries];
|
|
255
|
-
if (allMethods.length > 0) {
|
|
256
|
-
if (namespace === "_root") {
|
|
257
|
-
// Root-level methods go directly on the class
|
|
258
|
-
for (const method of allMethods) {
|
|
259
|
-
methodBlocks.push(method.replace(/^ /, " "));
|
|
260
|
-
}
|
|
261
|
-
} else {
|
|
262
|
-
methodBlocks.push(` ${methodNs} = {\n${allMethods.join(",\n")}\n };`);
|
|
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, []);
|
|
263
275
|
}
|
|
276
|
+
result.get(childNs)!.push(...childDefs);
|
|
264
277
|
}
|
|
265
278
|
}
|
|
266
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
|
+
|
|
267
317
|
return `// Auto-generated by donkeylabs generate
|
|
268
318
|
// DO NOT EDIT MANUALLY
|
|
269
319
|
|