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