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.
- package/dist/index.js +111 -108
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
69
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
90
|
-
return ` ${param.name}${
|
|
93
|
+
const optional = param.in === "path" || param.required ? "" : "?";
|
|
94
|
+
return ` ${param.name}${optional}: ${resolveType(param.schema)};`;
|
|
91
95
|
});
|
|
92
|
-
|
|
96
|
+
g.types.push(`export type ${capitalize(opId)}Params = {
|
|
93
97
|
${paramProps.join(`
|
|
94
98
|
`)}
|
|
95
99
|
};`);
|
|
96
100
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 = !!
|
|
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) =>
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
197
|
-
`);
|
|
198
|
-
}
|
|
199
|
-
function capitalize(str) {
|
|
200
|
-
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
197
|
+
return groups;
|
|
201
198
|
}
|
|
202
|
-
function
|
|
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 } =
|
|
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
|
|
279
|
-
const
|
|
280
|
-
const baseUrl =
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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);
|