vite-plugin-openapi-codegen 1.1.1 → 1.1.3

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/cli.mjs ADDED
@@ -0,0 +1,812 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { loadConfigFromFile } from "vite-plus";
5
+ import { pathToFileURL } from "node:url";
6
+ import { parse } from "yaml";
7
+ import * as ts from "typescript";
8
+ //#region src/ast.ts
9
+ const AST_PRINTER = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
10
+ function renderApiSource(entries, generatedHeader) {
11
+ const statements = [];
12
+ const seenGroups = /* @__PURE__ */ new Set();
13
+ for (const entry of entries) {
14
+ const parameters = entry.pathTypeExpr == null ? [] : [ts.factory.createParameterDeclaration(void 0, void 0, ts.factory.createIdentifier("params"), void 0, createTypeNodeFromText(entry.pathTypeExpr))];
15
+ const declaration = ts.factory.createFunctionDeclaration([createExportModifier()], void 0, ts.factory.createIdentifier(entry.funcName), void 0, parameters, ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), ts.factory.createBlock([ts.factory.createReturnStatement(entry.pathTypeExpr == null ? ts.factory.createStringLiteral(entry.strippedPath) : createPathExpression(entry.strippedPath))], true));
16
+ statements.push(seenGroups.has(entry.group) ? declaration : createGeneratedBannerComment(declaration, capitalize$1(entry.group)));
17
+ seenGroups.add(entry.group);
18
+ }
19
+ const sourceStatements = [];
20
+ const apiTypeImports = collectApiTypeImports(entries.map((entry) => entry.pathTypeExpr));
21
+ if (apiTypeImports.length > 0) sourceStatements.push(createTypeOnlyImport(apiTypeImports, "./api-types"));
22
+ sourceStatements.push(...statements);
23
+ return printGeneratedFile(sourceStatements, generatedHeader);
24
+ }
25
+ function renderClientSource(model, generatedHeader, httpClient) {
26
+ const statements = [createTypeOnlyImport([httpClient.requestOptionsType], httpClient.module), createValueImport([{ name: httpClient.jsonFunction }, { name: httpClient.voidFunction }], httpClient.module)];
27
+ const apiTypeImports = collectApiTypeImports(model.operations.flatMap((operation) => [
28
+ operation.bodyChannel.typeRef?.sourceExpr,
29
+ operation.pathChannel.typeRef?.sourceExpr,
30
+ operation.queryChannel.typeRef?.sourceExpr,
31
+ operation.responseTypeExpr,
32
+ operation.returnTypeExpr
33
+ ]));
34
+ if (apiTypeImports.length > 0) statements.push(createTypeOnlyImport(apiTypeImports, "./api-types"));
35
+ statements.push(createValueImport(model.operations.map((operation) => ({
36
+ alias: operation.builderAlias,
37
+ name: operation.funcName
38
+ })), "./api"));
39
+ const runtimeTypeExpr = httpClient.omitKeys.length > 0 ? `Omit<${httpClient.requestOptionsType}, ${httpClient.omitKeys.map((k) => `'${k}'`).join(" | ")}>` : httpClient.requestOptionsType;
40
+ statements.push(ts.factory.createTypeAliasDeclaration(void 0, ts.factory.createIdentifier("RuntimeRequestOptions"), void 0, createTypeNodeFromText(runtimeTypeExpr)));
41
+ if (model.needsSearchParamsHelper) statements.push(createBuildSearchParamsFunction());
42
+ const seenGroups = /* @__PURE__ */ new Set();
43
+ for (const operation of model.operations) {
44
+ const optionsInterface = createClientOptionsDeclaration(operation);
45
+ statements.push(seenGroups.has(operation.group) ? optionsInterface : createGeneratedBannerComment(optionsInterface, capitalize$1(operation.group)));
46
+ statements.push(createClientFunctionDeclaration(operation));
47
+ seenGroups.add(operation.group);
48
+ }
49
+ return printGeneratedFile(statements, generatedHeader);
50
+ }
51
+ function renderOperationTypeAliases(typeAliases) {
52
+ if (typeAliases.length === 0) return "";
53
+ return `${typeAliases.map((alias) => `export type ${alias.typeName} = ${alias.definitionExpr};`).join("\n")}\n`;
54
+ }
55
+ function printGeneratedFile(statements, generatedHeader) {
56
+ const sourceFile = ts.factory.createSourceFile(statements, ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
57
+ const printed = AST_PRINTER.printFile(sourceFile).trim();
58
+ return printed.length > 0 ? `${generatedHeader.join("\n")}\n\n${printed}\n` : `${generatedHeader.join("\n")}\n`;
59
+ }
60
+ function parseExpression(sourceText) {
61
+ const statement = ts.createSourceFile("generated-expression.ts", `const value = ${sourceText}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS).statements[0];
62
+ if (!statement || !ts.isVariableStatement(statement)) throw new Error(`Failed to parse expression: ${sourceText}`);
63
+ const declaration = statement.declarationList.declarations[0];
64
+ if (!declaration?.initializer) throw new Error(`Missing parsed initializer for: ${sourceText}`);
65
+ return declaration.initializer;
66
+ }
67
+ function createTypeOnlyImport(names, moduleSpecifier) {
68
+ return ts.factory.createImportDeclaration(void 0, ts.factory.createImportClause(true, void 0, ts.factory.createNamedImports(names.map((name) => ts.factory.createImportSpecifier(false, void 0, ts.factory.createIdentifier(name))))), ts.factory.createStringLiteral(moduleSpecifier));
69
+ }
70
+ function createValueImport(specifiers, moduleSpecifier) {
71
+ return ts.factory.createImportDeclaration(void 0, ts.factory.createImportClause(false, void 0, ts.factory.createNamedImports(specifiers.map((specifier) => ts.factory.createImportSpecifier(false, specifier.alias ? ts.factory.createIdentifier(specifier.name) : void 0, ts.factory.createIdentifier(specifier.alias ?? specifier.name))))), ts.factory.createStringLiteral(moduleSpecifier));
72
+ }
73
+ function createExportModifier() {
74
+ return ts.factory.createModifier(ts.SyntaxKind.ExportKeyword);
75
+ }
76
+ function createGeneratedBannerComment(node, text) {
77
+ return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, ` ${text}`, true);
78
+ }
79
+ function collectApiTypeImports(typeExprs) {
80
+ const names = /* @__PURE__ */ new Set();
81
+ for (const typeExpr of typeExprs) {
82
+ if (!typeExpr) continue;
83
+ if (typeExpr.includes("components[")) names.add("components");
84
+ if (typeExpr.includes("operations[")) {
85
+ names.add("operations");
86
+ continue;
87
+ }
88
+ const name = typeExpr.trim();
89
+ if (/^[A-Za-z_$][\w$]*$/.test(name) && !isBuiltinTypeName(name)) names.add(name);
90
+ }
91
+ return [...names].sort();
92
+ }
93
+ function isBuiltinTypeName(name) {
94
+ return new Set([
95
+ "AbortSignal",
96
+ "Array",
97
+ "Blob",
98
+ "Date",
99
+ "Error",
100
+ "File",
101
+ "FormData",
102
+ "Map",
103
+ "Omit",
104
+ "Promise",
105
+ "Record",
106
+ "Set",
107
+ "URLSearchParams",
108
+ "boolean",
109
+ "never",
110
+ "null",
111
+ "number",
112
+ "object",
113
+ "string",
114
+ "undefined",
115
+ "unknown",
116
+ "void"
117
+ ]).has(name);
118
+ }
119
+ function createPathExpression(strippedPath) {
120
+ const matches = [...strippedPath.matchAll(/\{(\w+)\}/g)];
121
+ if (matches.length === 0) return ts.factory.createStringLiteral(strippedPath);
122
+ const [firstMatch] = matches;
123
+ const spans = matches.map((match, index) => {
124
+ const nextMatch = matches[index + 1];
125
+ const literalText = strippedPath.slice(match.index + match[0].length, nextMatch?.index ?? strippedPath.length);
126
+ return ts.factory.createTemplateSpan(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("params"), match[1]), nextMatch ? ts.factory.createTemplateMiddle(literalText) : ts.factory.createTemplateTail(literalText));
127
+ });
128
+ return ts.factory.createTemplateExpression(ts.factory.createTemplateHead(strippedPath.slice(0, firstMatch.index)), spans);
129
+ }
130
+ function createTypeNodeFromText(sourceText) {
131
+ const stmt = ts.createSourceFile("__type__.ts", `type __T__ = ${sourceText}`, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS).statements[0];
132
+ if (!stmt || !ts.isTypeAliasDeclaration(stmt)) throw new Error(`Failed to parse type expression: ${sourceText}`);
133
+ return stmt.type;
134
+ }
135
+ function createClientOptionsDeclaration(operation) {
136
+ return ts.factory.createInterfaceDeclaration([createExportModifier()], ts.factory.createIdentifier(operation.optionTypeName), void 0, void 0, [
137
+ createClientChannelField("query", operation.queryChannel),
138
+ createClientChannelField("path", operation.pathChannel),
139
+ createClientChannelField("body", operation.bodyChannel),
140
+ ts.factory.createPropertySignature(void 0, ts.factory.createIdentifier("signal"), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createTypeReferenceNode("AbortSignal"))
141
+ ]);
142
+ }
143
+ function createClientChannelField(key, channel) {
144
+ 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));
145
+ }
146
+ function createClientFunctionDeclaration(operation) {
147
+ const requestProperties = [ts.factory.createSpreadAssignment(ts.factory.createIdentifier("requestOptions")), ts.factory.createPropertyAssignment(ts.factory.createIdentifier("method"), ts.factory.createStringLiteral(operation.methodUpper))];
148
+ if (operation.queryChannel.present) requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("searchParams"), ts.factory.createCallExpression(ts.factory.createIdentifier("buildSearchParams"), void 0, [ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "query")])));
149
+ if (operation.bodyChannel.present) requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("json"), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "body")));
150
+ requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("signal"), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "signal")));
151
+ const requestCall = ts.factory.createCallExpression(ts.factory.createIdentifier(operation.requestFunction), operation.responseTypeExpr ? [createTypeNodeFromText(operation.responseTypeExpr)] : void 0, [parseExpression(operation.pathInvocationExpr), ts.factory.createObjectLiteralExpression(requestProperties, true)]);
152
+ return ts.factory.createFunctionDeclaration([createExportModifier()], void 0, ts.factory.createIdentifier(operation.funcName), void 0, [ts.factory.createParameterDeclaration(void 0, void 0, ts.factory.createIdentifier("options"), void 0, ts.factory.createTypeReferenceNode(operation.optionTypeName)), ts.factory.createParameterDeclaration(void 0, void 0, ts.factory.createIdentifier("requestOptions"), void 0, ts.factory.createTypeReferenceNode("RuntimeRequestOptions"), ts.factory.createObjectLiteralExpression())], createTypeNodeFromText(operation.returnTypeExpr), ts.factory.createBlock([ts.factory.createReturnStatement(requestCall)], true));
153
+ }
154
+ function createBuildSearchParamsFunction() {
155
+ return ts.factory.createFunctionDeclaration(void 0, void 0, ts.factory.createIdentifier("buildSearchParams"), void 0, [ts.factory.createParameterDeclaration(void 0, void 0, ts.factory.createIdentifier("query"), void 0, createTypeNodeFromText("Record<string, unknown> | undefined"))], createTypeNodeFromText("URLSearchParams | undefined"), ts.factory.createBlock([
156
+ ts.factory.createIfStatement(ts.factory.createBinaryExpression(ts.factory.createIdentifier("query"), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createIdentifier("undefined")), ts.factory.createReturnStatement(ts.factory.createIdentifier("undefined"))),
157
+ ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier("entries"), void 0, void 0, parseExpression("Object.entries(query).filter(([, value]) => value != null)"))], ts.NodeFlags.Const)),
158
+ ts.factory.createIfStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("entries"), "length"), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createNumericLiteral("0")), ts.factory.createReturnStatement(ts.factory.createIdentifier("undefined"))),
159
+ ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier("searchParams"), void 0, void 0, ts.factory.createNewExpression(ts.factory.createIdentifier("URLSearchParams"), void 0, []))], ts.NodeFlags.Const)),
160
+ ts.factory.createForOfStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createArrayBindingPattern([ts.factory.createBindingElement(void 0, void 0, "key"), ts.factory.createBindingElement(void 0, void 0, "value")]))], ts.NodeFlags.Const), ts.factory.createIdentifier("entries"), ts.factory.createBlock([ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("searchParams"), "set"), void 0, [ts.factory.createIdentifier("key"), ts.factory.createCallExpression(ts.factory.createIdentifier("String"), void 0, [ts.factory.createIdentifier("value")])]))], true)),
161
+ ts.factory.createReturnStatement(ts.factory.createIdentifier("searchParams"))
162
+ ], true));
163
+ }
164
+ function capitalize$1(value) {
165
+ if (value.length === 0) return value;
166
+ return `${value[0].toUpperCase()}${value.slice(1)}`;
167
+ }
168
+ //#endregion
169
+ //#region src/normalization.ts
170
+ const HTTP_METHODS = [
171
+ "get",
172
+ "put",
173
+ "post",
174
+ "delete",
175
+ "patch"
176
+ ];
177
+ function buildClientRenderModelFromOperations(operations, spec, requestFunctionNames = {
178
+ json: "requestJson",
179
+ void: "requestVoid"
180
+ }) {
181
+ const context = buildNormalizationContext(spec);
182
+ const normalized = operations.map((entry) => normalizeOperation(entry, context, requestFunctionNames));
183
+ return {
184
+ operations: normalized,
185
+ needsSearchParamsHelper: normalized.some((operation) => operation.queryChannel.present),
186
+ typeAliases: collectTypeAliases(normalized)
187
+ };
188
+ }
189
+ function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
190
+ const apiPaths = Object.keys(spec.paths ?? {}).filter((path) => path.startsWith(pathPrefix)).sort();
191
+ if (apiPaths.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
192
+ const entries = [];
193
+ for (const apiPath of apiPaths) {
194
+ const pathItem = spec.paths?.[apiPath];
195
+ if (!pathItem) continue;
196
+ for (const method of HTTP_METHODS) {
197
+ const operation = pathItem[method];
198
+ if (!operation?.operationId) continue;
199
+ const strippedPath = stripPrefix ? apiPath.replace(pathPrefix, "") : apiPath;
200
+ entries.push({
201
+ apiPath,
202
+ funcName: makeFuncName(operation.operationId, method, apiPath, pathPrefix),
203
+ group: strippedPath.split("/")[0] ?? "misc",
204
+ method,
205
+ operation,
206
+ operationId: operation.operationId,
207
+ strippedPath
208
+ });
209
+ }
210
+ }
211
+ return entries;
212
+ }
213
+ function getEffectiveParametersByLocation(entry, location) {
214
+ return (entry.operation.parameters ?? []).filter((parameter) => getEffectiveParameterLocation(entry.apiPath, parameter) === location);
215
+ }
216
+ function warnOnParameterLocationMismatch(operations) {
217
+ for (const entry of operations) for (const parameter of entry.operation.parameters ?? []) {
218
+ const effectiveLocation = getEffectiveParameterLocation(entry.apiPath, parameter);
219
+ if (effectiveLocation === parameter.in) continue;
220
+ console.warn(`[openapi-codegen] normalized parameter "${parameter.name}" for "${entry.operationId}" from ${parameter.in} to ${effectiveLocation}.`);
221
+ }
222
+ }
223
+ function buildNormalizationContext(spec) {
224
+ const schemaAliasIndex = /* @__PURE__ */ new Map();
225
+ const schemaNames = /* @__PURE__ */ new Set();
226
+ for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
227
+ schemaNames.add(name);
228
+ const props = Object.keys(schema.properties ?? {}).sort().join(",");
229
+ if (!props) continue;
230
+ if (!schemaAliasIndex.has(props)) schemaAliasIndex.set(props, []);
231
+ schemaAliasIndex.get(props)?.push(name);
232
+ }
233
+ return {
234
+ schemaAliasIndex,
235
+ schemaNames,
236
+ usedTypeNames: new Set(schemaNames)
237
+ };
238
+ }
239
+ function getParametersByLocation(operation, location) {
240
+ return (operation.parameters ?? []).filter((parameter) => parameter.in === location);
241
+ }
242
+ function hasRequiredChannel(parameters) {
243
+ return parameters.some((parameter) => parameter.required);
244
+ }
245
+ function getJsonRequestBody(operation) {
246
+ const requestBody = operation.requestBody;
247
+ if (!requestBody) return void 0;
248
+ const jsonBody = requestBody.content?.["application/json"];
249
+ if (!jsonBody) throw new Error(`Operation "${operation.operationId ?? "unknown"}" has a requestBody but no application/json content`);
250
+ return jsonBody;
251
+ }
252
+ function getSuccessResponseInfo(operation) {
253
+ const successResponses = Object.entries(operation.responses ?? {}).filter(([statusKey]) => isSuccessStatus(statusKey)).sort(([left], [right]) => Number(left) - Number(right));
254
+ if (successResponses.length === 0) throw new Error(`Operation "${operation.operationId ?? "unknown"}" has no 2xx success response`);
255
+ const withJson = successResponses.find(([, response]) => response.content?.["application/json"]);
256
+ if (withJson) return {
257
+ hasJsonBody: true,
258
+ statusKey: withJson[0]
259
+ };
260
+ return {
261
+ hasJsonBody: false,
262
+ statusKey: successResponses[0][0]
263
+ };
264
+ }
265
+ function isSuccessStatus(statusKey) {
266
+ const value = Number(statusKey);
267
+ return Number.isInteger(value) && value >= 200 && value < 300;
268
+ }
269
+ function formatStatusKey(statusKey) {
270
+ return Number.isInteger(Number(statusKey)) ? statusKey : `'${statusKey}'`;
271
+ }
272
+ function getBuilderAlias(funcName) {
273
+ return `build${capitalize(funcName)}Path`;
274
+ }
275
+ function getClientOptionTypeName(funcName) {
276
+ return `${capitalize(funcName)}Options`;
277
+ }
278
+ function makeFuncName(operationId, method, apiPath, pathPrefix = "/api/") {
279
+ const normalizedOperationId = toFunctionName(operationId);
280
+ if (normalizedOperationId.length > 0) return normalizedOperationId;
281
+ const segments = apiPath.replace(pathPrefix, "").split("/");
282
+ const result = [];
283
+ for (const segment of segments) {
284
+ if (segment.startsWith("{")) {
285
+ const resource = segment.slice(1, -1).replace(/_id$/, "");
286
+ if (result.length > 0) result[result.length - 1] = resource;
287
+ continue;
288
+ }
289
+ result.push(segment);
290
+ }
291
+ return `${method}${capitalize(result.map((segment, index) => {
292
+ const clean = segment.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
293
+ return index === 0 ? clean : `${clean[0].toUpperCase()}${clean.slice(1)}`;
294
+ }).join(""))}`;
295
+ }
296
+ function toFunctionName(value) {
297
+ const segments = value.split(/[^a-zA-Z0-9]+/g).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
298
+ if (segments.length === 0) return "";
299
+ const [firstSegment, ...restSegments] = segments;
300
+ const normalized = [firstSegment[0].toLowerCase() + firstSegment.slice(1), ...restSegments.map((segment) => capitalize(segment.toLowerCase()))].join("");
301
+ return /^[A-Za-z_$]/.test(normalized) ? normalized : `_${normalized}`;
302
+ }
303
+ function capitalize(value) {
304
+ if (value.length === 0) return value;
305
+ return `${value[0].toUpperCase()}${value.slice(1)}`;
306
+ }
307
+ function getEffectiveParameterLocation(apiPath, parameter) {
308
+ if (parameter.in !== "path") return parameter.in;
309
+ return getTemplateParameterNames(apiPath).has(parameter.name) ? "path" : "query";
310
+ }
311
+ function getTemplateParameterNames(apiPath) {
312
+ const matches = apiPath.match(/\{(\w+)\}/g) ?? [];
313
+ return new Set(matches.map((match) => match.slice(1, -1)));
314
+ }
315
+ function resolveParameterTypeReference(entry, context, location) {
316
+ const effectiveParameters = getEffectiveParametersByLocation(entry, location);
317
+ if (effectiveParameters.length === 0) return null;
318
+ const schemaTypeRef = resolveAlias(context, effectiveParameters.map((parameter) => parameter.name), entry.operation.tags?.[0]);
319
+ if (schemaTypeRef) return schemaTypeRef;
320
+ const rawParameters = getParametersByLocation(entry.operation, location);
321
+ const typeName = allocateOperationTypeName(context, entry.funcName, location === "path" ? "Path" : "Query");
322
+ if (hasSameParameterNames(rawParameters, effectiveParameters)) return {
323
+ aliasDefinitionExpr: `operations['${entry.operationId}']['parameters']['${location}']`,
324
+ sourceExpr: `operations['${entry.operationId}']['parameters']['${location}']`,
325
+ typeName
326
+ };
327
+ return {
328
+ aliasDefinitionExpr: renderInlineParameterObject(effectiveParameters),
329
+ sourceExpr: renderInlineParameterObject(effectiveParameters),
330
+ typeName
331
+ };
332
+ }
333
+ function normalizeOperation(entry, context, requestFunctionNames) {
334
+ const successResponse = getSuccessResponseInfo(entry.operation);
335
+ const builderAlias = getBuilderAlias(entry.funcName);
336
+ const pathChannel = normalizeParameterChannel(entry, context, "path");
337
+ const queryChannel = normalizeParameterChannel(entry, context, "query");
338
+ const bodyChannel = normalizeBodyChannel(entry, context);
339
+ const responseTypeRef = resolveResponseTypeReference(entry, context, successResponse);
340
+ return {
341
+ bodyChannel,
342
+ builderAlias,
343
+ entry,
344
+ optionTypeName: getClientOptionTypeName(entry.funcName),
345
+ pathChannel,
346
+ pathInvocationExpr: pathChannel.present ? `${builderAlias}(options.path)` : `${builderAlias}()`,
347
+ queryChannel,
348
+ requestFunction: responseTypeRef ? requestFunctionNames.json : requestFunctionNames.void,
349
+ responseTypeRef,
350
+ returnTypeExpr: responseTypeRef ? `Promise<${responseTypeRef.typeName}>` : "Promise<void>"
351
+ };
352
+ }
353
+ function normalizeParameterChannel(entry, context, location) {
354
+ const parameters = getEffectiveParametersByLocation(entry, location);
355
+ if (parameters.length === 0) return {
356
+ present: false,
357
+ required: false,
358
+ typeRef: null
359
+ };
360
+ return {
361
+ present: true,
362
+ required: hasRequiredChannel(parameters),
363
+ typeRef: resolveParameterTypeReference(entry, context, location)
364
+ };
365
+ }
366
+ function normalizeBodyChannel(entry, context) {
367
+ const typeRef = resolveRequestBodyTypeReference(entry, context);
368
+ if (!typeRef) return {
369
+ present: false,
370
+ required: false,
371
+ typeRef: null
372
+ };
373
+ return {
374
+ present: true,
375
+ required: entry.operation.requestBody?.required !== false,
376
+ typeRef
377
+ };
378
+ }
379
+ function resolveRequestBodyTypeReference(entry, context) {
380
+ const jsonBody = getJsonRequestBody(entry.operation);
381
+ if (!jsonBody) return null;
382
+ const schemaTypeRef = resolveSchemaTypeReference(context, jsonBody.schema);
383
+ if (schemaTypeRef) return schemaTypeRef;
384
+ return {
385
+ aliasDefinitionExpr: `operations['${entry.operationId}']['requestBody']['content']['application/json']`,
386
+ sourceExpr: `operations['${entry.operationId}']['requestBody']['content']['application/json']`,
387
+ typeName: allocateOperationTypeName(context, entry.funcName, "Request")
388
+ };
389
+ }
390
+ function resolveResponseTypeReference(entry, context, successResponse) {
391
+ if (!successResponse.hasJsonBody) return null;
392
+ const jsonContent = (entry.operation.responses?.[successResponse.statusKey])?.content?.["application/json"];
393
+ const schemaTypeRef = resolveSchemaTypeReference(context, jsonContent?.schema);
394
+ if (schemaTypeRef) return schemaTypeRef;
395
+ return {
396
+ aliasDefinitionExpr: `operations['${entry.operationId}']['responses'][${formatStatusKey(successResponse.statusKey)}]['content']['application/json']`,
397
+ sourceExpr: `operations['${entry.operationId}']['responses'][${formatStatusKey(successResponse.statusKey)}]['content']['application/json']`,
398
+ typeName: allocateOperationTypeName(context, entry.funcName, "Response")
399
+ };
400
+ }
401
+ function hasSameParameterNames(left, right) {
402
+ if (left.length !== right.length) return false;
403
+ const leftNames = left.map((parameter) => parameter.name).sort();
404
+ const rightNames = right.map((parameter) => parameter.name).sort();
405
+ return leftNames.every((name, index) => name === rightNames[index]);
406
+ }
407
+ function resolveAlias(context, parameterNames, tag) {
408
+ const key = [...parameterNames].sort().join(",");
409
+ const candidates = context.schemaAliasIndex.get(key);
410
+ if (!candidates || candidates.length === 0) return;
411
+ if (candidates.length === 1) return createSchemaTypeReference(candidates[0]);
412
+ if (tag) {
413
+ const singularTag = tag.replace(/s$/, "");
414
+ const prefix = `${singularTag[0]?.toUpperCase() ?? ""}${singularTag.slice(1)}`;
415
+ const match = candidates.find((candidate) => candidate.startsWith(prefix));
416
+ if (match) return createSchemaTypeReference(match);
417
+ }
418
+ return createSchemaTypeReference(candidates[0]);
419
+ }
420
+ function resolveSchemaTypeReference(context, schema) {
421
+ const ref = readSchemaRef(schema);
422
+ if (!ref) return;
423
+ const schemaName = ref.split("/").pop();
424
+ if (!schemaName || !context.schemaNames.has(schemaName)) return;
425
+ return createSchemaTypeReference(schemaName);
426
+ }
427
+ function createSchemaTypeReference(schemaName) {
428
+ return {
429
+ aliasDefinitionExpr: null,
430
+ sourceExpr: `components['schemas']['${schemaName}']`,
431
+ typeName: schemaName
432
+ };
433
+ }
434
+ function readSchemaRef(schema) {
435
+ if (!schema || typeof schema !== "object") return;
436
+ const maybeRef = schema.$ref;
437
+ return typeof maybeRef === "string" ? maybeRef : void 0;
438
+ }
439
+ function renderInlineParameterObject(parameters) {
440
+ return `{ ${parameters.map((parameter) => {
441
+ const optionalMarker = parameter.required ? "" : "?";
442
+ return `${parameter.name}${optionalMarker}: ${renderPrimitiveSchemaType(parameter.schema)}`;
443
+ }).join("; ")} }`;
444
+ }
445
+ function renderPrimitiveSchemaType(schema) {
446
+ const type = schema?.type;
447
+ if (!type) return "unknown";
448
+ if (Array.isArray(type)) return type.map((memberType) => mapPrimitiveType(memberType)).join(" | ");
449
+ return mapPrimitiveType(type);
450
+ }
451
+ function allocateOperationTypeName(context, funcName, suffix) {
452
+ const preferredName = `${capitalize(funcName)}${suffix}`;
453
+ if (!context.usedTypeNames.has(preferredName)) {
454
+ context.usedTypeNames.add(preferredName);
455
+ return preferredName;
456
+ }
457
+ let counter = 2;
458
+ let candidate = `${preferredName}_${counter}`;
459
+ while (context.usedTypeNames.has(candidate)) {
460
+ counter += 1;
461
+ candidate = `${preferredName}_${counter}`;
462
+ }
463
+ context.usedTypeNames.add(candidate);
464
+ return candidate;
465
+ }
466
+ function collectTypeAliases(operations) {
467
+ const aliases = /* @__PURE__ */ new Map();
468
+ for (const operation of operations) {
469
+ collectTypeAlias(aliases, operation.pathChannel.typeRef);
470
+ collectTypeAlias(aliases, operation.queryChannel.typeRef);
471
+ collectTypeAlias(aliases, operation.bodyChannel.typeRef);
472
+ collectTypeAlias(aliases, operation.responseTypeRef);
473
+ }
474
+ return [...aliases.entries()].map(([typeName, definitionExpr]) => ({
475
+ definitionExpr,
476
+ typeName
477
+ })).sort((left, right) => left.typeName.localeCompare(right.typeName));
478
+ }
479
+ function collectTypeAlias(aliases, typeRef) {
480
+ if (!typeRef || typeRef.aliasDefinitionExpr == null) return;
481
+ const existing = aliases.get(typeRef.typeName);
482
+ if (existing && existing !== typeRef.aliasDefinitionExpr) throw new Error(`Conflicting generated type alias "${typeRef.typeName}" with incompatible definitions.`);
483
+ aliases.set(typeRef.typeName, typeRef.aliasDefinitionExpr);
484
+ }
485
+ function mapPrimitiveType(type) {
486
+ switch (type) {
487
+ case "integer":
488
+ case "number": return "number";
489
+ case "boolean": return "boolean";
490
+ case "null": return "null";
491
+ case "string": return "string";
492
+ default: return "unknown";
493
+ }
494
+ }
495
+ //#endregion
496
+ //#region src/plugin.ts
497
+ const HTTP_CLIENT_DEFAULTS = {
498
+ module: "#/integrations/http",
499
+ jsonFunction: "requestJson",
500
+ voidFunction: "requestVoid",
501
+ requestOptionsType: "ApiRequestOptions",
502
+ omitKeys: [
503
+ "json",
504
+ "method",
505
+ "searchParams",
506
+ "signal"
507
+ ]
508
+ };
509
+ function resolveHttpClientConfig(config) {
510
+ return {
511
+ ...HTTP_CLIENT_DEFAULTS,
512
+ ...config
513
+ };
514
+ }
515
+ const GENERATED_HEADER = ["// This file is auto-generated by vite-plugin-openapi-codegen.", "// Do not edit manually. Changes will be overwritten on next build."];
516
+ async function generateApiTypes(source, outputDir, useTypeAliases) {
517
+ const { default: openapiTS, astToString } = await import("openapi-typescript");
518
+ const contents = astToString(await openapiTS(source, useTypeAliases ? {
519
+ rootTypes: true,
520
+ rootTypesKeepCasing: true,
521
+ rootTypesNoSchemaPrefix: true
522
+ } : void 0));
523
+ writeFileSync(resolve(outputDir, "api-types.d.ts"), `${GENERATED_HEADER.join("\n")}\n\n${contents}`);
524
+ }
525
+ function createApiEntries(normalizedOps, useTypeAliases) {
526
+ return normalizedOps.map((op) => ({
527
+ funcName: op.entry.funcName,
528
+ group: op.entry.group,
529
+ pathTypeExpr: op.pathChannel.typeRef == null ? null : useTypeAliases ? op.pathChannel.typeRef.typeName : op.pathChannel.typeRef.sourceExpr,
530
+ strippedPath: op.entry.strippedPath
531
+ }));
532
+ }
533
+ function createClientRenderModel(model, useTypeAliases) {
534
+ const resolveTypeExpr = (typeRef) => typeRef == null ? null : useTypeAliases ? typeRef.typeName : typeRef.sourceExpr;
535
+ const resolveReturnTypeExpr = (operation) => {
536
+ const responseTypeExpr = resolveTypeExpr(operation.responseTypeRef);
537
+ return responseTypeExpr ? `Promise<${responseTypeExpr}>` : "Promise<void>";
538
+ };
539
+ return {
540
+ needsSearchParamsHelper: model.needsSearchParamsHelper,
541
+ operations: model.operations.map((operation) => ({
542
+ bodyChannel: resolveChannel(operation.bodyChannel, useTypeAliases),
543
+ builderAlias: operation.builderAlias,
544
+ funcName: operation.entry.funcName,
545
+ group: operation.entry.group,
546
+ methodUpper: operation.entry.method.toUpperCase(),
547
+ optionTypeName: operation.optionTypeName,
548
+ pathChannel: resolveChannel(operation.pathChannel, useTypeAliases),
549
+ pathInvocationExpr: operation.pathInvocationExpr,
550
+ queryChannel: resolveChannel(operation.queryChannel, useTypeAliases),
551
+ requestFunction: operation.requestFunction,
552
+ responseTypeExpr: resolveTypeExpr(operation.responseTypeRef),
553
+ returnTypeExpr: resolveReturnTypeExpr(operation)
554
+ }))
555
+ };
556
+ }
557
+ function resolveChannel(channel, useTypeAliases) {
558
+ if (!channel.typeRef) return channel;
559
+ return {
560
+ ...channel,
561
+ typeRef: {
562
+ ...channel.typeRef,
563
+ sourceExpr: useTypeAliases ? channel.typeRef.typeName : channel.typeRef.sourceExpr
564
+ }
565
+ };
566
+ }
567
+ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
568
+ const pathPrefix = options.pathPrefix ?? "/api/";
569
+ const stripPrefix = options.stripPrefix ?? true;
570
+ const httpClient = resolveHttpClientConfig(options.httpClient);
571
+ const useTypeAliases = options.typeAliases ?? false;
572
+ const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
573
+ json: httpClient.jsonFunction,
574
+ void: httpClient.voidFunction
575
+ });
576
+ if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
577
+ return {
578
+ api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
579
+ client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
580
+ ...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
581
+ };
582
+ }
583
+ async function loadOpenAPIInput(root, input) {
584
+ if (isHttpUrl(input)) {
585
+ const sourceText = await fetchRemoteOpenAPIInput(input);
586
+ return {
587
+ apiTypesSource: sourceText,
588
+ spec: parseOpenAPISpec(sourceText, input)
589
+ };
590
+ }
591
+ const inputPath = resolve(root, input);
592
+ const sourceText = readFileSync(inputPath, "utf-8");
593
+ return {
594
+ apiTypesSource: pathToFileURL(inputPath),
595
+ spec: parseOpenAPISpec(sourceText, inputPath)
596
+ };
597
+ }
598
+ function isHttpUrl(value) {
599
+ try {
600
+ const url = new URL(value);
601
+ return url.protocol === "http:" || url.protocol === "https:";
602
+ } catch {
603
+ return false;
604
+ }
605
+ }
606
+ async function fetchRemoteOpenAPIInput(input) {
607
+ const response = await fetch(input);
608
+ if (!response.ok) throw new Error(`[openapi-codegen] Failed to fetch OpenAPI document from ${input}: ${response.status} ${response.statusText}`);
609
+ return response.text();
610
+ }
611
+ function parseOpenAPISpec(sourceText, inputLabel) {
612
+ const parsed = parse(sourceText);
613
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`[openapi-codegen] Expected OpenAPI document object from ${inputLabel}`);
614
+ return parsed;
615
+ }
616
+ async function generateOpenAPIArtifacts(root, options) {
617
+ const outputDir = resolve(root, options.output);
618
+ mkdirSync(outputDir, { recursive: true });
619
+ const { apiTypesSource, spec } = await loadOpenAPIInput(root, options.input);
620
+ const operations = collectOperations(spec, options.pathPrefix ?? "/api/", options.stripPrefix ?? true);
621
+ const artifacts = renderGeneratedArtifacts(spec, options, operations);
622
+ warnOnParameterLocationMismatch(operations);
623
+ await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
624
+ if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
625
+ writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
626
+ writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
627
+ }
628
+ //#endregion
629
+ //#region src/cli.ts
630
+ const CONFIG_FILE_NAMES = [
631
+ "vite.config.ts",
632
+ "vite.config.mts",
633
+ "vite.config.js",
634
+ "vite.config.mjs",
635
+ "vite.config.cjs",
636
+ "vite.config.cts"
637
+ ];
638
+ async function main(argv) {
639
+ const flags = parseArgs(argv);
640
+ if (flags.help) {
641
+ printHelp();
642
+ return;
643
+ }
644
+ const root = resolve(process.cwd(), flags.root ?? ".");
645
+ const options = mergeOptions(await loadOptionsFromViteConfig(root, flags.config), createOptionsFromFlags(flags));
646
+ if (!options.input || !options.output) throw new Error("[openapi-codegen] Missing required options. Provide --input and --output or configure openapiCodegen() in vite.config.");
647
+ await generateOpenAPIArtifacts(root, options);
648
+ console.log(`[openapi-codegen] generated files in ${resolve(root, options.output)}`);
649
+ }
650
+ function parseArgs(argv) {
651
+ const flags = {};
652
+ for (let index = 0; index < argv.length; index++) {
653
+ const arg = argv[index];
654
+ const [name, inlineValue] = arg.split("=", 2);
655
+ switch (name) {
656
+ case "--config":
657
+ flags.config = readValue(argv, ++index, inlineValue, name);
658
+ break;
659
+ case "--help":
660
+ case "-h":
661
+ flags.help = true;
662
+ break;
663
+ case "--http-client-json-function":
664
+ flags.httpClientJsonFunction = readValue(argv, ++index, inlineValue, name);
665
+ break;
666
+ case "--http-client-module":
667
+ flags.httpClientModule = readValue(argv, ++index, inlineValue, name);
668
+ break;
669
+ case "--http-client-omit-keys":
670
+ flags.httpClientOmitKeys = parseCommaList(readValue(argv, ++index, inlineValue, name));
671
+ break;
672
+ case "--http-client-request-options-type":
673
+ flags.httpClientRequestOptionsType = readValue(argv, ++index, inlineValue, name);
674
+ break;
675
+ case "--http-client-void-function":
676
+ flags.httpClientVoidFunction = readValue(argv, ++index, inlineValue, name);
677
+ break;
678
+ case "--input":
679
+ flags.input = readValue(argv, ++index, inlineValue, name);
680
+ break;
681
+ case "--output":
682
+ flags.output = readValue(argv, ++index, inlineValue, name);
683
+ break;
684
+ case "--path-prefix":
685
+ flags.pathPrefix = readValue(argv, ++index, inlineValue, name);
686
+ break;
687
+ case "--root":
688
+ flags.root = readValue(argv, ++index, inlineValue, name);
689
+ break;
690
+ case "--strip-prefix":
691
+ flags.stripPrefix = parseBoolean(readValue(argv, ++index, inlineValue, name), name);
692
+ break;
693
+ case "--type-aliases":
694
+ flags.typeAliases = true;
695
+ break;
696
+ case "--no-strip-prefix":
697
+ flags.stripPrefix = false;
698
+ break;
699
+ case "--no-type-aliases":
700
+ flags.typeAliases = false;
701
+ break;
702
+ default: throw new Error(`[openapi-codegen] Unknown option: ${arg}`);
703
+ }
704
+ if (inlineValue !== void 0) index--;
705
+ }
706
+ return flags;
707
+ }
708
+ function readValue(argv, index, inlineValue, name) {
709
+ if (inlineValue !== void 0) return inlineValue;
710
+ const value = argv[index];
711
+ if (!value || value.startsWith("--")) throw new Error(`[openapi-codegen] Missing value for ${name}`);
712
+ return value;
713
+ }
714
+ function parseBoolean(value, name) {
715
+ if (value === "true") return true;
716
+ if (value === "false") return false;
717
+ throw new Error(`[openapi-codegen] Expected ${name} to be true or false`);
718
+ }
719
+ function parseCommaList(value) {
720
+ if (value.length === 0) return [];
721
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
722
+ }
723
+ async function loadOptionsFromViteConfig(root, configPath) {
724
+ const resolvedConfigPath = resolveViteConfigPath(root, configPath);
725
+ if (!resolvedConfigPath) return {};
726
+ return extractOpenAPICodegenOptions((await loadConfigFromFile({
727
+ command: "serve",
728
+ mode: "development"
729
+ }, resolvedConfigPath, root, "silent"))?.config.plugins) ?? {};
730
+ }
731
+ function resolveViteConfigPath(root, configPath) {
732
+ if (configPath) {
733
+ const resolved = resolve(root, configPath);
734
+ if (!existsSync(resolved)) throw new Error(`[openapi-codegen] Vite config not found: ${resolved}`);
735
+ return resolved;
736
+ }
737
+ return CONFIG_FILE_NAMES.map((fileName) => resolve(root, fileName)).find((filePath) => existsSync(filePath));
738
+ }
739
+ function extractOpenAPICodegenOptions(plugins) {
740
+ return flattenPlugins(plugins).find(isOpenAPICodegenPlugin)?.api?.options;
741
+ }
742
+ function flattenPlugins(value) {
743
+ if (!Array.isArray(value)) return [];
744
+ return value.flatMap((plugin) => {
745
+ if (Array.isArray(plugin)) return flattenPlugins(plugin);
746
+ if (!plugin || typeof plugin !== "object") return [];
747
+ return [plugin];
748
+ });
749
+ }
750
+ function isOpenAPICodegenPlugin(plugin) {
751
+ return plugin.name === "openapi-codegen" && Boolean(plugin.api?.options);
752
+ }
753
+ function createOptionsFromFlags(flags) {
754
+ const httpClient = createHttpClientConfigFromFlags(flags);
755
+ return {
756
+ ...flags.input ? { input: flags.input } : {},
757
+ ...flags.output ? { output: flags.output } : {},
758
+ ...flags.pathPrefix ? { pathPrefix: flags.pathPrefix } : {},
759
+ ...flags.stripPrefix !== void 0 ? { stripPrefix: flags.stripPrefix } : {},
760
+ ...flags.typeAliases !== void 0 ? { typeAliases: flags.typeAliases } : {},
761
+ ...httpClient ? { httpClient } : {}
762
+ };
763
+ }
764
+ function createHttpClientConfigFromFlags(flags) {
765
+ const httpClient = {
766
+ ...flags.httpClientModule ? { module: flags.httpClientModule } : {},
767
+ ...flags.httpClientJsonFunction ? { jsonFunction: flags.httpClientJsonFunction } : {},
768
+ ...flags.httpClientVoidFunction ? { voidFunction: flags.httpClientVoidFunction } : {},
769
+ ...flags.httpClientRequestOptionsType ? { requestOptionsType: flags.httpClientRequestOptionsType } : {},
770
+ ...flags.httpClientOmitKeys ? { omitKeys: flags.httpClientOmitKeys } : {}
771
+ };
772
+ return Object.keys(httpClient).length > 0 ? httpClient : void 0;
773
+ }
774
+ function mergeOptions(configOptions, explicitOptions) {
775
+ return {
776
+ ...configOptions,
777
+ ...explicitOptions,
778
+ httpClient: configOptions.httpClient || explicitOptions.httpClient ? {
779
+ ...configOptions.httpClient,
780
+ ...explicitOptions.httpClient
781
+ } : void 0
782
+ };
783
+ }
784
+ function printHelp() {
785
+ console.log(`Usage: vg [options]
786
+
787
+ Generate OpenAPI client files once.
788
+
789
+ Options:
790
+ --input <path-or-url> OpenAPI JSON/YAML input
791
+ --output <dir> Generated output directory
792
+ --root <dir> Project root, defaults to cwd
793
+ --config <file> Vite config path, defaults to vite.config.*
794
+ --path-prefix <prefix> Path prefix filter
795
+ --strip-prefix <true|false> Strip path prefix from generated paths
796
+ --no-strip-prefix Disable path prefix stripping
797
+ --type-aliases Enable top-level generated type aliases
798
+ --no-type-aliases Disable top-level generated type aliases
799
+ --http-client-module <module> Runtime HTTP module import path
800
+ --http-client-json-function <name> JSON request function name
801
+ --http-client-void-function <name> Void request function name
802
+ --http-client-request-options-type <name> Request options type name
803
+ --http-client-omit-keys <keys> Comma-separated keys omitted from request options
804
+ -h, --help Show help
805
+ `);
806
+ }
807
+ main(process.argv.slice(2)).catch((error) => {
808
+ console.error(error instanceof Error ? error.message : error);
809
+ process.exitCode = 1;
810
+ });
811
+ //#endregion
812
+ export {};