elysia-openapi-codegen 0.1.6 → 0.1.7

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 (2) hide show
  1. package/dist/index.js +111 -108
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -3,31 +3,27 @@
3
3
  // index.ts
4
4
  import fs from "fs";
5
5
  import path from "path";
6
- import https from "https";
7
- import http from "http";
8
6
  async function fetchSpec(source) {
9
7
  if (source.startsWith("http://") || source.startsWith("https://")) {
10
- return new Promise((resolve, reject) => {
11
- const client = source.startsWith("https://") ? https : http;
12
- client.get(source, (res) => {
13
- let data = "";
14
- res.on("data", (chunk) => data += chunk);
15
- res.on("end", () => {
16
- try {
17
- resolve(JSON.parse(data));
18
- } catch (e) {
19
- reject(new Error(`Failed to parse OpenAPI spec: ${e.message}`));
20
- }
21
- });
22
- res.on("error", reject);
23
- });
24
- });
8
+ const res = await fetch(source);
9
+ if (!res.ok)
10
+ throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
11
+ return res.json();
25
12
  }
26
13
  return JSON.parse(fs.readFileSync(source, "utf8"));
27
14
  }
15
+ var TYPE_MAP = {
16
+ string: "string",
17
+ number: "number",
18
+ integer: "number",
19
+ boolean: "boolean",
20
+ null: "null"
21
+ };
28
22
  function resolveType(schema) {
29
23
  if (!schema)
30
24
  return "any";
25
+ if (schema.format === "binary")
26
+ return "File | Blob";
31
27
  if (schema.$ref) {
32
28
  return schema.$ref.split("/").pop() || "any";
33
29
  }
@@ -38,8 +34,7 @@ function resolveType(schema) {
38
34
  if (!schema.properties)
39
35
  return "Record<string, any>";
40
36
  const props = Object.entries(schema.properties).map(([key, prop]) => {
41
- const isRequired = schema.required?.includes(key);
42
- const optional = isRequired ? "" : "?";
37
+ const optional = schema.required?.includes(key) ? "" : "?";
43
38
  return ` ${key}${optional}: ${resolveType(prop)};`;
44
39
  });
45
40
  return `{
@@ -56,23 +51,33 @@ ${props.join(`
56
51
  if (schema.nullable) {
57
52
  return `${resolveType({ ...schema, nullable: false })} | null`;
58
53
  }
59
- const typeMap = {
60
- string: "string",
61
- number: "number",
62
- integer: "number",
63
- boolean: "boolean",
64
- null: "null"
65
- };
66
- return schema.type && typeMap[schema.type] || "any";
54
+ return schema.type && TYPE_MAP[schema.type] || "any";
55
+ }
56
+ function capitalize(str) {
57
+ return str.charAt(0).toUpperCase() + str.slice(1);
58
+ }
59
+ function toSafeIdentifier(id) {
60
+ return id.replace(/[^a-zA-Z0-9_-]/g, "").replace(/[-_]+(.?)/g, (_, c) => c ? c.toUpperCase() : "");
61
+ }
62
+ function getPathGroup(pathUrl) {
63
+ const segment = pathUrl.split("/").filter(Boolean)[0] || "root";
64
+ return toSafeIdentifier(segment);
67
65
  }
68
- function generateTypes(spec) {
69
- const definitions = [];
66
+ function generateGroups(spec) {
67
+ const groups = {};
68
+ const getGroup = (name) => {
69
+ if (!groups[name])
70
+ groups[name] = { types: [], hooks: [] };
71
+ return groups[name];
72
+ };
70
73
  if (spec.components?.schemas) {
74
+ const g = getGroup("schemas");
71
75
  for (const [name, schema] of Object.entries(spec.components.schemas)) {
72
- definitions.push(`export type ${name} = ${resolveType(schema)};`);
76
+ g.types.push(`export type ${name} = ${resolveType(schema)};`);
73
77
  }
74
78
  }
75
79
  for (const [pathUrl, pathItem] of Object.entries(spec.paths)) {
80
+ const g = getGroup(getPathGroup(pathUrl));
76
81
  for (const [method, operation] of Object.entries(pathItem)) {
77
82
  if (!["get", "post", "put", "patch", "delete"].includes(method))
78
83
  continue;
@@ -80,45 +85,29 @@ function generateTypes(spec) {
80
85
  const opId = toSafeIdentifier(op.operationId || `${method}${pathUrl.replace(/[^a-zA-Z0-9]/g, "")}`);
81
86
  const response = op.responses?.["200"];
82
87
  if (response?.content?.["application/json"]?.schema) {
83
- const responseType = resolveType(response.content["application/json"].schema);
84
- definitions.push(`export type ${capitalize(opId)}Response = ${responseType};`);
88
+ g.types.push(`export type ${capitalize(opId)}Response = ${resolveType(response.content["application/json"].schema)};`);
85
89
  }
86
90
  const params = op.parameters || [];
87
91
  if (params.length > 0) {
88
92
  const paramProps = params.map((param) => {
89
- const required = param.required ? "" : "?";
90
- return ` ${param.name}${required}: ${resolveType(param.schema)};`;
93
+ const optional = param.in === "path" || param.required ? "" : "?";
94
+ return ` ${param.name}${optional}: ${resolveType(param.schema)};`;
91
95
  });
92
- definitions.push(`export type ${capitalize(opId)}Params = {
96
+ g.types.push(`export type ${capitalize(opId)}Params = {
93
97
  ${paramProps.join(`
94
98
  `)}
95
99
  };`);
96
100
  }
97
- if (op.requestBody?.content?.["application/json"]?.schema) {
98
- const bodyType = resolveType(op.requestBody.content["application/json"].schema);
99
- definitions.push(`export type ${capitalize(opId)}Body = ${bodyType};`);
101
+ const isMultipart = !op.requestBody?.content?.["application/json"] && !!op.requestBody?.content?.["multipart/form-data"];
102
+ const bodySchema = op.requestBody?.content?.["application/json"]?.schema ?? op.requestBody?.content?.["multipart/form-data"]?.schema;
103
+ if (bodySchema) {
104
+ g.types.push(`export type ${capitalize(opId)}Body = ${resolveType(bodySchema)};`);
100
105
  }
101
- }
102
- }
103
- return definitions.join(`
104
-
105
- `);
106
- }
107
- function generateHooks(spec) {
108
- const hooks = [];
109
- const baseUrl = spec.servers?.[0]?.url || "";
110
- for (const [pathUrl, pathItem] of Object.entries(spec.paths)) {
111
- for (const [method, operation] of Object.entries(pathItem)) {
112
- if (!["get", "post", "put", "patch", "delete"].includes(method))
113
- continue;
114
- const op = operation;
115
- const opId = toSafeIdentifier(op.operationId || `${method}${pathUrl.replace(/[^a-zA-Z0-9]/g, "")}`);
116
- const hasResponse = !!op.responses?.["200"]?.content?.["application/json"]?.schema;
106
+ const hasResponse = !!response?.content?.["application/json"]?.schema;
117
107
  const responseType = hasResponse ? `${capitalize(opId)}Response` : "any";
118
- const params = op.parameters || [];
119
108
  const hasParams = params.length > 0;
120
109
  const paramsType = hasParams ? `${capitalize(opId)}Params` : "void";
121
- const hasBody = !!op.requestBody?.content?.["application/json"]?.schema;
110
+ const hasBody = !!bodySchema;
122
111
  const bodyType = hasBody ? `${capitalize(opId)}Body` : "void";
123
112
  if (method === "get") {
124
113
  const pathParamsList = params.filter((p) => p.in === "path");
@@ -127,22 +116,35 @@ function generateHooks(spec) {
127
116
  for (const p of pathParamsList) {
128
117
  urlPath = urlPath.replace(`{${p.name}}`, `\${params.${p.name}}`);
129
118
  }
130
- const qsLines = queryParamsList.map((p) => ` if (params?.${p.name} !== undefined) _qs.set('${p.name}', String(params.${p.name}));`).join(`
119
+ const qsLines = (indent) => queryParamsList.map((p) => `${indent}if (params?.${p.name} !== undefined) _qs.set('${p.name}', String(params.${p.name}));`).join(`
131
120
  `);
121
+ if (!hasResponse) {
122
+ const body = queryParamsList.length > 0 ? ` const _qs = new URLSearchParams();
123
+ ${qsLines(" ")}
124
+ const _qstr = _qs.toString();
125
+ return \`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`;` : ` return \`\${baseUrl}${urlPath}\`;`;
126
+ g.hooks.push(`
127
+ export const ${opId}Url = (params${hasParams ? "" : "?"}: ${paramsType}): string => {
128
+ ${body}
129
+ };`);
130
+ continue;
131
+ }
132
132
  const fetchStatement = queryParamsList.length > 0 ? ` const _qs = new URLSearchParams();
133
- ${qsLines}
133
+ ${qsLines(" ")}
134
134
  const _qstr = _qs.toString();
135
135
  const res = await fetch(\`\${baseUrl}${urlPath}\${_qstr ? '?' + _qstr : ''}\`);` : ` const res = await fetch(\`\${baseUrl}${urlPath}\`);`;
136
- hooks.push(`
136
+ const enabledGuard = pathParamsList.length > 0 ? `
137
+ enabled: ${pathParamsList.map((p) => `params.${p.name} != null`).join(" && ")},` : "";
138
+ g.hooks.push(`
137
139
  export const use${capitalize(opId)} = (
138
140
  params${hasParams ? "" : "?"}: ${paramsType},
139
141
  options?: Omit<UseQueryOptions<${responseType}>, 'queryKey' | 'queryFn'>
140
142
  ) => {
141
143
  return useQuery<${responseType}>({
142
- queryKey: ['${opId}', params],
144
+ queryKey: ['${opId}', params],${enabledGuard}
143
145
  queryFn: async () => {
144
146
  ${fetchStatement}
145
- if (!res.ok) throw new Error('API Error');
147
+ if (!res.ok) throw new Error(\`\${res.status}: \${await res.text()}\`);
146
148
  return res.json();
147
149
  },
148
150
  ...options,
@@ -159,32 +161,31 @@ ${fetchStatement}
159
161
  let mutationArg;
160
162
  if (hasBody && hasPathParams) {
161
163
  inputType = `${capitalize(opId)}Params & ${capitalize(opId)}Body`;
162
- const pathParamNames = pathParamsList.map((p) => p.name).join(", ");
163
- mutationArg = `{ ${pathParamNames}, ...body }`;
164
+ mutationArg = `{ ${pathParamsList.map((p) => p.name).join(", ")}, ...body }`;
164
165
  } else if (hasBody) {
165
166
  inputType = bodyType;
166
167
  mutationArg = "body";
167
168
  } else if (hasPathParams) {
168
169
  inputType = paramsType;
169
- const pathParamNames = pathParamsList.map((p) => p.name).join(", ");
170
- mutationArg = `{ ${pathParamNames} }`;
170
+ mutationArg = `{ ${pathParamsList.map((p) => p.name).join(", ")} }`;
171
171
  } else {
172
172
  inputType = "void";
173
173
  mutationArg = "";
174
174
  }
175
- const fetchBodyLine = hasBody ? `
175
+ const fetchBodyLines = hasBody ? isMultipart ? `
176
+ body: Object.entries(body as Record<string, unknown>).reduce((f, [k, v]) => { f.append(k, v as any); return f; }, new FormData()),` : `
177
+ headers: { 'Content-Type': 'application/json' },
176
178
  body: JSON.stringify(body),` : "";
177
- hooks.push(`
179
+ g.hooks.push(`
178
180
  export const use${capitalize(opId)} = (
179
181
  options?: UseMutationOptions<${responseType}, Error, ${inputType}>
180
182
  ) => {
181
183
  return useMutation<${responseType}, Error, ${inputType}>({
182
184
  mutationFn: async (${mutationArg}) => {
183
185
  const res = await fetch(\`\${baseUrl}${urlPath}\`, {
184
- method: '${method.toUpperCase()}',
185
- headers: { 'Content-Type': 'application/json' },${fetchBodyLine}
186
+ method: '${method.toUpperCase()}',${fetchBodyLines}
186
187
  });
187
- if (!res.ok) throw new Error('API Error');
188
+ if (!res.ok) throw new Error(\`\${res.status}: \${await res.text()}\`);
188
189
  return res.json();
189
190
  },
190
191
  ...options,
@@ -193,21 +194,11 @@ export const use${capitalize(opId)} = (
193
194
  }
194
195
  }
195
196
  }
196
- return hooks.join(`
197
- `);
198
- }
199
- function capitalize(str) {
200
- return str.charAt(0).toUpperCase() + str.slice(1);
197
+ return groups;
201
198
  }
202
- function toSafeIdentifier(id) {
203
- return id.replace(/[^a-zA-Z0-9_-]/g, "").replace(/[-_]+(.?)/g, (_, c) => c ? c.toUpperCase() : "");
204
- }
205
- async function parseArgs() {
199
+ function parseArgs() {
206
200
  const args = process.argv.slice(2);
207
- const config = {
208
- input: "",
209
- output: ""
210
- };
201
+ const config = { input: "", output: "" };
211
202
  for (let i = 0;i < args.length; i++) {
212
203
  const arg = args[i];
213
204
  if (!arg)
@@ -250,12 +241,7 @@ Arguments:
250
241
  -o, --output <output> Output directory for generated files
251
242
 
252
243
  Examples:
253
- # Using flags
254
244
  elysia-codegen -i https://api.example.com/openapi.json -o ./src/api
255
- elysia-codegen --input ./openapi.json --output ./generated
256
-
257
- # Using positional arguments
258
- elysia-codegen https://api.example.com/openapi.json ./src/api
259
245
  elysia-codegen ./openapi.json ./generated
260
246
  `);
261
247
  }
@@ -265,7 +251,7 @@ async function main() {
265
251
  showHelp();
266
252
  process.exit(0);
267
253
  }
268
- const { input, output } = await parseArgs();
254
+ const { input, output } = parseArgs();
269
255
  if (!input || !output) {
270
256
  console.error(`Error: Both input source and output directory are required.
271
257
  `);
@@ -275,28 +261,45 @@ async function main() {
275
261
  console.log("Fetching OpenAPI spec...");
276
262
  try {
277
263
  const spec = await fetchSpec(input);
278
- const types = generateTypes(spec);
279
- const hooks = generateHooks(spec);
280
- const baseUrl = spec.servers?.[0]?.url || "";
281
- const fileContent = `/* eslint-disable */
282
- /**
283
- * Auto-generated by Elysia OpenAPI Codegen
284
- */
285
-
286
- import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
287
-
288
- const baseUrl = '${baseUrl}';
289
-
290
- ${types}
291
-
292
- ${hooks}
293
- `;
264
+ const groups = generateGroups(spec);
265
+ const serverPath = spec.servers?.[0]?.url || "";
266
+ const baseUrl = input.startsWith("http://") || input.startsWith("https://") ? new URL(input).origin + serverPath : serverPath;
294
267
  if (!fs.existsSync(output)) {
295
268
  fs.mkdirSync(output, { recursive: true });
296
269
  }
297
- const outputPath = path.join(output, "generated.ts");
298
- fs.writeFileSync(outputPath, fileContent);
299
- console.log(`Successfully generated hooks at ${outputPath}`);
270
+ fs.writeFileSync(path.join(output, "base.ts"), `/* eslint-disable */
271
+ export const baseUrl = '${baseUrl}';
272
+ `);
273
+ const groupNames = [];
274
+ for (const [groupName, { types, hooks }] of Object.entries(groups)) {
275
+ if (types.length === 0 && hooks.length === 0)
276
+ continue;
277
+ const hasHooks = hooks.length > 0;
278
+ const sections = [`/* eslint-disable */`, `import { baseUrl } from './base';`];
279
+ if (hasHooks) {
280
+ sections.push(`import { useQuery, useMutation, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';`);
281
+ }
282
+ if (types.length > 0)
283
+ sections.push(types.join(`
284
+
285
+ `));
286
+ if (hooks.length > 0)
287
+ sections.push(hooks.join(`
288
+ `));
289
+ fs.writeFileSync(path.join(output, `${groupName}.ts`), sections.join(`
290
+
291
+ `) + `
292
+ `);
293
+ groupNames.push(groupName);
294
+ console.log(` ↳ ${groupName}.ts (${types.length} types, ${hooks.length} hooks)`);
295
+ }
296
+ const indexContent = ["base", ...groupNames].map((f) => `export * from './${f}';`).join(`
297
+ `);
298
+ fs.writeFileSync(path.join(output, "index.ts"), `/* eslint-disable */
299
+ ${indexContent}
300
+ `);
301
+ console.log(`
302
+ Generated ${groupNames.length + 2} files at ${output}`);
300
303
  } catch (err) {
301
304
  console.error("Generation failed:", err instanceof Error ? err.message : String(err));
302
305
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elysia-openapi-codegen",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "author": "Khantamir mkhantamir77@gmail.com",
5
5
  "repository": {
6
6
  "type": "git",