elysia-openapi-codegen 0.2.0 → 0.2.1
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/README.md +1 -1
- package/dist/index.js +72 -34
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -64,11 +64,11 @@ function getPathGroup(pathUrl) {
|
|
|
64
64
|
const segment = pathUrl.split("/").filter(Boolean)[0] || "root";
|
|
65
65
|
return toSafeIdentifier(segment);
|
|
66
66
|
}
|
|
67
|
-
function generateGroups(spec) {
|
|
67
|
+
function generateGroups(spec, hooksMode) {
|
|
68
68
|
const groups = {};
|
|
69
69
|
const getGroup = (name) => {
|
|
70
70
|
if (!groups[name])
|
|
71
|
-
groups[name] = { types: [],
|
|
71
|
+
groups[name] = { types: [], functions: [] };
|
|
72
72
|
return groups[name];
|
|
73
73
|
};
|
|
74
74
|
if (spec.components?.schemas) {
|
|
@@ -129,23 +129,24 @@ ${paramProps.join(`
|
|
|
129
129
|
${qsLines(" ")}
|
|
130
130
|
const _qstr = _qs.toString();
|
|
131
131
|
return \`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`;` : ` return \`\${baseUrl}${urlPath}\`;`;
|
|
132
|
-
g.
|
|
132
|
+
g.functions.push(`
|
|
133
133
|
export const ${opId}Url = (params${hasParams ? "" : "?"}: ${paramsType}): string => {
|
|
134
134
|
${body}
|
|
135
135
|
};`);
|
|
136
136
|
continue;
|
|
137
137
|
}
|
|
138
|
-
|
|
138
|
+
if (hooksMode) {
|
|
139
|
+
const fetchStatement = queryParamsList.length > 0 ? ` const _qs = new URLSearchParams();
|
|
139
140
|
${qsLines(" ")}
|
|
140
141
|
const _qstr = _qs.toString();
|
|
141
142
|
const res = await fetch(\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`);` : ` const res = await fetch(\`\${baseUrl}${urlPath}\`);`;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
const guardParams = [
|
|
144
|
+
...pathParamsList,
|
|
145
|
+
...queryParamsList.filter((p) => p.required)
|
|
146
|
+
];
|
|
147
|
+
const enabledGuard = guardParams.length > 0 ? `
|
|
147
148
|
enabled: ${guardParams.map((p) => `params.${p.name} != null`).join(" && ")},` : "";
|
|
148
|
-
|
|
149
|
+
g.functions.push(`
|
|
149
150
|
export const use${capitalize(opId)} = (
|
|
150
151
|
params${hasParams ? "" : "?"}: ${paramsType},
|
|
151
152
|
options?: Omit<UseQueryOptions<${responseType}>, 'queryKey' | 'queryFn'>
|
|
@@ -160,6 +161,18 @@ ${fetchStatement}
|
|
|
160
161
|
...options,
|
|
161
162
|
});
|
|
162
163
|
};`);
|
|
164
|
+
} else {
|
|
165
|
+
const fetchStatement = queryParamsList.length > 0 ? ` const _qs = new URLSearchParams();
|
|
166
|
+
${qsLines(" ")}
|
|
167
|
+
const _qstr = _qs.toString();
|
|
168
|
+
const res = await fetch(\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`);` : ` const res = await fetch(\`\${baseUrl}${urlPath}\`);`;
|
|
169
|
+
g.functions.push(`
|
|
170
|
+
export async function ${opId}(params${hasParams ? "" : "?"}: ${paramsType}): Promise<${responseType}> {
|
|
171
|
+
${fetchStatement}
|
|
172
|
+
if (!res.ok) throw new Error(\`\${res.status}: \${await res.text()}\`);
|
|
173
|
+
return res.json();
|
|
174
|
+
}`);
|
|
175
|
+
}
|
|
163
176
|
} else {
|
|
164
177
|
const pathParamsList = params.filter((p) => p.in === "path");
|
|
165
178
|
const queryParamsList = params.filter((p) => p.in === "query");
|
|
@@ -172,40 +185,41 @@ ${fetchStatement}
|
|
|
172
185
|
}
|
|
173
186
|
urlPath = urlPath.replace(/\/+$/, "");
|
|
174
187
|
const mutQsCode = hasQueryParams ? `
|
|
175
|
-
|
|
176
|
-
${queryParamsList.map((p) => `
|
|
188
|
+
const _qs = new URLSearchParams();
|
|
189
|
+
${queryParamsList.map((p) => ` if (${p.name} !== undefined) _qs.set('${p.name}', String(${p.name}));`).join(`
|
|
177
190
|
`)}
|
|
178
|
-
|
|
191
|
+
const _qstr = _qs.toString();` : "";
|
|
179
192
|
const fetchUrlExpr = hasQueryParams ? `\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`` : `\`\${baseUrl}${urlPath}\``;
|
|
180
193
|
const allParamNames = [
|
|
181
194
|
...pathParamsList.map((p) => p.name),
|
|
182
195
|
...queryParamsList.map((p) => p.name)
|
|
183
196
|
];
|
|
184
197
|
let inputType;
|
|
185
|
-
let
|
|
198
|
+
let destructureArg;
|
|
186
199
|
if (hasBody && hasAnyParams) {
|
|
187
200
|
inputType = `${capitalize(opId)}Params & ${capitalize(opId)}Body`;
|
|
188
|
-
|
|
201
|
+
destructureArg = `{ ${allParamNames.join(", ")}, ...body }`;
|
|
189
202
|
} else if (hasBody) {
|
|
190
203
|
inputType = bodyType;
|
|
191
|
-
|
|
204
|
+
destructureArg = "body";
|
|
192
205
|
} else if (hasAnyParams) {
|
|
193
206
|
inputType = paramsType;
|
|
194
|
-
|
|
207
|
+
destructureArg = `{ ${allParamNames.join(", ")} }`;
|
|
195
208
|
} else {
|
|
196
209
|
inputType = "void";
|
|
197
|
-
|
|
210
|
+
destructureArg = "";
|
|
198
211
|
}
|
|
199
212
|
const fetchBodyLines = hasBody ? useFormData ? `
|
|
200
213
|
body: Object.entries(body as Record<string, unknown>).reduce((f, [k, v]) => { f.append(k, v as any); return f; }, new FormData()),` : `
|
|
201
214
|
headers: { 'Content-Type': 'application/json' },
|
|
202
215
|
body: JSON.stringify(body),` : "";
|
|
203
|
-
|
|
216
|
+
if (hooksMode) {
|
|
217
|
+
g.functions.push(`
|
|
204
218
|
export const use${capitalize(opId)} = (
|
|
205
219
|
options?: UseMutationOptions<${responseType}, Error, ${inputType}>
|
|
206
220
|
) => {
|
|
207
221
|
return useMutation<${responseType}, Error, ${inputType}>({
|
|
208
|
-
mutationFn: async (${
|
|
222
|
+
mutationFn: async (${destructureArg}) => {${mutQsCode.replace(/^ /gm, " ")}
|
|
209
223
|
const res = await fetch(${fetchUrlExpr}, {
|
|
210
224
|
method: '${method.toUpperCase()}',${fetchBodyLines}
|
|
211
225
|
});
|
|
@@ -215,6 +229,26 @@ export const use${capitalize(opId)} = (
|
|
|
215
229
|
...options,
|
|
216
230
|
});
|
|
217
231
|
};`);
|
|
232
|
+
} else {
|
|
233
|
+
const argDecl = inputType === "void" ? "" : `input: ${inputType}`;
|
|
234
|
+
const destructure = inputType === "void" ? "" : hasBody && hasAnyParams ? ` const { ${allParamNames.join(", ")}, ...body } = input;
|
|
235
|
+
` : hasAnyParams ? ` const { ${allParamNames.join(", ")} } = input;
|
|
236
|
+
` : ` const body = input;
|
|
237
|
+
`;
|
|
238
|
+
const fetchBodyLinesApi = hasBody ? useFormData ? `
|
|
239
|
+
body: Object.entries(body as Record<string, unknown>).reduce((f, [k, v]) => { f.append(k, v as any); return f; }, new FormData()),` : `
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
body: JSON.stringify(body),` : "";
|
|
242
|
+
g.functions.push(`
|
|
243
|
+
export async function ${opId}(${argDecl}): Promise<${responseType}> {
|
|
244
|
+
${destructure}${mutQsCode.replace(/^\n/, "")}
|
|
245
|
+
const res = await fetch(${fetchUrlExpr}, {
|
|
246
|
+
method: '${method.toUpperCase()}',${fetchBodyLinesApi}
|
|
247
|
+
});
|
|
248
|
+
if (!res.ok) throw new Error(\`\${res.status}: \${await res.text()}\`);
|
|
249
|
+
return res.json();
|
|
250
|
+
}`);
|
|
251
|
+
}
|
|
218
252
|
}
|
|
219
253
|
}
|
|
220
254
|
}
|
|
@@ -222,7 +256,7 @@ export const use${capitalize(opId)} = (
|
|
|
222
256
|
}
|
|
223
257
|
function parseArgs() {
|
|
224
258
|
const args = process.argv.slice(2);
|
|
225
|
-
const config = { input: "", output: "" };
|
|
259
|
+
const config = { input: "", output: "", hooks: false };
|
|
226
260
|
for (let i = 0;i < args.length; i++) {
|
|
227
261
|
const arg = args[i];
|
|
228
262
|
if (!arg)
|
|
@@ -241,6 +275,8 @@ function parseArgs() {
|
|
|
241
275
|
process.exit(1);
|
|
242
276
|
}
|
|
243
277
|
config.output = value;
|
|
278
|
+
} else if (arg === "--hooks" || arg === "-hooks") {
|
|
279
|
+
config.hooks = true;
|
|
244
280
|
} else if (!arg.startsWith("-")) {
|
|
245
281
|
if (!config.input)
|
|
246
282
|
config.input = arg;
|
|
@@ -253,19 +289,21 @@ function parseArgs() {
|
|
|
253
289
|
function showHelp() {
|
|
254
290
|
console.log(`
|
|
255
291
|
Elysia OpenAPI Code Generator
|
|
256
|
-
Generate React Query hooks
|
|
292
|
+
Generate TypeScript API functions or React Query hooks from OpenAPI specifications.
|
|
257
293
|
|
|
258
294
|
Usage:
|
|
259
|
-
elysia-codegen -i <source> -o <output>
|
|
260
|
-
elysia-codegen --input <source> --output <output>
|
|
295
|
+
elysia-codegen -i <source> -o <output> [--hooks]
|
|
296
|
+
elysia-codegen --input <source> --output <output> [--hooks]
|
|
261
297
|
elysia-codegen <source> <output>
|
|
262
298
|
|
|
263
299
|
Arguments:
|
|
264
300
|
-i, --input <source> OpenAPI spec source (URL or file path)
|
|
265
301
|
-o, --output <output> Output directory for generated files
|
|
302
|
+
--hooks, -hooks Generate React Query hooks instead of plain async functions
|
|
266
303
|
|
|
267
304
|
Examples:
|
|
268
305
|
elysia-codegen -i https://api.example.com/openapi.json -o ./src/api
|
|
306
|
+
elysia-codegen -i https://api.example.com/openapi.json -o ./src/api --hooks
|
|
269
307
|
elysia-codegen ./openapi.json ./generated
|
|
270
308
|
`);
|
|
271
309
|
}
|
|
@@ -275,17 +313,17 @@ async function main() {
|
|
|
275
313
|
showHelp();
|
|
276
314
|
process.exit(0);
|
|
277
315
|
}
|
|
278
|
-
const { input, output } = parseArgs();
|
|
316
|
+
const { input, output, hooks } = parseArgs();
|
|
279
317
|
if (!input || !output) {
|
|
280
318
|
console.error(`Error: Both input source and output directory are required.
|
|
281
319
|
`);
|
|
282
320
|
showHelp();
|
|
283
321
|
process.exit(1);
|
|
284
322
|
}
|
|
285
|
-
console.log(
|
|
323
|
+
console.log(`Fetching OpenAPI spec... (mode: ${hooks ? "hooks" : "api"})`);
|
|
286
324
|
try {
|
|
287
325
|
const spec = await fetchSpec(input);
|
|
288
|
-
const groups = generateGroups(spec);
|
|
326
|
+
const groups = generateGroups(spec, hooks);
|
|
289
327
|
const serverPath = spec.servers?.[0]?.url || "";
|
|
290
328
|
const baseUrl = input.startsWith("http://") || input.startsWith("https://") ? new URL(input).origin + serverPath : serverPath;
|
|
291
329
|
if (!fs.existsSync(output)) {
|
|
@@ -296,27 +334,27 @@ async function main() {
|
|
|
296
334
|
export let baseUrl = '${baseUrl}';
|
|
297
335
|
`);
|
|
298
336
|
const groupNames = [];
|
|
299
|
-
for (const [groupName, { types,
|
|
300
|
-
if (types.length === 0 &&
|
|
337
|
+
for (const [groupName, { types, functions }] of Object.entries(groups)) {
|
|
338
|
+
if (types.length === 0 && functions.length === 0)
|
|
301
339
|
continue;
|
|
302
|
-
const
|
|
340
|
+
const hasFunctions = functions.length > 0;
|
|
303
341
|
const sections = [`/* eslint-disable */`, `import { baseUrl } from './base';`];
|
|
304
|
-
if (
|
|
342
|
+
if (hasFunctions && hooks) {
|
|
305
343
|
sections.push(`import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';`);
|
|
306
344
|
}
|
|
307
345
|
if (types.length > 0)
|
|
308
346
|
sections.push(types.join(`
|
|
309
347
|
|
|
310
348
|
`));
|
|
311
|
-
if (
|
|
312
|
-
sections.push(
|
|
349
|
+
if (functions.length > 0)
|
|
350
|
+
sections.push(functions.join(`
|
|
313
351
|
`));
|
|
314
352
|
fs.writeFileSync(path.join(output, `${groupName}.ts`), sections.join(`
|
|
315
353
|
|
|
316
354
|
`) + `
|
|
317
355
|
`);
|
|
318
356
|
groupNames.push(groupName);
|
|
319
|
-
console.log(` ↳ ${groupName}.ts (${types.length} types, ${
|
|
357
|
+
console.log(` ↳ ${groupName}.ts (${types.length} types, ${functions.length} ${hooks ? "hooks" : "functions"})`);
|
|
320
358
|
}
|
|
321
359
|
const indexContent = ["base", ...groupNames].map((f) => `export * from './${f}';`).join(`
|
|
322
360
|
`);
|