vite-plugin-openapi-codegen 1.0.0 → 1.0.1
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 +9 -14
- package/dist/index.d.mts +1 -4
- package/dist/index.mjs +49 -71
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Generate typed API clients and path builders from an OpenAPI document during Vite builds.
|
|
4
4
|
|
|
5
|
-
The plugin reads your OpenAPI spec, runs `openapi-typescript`, and emits
|
|
5
|
+
The plugin reads your OpenAPI spec, runs `openapi-typescript`, and emits three files into your target directory:
|
|
6
6
|
|
|
7
7
|
- `api-types.d.ts` for raw OpenAPI-derived types
|
|
8
|
-
- `types.ts` for schema aliases
|
|
9
8
|
- `api.ts` for path builder functions
|
|
10
9
|
- `client.ts` for typed request helpers
|
|
11
10
|
|
|
@@ -42,7 +41,6 @@ When you run `vp dev` or `vp build`, the plugin generates:
|
|
|
42
41
|
```text
|
|
43
42
|
src/generated/
|
|
44
43
|
api-types.d.ts
|
|
45
|
-
types.ts
|
|
46
44
|
api.ts
|
|
47
45
|
client.ts
|
|
48
46
|
```
|
|
@@ -76,7 +74,6 @@ After either command, generated files are written to:
|
|
|
76
74
|
```text
|
|
77
75
|
example/src/generated/
|
|
78
76
|
api-types.d.ts
|
|
79
|
-
types.ts
|
|
80
77
|
api.ts
|
|
81
78
|
client.ts
|
|
82
79
|
```
|
|
@@ -139,7 +136,9 @@ export default defineConfig({
|
|
|
139
136
|
Given a spec path like `/api/users/{user_id}`, the plugin generates a path builder:
|
|
140
137
|
|
|
141
138
|
```ts
|
|
142
|
-
|
|
139
|
+
import type { components } from "./api-types";
|
|
140
|
+
|
|
141
|
+
export function getUser(params: components["schemas"]["UserPath"]): string {
|
|
143
142
|
return `users/${params.user_id}`;
|
|
144
143
|
}
|
|
145
144
|
```
|
|
@@ -147,9 +146,11 @@ export function getUser(params: UserPath): string {
|
|
|
147
146
|
And a typed client helper:
|
|
148
147
|
|
|
149
148
|
```ts
|
|
149
|
+
import type { components } from "./api-types";
|
|
150
|
+
|
|
150
151
|
export interface GetUserOptions {
|
|
151
152
|
query?: never;
|
|
152
|
-
path: UserPath;
|
|
153
|
+
path: components["schemas"]["UserPath"];
|
|
153
154
|
body?: never;
|
|
154
155
|
signal?: AbortSignal;
|
|
155
156
|
}
|
|
@@ -157,8 +158,8 @@ export interface GetUserOptions {
|
|
|
157
158
|
export function getUser(
|
|
158
159
|
options: GetUserOptions,
|
|
159
160
|
requestOptions: RuntimeRequestOptions = {},
|
|
160
|
-
): Promise<UserResponse> {
|
|
161
|
-
return requestJson<UserResponse>(buildGetUserPath(options.path), {
|
|
161
|
+
): Promise<components["schemas"]["UserResponse"]> {
|
|
162
|
+
return requestJson<components["schemas"]["UserResponse"]>(buildGetUserPath(options.path), {
|
|
162
163
|
...requestOptions,
|
|
163
164
|
method: "GET",
|
|
164
165
|
signal: options.signal,
|
|
@@ -189,7 +190,6 @@ interface Options {
|
|
|
189
190
|
requestOptionsType?: string;
|
|
190
191
|
omitKeys?: string[];
|
|
191
192
|
};
|
|
192
|
-
legacyAliases?: Record<string, string>;
|
|
193
193
|
}
|
|
194
194
|
```
|
|
195
195
|
|
|
@@ -213,10 +213,6 @@ Controls whether the `pathPrefix` is removed from generated path builders. The d
|
|
|
213
213
|
|
|
214
214
|
Overrides the runtime import path and symbol names used by generated clients.
|
|
215
215
|
|
|
216
|
-
### `legacyAliases`
|
|
217
|
-
|
|
218
|
-
Adds extra type aliases to `types.ts` so you can preserve older type names during migrations.
|
|
219
|
-
|
|
220
216
|
## Programmatic Usage
|
|
221
217
|
|
|
222
218
|
If you want to generate artifacts outside the Vite lifecycle, use `renderGeneratedArtifacts`:
|
|
@@ -234,7 +230,6 @@ const files = renderGeneratedArtifacts(spec, {
|
|
|
234
230
|
|
|
235
231
|
console.log(files.api);
|
|
236
232
|
console.log(files.client);
|
|
237
|
-
console.log(files.types);
|
|
238
233
|
```
|
|
239
234
|
|
|
240
235
|
## Development
|
package/dist/index.d.mts
CHANGED
|
@@ -75,15 +75,12 @@ interface Options {
|
|
|
75
75
|
stripPrefix?: boolean;
|
|
76
76
|
/** HTTP client configuration */
|
|
77
77
|
httpClient?: HttpClientConfig;
|
|
78
|
-
/** Legacy type aliases for types.ts: { OldName: 'NewName' } */
|
|
79
|
-
legacyAliases?: Record<string, string>;
|
|
80
78
|
}
|
|
81
79
|
interface GeneratedArtifacts {
|
|
82
80
|
api: string;
|
|
83
81
|
client: string;
|
|
84
|
-
types: string;
|
|
85
82
|
}
|
|
86
|
-
declare function renderGeneratedArtifacts(spec: OpenAPISpec, options: Pick<Options, "httpClient" | "
|
|
83
|
+
declare function renderGeneratedArtifacts(spec: OpenAPISpec, options: Pick<Options, "httpClient" | "pathPrefix" | "stripPrefix">, preCollectedOperations?: OperationEntry[]): GeneratedArtifacts;
|
|
87
84
|
declare function openapiCodegen(options: Options): Plugin;
|
|
88
85
|
//#endregion
|
|
89
86
|
export { type HttpClientConfig, type OpenAPISpec, type OperationEntry, type Options, openapiCodegen, renderGeneratedArtifacts };
|
package/dist/index.mjs
CHANGED
|
@@ -3,43 +3,31 @@ import { resolve } from "node:path";
|
|
|
3
3
|
import * as ts from "typescript";
|
|
4
4
|
//#region src/ast.ts
|
|
5
5
|
const AST_PRINTER = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
6
|
-
function renderTypesSource(schemaNames, legacyAliases, generatedHeader) {
|
|
7
|
-
const statements = [createTypeOnlyImport(["components"], "./api-types")];
|
|
8
|
-
for (const name of schemaNames) statements.push(ts.factory.createTypeAliasDeclaration([createExportModifier()], ts.factory.createIdentifier(name), void 0, createIndexedAccessTypeNode("components", ["schemas", name])));
|
|
9
|
-
if (legacyAliases && Object.keys(legacyAliases).length > 0) Object.entries(legacyAliases).forEach(([alias, target], index) => {
|
|
10
|
-
const statement = ts.factory.createTypeAliasDeclaration([createExportModifier()], ts.factory.createIdentifier(alias), void 0, createTypeNodeFromText(target));
|
|
11
|
-
statements.push(index === 0 ? createGeneratedBannerComment(statement, "Legacy aliases") : statement);
|
|
12
|
-
});
|
|
13
|
-
return printGeneratedFile(statements, generatedHeader);
|
|
14
|
-
}
|
|
15
6
|
function renderApiSource(entries, generatedHeader) {
|
|
16
7
|
const statements = [];
|
|
17
8
|
const seenGroups = /* @__PURE__ */ new Set();
|
|
18
|
-
let needsOperationsImport = false;
|
|
19
|
-
const typeAliasImports = /* @__PURE__ */ new Set();
|
|
20
9
|
for (const entry of entries) {
|
|
21
10
|
const parameters = entry.pathTypeExpr == null ? [] : [ts.factory.createParameterDeclaration(void 0, void 0, ts.factory.createIdentifier("params"), void 0, createTypeNodeFromText(entry.pathTypeExpr))];
|
|
22
|
-
if (entry.pathTypeExpr) {
|
|
23
|
-
if (entry.pathTypeExpr.includes("operations[")) needsOperationsImport = true;
|
|
24
|
-
else if (/^[A-Z]\w*$/.test(entry.pathTypeExpr)) typeAliasImports.add(entry.pathTypeExpr);
|
|
25
|
-
}
|
|
26
11
|
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));
|
|
27
12
|
statements.push(seenGroups.has(entry.group) ? declaration : createGeneratedBannerComment(declaration, capitalize$1(entry.group)));
|
|
28
13
|
seenGroups.add(entry.group);
|
|
29
14
|
}
|
|
30
15
|
const sourceStatements = [];
|
|
31
|
-
|
|
32
|
-
if (
|
|
16
|
+
const apiTypeImports = collectApiTypeImports(entries.map((entry) => entry.pathTypeExpr));
|
|
17
|
+
if (apiTypeImports.length > 0) sourceStatements.push(createTypeOnlyImport(apiTypeImports, "./api-types"));
|
|
33
18
|
sourceStatements.push(...statements);
|
|
34
19
|
return printGeneratedFile(sourceStatements, generatedHeader);
|
|
35
20
|
}
|
|
36
21
|
function renderClientSource(model, generatedHeader, httpClient) {
|
|
37
|
-
const statements = [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
22
|
+
const statements = [createTypeOnlyImport([httpClient.requestOptionsType], httpClient.module), createValueImport([{ name: httpClient.jsonFunction }, { name: httpClient.voidFunction }], httpClient.module)];
|
|
23
|
+
const apiTypeImports = collectApiTypeImports(model.operations.flatMap((operation) => [
|
|
24
|
+
operation.bodyChannel.typeExpr,
|
|
25
|
+
operation.pathChannel.typeExpr,
|
|
26
|
+
operation.queryChannel.typeExpr,
|
|
27
|
+
operation.responseTypeExpr,
|
|
28
|
+
operation.returnTypeExpr
|
|
29
|
+
]));
|
|
30
|
+
if (apiTypeImports.length > 0) statements.push(createTypeOnlyImport(apiTypeImports, "./api-types"));
|
|
43
31
|
statements.push(createValueImport(model.operations.map((operation) => ({
|
|
44
32
|
alias: operation.builderAlias,
|
|
45
33
|
name: operation.funcName
|
|
@@ -80,13 +68,14 @@ function createExportModifier() {
|
|
|
80
68
|
function createGeneratedBannerComment(node, text) {
|
|
81
69
|
return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, ` ${text}`, true);
|
|
82
70
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
71
|
+
function collectApiTypeImports(typeExprs) {
|
|
72
|
+
const names = /* @__PURE__ */ new Set();
|
|
73
|
+
for (const typeExpr of typeExprs) {
|
|
74
|
+
if (!typeExpr) continue;
|
|
75
|
+
if (typeExpr.includes("components[")) names.add("components");
|
|
76
|
+
if (typeExpr.includes("operations[")) names.add("operations");
|
|
77
|
+
}
|
|
78
|
+
return [...names].sort();
|
|
90
79
|
}
|
|
91
80
|
function createPathExpression(strippedPath) {
|
|
92
81
|
const matches = [...strippedPath.matchAll(/\{(\w+)\}/g)];
|
|
@@ -151,12 +140,10 @@ function buildClientRenderModelFromOperations(operations, spec, requestFunctionN
|
|
|
151
140
|
void: "requestVoid"
|
|
152
141
|
}) {
|
|
153
142
|
const context = buildNormalizationContext(spec);
|
|
154
|
-
const
|
|
155
|
-
const normalized = operations.map((entry) => normalizeOperation(entry, context, typeImports, requestFunctionNames));
|
|
143
|
+
const normalized = operations.map((entry) => normalizeOperation(entry, context, requestFunctionNames));
|
|
156
144
|
return {
|
|
157
145
|
operations: normalized,
|
|
158
|
-
needsSearchParamsHelper: normalized.some((operation) => operation.queryChannel.present)
|
|
159
|
-
typeImports: [...typeImports].sort()
|
|
146
|
+
needsSearchParamsHelper: normalized.some((operation) => operation.queryChannel.present)
|
|
160
147
|
};
|
|
161
148
|
}
|
|
162
149
|
function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
|
|
@@ -275,24 +262,21 @@ function getTemplateParameterNames(apiPath) {
|
|
|
275
262
|
const matches = apiPath.match(/\{(\w+)\}/g) ?? [];
|
|
276
263
|
return new Set(matches.map((match) => match.slice(1, -1)));
|
|
277
264
|
}
|
|
278
|
-
function resolveParameterTypeExpression(entry, context, location
|
|
265
|
+
function resolveParameterTypeExpression(entry, context, location) {
|
|
279
266
|
const effectiveParameters = getEffectiveParametersByLocation(entry, location);
|
|
280
267
|
if (effectiveParameters.length === 0) return "never";
|
|
281
|
-
const
|
|
282
|
-
if (
|
|
283
|
-
typeImports.add(alias);
|
|
284
|
-
return alias;
|
|
285
|
-
}
|
|
268
|
+
const schemaTypeExpr = resolveAlias(context, effectiveParameters.map((parameter) => parameter.name), entry.operation.tags?.[0]);
|
|
269
|
+
if (schemaTypeExpr) return schemaTypeExpr;
|
|
286
270
|
if (hasSameParameterNames(getParametersByLocation(entry.operation, location), effectiveParameters)) return `operations['${entry.operationId}']['parameters']['${location}']`;
|
|
287
271
|
return renderInlineParameterObject(effectiveParameters);
|
|
288
272
|
}
|
|
289
|
-
function normalizeOperation(entry, context,
|
|
273
|
+
function normalizeOperation(entry, context, requestFunctionNames) {
|
|
290
274
|
const successResponse = getSuccessResponseInfo(entry.operation);
|
|
291
275
|
const builderAlias = getBuilderAlias(entry.funcName);
|
|
292
|
-
const pathChannel = normalizeParameterChannel(entry, context, "path"
|
|
293
|
-
const queryChannel = normalizeParameterChannel(entry, context, "query"
|
|
294
|
-
const bodyChannel = normalizeBodyChannel(entry, context
|
|
295
|
-
const responseTypeExpr = resolveResponseTypeExpression(entry, context,
|
|
276
|
+
const pathChannel = normalizeParameterChannel(entry, context, "path");
|
|
277
|
+
const queryChannel = normalizeParameterChannel(entry, context, "query");
|
|
278
|
+
const bodyChannel = normalizeBodyChannel(entry, context);
|
|
279
|
+
const responseTypeExpr = resolveResponseTypeExpression(entry, context, successResponse);
|
|
296
280
|
return {
|
|
297
281
|
bodyChannel,
|
|
298
282
|
builderAlias,
|
|
@@ -306,7 +290,7 @@ function normalizeOperation(entry, context, typeImports, requestFunctionNames) {
|
|
|
306
290
|
returnTypeExpr: responseTypeExpr ? `Promise<${responseTypeExpr}>` : "Promise<void>"
|
|
307
291
|
};
|
|
308
292
|
}
|
|
309
|
-
function normalizeParameterChannel(entry, context, location
|
|
293
|
+
function normalizeParameterChannel(entry, context, location) {
|
|
310
294
|
const parameters = getEffectiveParametersByLocation(entry, location);
|
|
311
295
|
if (parameters.length === 0) return {
|
|
312
296
|
present: false,
|
|
@@ -316,11 +300,11 @@ function normalizeParameterChannel(entry, context, location, typeImports) {
|
|
|
316
300
|
return {
|
|
317
301
|
present: true,
|
|
318
302
|
required: hasRequiredChannel(parameters),
|
|
319
|
-
typeExpr: resolveParameterTypeExpression(entry, context, location
|
|
303
|
+
typeExpr: resolveParameterTypeExpression(entry, context, location)
|
|
320
304
|
};
|
|
321
305
|
}
|
|
322
|
-
function normalizeBodyChannel(entry, context
|
|
323
|
-
const typeExpr = resolveRequestBodyTypeExpression(entry, context
|
|
306
|
+
function normalizeBodyChannel(entry, context) {
|
|
307
|
+
const typeExpr = resolveRequestBodyTypeExpression(entry, context);
|
|
324
308
|
if (!typeExpr) return {
|
|
325
309
|
present: false,
|
|
326
310
|
required: false,
|
|
@@ -332,18 +316,18 @@ function normalizeBodyChannel(entry, context, typeImports) {
|
|
|
332
316
|
typeExpr
|
|
333
317
|
};
|
|
334
318
|
}
|
|
335
|
-
function resolveRequestBodyTypeExpression(entry, context
|
|
319
|
+
function resolveRequestBodyTypeExpression(entry, context) {
|
|
336
320
|
const jsonBody = getJsonRequestBody(entry.operation);
|
|
337
321
|
if (!jsonBody) return null;
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
322
|
+
const schemaTypeExpr = resolveSchemaTypeExpression(context, jsonBody.schema);
|
|
323
|
+
if (schemaTypeExpr) return schemaTypeExpr;
|
|
340
324
|
return `operations['${entry.operationId}']['requestBody']['content']['application/json']`;
|
|
341
325
|
}
|
|
342
|
-
function resolveResponseTypeExpression(entry, context,
|
|
326
|
+
function resolveResponseTypeExpression(entry, context, successResponse) {
|
|
343
327
|
if (!successResponse.hasJsonBody) return null;
|
|
344
328
|
const jsonContent = (entry.operation.responses?.[successResponse.statusKey])?.content?.["application/json"];
|
|
345
|
-
const
|
|
346
|
-
if (
|
|
329
|
+
const schemaTypeExpr = resolveSchemaTypeExpression(context, jsonContent?.schema);
|
|
330
|
+
if (schemaTypeExpr) return schemaTypeExpr;
|
|
347
331
|
return `operations['${entry.operationId}']['responses'][${formatStatusKey(successResponse.statusKey)}]['content']['application/json']`;
|
|
348
332
|
}
|
|
349
333
|
function hasSameParameterNames(left, right) {
|
|
@@ -356,22 +340,24 @@ function resolveAlias(context, parameterNames, tag) {
|
|
|
356
340
|
const key = [...parameterNames].sort().join(",");
|
|
357
341
|
const candidates = context.schemaAliasIndex.get(key);
|
|
358
342
|
if (!candidates || candidates.length === 0) return;
|
|
359
|
-
if (candidates.length === 1) return candidates[0];
|
|
343
|
+
if (candidates.length === 1) return createSchemaTypeExpression(candidates[0]);
|
|
360
344
|
if (tag) {
|
|
361
345
|
const singularTag = tag.replace(/s$/, "");
|
|
362
346
|
const prefix = `${singularTag[0]?.toUpperCase() ?? ""}${singularTag.slice(1)}`;
|
|
363
347
|
const match = candidates.find((candidate) => candidate.startsWith(prefix));
|
|
364
|
-
if (match) return match;
|
|
348
|
+
if (match) return createSchemaTypeExpression(match);
|
|
365
349
|
}
|
|
366
|
-
return candidates[0];
|
|
350
|
+
return createSchemaTypeExpression(candidates[0]);
|
|
367
351
|
}
|
|
368
|
-
function
|
|
352
|
+
function resolveSchemaTypeExpression(context, schema) {
|
|
369
353
|
const ref = readSchemaRef(schema);
|
|
370
354
|
if (!ref) return;
|
|
371
355
|
const schemaName = ref.split("/").pop();
|
|
372
356
|
if (!schemaName || !context.schemaNames.has(schemaName)) return;
|
|
373
|
-
|
|
374
|
-
|
|
357
|
+
return createSchemaTypeExpression(schemaName);
|
|
358
|
+
}
|
|
359
|
+
function createSchemaTypeExpression(schemaName) {
|
|
360
|
+
return `components['schemas']['${schemaName}']`;
|
|
375
361
|
}
|
|
376
362
|
function readSchemaRef(schema) {
|
|
377
363
|
if (!schema || typeof schema !== "object") return;
|
|
@@ -426,11 +412,6 @@ async function generateApiTypes(inputPath, outputDir) {
|
|
|
426
412
|
const contents = astToString(await openapiTS(new URL(`file://${inputPath}`)));
|
|
427
413
|
writeFileSync(resolve(outputDir, "api-types.d.ts"), `${GENERATED_HEADER.join("\n")}\n\n${contents}`);
|
|
428
414
|
}
|
|
429
|
-
function renderTypesBarrel(spec, legacyAliases) {
|
|
430
|
-
const schemaNames = Object.keys(spec.components?.schemas ?? {}).sort();
|
|
431
|
-
if (schemaNames.length === 0) throw new Error("No schemas found in openapi.json");
|
|
432
|
-
return renderTypesSource(schemaNames, legacyAliases, GENERATED_HEADER);
|
|
433
|
-
}
|
|
434
415
|
function createApiEntries(normalizedOps) {
|
|
435
416
|
return normalizedOps.map((op) => ({
|
|
436
417
|
funcName: op.entry.funcName,
|
|
@@ -455,8 +436,7 @@ function createClientRenderModel(model) {
|
|
|
455
436
|
requestFunction: operation.requestFunction,
|
|
456
437
|
responseTypeExpr: operation.responseTypeExpr,
|
|
457
438
|
returnTypeExpr: operation.returnTypeExpr
|
|
458
|
-
}))
|
|
459
|
-
typeImports: model.typeImports
|
|
439
|
+
}))
|
|
460
440
|
};
|
|
461
441
|
}
|
|
462
442
|
function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
|
|
@@ -470,8 +450,7 @@ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
|
|
|
470
450
|
if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
|
|
471
451
|
return {
|
|
472
452
|
api: renderApiSource(createApiEntries(clientModel.operations), GENERATED_HEADER),
|
|
473
|
-
client: renderClientSource(createClientRenderModel(clientModel), GENERATED_HEADER, httpClient)
|
|
474
|
-
types: renderTypesBarrel(spec, options.legacyAliases)
|
|
453
|
+
client: renderClientSource(createClientRenderModel(clientModel), GENERATED_HEADER, httpClient)
|
|
475
454
|
};
|
|
476
455
|
}
|
|
477
456
|
async function generate(root, options) {
|
|
@@ -483,7 +462,6 @@ async function generate(root, options) {
|
|
|
483
462
|
const artifacts = renderGeneratedArtifacts(spec, options, operations);
|
|
484
463
|
warnOnParameterLocationMismatch(operations);
|
|
485
464
|
await generateApiTypes(inputPath, outputDir);
|
|
486
|
-
writeFileSync(resolve(outputDir, "types.ts"), artifacts.types);
|
|
487
465
|
writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
|
|
488
466
|
writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
|
|
489
467
|
}
|