vite-plugin-openapi-codegen 1.1.2 → 1.2.0
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 +25 -7
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +848 -0
- package/dist/index.mjs +60 -27
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The plugin reads your OpenAPI JSON or YAML spec, runs `openapi-typescript`, and
|
|
|
8
8
|
- `api.ts` for path builder functions
|
|
9
9
|
- `client.ts` for typed request helpers
|
|
10
10
|
|
|
11
|
-
It also
|
|
11
|
+
It also regenerates local input specs through Vite HMR when the spec changes. In dev mode, generation runs in the background so Vite can finish starting even if the remote spec is temporarily unavailable. Online `http://` and `https://` inputs are fetched once when Vite starts. Build mode skips the plugin entirely. For manual generation, the package also ships a `vg` CLI that reads your local `vite.config.*` and runs the same generator once.
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
@@ -36,6 +36,22 @@ export default defineConfig({
|
|
|
36
36
|
});
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
+
If you want to reuse the same configuration outside Vite, run the CLI from the project root:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
vg
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
By default, `vg` searches for `vite.config.ts`, `vite.config.mts`, `vite.config.js`, `vite.config.mjs`, `vite.config.cjs`, or `vite.config.cts` in the current working directory. It reads the `openapiCodegen(...)` plugin options from that file, then generates once with those values.
|
|
46
|
+
|
|
47
|
+
You can also pass explicit flags. CLI flags always win over the config file:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
vg --root ./example/local
|
|
51
|
+
vg --input openapi.yaml --output src/generated
|
|
52
|
+
vg --path-prefix /v1/ --type-aliases
|
|
53
|
+
```
|
|
54
|
+
|
|
39
55
|
Online YAML inputs use the same option:
|
|
40
56
|
|
|
41
57
|
```ts
|
|
@@ -209,6 +225,8 @@ The generated client shape depends on the OpenAPI operation:
|
|
|
209
225
|
- path parameters become `options.path`
|
|
210
226
|
- query parameters become `options.query`
|
|
211
227
|
- JSON request bodies become `options.body`
|
|
228
|
+
- `application/octet-stream` request bodies become `options.body` with `contentType: "application/octet-stream"`
|
|
229
|
+
- `multipart/form-data` request bodies become `options.body` as `FormData`
|
|
212
230
|
- JSON responses become typed `Promise<T>`
|
|
213
231
|
- empty responses use the configured void request function
|
|
214
232
|
|
|
@@ -233,29 +251,29 @@ interface Options {
|
|
|
233
251
|
|
|
234
252
|
### `input`
|
|
235
253
|
|
|
236
|
-
Path or URL to the OpenAPI JSON/YAML document. Local paths are resolved relative to the Vite project root and watched in dev mode. Online `http://` and `https://` URLs are fetched once at startup and are not watched.
|
|
254
|
+
Path or URL to the OpenAPI JSON/YAML document. Local paths are resolved relative to the Vite project root and watched in dev mode. Online `http://` and `https://` URLs are fetched once at startup and are not watched. The `vg` CLI reads this value from `vite.config.*` when present, unless you override it with `--input`.
|
|
237
255
|
|
|
238
256
|
In dev mode, generation errors are logged and do not stop Vite from starting. In build mode, the plugin is skipped entirely.
|
|
239
257
|
|
|
240
258
|
### `output`
|
|
241
259
|
|
|
242
|
-
Directory where generated files are written, relative to the Vite project root.
|
|
260
|
+
Directory where generated files are written, relative to the Vite project root. The `vg` CLI reads this value from `vite.config.*` when present, unless you override it with `--output`.
|
|
243
261
|
|
|
244
262
|
### `pathPrefix`
|
|
245
263
|
|
|
246
|
-
Only paths starting with this prefix are included. The default is `"/api/"`.
|
|
264
|
+
Only paths starting with this prefix are included. The default is `"/api/"`. The `vg` CLI reads this value from `vite.config.*` when present, unless you override it with `--path-prefix`.
|
|
247
265
|
|
|
248
266
|
### `stripPrefix`
|
|
249
267
|
|
|
250
|
-
Controls whether the `pathPrefix` is removed from generated path builders. The default is `true`.
|
|
268
|
+
Controls whether the `pathPrefix` is removed from generated path builders. The default is `true`. The `vg` CLI reads this value from `vite.config.*` when present, unless you override it with `--strip-prefix` or `--no-strip-prefix`.
|
|
251
269
|
|
|
252
270
|
### `typeAliases`
|
|
253
271
|
|
|
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.
|
|
272
|
+
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. The `vg` CLI reads this value from `vite.config.*` when present, unless you override it with `--type-aliases` or `--no-type-aliases`.
|
|
255
273
|
|
|
256
274
|
### `httpClient`
|
|
257
275
|
|
|
258
|
-
Overrides the runtime import path and symbol names used by generated clients.
|
|
276
|
+
Overrides the runtime import path and symbol names used by generated clients. The `vg` CLI reads this object from `vite.config.*` when present, unless you override the corresponding HTTP client flags.
|
|
259
277
|
|
|
260
278
|
## Programmatic Usage
|
|
261
279
|
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
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) {
|
|
150
|
+
if (operation.bodyContentType === "json") requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("json"), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "body")));
|
|
151
|
+
else requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("body"), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "body")));
|
|
152
|
+
if (operation.bodyContentType === "binary") requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("contentType"), ts.factory.createStringLiteral("application/octet-stream")));
|
|
153
|
+
}
|
|
154
|
+
requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("signal"), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "signal")));
|
|
155
|
+
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)]);
|
|
156
|
+
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));
|
|
157
|
+
}
|
|
158
|
+
function createBuildSearchParamsFunction() {
|
|
159
|
+
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([
|
|
160
|
+
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"))),
|
|
161
|
+
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)),
|
|
162
|
+
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"))),
|
|
163
|
+
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)),
|
|
164
|
+
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)),
|
|
165
|
+
ts.factory.createReturnStatement(ts.factory.createIdentifier("searchParams"))
|
|
166
|
+
], true));
|
|
167
|
+
}
|
|
168
|
+
function capitalize$1(value) {
|
|
169
|
+
if (value.length === 0) return value;
|
|
170
|
+
return `${value[0].toUpperCase()}${value.slice(1)}`;
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region src/normalization.ts
|
|
174
|
+
const HTTP_METHODS = [
|
|
175
|
+
"get",
|
|
176
|
+
"put",
|
|
177
|
+
"post",
|
|
178
|
+
"delete",
|
|
179
|
+
"patch"
|
|
180
|
+
];
|
|
181
|
+
function buildClientRenderModelFromOperations(operations, spec, requestFunctionNames = {
|
|
182
|
+
json: "requestJson",
|
|
183
|
+
void: "requestVoid"
|
|
184
|
+
}) {
|
|
185
|
+
const context = buildNormalizationContext(spec);
|
|
186
|
+
const normalized = operations.map((entry) => normalizeOperation(entry, context, requestFunctionNames));
|
|
187
|
+
return {
|
|
188
|
+
operations: normalized,
|
|
189
|
+
needsSearchParamsHelper: normalized.some((operation) => operation.queryChannel.present),
|
|
190
|
+
typeAliases: collectTypeAliases(normalized)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
|
|
194
|
+
const apiPaths = Object.keys(spec.paths ?? {}).filter((path) => path.startsWith(pathPrefix)).sort();
|
|
195
|
+
if (apiPaths.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
|
|
196
|
+
const entries = [];
|
|
197
|
+
for (const apiPath of apiPaths) {
|
|
198
|
+
const pathItem = spec.paths?.[apiPath];
|
|
199
|
+
if (!pathItem) continue;
|
|
200
|
+
for (const method of HTTP_METHODS) {
|
|
201
|
+
const operation = pathItem[method];
|
|
202
|
+
if (!operation?.operationId) continue;
|
|
203
|
+
const strippedPath = stripPrefix ? apiPath.replace(pathPrefix, "") : apiPath;
|
|
204
|
+
entries.push({
|
|
205
|
+
apiPath,
|
|
206
|
+
funcName: makeFuncName(operation.operationId, method, apiPath, pathPrefix),
|
|
207
|
+
group: strippedPath.split("/")[0] ?? "misc",
|
|
208
|
+
method,
|
|
209
|
+
operation,
|
|
210
|
+
operationId: operation.operationId,
|
|
211
|
+
strippedPath
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return entries;
|
|
216
|
+
}
|
|
217
|
+
function getEffectiveParametersByLocation(entry, location) {
|
|
218
|
+
return (entry.operation.parameters ?? []).filter((parameter) => getEffectiveParameterLocation(entry.apiPath, parameter) === location);
|
|
219
|
+
}
|
|
220
|
+
function warnOnParameterLocationMismatch(operations) {
|
|
221
|
+
for (const entry of operations) for (const parameter of entry.operation.parameters ?? []) {
|
|
222
|
+
const effectiveLocation = getEffectiveParameterLocation(entry.apiPath, parameter);
|
|
223
|
+
if (effectiveLocation === parameter.in) continue;
|
|
224
|
+
console.warn(`[openapi-codegen] normalized parameter "${parameter.name}" for "${entry.operationId}" from ${parameter.in} to ${effectiveLocation}.`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function buildNormalizationContext(spec) {
|
|
228
|
+
const schemaAliasIndex = /* @__PURE__ */ new Map();
|
|
229
|
+
const schemaNames = /* @__PURE__ */ new Set();
|
|
230
|
+
for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
|
|
231
|
+
schemaNames.add(name);
|
|
232
|
+
const props = Object.keys(schema.properties ?? {}).sort().join(",");
|
|
233
|
+
if (!props) continue;
|
|
234
|
+
if (!schemaAliasIndex.has(props)) schemaAliasIndex.set(props, []);
|
|
235
|
+
schemaAliasIndex.get(props)?.push(name);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
schemaAliasIndex,
|
|
239
|
+
schemaNames,
|
|
240
|
+
usedTypeNames: new Set(schemaNames)
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function getParametersByLocation(operation, location) {
|
|
244
|
+
return (operation.parameters ?? []).filter((parameter) => parameter.in === location);
|
|
245
|
+
}
|
|
246
|
+
function hasRequiredChannel(parameters) {
|
|
247
|
+
return parameters.some((parameter) => parameter.required);
|
|
248
|
+
}
|
|
249
|
+
function getSuccessResponseInfo(operation) {
|
|
250
|
+
const successResponses = Object.entries(operation.responses ?? {}).filter(([statusKey]) => isSuccessStatus(statusKey)).sort(([left], [right]) => Number(left) - Number(right));
|
|
251
|
+
if (successResponses.length === 0) throw new Error(`Operation "${operation.operationId ?? "unknown"}" has no 2xx success response`);
|
|
252
|
+
const withJson = successResponses.find(([, response]) => response.content?.["application/json"]);
|
|
253
|
+
if (withJson) return {
|
|
254
|
+
hasJsonBody: true,
|
|
255
|
+
statusKey: withJson[0]
|
|
256
|
+
};
|
|
257
|
+
return {
|
|
258
|
+
hasJsonBody: false,
|
|
259
|
+
statusKey: successResponses[0][0]
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function isSuccessStatus(statusKey) {
|
|
263
|
+
const value = Number(statusKey);
|
|
264
|
+
return Number.isInteger(value) && value >= 200 && value < 300;
|
|
265
|
+
}
|
|
266
|
+
function formatStatusKey(statusKey) {
|
|
267
|
+
return Number.isInteger(Number(statusKey)) ? statusKey : `'${statusKey}'`;
|
|
268
|
+
}
|
|
269
|
+
function getBuilderAlias(funcName) {
|
|
270
|
+
return `build${capitalize(funcName)}Path`;
|
|
271
|
+
}
|
|
272
|
+
function getClientOptionTypeName(funcName) {
|
|
273
|
+
return `${capitalize(funcName)}Options`;
|
|
274
|
+
}
|
|
275
|
+
function makeFuncName(operationId, method, apiPath, pathPrefix = "/api/") {
|
|
276
|
+
const normalizedOperationId = toFunctionName(operationId);
|
|
277
|
+
if (normalizedOperationId.length > 0) return normalizedOperationId;
|
|
278
|
+
const segments = apiPath.replace(pathPrefix, "").split("/");
|
|
279
|
+
const result = [];
|
|
280
|
+
for (const segment of segments) {
|
|
281
|
+
if (segment.startsWith("{")) {
|
|
282
|
+
const resource = segment.slice(1, -1).replace(/_id$/, "");
|
|
283
|
+
if (result.length > 0) result[result.length - 1] = resource;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
result.push(segment);
|
|
287
|
+
}
|
|
288
|
+
return `${method}${capitalize(result.map((segment, index) => {
|
|
289
|
+
const clean = segment.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
|
|
290
|
+
return index === 0 ? clean : `${clean[0].toUpperCase()}${clean.slice(1)}`;
|
|
291
|
+
}).join(""))}`;
|
|
292
|
+
}
|
|
293
|
+
function toFunctionName(value) {
|
|
294
|
+
const segments = value.split(/[^a-zA-Z0-9]+/g).map((segment) => segment.trim()).filter((segment) => segment.length > 0);
|
|
295
|
+
if (segments.length === 0) return "";
|
|
296
|
+
const [firstSegment, ...restSegments] = segments;
|
|
297
|
+
const normalized = [firstSegment[0].toLowerCase() + firstSegment.slice(1), ...restSegments.map((segment) => capitalize(segment.toLowerCase()))].join("");
|
|
298
|
+
return /^[A-Za-z_$]/.test(normalized) ? normalized : `_${normalized}`;
|
|
299
|
+
}
|
|
300
|
+
function capitalize(value) {
|
|
301
|
+
if (value.length === 0) return value;
|
|
302
|
+
return `${value[0].toUpperCase()}${value.slice(1)}`;
|
|
303
|
+
}
|
|
304
|
+
function getEffectiveParameterLocation(apiPath, parameter) {
|
|
305
|
+
if (parameter.in !== "path") return parameter.in;
|
|
306
|
+
return getTemplateParameterNames(apiPath).has(parameter.name) ? "path" : "query";
|
|
307
|
+
}
|
|
308
|
+
function getTemplateParameterNames(apiPath) {
|
|
309
|
+
const matches = apiPath.match(/\{(\w+)\}/g) ?? [];
|
|
310
|
+
return new Set(matches.map((match) => match.slice(1, -1)));
|
|
311
|
+
}
|
|
312
|
+
function resolveParameterTypeReference(entry, context, location) {
|
|
313
|
+
const effectiveParameters = getEffectiveParametersByLocation(entry, location);
|
|
314
|
+
if (effectiveParameters.length === 0) return null;
|
|
315
|
+
const schemaTypeRef = resolveAlias(context, effectiveParameters.map((parameter) => parameter.name), entry.operation.tags?.[0]);
|
|
316
|
+
if (schemaTypeRef) return schemaTypeRef;
|
|
317
|
+
const rawParameters = getParametersByLocation(entry.operation, location);
|
|
318
|
+
const typeName = allocateOperationTypeName(context, entry.funcName, location === "path" ? "Path" : "Query");
|
|
319
|
+
if (hasSameParameterNames(rawParameters, effectiveParameters)) return {
|
|
320
|
+
aliasDefinitionExpr: `operations['${entry.operationId}']['parameters']['${location}']`,
|
|
321
|
+
sourceExpr: `operations['${entry.operationId}']['parameters']['${location}']`,
|
|
322
|
+
typeName
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
aliasDefinitionExpr: renderInlineParameterObject(effectiveParameters),
|
|
326
|
+
sourceExpr: renderInlineParameterObject(effectiveParameters),
|
|
327
|
+
typeName
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function normalizeOperation(entry, context, requestFunctionNames) {
|
|
331
|
+
const successResponse = getSuccessResponseInfo(entry.operation);
|
|
332
|
+
const builderAlias = getBuilderAlias(entry.funcName);
|
|
333
|
+
const pathChannel = normalizeParameterChannel(entry, context, "path");
|
|
334
|
+
const queryChannel = normalizeParameterChannel(entry, context, "query");
|
|
335
|
+
const bodyResolution = normalizeBodyChannel(entry, context);
|
|
336
|
+
const responseTypeRef = resolveResponseTypeReference(entry, context, successResponse);
|
|
337
|
+
return {
|
|
338
|
+
bodyChannel: bodyResolution.channel,
|
|
339
|
+
bodyContentType: bodyResolution.contentType,
|
|
340
|
+
builderAlias,
|
|
341
|
+
entry,
|
|
342
|
+
optionTypeName: getClientOptionTypeName(entry.funcName),
|
|
343
|
+
pathChannel,
|
|
344
|
+
pathInvocationExpr: pathChannel.present ? `${builderAlias}(options.path)` : `${builderAlias}()`,
|
|
345
|
+
queryChannel,
|
|
346
|
+
requestFunction: responseTypeRef ? requestFunctionNames.json : requestFunctionNames.void,
|
|
347
|
+
responseTypeRef,
|
|
348
|
+
returnTypeExpr: responseTypeRef ? `Promise<${responseTypeRef.typeName}>` : "Promise<void>"
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
function normalizeParameterChannel(entry, context, location) {
|
|
352
|
+
const parameters = getEffectiveParametersByLocation(entry, location);
|
|
353
|
+
if (parameters.length === 0) return {
|
|
354
|
+
present: false,
|
|
355
|
+
required: false,
|
|
356
|
+
typeRef: null
|
|
357
|
+
};
|
|
358
|
+
return {
|
|
359
|
+
present: true,
|
|
360
|
+
required: hasRequiredChannel(parameters),
|
|
361
|
+
typeRef: resolveParameterTypeReference(entry, context, location)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function normalizeBodyChannel(entry, context) {
|
|
365
|
+
const requestBody = entry.operation.requestBody;
|
|
366
|
+
if (!requestBody) return {
|
|
367
|
+
channel: {
|
|
368
|
+
present: false,
|
|
369
|
+
required: false,
|
|
370
|
+
typeRef: null
|
|
371
|
+
},
|
|
372
|
+
contentType: null
|
|
373
|
+
};
|
|
374
|
+
const content = requestBody.content ?? {};
|
|
375
|
+
const jsonBody = content["application/json"];
|
|
376
|
+
if (jsonBody) return {
|
|
377
|
+
channel: createRequestBodyChannel(entry, context, jsonBody, "json"),
|
|
378
|
+
contentType: "json"
|
|
379
|
+
};
|
|
380
|
+
const binaryBody = content["application/octet-stream"];
|
|
381
|
+
if (binaryBody) return {
|
|
382
|
+
channel: createRequestBodyChannel(entry, context, binaryBody, "binary"),
|
|
383
|
+
contentType: "binary"
|
|
384
|
+
};
|
|
385
|
+
const multipartBody = content["multipart/form-data"];
|
|
386
|
+
if (multipartBody) return {
|
|
387
|
+
channel: createRequestBodyChannel(entry, context, multipartBody, "formData"),
|
|
388
|
+
contentType: "formData"
|
|
389
|
+
};
|
|
390
|
+
throw new Error(`Operation "${entry.operationId ?? "unknown"}" has a requestBody but no supported content type`);
|
|
391
|
+
}
|
|
392
|
+
function createRequestBodyChannel(entry, context, body, kind) {
|
|
393
|
+
const typeRef = resolveRequestBodyTypeReference(entry, context, body, kind);
|
|
394
|
+
if (!typeRef) return {
|
|
395
|
+
present: false,
|
|
396
|
+
required: false,
|
|
397
|
+
typeRef: null
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
present: true,
|
|
401
|
+
required: entry.operation.requestBody?.required !== false,
|
|
402
|
+
typeRef
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function resolveRequestBodyTypeReference(entry, context, body, kind) {
|
|
406
|
+
if (kind === "formData") return {
|
|
407
|
+
aliasDefinitionExpr: "FormData",
|
|
408
|
+
sourceExpr: "FormData",
|
|
409
|
+
typeName: allocateOperationTypeName(context, entry.funcName, "Request")
|
|
410
|
+
};
|
|
411
|
+
if (kind === "binary") return {
|
|
412
|
+
aliasDefinitionExpr: "Blob | File | ArrayBuffer | string",
|
|
413
|
+
sourceExpr: "Blob | File | ArrayBuffer | string",
|
|
414
|
+
typeName: allocateOperationTypeName(context, entry.funcName, "Request")
|
|
415
|
+
};
|
|
416
|
+
const schemaTypeRef = resolveSchemaTypeReference(context, body.schema);
|
|
417
|
+
if (schemaTypeRef) return schemaTypeRef;
|
|
418
|
+
return {
|
|
419
|
+
aliasDefinitionExpr: `operations['${entry.operationId}']['requestBody']['content']['application/json']`,
|
|
420
|
+
sourceExpr: `operations['${entry.operationId}']['requestBody']['content']['application/json']`,
|
|
421
|
+
typeName: allocateOperationTypeName(context, entry.funcName, "Request")
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function resolveResponseTypeReference(entry, context, successResponse) {
|
|
425
|
+
if (!successResponse.hasJsonBody) return null;
|
|
426
|
+
const jsonContent = (entry.operation.responses?.[successResponse.statusKey])?.content?.["application/json"];
|
|
427
|
+
const schemaTypeRef = resolveSchemaTypeReference(context, jsonContent?.schema);
|
|
428
|
+
if (schemaTypeRef) return schemaTypeRef;
|
|
429
|
+
return {
|
|
430
|
+
aliasDefinitionExpr: `operations['${entry.operationId}']['responses'][${formatStatusKey(successResponse.statusKey)}]['content']['application/json']`,
|
|
431
|
+
sourceExpr: `operations['${entry.operationId}']['responses'][${formatStatusKey(successResponse.statusKey)}]['content']['application/json']`,
|
|
432
|
+
typeName: allocateOperationTypeName(context, entry.funcName, "Response")
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function hasSameParameterNames(left, right) {
|
|
436
|
+
if (left.length !== right.length) return false;
|
|
437
|
+
const leftNames = left.map((parameter) => parameter.name).sort();
|
|
438
|
+
const rightNames = right.map((parameter) => parameter.name).sort();
|
|
439
|
+
return leftNames.every((name, index) => name === rightNames[index]);
|
|
440
|
+
}
|
|
441
|
+
function resolveAlias(context, parameterNames, tag) {
|
|
442
|
+
const key = [...parameterNames].sort().join(",");
|
|
443
|
+
const candidates = context.schemaAliasIndex.get(key);
|
|
444
|
+
if (!candidates || candidates.length === 0) return;
|
|
445
|
+
if (candidates.length === 1) return createSchemaTypeReference(candidates[0]);
|
|
446
|
+
if (tag) {
|
|
447
|
+
const singularTag = tag.replace(/s$/, "");
|
|
448
|
+
const prefix = `${singularTag[0]?.toUpperCase() ?? ""}${singularTag.slice(1)}`;
|
|
449
|
+
const match = candidates.find((candidate) => candidate.startsWith(prefix));
|
|
450
|
+
if (match) return createSchemaTypeReference(match);
|
|
451
|
+
}
|
|
452
|
+
return createSchemaTypeReference(candidates[0]);
|
|
453
|
+
}
|
|
454
|
+
function resolveSchemaTypeReference(context, schema) {
|
|
455
|
+
const ref = readSchemaRef(schema);
|
|
456
|
+
if (!ref) return;
|
|
457
|
+
const schemaName = ref.split("/").pop();
|
|
458
|
+
if (!schemaName || !context.schemaNames.has(schemaName)) return;
|
|
459
|
+
return createSchemaTypeReference(schemaName);
|
|
460
|
+
}
|
|
461
|
+
function createSchemaTypeReference(schemaName) {
|
|
462
|
+
return {
|
|
463
|
+
aliasDefinitionExpr: null,
|
|
464
|
+
sourceExpr: `components['schemas']['${schemaName}']`,
|
|
465
|
+
typeName: schemaName
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
function readSchemaRef(schema) {
|
|
469
|
+
if (!schema || typeof schema !== "object") return;
|
|
470
|
+
const maybeRef = schema.$ref;
|
|
471
|
+
return typeof maybeRef === "string" ? maybeRef : void 0;
|
|
472
|
+
}
|
|
473
|
+
function renderInlineParameterObject(parameters) {
|
|
474
|
+
return `{ ${parameters.map((parameter) => {
|
|
475
|
+
const optionalMarker = parameter.required ? "" : "?";
|
|
476
|
+
return `${parameter.name}${optionalMarker}: ${renderPrimitiveSchemaType(parameter.schema)}`;
|
|
477
|
+
}).join("; ")} }`;
|
|
478
|
+
}
|
|
479
|
+
function renderPrimitiveSchemaType(schema) {
|
|
480
|
+
const type = schema?.type;
|
|
481
|
+
if (!type) return "unknown";
|
|
482
|
+
if (Array.isArray(type)) return type.map((memberType) => mapPrimitiveType(memberType)).join(" | ");
|
|
483
|
+
return mapPrimitiveType(type);
|
|
484
|
+
}
|
|
485
|
+
function allocateOperationTypeName(context, funcName, suffix) {
|
|
486
|
+
const preferredName = `${capitalize(funcName)}${suffix}`;
|
|
487
|
+
if (!context.usedTypeNames.has(preferredName)) {
|
|
488
|
+
context.usedTypeNames.add(preferredName);
|
|
489
|
+
return preferredName;
|
|
490
|
+
}
|
|
491
|
+
let counter = 2;
|
|
492
|
+
let candidate = `${preferredName}_${counter}`;
|
|
493
|
+
while (context.usedTypeNames.has(candidate)) {
|
|
494
|
+
counter += 1;
|
|
495
|
+
candidate = `${preferredName}_${counter}`;
|
|
496
|
+
}
|
|
497
|
+
context.usedTypeNames.add(candidate);
|
|
498
|
+
return candidate;
|
|
499
|
+
}
|
|
500
|
+
function collectTypeAliases(operations) {
|
|
501
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
502
|
+
for (const operation of operations) {
|
|
503
|
+
collectTypeAlias(aliases, operation.pathChannel.typeRef);
|
|
504
|
+
collectTypeAlias(aliases, operation.queryChannel.typeRef);
|
|
505
|
+
collectTypeAlias(aliases, operation.bodyChannel.typeRef);
|
|
506
|
+
collectTypeAlias(aliases, operation.responseTypeRef);
|
|
507
|
+
}
|
|
508
|
+
return [...aliases.entries()].map(([typeName, definitionExpr]) => ({
|
|
509
|
+
definitionExpr,
|
|
510
|
+
typeName
|
|
511
|
+
})).sort((left, right) => left.typeName.localeCompare(right.typeName));
|
|
512
|
+
}
|
|
513
|
+
function collectTypeAlias(aliases, typeRef) {
|
|
514
|
+
if (!typeRef || typeRef.aliasDefinitionExpr == null) return;
|
|
515
|
+
const existing = aliases.get(typeRef.typeName);
|
|
516
|
+
if (existing && existing !== typeRef.aliasDefinitionExpr) throw new Error(`Conflicting generated type alias "${typeRef.typeName}" with incompatible definitions.`);
|
|
517
|
+
aliases.set(typeRef.typeName, typeRef.aliasDefinitionExpr);
|
|
518
|
+
}
|
|
519
|
+
function mapPrimitiveType(type) {
|
|
520
|
+
switch (type) {
|
|
521
|
+
case "integer":
|
|
522
|
+
case "number": return "number";
|
|
523
|
+
case "boolean": return "boolean";
|
|
524
|
+
case "null": return "null";
|
|
525
|
+
case "string": return "string";
|
|
526
|
+
default: return "unknown";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/plugin.ts
|
|
531
|
+
const HTTP_CLIENT_DEFAULTS = {
|
|
532
|
+
module: "#/integrations/http",
|
|
533
|
+
jsonFunction: "requestJson",
|
|
534
|
+
voidFunction: "requestVoid",
|
|
535
|
+
requestOptionsType: "ApiRequestOptions",
|
|
536
|
+
omitKeys: [
|
|
537
|
+
"json",
|
|
538
|
+
"method",
|
|
539
|
+
"searchParams",
|
|
540
|
+
"signal"
|
|
541
|
+
]
|
|
542
|
+
};
|
|
543
|
+
function resolveHttpClientConfig(config) {
|
|
544
|
+
return {
|
|
545
|
+
...HTTP_CLIENT_DEFAULTS,
|
|
546
|
+
...config
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
const GENERATED_HEADER = ["// This file is auto-generated by vite-plugin-openapi-codegen.", "// Do not edit manually. Changes will be overwritten on next build."];
|
|
550
|
+
async function generateApiTypes(source, outputDir, useTypeAliases) {
|
|
551
|
+
const { default: openapiTS, astToString } = await import("openapi-typescript");
|
|
552
|
+
const contents = astToString(await openapiTS(source, useTypeAliases ? {
|
|
553
|
+
rootTypes: true,
|
|
554
|
+
rootTypesKeepCasing: true,
|
|
555
|
+
rootTypesNoSchemaPrefix: true
|
|
556
|
+
} : void 0));
|
|
557
|
+
writeFileSync(resolve(outputDir, "api-types.d.ts"), `${GENERATED_HEADER.join("\n")}\n\n${contents}`);
|
|
558
|
+
}
|
|
559
|
+
function createApiEntries(normalizedOps, useTypeAliases) {
|
|
560
|
+
return normalizedOps.map((op) => ({
|
|
561
|
+
funcName: op.entry.funcName,
|
|
562
|
+
group: op.entry.group,
|
|
563
|
+
bodyContentType: op.bodyContentType,
|
|
564
|
+
pathTypeExpr: op.pathChannel.typeRef == null ? null : useTypeAliases ? op.pathChannel.typeRef.typeName : op.pathChannel.typeRef.sourceExpr,
|
|
565
|
+
strippedPath: op.entry.strippedPath
|
|
566
|
+
}));
|
|
567
|
+
}
|
|
568
|
+
function createClientRenderModel(model, useTypeAliases) {
|
|
569
|
+
const resolveTypeExpr = (typeRef) => typeRef == null ? null : useTypeAliases ? typeRef.typeName : typeRef.sourceExpr;
|
|
570
|
+
const resolveReturnTypeExpr = (operation) => {
|
|
571
|
+
const responseTypeExpr = resolveTypeExpr(operation.responseTypeRef);
|
|
572
|
+
return responseTypeExpr ? `Promise<${responseTypeExpr}>` : "Promise<void>";
|
|
573
|
+
};
|
|
574
|
+
return {
|
|
575
|
+
needsSearchParamsHelper: model.needsSearchParamsHelper,
|
|
576
|
+
operations: model.operations.map((operation) => ({
|
|
577
|
+
bodyChannel: resolveChannel(operation.bodyChannel, useTypeAliases),
|
|
578
|
+
bodyContentType: operation.bodyContentType,
|
|
579
|
+
builderAlias: operation.builderAlias,
|
|
580
|
+
funcName: operation.entry.funcName,
|
|
581
|
+
group: operation.entry.group,
|
|
582
|
+
methodUpper: operation.entry.method.toUpperCase(),
|
|
583
|
+
optionTypeName: operation.optionTypeName,
|
|
584
|
+
pathChannel: resolveChannel(operation.pathChannel, useTypeAliases),
|
|
585
|
+
pathInvocationExpr: operation.pathInvocationExpr,
|
|
586
|
+
queryChannel: resolveChannel(operation.queryChannel, useTypeAliases),
|
|
587
|
+
requestFunction: operation.requestFunction,
|
|
588
|
+
responseTypeExpr: resolveTypeExpr(operation.responseTypeRef),
|
|
589
|
+
returnTypeExpr: resolveReturnTypeExpr(operation)
|
|
590
|
+
}))
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function resolveChannel(channel, useTypeAliases) {
|
|
594
|
+
if (!channel.typeRef) return channel;
|
|
595
|
+
return {
|
|
596
|
+
...channel,
|
|
597
|
+
typeRef: {
|
|
598
|
+
...channel.typeRef,
|
|
599
|
+
sourceExpr: useTypeAliases ? channel.typeRef.typeName : channel.typeRef.sourceExpr
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
|
|
604
|
+
const pathPrefix = options.pathPrefix ?? "/api/";
|
|
605
|
+
const stripPrefix = options.stripPrefix ?? true;
|
|
606
|
+
const httpClient = resolveHttpClientConfig(options.httpClient);
|
|
607
|
+
const useTypeAliases = options.typeAliases ?? false;
|
|
608
|
+
const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
|
|
609
|
+
json: httpClient.jsonFunction,
|
|
610
|
+
void: httpClient.voidFunction
|
|
611
|
+
});
|
|
612
|
+
if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
|
|
613
|
+
return {
|
|
614
|
+
api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
|
|
615
|
+
client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
|
|
616
|
+
...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
async function loadOpenAPIInput(root, input) {
|
|
620
|
+
if (isHttpUrl(input)) {
|
|
621
|
+
const sourceText = await fetchRemoteOpenAPIInput(input);
|
|
622
|
+
return {
|
|
623
|
+
apiTypesSource: sourceText,
|
|
624
|
+
spec: parseOpenAPISpec(sourceText, input)
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
const inputPath = resolve(root, input);
|
|
628
|
+
const sourceText = readFileSync(inputPath, "utf-8");
|
|
629
|
+
return {
|
|
630
|
+
apiTypesSource: pathToFileURL(inputPath),
|
|
631
|
+
spec: parseOpenAPISpec(sourceText, inputPath)
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function isHttpUrl(value) {
|
|
635
|
+
try {
|
|
636
|
+
const url = new URL(value);
|
|
637
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
638
|
+
} catch {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
async function fetchRemoteOpenAPIInput(input) {
|
|
643
|
+
const response = await fetch(input);
|
|
644
|
+
if (!response.ok) throw new Error(`[openapi-codegen] Failed to fetch OpenAPI document from ${input}: ${response.status} ${response.statusText}`);
|
|
645
|
+
return response.text();
|
|
646
|
+
}
|
|
647
|
+
function parseOpenAPISpec(sourceText, inputLabel) {
|
|
648
|
+
const parsed = parse(sourceText);
|
|
649
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`[openapi-codegen] Expected OpenAPI document object from ${inputLabel}`);
|
|
650
|
+
return parsed;
|
|
651
|
+
}
|
|
652
|
+
async function generateOpenAPIArtifacts(root, options) {
|
|
653
|
+
const outputDir = resolve(root, options.output);
|
|
654
|
+
mkdirSync(outputDir, { recursive: true });
|
|
655
|
+
const { apiTypesSource, spec } = await loadOpenAPIInput(root, options.input);
|
|
656
|
+
const operations = collectOperations(spec, options.pathPrefix ?? "/api/", options.stripPrefix ?? true);
|
|
657
|
+
const artifacts = renderGeneratedArtifacts(spec, options, operations);
|
|
658
|
+
warnOnParameterLocationMismatch(operations);
|
|
659
|
+
await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
|
|
660
|
+
if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
|
|
661
|
+
writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
|
|
662
|
+
writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
|
|
663
|
+
}
|
|
664
|
+
//#endregion
|
|
665
|
+
//#region src/cli.ts
|
|
666
|
+
const CONFIG_FILE_NAMES = [
|
|
667
|
+
"vite.config.ts",
|
|
668
|
+
"vite.config.mts",
|
|
669
|
+
"vite.config.js",
|
|
670
|
+
"vite.config.mjs",
|
|
671
|
+
"vite.config.cjs",
|
|
672
|
+
"vite.config.cts"
|
|
673
|
+
];
|
|
674
|
+
async function main(argv) {
|
|
675
|
+
const flags = parseArgs(argv);
|
|
676
|
+
if (flags.help) {
|
|
677
|
+
printHelp();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const root = resolve(process.cwd(), flags.root ?? ".");
|
|
681
|
+
const options = mergeOptions(await loadOptionsFromViteConfig(root, flags.config), createOptionsFromFlags(flags));
|
|
682
|
+
if (!options.input || !options.output) throw new Error("[openapi-codegen] Missing required options. Provide --input and --output or configure openapiCodegen() in vite.config.");
|
|
683
|
+
await generateOpenAPIArtifacts(root, options);
|
|
684
|
+
console.log(`[openapi-codegen] generated files in ${resolve(root, options.output)}`);
|
|
685
|
+
}
|
|
686
|
+
function parseArgs(argv) {
|
|
687
|
+
const flags = {};
|
|
688
|
+
for (let index = 0; index < argv.length; index++) {
|
|
689
|
+
const arg = argv[index];
|
|
690
|
+
const [name, inlineValue] = arg.split("=", 2);
|
|
691
|
+
switch (name) {
|
|
692
|
+
case "--config":
|
|
693
|
+
flags.config = readValue(argv, ++index, inlineValue, name);
|
|
694
|
+
break;
|
|
695
|
+
case "--help":
|
|
696
|
+
case "-h":
|
|
697
|
+
flags.help = true;
|
|
698
|
+
break;
|
|
699
|
+
case "--http-client-json-function":
|
|
700
|
+
flags.httpClientJsonFunction = readValue(argv, ++index, inlineValue, name);
|
|
701
|
+
break;
|
|
702
|
+
case "--http-client-module":
|
|
703
|
+
flags.httpClientModule = readValue(argv, ++index, inlineValue, name);
|
|
704
|
+
break;
|
|
705
|
+
case "--http-client-omit-keys":
|
|
706
|
+
flags.httpClientOmitKeys = parseCommaList(readValue(argv, ++index, inlineValue, name));
|
|
707
|
+
break;
|
|
708
|
+
case "--http-client-request-options-type":
|
|
709
|
+
flags.httpClientRequestOptionsType = readValue(argv, ++index, inlineValue, name);
|
|
710
|
+
break;
|
|
711
|
+
case "--http-client-void-function":
|
|
712
|
+
flags.httpClientVoidFunction = readValue(argv, ++index, inlineValue, name);
|
|
713
|
+
break;
|
|
714
|
+
case "--input":
|
|
715
|
+
flags.input = readValue(argv, ++index, inlineValue, name);
|
|
716
|
+
break;
|
|
717
|
+
case "--output":
|
|
718
|
+
flags.output = readValue(argv, ++index, inlineValue, name);
|
|
719
|
+
break;
|
|
720
|
+
case "--path-prefix":
|
|
721
|
+
flags.pathPrefix = readValue(argv, ++index, inlineValue, name);
|
|
722
|
+
break;
|
|
723
|
+
case "--root":
|
|
724
|
+
flags.root = readValue(argv, ++index, inlineValue, name);
|
|
725
|
+
break;
|
|
726
|
+
case "--strip-prefix":
|
|
727
|
+
flags.stripPrefix = parseBoolean(readValue(argv, ++index, inlineValue, name), name);
|
|
728
|
+
break;
|
|
729
|
+
case "--type-aliases":
|
|
730
|
+
flags.typeAliases = true;
|
|
731
|
+
break;
|
|
732
|
+
case "--no-strip-prefix":
|
|
733
|
+
flags.stripPrefix = false;
|
|
734
|
+
break;
|
|
735
|
+
case "--no-type-aliases":
|
|
736
|
+
flags.typeAliases = false;
|
|
737
|
+
break;
|
|
738
|
+
default: throw new Error(`[openapi-codegen] Unknown option: ${arg}`);
|
|
739
|
+
}
|
|
740
|
+
if (inlineValue !== void 0) index--;
|
|
741
|
+
}
|
|
742
|
+
return flags;
|
|
743
|
+
}
|
|
744
|
+
function readValue(argv, index, inlineValue, name) {
|
|
745
|
+
if (inlineValue !== void 0) return inlineValue;
|
|
746
|
+
const value = argv[index];
|
|
747
|
+
if (!value || value.startsWith("--")) throw new Error(`[openapi-codegen] Missing value for ${name}`);
|
|
748
|
+
return value;
|
|
749
|
+
}
|
|
750
|
+
function parseBoolean(value, name) {
|
|
751
|
+
if (value === "true") return true;
|
|
752
|
+
if (value === "false") return false;
|
|
753
|
+
throw new Error(`[openapi-codegen] Expected ${name} to be true or false`);
|
|
754
|
+
}
|
|
755
|
+
function parseCommaList(value) {
|
|
756
|
+
if (value.length === 0) return [];
|
|
757
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
758
|
+
}
|
|
759
|
+
async function loadOptionsFromViteConfig(root, configPath) {
|
|
760
|
+
const resolvedConfigPath = resolveViteConfigPath(root, configPath);
|
|
761
|
+
if (!resolvedConfigPath) return {};
|
|
762
|
+
return extractOpenAPICodegenOptions((await loadConfigFromFile({
|
|
763
|
+
command: "serve",
|
|
764
|
+
mode: "development"
|
|
765
|
+
}, resolvedConfigPath, root, "silent"))?.config.plugins) ?? {};
|
|
766
|
+
}
|
|
767
|
+
function resolveViteConfigPath(root, configPath) {
|
|
768
|
+
if (configPath) {
|
|
769
|
+
const resolved = resolve(root, configPath);
|
|
770
|
+
if (!existsSync(resolved)) throw new Error(`[openapi-codegen] Vite config not found: ${resolved}`);
|
|
771
|
+
return resolved;
|
|
772
|
+
}
|
|
773
|
+
return CONFIG_FILE_NAMES.map((fileName) => resolve(root, fileName)).find((filePath) => existsSync(filePath));
|
|
774
|
+
}
|
|
775
|
+
function extractOpenAPICodegenOptions(plugins) {
|
|
776
|
+
return flattenPlugins(plugins).find(isOpenAPICodegenPlugin)?.api?.options;
|
|
777
|
+
}
|
|
778
|
+
function flattenPlugins(value) {
|
|
779
|
+
if (!Array.isArray(value)) return [];
|
|
780
|
+
return value.flatMap((plugin) => {
|
|
781
|
+
if (Array.isArray(plugin)) return flattenPlugins(plugin);
|
|
782
|
+
if (!plugin || typeof plugin !== "object") return [];
|
|
783
|
+
return [plugin];
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function isOpenAPICodegenPlugin(plugin) {
|
|
787
|
+
return plugin.name === "openapi-codegen" && Boolean(plugin.api?.options);
|
|
788
|
+
}
|
|
789
|
+
function createOptionsFromFlags(flags) {
|
|
790
|
+
const httpClient = createHttpClientConfigFromFlags(flags);
|
|
791
|
+
return {
|
|
792
|
+
...flags.input ? { input: flags.input } : {},
|
|
793
|
+
...flags.output ? { output: flags.output } : {},
|
|
794
|
+
...flags.pathPrefix ? { pathPrefix: flags.pathPrefix } : {},
|
|
795
|
+
...flags.stripPrefix !== void 0 ? { stripPrefix: flags.stripPrefix } : {},
|
|
796
|
+
...flags.typeAliases !== void 0 ? { typeAliases: flags.typeAliases } : {},
|
|
797
|
+
...httpClient ? { httpClient } : {}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function createHttpClientConfigFromFlags(flags) {
|
|
801
|
+
const httpClient = {
|
|
802
|
+
...flags.httpClientModule ? { module: flags.httpClientModule } : {},
|
|
803
|
+
...flags.httpClientJsonFunction ? { jsonFunction: flags.httpClientJsonFunction } : {},
|
|
804
|
+
...flags.httpClientVoidFunction ? { voidFunction: flags.httpClientVoidFunction } : {},
|
|
805
|
+
...flags.httpClientRequestOptionsType ? { requestOptionsType: flags.httpClientRequestOptionsType } : {},
|
|
806
|
+
...flags.httpClientOmitKeys ? { omitKeys: flags.httpClientOmitKeys } : {}
|
|
807
|
+
};
|
|
808
|
+
return Object.keys(httpClient).length > 0 ? httpClient : void 0;
|
|
809
|
+
}
|
|
810
|
+
function mergeOptions(configOptions, explicitOptions) {
|
|
811
|
+
return {
|
|
812
|
+
...configOptions,
|
|
813
|
+
...explicitOptions,
|
|
814
|
+
httpClient: configOptions.httpClient || explicitOptions.httpClient ? {
|
|
815
|
+
...configOptions.httpClient,
|
|
816
|
+
...explicitOptions.httpClient
|
|
817
|
+
} : void 0
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
function printHelp() {
|
|
821
|
+
console.log(`Usage: vg [options]
|
|
822
|
+
|
|
823
|
+
Generate OpenAPI client files once.
|
|
824
|
+
|
|
825
|
+
Options:
|
|
826
|
+
--input <path-or-url> OpenAPI JSON/YAML input
|
|
827
|
+
--output <dir> Generated output directory
|
|
828
|
+
--root <dir> Project root, defaults to cwd
|
|
829
|
+
--config <file> Vite config path, defaults to vite.config.*
|
|
830
|
+
--path-prefix <prefix> Path prefix filter
|
|
831
|
+
--strip-prefix <true|false> Strip path prefix from generated paths
|
|
832
|
+
--no-strip-prefix Disable path prefix stripping
|
|
833
|
+
--type-aliases Enable top-level generated type aliases
|
|
834
|
+
--no-type-aliases Disable top-level generated type aliases
|
|
835
|
+
--http-client-module <module> Runtime HTTP module import path
|
|
836
|
+
--http-client-json-function <name> JSON request function name
|
|
837
|
+
--http-client-void-function <name> Void request function name
|
|
838
|
+
--http-client-request-options-type <name> Request options type name
|
|
839
|
+
--http-client-omit-keys <keys> Comma-separated keys omitted from request options
|
|
840
|
+
-h, --help Show help
|
|
841
|
+
`);
|
|
842
|
+
}
|
|
843
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
844
|
+
console.error(error instanceof Error ? error.message : error);
|
|
845
|
+
process.exitCode = 1;
|
|
846
|
+
});
|
|
847
|
+
//#endregion
|
|
848
|
+
export {};
|
package/dist/index.mjs
CHANGED
|
@@ -144,7 +144,11 @@ function createClientChannelField(key, channel) {
|
|
|
144
144
|
function createClientFunctionDeclaration(operation) {
|
|
145
145
|
const requestProperties = [ts.factory.createSpreadAssignment(ts.factory.createIdentifier("requestOptions")), ts.factory.createPropertyAssignment(ts.factory.createIdentifier("method"), ts.factory.createStringLiteral(operation.methodUpper))];
|
|
146
146
|
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")])));
|
|
147
|
-
if (operation.bodyChannel.present)
|
|
147
|
+
if (operation.bodyChannel.present) {
|
|
148
|
+
if (operation.bodyContentType === "json") requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("json"), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "body")));
|
|
149
|
+
else requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("body"), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "body")));
|
|
150
|
+
if (operation.bodyContentType === "binary") requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("contentType"), ts.factory.createStringLiteral("application/octet-stream")));
|
|
151
|
+
}
|
|
148
152
|
requestProperties.push(ts.factory.createPropertyAssignment(ts.factory.createIdentifier("signal"), ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "signal")));
|
|
149
153
|
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)]);
|
|
150
154
|
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));
|
|
@@ -240,13 +244,6 @@ function getParametersByLocation(operation, location) {
|
|
|
240
244
|
function hasRequiredChannel(parameters) {
|
|
241
245
|
return parameters.some((parameter) => parameter.required);
|
|
242
246
|
}
|
|
243
|
-
function getJsonRequestBody(operation) {
|
|
244
|
-
const requestBody = operation.requestBody;
|
|
245
|
-
if (!requestBody) return void 0;
|
|
246
|
-
const jsonBody = requestBody.content?.["application/json"];
|
|
247
|
-
if (!jsonBody) throw new Error(`Operation "${operation.operationId ?? "unknown"}" has a requestBody but no application/json content`);
|
|
248
|
-
return jsonBody;
|
|
249
|
-
}
|
|
250
247
|
function getSuccessResponseInfo(operation) {
|
|
251
248
|
const successResponses = Object.entries(operation.responses ?? {}).filter(([statusKey]) => isSuccessStatus(statusKey)).sort(([left], [right]) => Number(left) - Number(right));
|
|
252
249
|
if (successResponses.length === 0) throw new Error(`Operation "${operation.operationId ?? "unknown"}" has no 2xx success response`);
|
|
@@ -333,10 +330,11 @@ function normalizeOperation(entry, context, requestFunctionNames) {
|
|
|
333
330
|
const builderAlias = getBuilderAlias(entry.funcName);
|
|
334
331
|
const pathChannel = normalizeParameterChannel(entry, context, "path");
|
|
335
332
|
const queryChannel = normalizeParameterChannel(entry, context, "query");
|
|
336
|
-
const
|
|
333
|
+
const bodyResolution = normalizeBodyChannel(entry, context);
|
|
337
334
|
const responseTypeRef = resolveResponseTypeReference(entry, context, successResponse);
|
|
338
335
|
return {
|
|
339
|
-
bodyChannel,
|
|
336
|
+
bodyChannel: bodyResolution.channel,
|
|
337
|
+
bodyContentType: bodyResolution.contentType,
|
|
340
338
|
builderAlias,
|
|
341
339
|
entry,
|
|
342
340
|
optionTypeName: getClientOptionTypeName(entry.funcName),
|
|
@@ -362,7 +360,35 @@ function normalizeParameterChannel(entry, context, location) {
|
|
|
362
360
|
};
|
|
363
361
|
}
|
|
364
362
|
function normalizeBodyChannel(entry, context) {
|
|
365
|
-
const
|
|
363
|
+
const requestBody = entry.operation.requestBody;
|
|
364
|
+
if (!requestBody) return {
|
|
365
|
+
channel: {
|
|
366
|
+
present: false,
|
|
367
|
+
required: false,
|
|
368
|
+
typeRef: null
|
|
369
|
+
},
|
|
370
|
+
contentType: null
|
|
371
|
+
};
|
|
372
|
+
const content = requestBody.content ?? {};
|
|
373
|
+
const jsonBody = content["application/json"];
|
|
374
|
+
if (jsonBody) return {
|
|
375
|
+
channel: createRequestBodyChannel(entry, context, jsonBody, "json"),
|
|
376
|
+
contentType: "json"
|
|
377
|
+
};
|
|
378
|
+
const binaryBody = content["application/octet-stream"];
|
|
379
|
+
if (binaryBody) return {
|
|
380
|
+
channel: createRequestBodyChannel(entry, context, binaryBody, "binary"),
|
|
381
|
+
contentType: "binary"
|
|
382
|
+
};
|
|
383
|
+
const multipartBody = content["multipart/form-data"];
|
|
384
|
+
if (multipartBody) return {
|
|
385
|
+
channel: createRequestBodyChannel(entry, context, multipartBody, "formData"),
|
|
386
|
+
contentType: "formData"
|
|
387
|
+
};
|
|
388
|
+
throw new Error(`Operation "${entry.operationId ?? "unknown"}" has a requestBody but no supported content type`);
|
|
389
|
+
}
|
|
390
|
+
function createRequestBodyChannel(entry, context, body, kind) {
|
|
391
|
+
const typeRef = resolveRequestBodyTypeReference(entry, context, body, kind);
|
|
366
392
|
if (!typeRef) return {
|
|
367
393
|
present: false,
|
|
368
394
|
required: false,
|
|
@@ -374,10 +400,18 @@ function normalizeBodyChannel(entry, context) {
|
|
|
374
400
|
typeRef
|
|
375
401
|
};
|
|
376
402
|
}
|
|
377
|
-
function resolveRequestBodyTypeReference(entry, context) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
403
|
+
function resolveRequestBodyTypeReference(entry, context, body, kind) {
|
|
404
|
+
if (kind === "formData") return {
|
|
405
|
+
aliasDefinitionExpr: "FormData",
|
|
406
|
+
sourceExpr: "FormData",
|
|
407
|
+
typeName: allocateOperationTypeName(context, entry.funcName, "Request")
|
|
408
|
+
};
|
|
409
|
+
if (kind === "binary") return {
|
|
410
|
+
aliasDefinitionExpr: "Blob | File | ArrayBuffer | string",
|
|
411
|
+
sourceExpr: "Blob | File | ArrayBuffer | string",
|
|
412
|
+
typeName: allocateOperationTypeName(context, entry.funcName, "Request")
|
|
413
|
+
};
|
|
414
|
+
const schemaTypeRef = resolveSchemaTypeReference(context, body.schema);
|
|
381
415
|
if (schemaTypeRef) return schemaTypeRef;
|
|
382
416
|
return {
|
|
383
417
|
aliasDefinitionExpr: `operations['${entry.operationId}']['requestBody']['content']['application/json']`,
|
|
@@ -524,6 +558,7 @@ function createApiEntries(normalizedOps, useTypeAliases) {
|
|
|
524
558
|
return normalizedOps.map((op) => ({
|
|
525
559
|
funcName: op.entry.funcName,
|
|
526
560
|
group: op.entry.group,
|
|
561
|
+
bodyContentType: op.bodyContentType,
|
|
527
562
|
pathTypeExpr: op.pathChannel.typeRef == null ? null : useTypeAliases ? op.pathChannel.typeRef.typeName : op.pathChannel.typeRef.sourceExpr,
|
|
528
563
|
strippedPath: op.entry.strippedPath
|
|
529
564
|
}));
|
|
@@ -538,6 +573,7 @@ function createClientRenderModel(model, useTypeAliases) {
|
|
|
538
573
|
needsSearchParamsHelper: model.needsSearchParamsHelper,
|
|
539
574
|
operations: model.operations.map((operation) => ({
|
|
540
575
|
bodyChannel: resolveChannel(operation.bodyChannel, useTypeAliases),
|
|
576
|
+
bodyContentType: operation.bodyContentType,
|
|
541
577
|
builderAlias: operation.builderAlias,
|
|
542
578
|
funcName: operation.entry.funcName,
|
|
543
579
|
group: operation.entry.group,
|
|
@@ -611,7 +647,7 @@ function parseOpenAPISpec(sourceText, inputLabel) {
|
|
|
611
647
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`[openapi-codegen] Expected OpenAPI document object from ${inputLabel}`);
|
|
612
648
|
return parsed;
|
|
613
649
|
}
|
|
614
|
-
async function
|
|
650
|
+
async function generateOpenAPIArtifacts(root, options) {
|
|
615
651
|
const outputDir = resolve(root, options.output);
|
|
616
652
|
mkdirSync(outputDir, { recursive: true });
|
|
617
653
|
const { apiTypesSource, spec } = await loadOpenAPIInput(root, options.input);
|
|
@@ -629,6 +665,7 @@ function openapiCodegen(options) {
|
|
|
629
665
|
return {
|
|
630
666
|
name: "openapi-codegen",
|
|
631
667
|
enforce: "pre",
|
|
668
|
+
api: { options },
|
|
632
669
|
configResolved(config) {
|
|
633
670
|
root = config.root;
|
|
634
671
|
command = config.command;
|
|
@@ -639,24 +676,20 @@ function openapiCodegen(options) {
|
|
|
639
676
|
return;
|
|
640
677
|
}
|
|
641
678
|
},
|
|
642
|
-
|
|
679
|
+
async handleHotUpdate(ctx) {
|
|
643
680
|
if (isHttpUrl(options.input)) return;
|
|
644
681
|
const inputPath = resolve(root, options.input);
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
console.log("[openapi-codegen] regeneration complete.");
|
|
651
|
-
} });
|
|
652
|
-
}
|
|
653
|
-
});
|
|
682
|
+
if (resolve(ctx.file) !== inputPath) return;
|
|
683
|
+
console.log("[openapi-codegen] openapi.json changed, regenerating...");
|
|
684
|
+
await runDevelopmentGeneration(root, options, { onSuccess: () => {
|
|
685
|
+
console.log("[openapi-codegen] regeneration complete.");
|
|
686
|
+
} });
|
|
654
687
|
}
|
|
655
688
|
};
|
|
656
689
|
}
|
|
657
690
|
async function runDevelopmentGeneration(root, options, handlers) {
|
|
658
691
|
try {
|
|
659
|
-
await
|
|
692
|
+
await generateOpenAPIArtifacts(root, options);
|
|
660
693
|
handlers?.onSuccess?.();
|
|
661
694
|
} catch (error) {
|
|
662
695
|
console.error("[openapi-codegen] generation failed during dev mode.", error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vite-plugin-openapi-codegen",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Vite plugin that generates typed API clients and route builders from OpenAPI specs",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"api-client",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"type": "git",
|
|
23
23
|
"url": "git+https://github.com/GGGLHHH/vite-plugin-openapi-codegen.git"
|
|
24
24
|
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"vg": "./dist/cli.mjs"
|
|
27
|
+
},
|
|
25
28
|
"files": [
|
|
26
29
|
"dist"
|
|
27
30
|
],
|
|
@@ -30,6 +33,7 @@
|
|
|
30
33
|
"types": "./dist/index.d.mts",
|
|
31
34
|
"exports": {
|
|
32
35
|
".": "./dist/index.mjs",
|
|
36
|
+
"./cli": "./dist/cli.mjs",
|
|
33
37
|
"./package.json": "./package.json"
|
|
34
38
|
},
|
|
35
39
|
"publishConfig": {
|