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 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 four files into your target directory:
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
- export function getUser(params: UserPath): string {
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" | "legacyAliases" | "pathPrefix" | "stripPrefix">, preCollectedOperations?: OperationEntry[]): GeneratedArtifacts;
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
- if (needsOperationsImport) sourceStatements.push(createTypeOnlyImport(["operations"], "./api-types"));
32
- if (typeAliasImports.size > 0) sourceStatements.push(createTypeOnlyImport([...typeAliasImports].sort(), "./types"));
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
- createTypeOnlyImport([httpClient.requestOptionsType], httpClient.module),
39
- createValueImport([{ name: httpClient.jsonFunction }, { name: httpClient.voidFunction }], httpClient.module),
40
- createTypeOnlyImport(["operations"], "./api-types")
41
- ];
42
- if (model.typeImports.length > 0) statements.push(createTypeOnlyImport(model.typeImports, "./types"));
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 createStringLiteralType(value) {
84
- return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(value));
85
- }
86
- function createIndexedAccessTypeNode(rootName, indices) {
87
- let current = ts.factory.createTypeReferenceNode(rootName);
88
- for (const index of indices) current = ts.factory.createIndexedAccessTypeNode(current, createStringLiteralType(index));
89
- return current;
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 typeImports = /* @__PURE__ */ new Set();
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, typeImports) {
265
+ function resolveParameterTypeExpression(entry, context, location) {
279
266
  const effectiveParameters = getEffectiveParametersByLocation(entry, location);
280
267
  if (effectiveParameters.length === 0) return "never";
281
- const alias = resolveAlias(context, effectiveParameters.map((parameter) => parameter.name), entry.operation.tags?.[0]);
282
- if (alias) {
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, typeImports, requestFunctionNames) {
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", typeImports);
293
- const queryChannel = normalizeParameterChannel(entry, context, "query", typeImports);
294
- const bodyChannel = normalizeBodyChannel(entry, context, typeImports);
295
- const responseTypeExpr = resolveResponseTypeExpression(entry, context, typeImports, successResponse);
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, typeImports) {
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, typeImports)
303
+ typeExpr: resolveParameterTypeExpression(entry, context, location)
320
304
  };
321
305
  }
322
- function normalizeBodyChannel(entry, context, typeImports) {
323
- const typeExpr = resolveRequestBodyTypeExpression(entry, context, typeImports);
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, typeImports) {
319
+ function resolveRequestBodyTypeExpression(entry, context) {
336
320
  const jsonBody = getJsonRequestBody(entry.operation);
337
321
  if (!jsonBody) return null;
338
- const alias = resolveSchemaAlias(context, jsonBody.schema, typeImports);
339
- if (alias) return alias;
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, typeImports, successResponse) {
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 alias = resolveSchemaAlias(context, jsonContent?.schema, typeImports);
346
- if (alias) return alias;
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 resolveSchemaAlias(context, schema, typeImports) {
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
- typeImports.add(schemaName);
374
- return schemaName;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-openapi-codegen",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Vite plugin that generates typed API clients and route builders from OpenAPI specs",
5
5
  "keywords": [
6
6
  "api-client",