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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +72 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,7 +19,7 @@ bunx elysia-openapi-codegen -i https://api.example.com/openapi.json -o ./src/api
19
19
 
20
20
  ## Installation
21
21
 
22
- ### Using Bun (Recommended)
22
+ ### Using Bun
23
23
 
24
24
  ```bash
25
25
  bun add -d elysia-openapi-codegen
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: [], hooks: [] };
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.hooks.push(`
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
- const fetchStatement = queryParamsList.length > 0 ? ` const _qs = new URLSearchParams();
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
- const guardParams = [
143
- ...pathParamsList,
144
- ...queryParamsList.filter((p) => p.required)
145
- ];
146
- const enabledGuard = guardParams.length > 0 ? `
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
- g.hooks.push(`
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
- const _qs = new URLSearchParams();
176
- ${queryParamsList.map((p) => ` if (${p.name} !== undefined) _qs.set('${p.name}', String(${p.name}));`).join(`
188
+ const _qs = new URLSearchParams();
189
+ ${queryParamsList.map((p) => ` if (${p.name} !== undefined) _qs.set('${p.name}', String(${p.name}));`).join(`
177
190
  `)}
178
- const _qstr = _qs.toString();` : "";
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 mutationArg;
198
+ let destructureArg;
186
199
  if (hasBody && hasAnyParams) {
187
200
  inputType = `${capitalize(opId)}Params & ${capitalize(opId)}Body`;
188
- mutationArg = `{ ${allParamNames.join(", ")}, ...body }`;
201
+ destructureArg = `{ ${allParamNames.join(", ")}, ...body }`;
189
202
  } else if (hasBody) {
190
203
  inputType = bodyType;
191
- mutationArg = "body";
204
+ destructureArg = "body";
192
205
  } else if (hasAnyParams) {
193
206
  inputType = paramsType;
194
- mutationArg = `{ ${allParamNames.join(", ")} }`;
207
+ destructureArg = `{ ${allParamNames.join(", ")} }`;
195
208
  } else {
196
209
  inputType = "void";
197
- mutationArg = "";
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
- g.hooks.push(`
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 (${mutationArg}) => {${mutQsCode}
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 and TypeScript types from OpenAPI specifications.
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("Fetching OpenAPI spec...");
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, hooks }] of Object.entries(groups)) {
300
- if (types.length === 0 && hooks.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 hasHooks = hooks.length > 0;
340
+ const hasFunctions = functions.length > 0;
303
341
  const sections = [`/* eslint-disable */`, `import { baseUrl } from './base';`];
304
- if (hasHooks) {
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 (hooks.length > 0)
312
- sections.push(hooks.join(`
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, ${hooks.length} hooks)`);
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
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elysia-openapi-codegen",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "author": "Khantamir mkhantamir77@gmail.com",
5
5
  "repository": {
6
6
  "type": "git",