vite-plugin-openapi-codegen 1.1.1 → 1.1.2
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 +24 -0
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +177 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -149,6 +149,25 @@ export default defineConfig({
|
|
|
149
149
|
});
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
If you want shorter generated type references, enable `typeAliases`. This keeps the type name suffixes, but avoids long chains like `components["schemas"]["CreateUserRequest"]` and `operations["list_users"]["parameters"]["query"]` in the generated output:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { defineConfig } from "vite-plus";
|
|
156
|
+
import { openapiCodegen } from "vite-plugin-openapi-codegen";
|
|
157
|
+
|
|
158
|
+
export default defineConfig({
|
|
159
|
+
plugins: [
|
|
160
|
+
openapiCodegen({
|
|
161
|
+
input: "openapi.json",
|
|
162
|
+
output: "src/generated",
|
|
163
|
+
typeAliases: true,
|
|
164
|
+
}),
|
|
165
|
+
],
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
When enabled, the plugin writes the raw OpenAPI types to `api-types.d.ts` and adds top-level aliases for schema and operation types that the generated `api.ts` and `client.ts` files import directly.
|
|
170
|
+
|
|
152
171
|
## Generated Output
|
|
153
172
|
|
|
154
173
|
Given a spec path like `/api/users/{user_id}`, the plugin generates a path builder:
|
|
@@ -201,6 +220,7 @@ interface Options {
|
|
|
201
220
|
output: string;
|
|
202
221
|
pathPrefix?: string;
|
|
203
222
|
stripPrefix?: boolean;
|
|
223
|
+
typeAliases?: boolean;
|
|
204
224
|
httpClient?: {
|
|
205
225
|
module?: string;
|
|
206
226
|
jsonFunction?: string;
|
|
@@ -229,6 +249,10 @@ Only paths starting with this prefix are included. The default is `"/api/"`.
|
|
|
229
249
|
|
|
230
250
|
Controls whether the `pathPrefix` is removed from generated path builders. The default is `true`.
|
|
231
251
|
|
|
252
|
+
### `typeAliases`
|
|
253
|
+
|
|
254
|
+
When enabled, the plugin generates top-level aliases for schema and operation types and makes the emitted `api.ts` and `client.ts` import those shorter names. The suffixes stay intact, so generated names remain readable while avoiding long `components["schemas"][...]` and `operations["..."][...]` chains. The default is `false` to preserve existing output.
|
|
255
|
+
|
|
232
256
|
### `httpClient`
|
|
233
257
|
|
|
234
258
|
Overrides the runtime import path and symbol names used by generated clients.
|
package/dist/index.d.mts
CHANGED
|
@@ -75,12 +75,15 @@ interface Options {
|
|
|
75
75
|
stripPrefix?: boolean;
|
|
76
76
|
/** HTTP client configuration */
|
|
77
77
|
httpClient?: HttpClientConfig;
|
|
78
|
+
/** Generate and consume top-level type aliases. Default: false */
|
|
79
|
+
typeAliases?: boolean;
|
|
78
80
|
}
|
|
79
81
|
interface GeneratedArtifacts {
|
|
80
82
|
api: string;
|
|
83
|
+
apiTypes?: string;
|
|
81
84
|
client: string;
|
|
82
85
|
}
|
|
83
|
-
declare function renderGeneratedArtifacts(spec: OpenAPISpec, options: Pick<Options, "httpClient" | "pathPrefix" | "stripPrefix">, preCollectedOperations?: OperationEntry[]): GeneratedArtifacts;
|
|
86
|
+
declare function renderGeneratedArtifacts(spec: OpenAPISpec, options: Pick<Options, "httpClient" | "pathPrefix" | "stripPrefix" | "typeAliases">, preCollectedOperations?: OperationEntry[]): GeneratedArtifacts;
|
|
84
87
|
declare function openapiCodegen(options: Options): Plugin;
|
|
85
88
|
//#endregion
|
|
86
89
|
export { type HttpClientConfig, type OpenAPISpec, type OperationEntry, type Options, openapiCodegen, renderGeneratedArtifacts };
|
package/dist/index.mjs
CHANGED
|
@@ -23,9 +23,9 @@ function renderApiSource(entries, generatedHeader) {
|
|
|
23
23
|
function renderClientSource(model, generatedHeader, httpClient) {
|
|
24
24
|
const statements = [createTypeOnlyImport([httpClient.requestOptionsType], httpClient.module), createValueImport([{ name: httpClient.jsonFunction }, { name: httpClient.voidFunction }], httpClient.module)];
|
|
25
25
|
const apiTypeImports = collectApiTypeImports(model.operations.flatMap((operation) => [
|
|
26
|
-
operation.bodyChannel.
|
|
27
|
-
operation.pathChannel.
|
|
28
|
-
operation.queryChannel.
|
|
26
|
+
operation.bodyChannel.typeRef?.sourceExpr,
|
|
27
|
+
operation.pathChannel.typeRef?.sourceExpr,
|
|
28
|
+
operation.queryChannel.typeRef?.sourceExpr,
|
|
29
29
|
operation.responseTypeExpr,
|
|
30
30
|
operation.returnTypeExpr
|
|
31
31
|
]));
|
|
@@ -46,6 +46,10 @@ function renderClientSource(model, generatedHeader, httpClient) {
|
|
|
46
46
|
}
|
|
47
47
|
return printGeneratedFile(statements, generatedHeader);
|
|
48
48
|
}
|
|
49
|
+
function renderOperationTypeAliases(typeAliases) {
|
|
50
|
+
if (typeAliases.length === 0) return "";
|
|
51
|
+
return `${typeAliases.map((alias) => `export type ${alias.typeName} = ${alias.definitionExpr};`).join("\n")}\n`;
|
|
52
|
+
}
|
|
49
53
|
function printGeneratedFile(statements, generatedHeader) {
|
|
50
54
|
const sourceFile = ts.factory.createSourceFile(statements, ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
|
|
51
55
|
const printed = AST_PRINTER.printFile(sourceFile).trim();
|
|
@@ -75,10 +79,41 @@ function collectApiTypeImports(typeExprs) {
|
|
|
75
79
|
for (const typeExpr of typeExprs) {
|
|
76
80
|
if (!typeExpr) continue;
|
|
77
81
|
if (typeExpr.includes("components[")) names.add("components");
|
|
78
|
-
if (typeExpr.includes("operations["))
|
|
82
|
+
if (typeExpr.includes("operations[")) {
|
|
83
|
+
names.add("operations");
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const name = typeExpr.trim();
|
|
87
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name) && !isBuiltinTypeName(name)) names.add(name);
|
|
79
88
|
}
|
|
80
89
|
return [...names].sort();
|
|
81
90
|
}
|
|
91
|
+
function isBuiltinTypeName(name) {
|
|
92
|
+
return new Set([
|
|
93
|
+
"AbortSignal",
|
|
94
|
+
"Array",
|
|
95
|
+
"Blob",
|
|
96
|
+
"Date",
|
|
97
|
+
"Error",
|
|
98
|
+
"File",
|
|
99
|
+
"FormData",
|
|
100
|
+
"Map",
|
|
101
|
+
"Omit",
|
|
102
|
+
"Promise",
|
|
103
|
+
"Record",
|
|
104
|
+
"Set",
|
|
105
|
+
"URLSearchParams",
|
|
106
|
+
"boolean",
|
|
107
|
+
"never",
|
|
108
|
+
"null",
|
|
109
|
+
"number",
|
|
110
|
+
"object",
|
|
111
|
+
"string",
|
|
112
|
+
"undefined",
|
|
113
|
+
"unknown",
|
|
114
|
+
"void"
|
|
115
|
+
]).has(name);
|
|
116
|
+
}
|
|
82
117
|
function createPathExpression(strippedPath) {
|
|
83
118
|
const matches = [...strippedPath.matchAll(/\{(\w+)\}/g)];
|
|
84
119
|
if (matches.length === 0) return ts.factory.createStringLiteral(strippedPath);
|
|
@@ -104,7 +139,7 @@ function createClientOptionsDeclaration(operation) {
|
|
|
104
139
|
]);
|
|
105
140
|
}
|
|
106
141
|
function createClientChannelField(key, channel) {
|
|
107
|
-
return ts.factory.createPropertySignature(void 0, ts.factory.createIdentifier(key), channel.present && channel.required ? void 0 : ts.factory.createToken(ts.SyntaxKind.QuestionToken), channel.present && channel.
|
|
142
|
+
return ts.factory.createPropertySignature(void 0, ts.factory.createIdentifier(key), channel.present && channel.required ? void 0 : ts.factory.createToken(ts.SyntaxKind.QuestionToken), channel.present && channel.typeRef ? createTypeNodeFromText(channel.typeRef.sourceExpr) : ts.factory.createKeywordTypeNode(ts.SyntaxKind.NeverKeyword));
|
|
108
143
|
}
|
|
109
144
|
function createClientFunctionDeclaration(operation) {
|
|
110
145
|
const requestProperties = [ts.factory.createSpreadAssignment(ts.factory.createIdentifier("requestOptions")), ts.factory.createPropertyAssignment(ts.factory.createIdentifier("method"), ts.factory.createStringLiteral(operation.methodUpper))];
|
|
@@ -145,7 +180,8 @@ function buildClientRenderModelFromOperations(operations, spec, requestFunctionN
|
|
|
145
180
|
const normalized = operations.map((entry) => normalizeOperation(entry, context, requestFunctionNames));
|
|
146
181
|
return {
|
|
147
182
|
operations: normalized,
|
|
148
|
-
needsSearchParamsHelper: normalized.some((operation) => operation.queryChannel.present)
|
|
183
|
+
needsSearchParamsHelper: normalized.some((operation) => operation.queryChannel.present),
|
|
184
|
+
typeAliases: collectTypeAliases(normalized)
|
|
149
185
|
};
|
|
150
186
|
}
|
|
151
187
|
function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
|
|
@@ -161,7 +197,7 @@ function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
|
|
|
161
197
|
const strippedPath = stripPrefix ? apiPath.replace(pathPrefix, "") : apiPath;
|
|
162
198
|
entries.push({
|
|
163
199
|
apiPath,
|
|
164
|
-
funcName: makeFuncName(method, apiPath, pathPrefix),
|
|
200
|
+
funcName: makeFuncName(operation.operationId, method, apiPath, pathPrefix),
|
|
165
201
|
group: strippedPath.split("/")[0] ?? "misc",
|
|
166
202
|
method,
|
|
167
203
|
operation,
|
|
@@ -194,7 +230,8 @@ function buildNormalizationContext(spec) {
|
|
|
194
230
|
}
|
|
195
231
|
return {
|
|
196
232
|
schemaAliasIndex,
|
|
197
|
-
schemaNames
|
|
233
|
+
schemaNames,
|
|
234
|
+
usedTypeNames: new Set(schemaNames)
|
|
198
235
|
};
|
|
199
236
|
}
|
|
200
237
|
function getParametersByLocation(operation, location) {
|
|
@@ -236,7 +273,9 @@ function getBuilderAlias(funcName) {
|
|
|
236
273
|
function getClientOptionTypeName(funcName) {
|
|
237
274
|
return `${capitalize(funcName)}Options`;
|
|
238
275
|
}
|
|
239
|
-
function makeFuncName(method, apiPath, pathPrefix = "/api/") {
|
|
276
|
+
function makeFuncName(operationId, method, apiPath, pathPrefix = "/api/") {
|
|
277
|
+
const normalizedOperationId = toFunctionName(operationId);
|
|
278
|
+
if (normalizedOperationId.length > 0) return normalizedOperationId;
|
|
240
279
|
const segments = apiPath.replace(pathPrefix, "").split("/");
|
|
241
280
|
const result = [];
|
|
242
281
|
for (const segment of segments) {
|
|
@@ -252,6 +291,13 @@ function makeFuncName(method, apiPath, pathPrefix = "/api/") {
|
|
|
252
291
|
return index === 0 ? clean : `${clean[0].toUpperCase()}${clean.slice(1)}`;
|
|
253
292
|
}).join(""))}`;
|
|
254
293
|
}
|
|
294
|
+
function toFunctionName(value) {
|
|
295
|
+
const segments = value.split(/[^a-zA-Z0-9]+/g).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
296
|
+
if (segments.length === 0) return "";
|
|
297
|
+
const [firstSegment, ...restSegments] = segments;
|
|
298
|
+
const normalized = [firstSegment[0].toLowerCase() + firstSegment.slice(1), ...restSegments.map((segment) => capitalize(segment.toLowerCase()))].join("");
|
|
299
|
+
return /^[A-Za-z_$]/.test(normalized) ? normalized : `_${normalized}`;
|
|
300
|
+
}
|
|
255
301
|
function capitalize(value) {
|
|
256
302
|
if (value.length === 0) return value;
|
|
257
303
|
return `${value[0].toUpperCase()}${value.slice(1)}`;
|
|
@@ -264,13 +310,23 @@ function getTemplateParameterNames(apiPath) {
|
|
|
264
310
|
const matches = apiPath.match(/\{(\w+)\}/g) ?? [];
|
|
265
311
|
return new Set(matches.map((match) => match.slice(1, -1)));
|
|
266
312
|
}
|
|
267
|
-
function
|
|
313
|
+
function resolveParameterTypeReference(entry, context, location) {
|
|
268
314
|
const effectiveParameters = getEffectiveParametersByLocation(entry, location);
|
|
269
|
-
if (effectiveParameters.length === 0) return
|
|
270
|
-
const
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
315
|
+
if (effectiveParameters.length === 0) return null;
|
|
316
|
+
const schemaTypeRef = resolveAlias(context, effectiveParameters.map((parameter) => parameter.name), entry.operation.tags?.[0]);
|
|
317
|
+
if (schemaTypeRef) return schemaTypeRef;
|
|
318
|
+
const rawParameters = getParametersByLocation(entry.operation, location);
|
|
319
|
+
const typeName = allocateOperationTypeName(context, entry.funcName, location === "path" ? "Path" : "Query");
|
|
320
|
+
if (hasSameParameterNames(rawParameters, effectiveParameters)) return {
|
|
321
|
+
aliasDefinitionExpr: `operations['${entry.operationId}']['parameters']['${location}']`,
|
|
322
|
+
sourceExpr: `operations['${entry.operationId}']['parameters']['${location}']`,
|
|
323
|
+
typeName
|
|
324
|
+
};
|
|
325
|
+
return {
|
|
326
|
+
aliasDefinitionExpr: renderInlineParameterObject(effectiveParameters),
|
|
327
|
+
sourceExpr: renderInlineParameterObject(effectiveParameters),
|
|
328
|
+
typeName
|
|
329
|
+
};
|
|
274
330
|
}
|
|
275
331
|
function normalizeOperation(entry, context, requestFunctionNames) {
|
|
276
332
|
const successResponse = getSuccessResponseInfo(entry.operation);
|
|
@@ -278,7 +334,7 @@ function normalizeOperation(entry, context, requestFunctionNames) {
|
|
|
278
334
|
const pathChannel = normalizeParameterChannel(entry, context, "path");
|
|
279
335
|
const queryChannel = normalizeParameterChannel(entry, context, "query");
|
|
280
336
|
const bodyChannel = normalizeBodyChannel(entry, context);
|
|
281
|
-
const
|
|
337
|
+
const responseTypeRef = resolveResponseTypeReference(entry, context, successResponse);
|
|
282
338
|
return {
|
|
283
339
|
bodyChannel,
|
|
284
340
|
builderAlias,
|
|
@@ -287,9 +343,9 @@ function normalizeOperation(entry, context, requestFunctionNames) {
|
|
|
287
343
|
pathChannel,
|
|
288
344
|
pathInvocationExpr: pathChannel.present ? `${builderAlias}(options.path)` : `${builderAlias}()`,
|
|
289
345
|
queryChannel,
|
|
290
|
-
requestFunction:
|
|
291
|
-
|
|
292
|
-
returnTypeExpr:
|
|
346
|
+
requestFunction: responseTypeRef ? requestFunctionNames.json : requestFunctionNames.void,
|
|
347
|
+
responseTypeRef,
|
|
348
|
+
returnTypeExpr: responseTypeRef ? `Promise<${responseTypeRef.typeName}>` : "Promise<void>"
|
|
293
349
|
};
|
|
294
350
|
}
|
|
295
351
|
function normalizeParameterChannel(entry, context, location) {
|
|
@@ -297,40 +353,48 @@ function normalizeParameterChannel(entry, context, location) {
|
|
|
297
353
|
if (parameters.length === 0) return {
|
|
298
354
|
present: false,
|
|
299
355
|
required: false,
|
|
300
|
-
|
|
356
|
+
typeRef: null
|
|
301
357
|
};
|
|
302
358
|
return {
|
|
303
359
|
present: true,
|
|
304
360
|
required: hasRequiredChannel(parameters),
|
|
305
|
-
|
|
361
|
+
typeRef: resolveParameterTypeReference(entry, context, location)
|
|
306
362
|
};
|
|
307
363
|
}
|
|
308
364
|
function normalizeBodyChannel(entry, context) {
|
|
309
|
-
const
|
|
310
|
-
if (!
|
|
365
|
+
const typeRef = resolveRequestBodyTypeReference(entry, context);
|
|
366
|
+
if (!typeRef) return {
|
|
311
367
|
present: false,
|
|
312
368
|
required: false,
|
|
313
|
-
|
|
369
|
+
typeRef: null
|
|
314
370
|
};
|
|
315
371
|
return {
|
|
316
372
|
present: true,
|
|
317
373
|
required: entry.operation.requestBody?.required !== false,
|
|
318
|
-
|
|
374
|
+
typeRef
|
|
319
375
|
};
|
|
320
376
|
}
|
|
321
|
-
function
|
|
377
|
+
function resolveRequestBodyTypeReference(entry, context) {
|
|
322
378
|
const jsonBody = getJsonRequestBody(entry.operation);
|
|
323
379
|
if (!jsonBody) return null;
|
|
324
|
-
const
|
|
325
|
-
if (
|
|
326
|
-
return
|
|
380
|
+
const schemaTypeRef = resolveSchemaTypeReference(context, jsonBody.schema);
|
|
381
|
+
if (schemaTypeRef) return schemaTypeRef;
|
|
382
|
+
return {
|
|
383
|
+
aliasDefinitionExpr: `operations['${entry.operationId}']['requestBody']['content']['application/json']`,
|
|
384
|
+
sourceExpr: `operations['${entry.operationId}']['requestBody']['content']['application/json']`,
|
|
385
|
+
typeName: allocateOperationTypeName(context, entry.funcName, "Request")
|
|
386
|
+
};
|
|
327
387
|
}
|
|
328
|
-
function
|
|
388
|
+
function resolveResponseTypeReference(entry, context, successResponse) {
|
|
329
389
|
if (!successResponse.hasJsonBody) return null;
|
|
330
390
|
const jsonContent = (entry.operation.responses?.[successResponse.statusKey])?.content?.["application/json"];
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
return
|
|
391
|
+
const schemaTypeRef = resolveSchemaTypeReference(context, jsonContent?.schema);
|
|
392
|
+
if (schemaTypeRef) return schemaTypeRef;
|
|
393
|
+
return {
|
|
394
|
+
aliasDefinitionExpr: `operations['${entry.operationId}']['responses'][${formatStatusKey(successResponse.statusKey)}]['content']['application/json']`,
|
|
395
|
+
sourceExpr: `operations['${entry.operationId}']['responses'][${formatStatusKey(successResponse.statusKey)}]['content']['application/json']`,
|
|
396
|
+
typeName: allocateOperationTypeName(context, entry.funcName, "Response")
|
|
397
|
+
};
|
|
334
398
|
}
|
|
335
399
|
function hasSameParameterNames(left, right) {
|
|
336
400
|
if (left.length !== right.length) return false;
|
|
@@ -342,24 +406,28 @@ function resolveAlias(context, parameterNames, tag) {
|
|
|
342
406
|
const key = [...parameterNames].sort().join(",");
|
|
343
407
|
const candidates = context.schemaAliasIndex.get(key);
|
|
344
408
|
if (!candidates || candidates.length === 0) return;
|
|
345
|
-
if (candidates.length === 1) return
|
|
409
|
+
if (candidates.length === 1) return createSchemaTypeReference(candidates[0]);
|
|
346
410
|
if (tag) {
|
|
347
411
|
const singularTag = tag.replace(/s$/, "");
|
|
348
412
|
const prefix = `${singularTag[0]?.toUpperCase() ?? ""}${singularTag.slice(1)}`;
|
|
349
413
|
const match = candidates.find((candidate) => candidate.startsWith(prefix));
|
|
350
|
-
if (match) return
|
|
414
|
+
if (match) return createSchemaTypeReference(match);
|
|
351
415
|
}
|
|
352
|
-
return
|
|
416
|
+
return createSchemaTypeReference(candidates[0]);
|
|
353
417
|
}
|
|
354
|
-
function
|
|
418
|
+
function resolveSchemaTypeReference(context, schema) {
|
|
355
419
|
const ref = readSchemaRef(schema);
|
|
356
420
|
if (!ref) return;
|
|
357
421
|
const schemaName = ref.split("/").pop();
|
|
358
422
|
if (!schemaName || !context.schemaNames.has(schemaName)) return;
|
|
359
|
-
return
|
|
423
|
+
return createSchemaTypeReference(schemaName);
|
|
360
424
|
}
|
|
361
|
-
function
|
|
362
|
-
return
|
|
425
|
+
function createSchemaTypeReference(schemaName) {
|
|
426
|
+
return {
|
|
427
|
+
aliasDefinitionExpr: null,
|
|
428
|
+
sourceExpr: `components['schemas']['${schemaName}']`,
|
|
429
|
+
typeName: schemaName
|
|
430
|
+
};
|
|
363
431
|
}
|
|
364
432
|
function readSchemaRef(schema) {
|
|
365
433
|
if (!schema || typeof schema !== "object") return;
|
|
@@ -378,6 +446,40 @@ function renderPrimitiveSchemaType(schema) {
|
|
|
378
446
|
if (Array.isArray(type)) return type.map((memberType) => mapPrimitiveType(memberType)).join(" | ");
|
|
379
447
|
return mapPrimitiveType(type);
|
|
380
448
|
}
|
|
449
|
+
function allocateOperationTypeName(context, funcName, suffix) {
|
|
450
|
+
const preferredName = `${capitalize(funcName)}${suffix}`;
|
|
451
|
+
if (!context.usedTypeNames.has(preferredName)) {
|
|
452
|
+
context.usedTypeNames.add(preferredName);
|
|
453
|
+
return preferredName;
|
|
454
|
+
}
|
|
455
|
+
let counter = 2;
|
|
456
|
+
let candidate = `${preferredName}_${counter}`;
|
|
457
|
+
while (context.usedTypeNames.has(candidate)) {
|
|
458
|
+
counter += 1;
|
|
459
|
+
candidate = `${preferredName}_${counter}`;
|
|
460
|
+
}
|
|
461
|
+
context.usedTypeNames.add(candidate);
|
|
462
|
+
return candidate;
|
|
463
|
+
}
|
|
464
|
+
function collectTypeAliases(operations) {
|
|
465
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const operation of operations) {
|
|
467
|
+
collectTypeAlias(aliases, operation.pathChannel.typeRef);
|
|
468
|
+
collectTypeAlias(aliases, operation.queryChannel.typeRef);
|
|
469
|
+
collectTypeAlias(aliases, operation.bodyChannel.typeRef);
|
|
470
|
+
collectTypeAlias(aliases, operation.responseTypeRef);
|
|
471
|
+
}
|
|
472
|
+
return [...aliases.entries()].map(([typeName, definitionExpr]) => ({
|
|
473
|
+
definitionExpr,
|
|
474
|
+
typeName
|
|
475
|
+
})).sort((left, right) => left.typeName.localeCompare(right.typeName));
|
|
476
|
+
}
|
|
477
|
+
function collectTypeAlias(aliases, typeRef) {
|
|
478
|
+
if (!typeRef || typeRef.aliasDefinitionExpr == null) return;
|
|
479
|
+
const existing = aliases.get(typeRef.typeName);
|
|
480
|
+
if (existing && existing !== typeRef.aliasDefinitionExpr) throw new Error(`Conflicting generated type alias "${typeRef.typeName}" with incompatible definitions.`);
|
|
481
|
+
aliases.set(typeRef.typeName, typeRef.aliasDefinitionExpr);
|
|
482
|
+
}
|
|
381
483
|
function mapPrimitiveType(type) {
|
|
382
484
|
switch (type) {
|
|
383
485
|
case "integer":
|
|
@@ -409,50 +511,71 @@ function resolveHttpClientConfig(config) {
|
|
|
409
511
|
};
|
|
410
512
|
}
|
|
411
513
|
const GENERATED_HEADER = ["// This file is auto-generated by vite-plugin-openapi-codegen.", "// Do not edit manually. Changes will be overwritten on next build."];
|
|
412
|
-
async function generateApiTypes(source, outputDir) {
|
|
514
|
+
async function generateApiTypes(source, outputDir, useTypeAliases) {
|
|
413
515
|
const { default: openapiTS, astToString } = await import("openapi-typescript");
|
|
414
|
-
const contents = astToString(await openapiTS(source
|
|
516
|
+
const contents = astToString(await openapiTS(source, useTypeAliases ? {
|
|
517
|
+
rootTypes: true,
|
|
518
|
+
rootTypesKeepCasing: true,
|
|
519
|
+
rootTypesNoSchemaPrefix: true
|
|
520
|
+
} : void 0));
|
|
415
521
|
writeFileSync(resolve(outputDir, "api-types.d.ts"), `${GENERATED_HEADER.join("\n")}\n\n${contents}`);
|
|
416
522
|
}
|
|
417
|
-
function createApiEntries(normalizedOps) {
|
|
523
|
+
function createApiEntries(normalizedOps, useTypeAliases) {
|
|
418
524
|
return normalizedOps.map((op) => ({
|
|
419
525
|
funcName: op.entry.funcName,
|
|
420
526
|
group: op.entry.group,
|
|
421
|
-
pathTypeExpr: op.pathChannel.
|
|
527
|
+
pathTypeExpr: op.pathChannel.typeRef == null ? null : useTypeAliases ? op.pathChannel.typeRef.typeName : op.pathChannel.typeRef.sourceExpr,
|
|
422
528
|
strippedPath: op.entry.strippedPath
|
|
423
529
|
}));
|
|
424
530
|
}
|
|
425
|
-
function createClientRenderModel(model) {
|
|
531
|
+
function createClientRenderModel(model, useTypeAliases) {
|
|
532
|
+
const resolveTypeExpr = (typeRef) => typeRef == null ? null : useTypeAliases ? typeRef.typeName : typeRef.sourceExpr;
|
|
533
|
+
const resolveReturnTypeExpr = (operation) => {
|
|
534
|
+
const responseTypeExpr = resolveTypeExpr(operation.responseTypeRef);
|
|
535
|
+
return responseTypeExpr ? `Promise<${responseTypeExpr}>` : "Promise<void>";
|
|
536
|
+
};
|
|
426
537
|
return {
|
|
427
538
|
needsSearchParamsHelper: model.needsSearchParamsHelper,
|
|
428
539
|
operations: model.operations.map((operation) => ({
|
|
429
|
-
bodyChannel: operation.bodyChannel,
|
|
540
|
+
bodyChannel: resolveChannel(operation.bodyChannel, useTypeAliases),
|
|
430
541
|
builderAlias: operation.builderAlias,
|
|
431
542
|
funcName: operation.entry.funcName,
|
|
432
543
|
group: operation.entry.group,
|
|
433
544
|
methodUpper: operation.entry.method.toUpperCase(),
|
|
434
545
|
optionTypeName: operation.optionTypeName,
|
|
435
|
-
pathChannel: operation.pathChannel,
|
|
546
|
+
pathChannel: resolveChannel(operation.pathChannel, useTypeAliases),
|
|
436
547
|
pathInvocationExpr: operation.pathInvocationExpr,
|
|
437
|
-
queryChannel: operation.queryChannel,
|
|
548
|
+
queryChannel: resolveChannel(operation.queryChannel, useTypeAliases),
|
|
438
549
|
requestFunction: operation.requestFunction,
|
|
439
|
-
responseTypeExpr: operation.
|
|
440
|
-
returnTypeExpr: operation
|
|
550
|
+
responseTypeExpr: resolveTypeExpr(operation.responseTypeRef),
|
|
551
|
+
returnTypeExpr: resolveReturnTypeExpr(operation)
|
|
441
552
|
}))
|
|
442
553
|
};
|
|
443
554
|
}
|
|
555
|
+
function resolveChannel(channel, useTypeAliases) {
|
|
556
|
+
if (!channel.typeRef) return channel;
|
|
557
|
+
return {
|
|
558
|
+
...channel,
|
|
559
|
+
typeRef: {
|
|
560
|
+
...channel.typeRef,
|
|
561
|
+
sourceExpr: useTypeAliases ? channel.typeRef.typeName : channel.typeRef.sourceExpr
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
444
565
|
function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
|
|
445
566
|
const pathPrefix = options.pathPrefix ?? "/api/";
|
|
446
567
|
const stripPrefix = options.stripPrefix ?? true;
|
|
447
568
|
const httpClient = resolveHttpClientConfig(options.httpClient);
|
|
569
|
+
const useTypeAliases = options.typeAliases ?? false;
|
|
448
570
|
const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
|
|
449
571
|
json: httpClient.jsonFunction,
|
|
450
572
|
void: httpClient.voidFunction
|
|
451
573
|
});
|
|
452
574
|
if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
|
|
453
575
|
return {
|
|
454
|
-
api: renderApiSource(createApiEntries(clientModel.operations), GENERATED_HEADER),
|
|
455
|
-
client: renderClientSource(createClientRenderModel(clientModel), GENERATED_HEADER, httpClient)
|
|
576
|
+
api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
|
|
577
|
+
client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
|
|
578
|
+
...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
|
|
456
579
|
};
|
|
457
580
|
}
|
|
458
581
|
async function loadOpenAPIInput(root, input) {
|
|
@@ -495,7 +618,8 @@ async function generate(root, options) {
|
|
|
495
618
|
const operations = collectOperations(spec, options.pathPrefix ?? "/api/", options.stripPrefix ?? true);
|
|
496
619
|
const artifacts = renderGeneratedArtifacts(spec, options, operations);
|
|
497
620
|
warnOnParameterLocationMismatch(operations);
|
|
498
|
-
await generateApiTypes(apiTypesSource, outputDir);
|
|
621
|
+
await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
|
|
622
|
+
if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
|
|
499
623
|
writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
|
|
500
624
|
writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
|
|
501
625
|
}
|