vite-plugin-openapi-codegen 1.0.0 → 1.1.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 CHANGED
@@ -1,15 +1,14 @@
1
1
  # vite-plugin-openapi-codegen
2
2
 
3
- Generate typed API clients and path builders from an OpenAPI document during Vite builds.
3
+ Generate typed API clients and path builders from a local or online OpenAPI document during Vite dev runs.
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 JSON or YAML 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
 
12
- It also watches the input spec in dev mode and regenerates the files when the spec changes.
11
+ It also watches local input specs in dev mode and regenerates the files 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.
13
12
 
14
13
  ## Installation
15
14
 
@@ -37,50 +36,66 @@ export default defineConfig({
37
36
  });
38
37
  ```
39
38
 
40
- When you run `vp dev` or `vp build`, the plugin generates:
39
+ Online YAML inputs use the same option:
40
+
41
+ ```ts
42
+ import { defineConfig } from "vite-plus";
43
+ import { openapiCodegen } from "vite-plugin-openapi-codegen";
44
+
45
+ export default defineConfig({
46
+ plugins: [
47
+ openapiCodegen({
48
+ input: "https://example.com/openapi.yaml",
49
+ output: "src/generated",
50
+ }),
51
+ ],
52
+ });
53
+ ```
54
+
55
+ When you run `vp dev`, the plugin generates:
41
56
 
42
57
  ```text
43
58
  src/generated/
44
59
  api-types.d.ts
45
- types.ts
46
60
  api.ts
47
61
  client.ts
48
62
  ```
49
63
 
50
- ## Real Example Project
64
+ ## Example Projects
51
65
 
52
- This repository includes a real minimal Vite project under `example/` that demonstrates
53
- automatic generation from `vite.config.ts`.
66
+ This repository includes two minimal Vite demos under `example/`:
54
67
 
55
- Key files:
68
+ - `example/local` demonstrates generation from the shared local file `example/openapi.json`
69
+ - `example/online` demonstrates generation from `http://localhost:8080/openapi.yaml`
70
+ - Both demos reuse the shared runtime helper in `example/src/http.ts`
56
71
 
57
- - `example/vite.config.ts` wires the plugin into Vite
58
- - `example/openapi.json` is the input spec
59
- - `example/src/http.ts` provides the runtime symbols used by generated clients
60
- - `example/src/generated/*` is generated during build/dev and is gitignored
72
+ Run the local demo in dev mode:
61
73
 
62
- Run the example build:
74
+ ```bash
75
+ vp dev example/local --config ./example/local/vite.config.ts
76
+ ```
77
+
78
+ Run the online demo in dev mode:
63
79
 
64
80
  ```bash
65
- vp build example --config ./example/vite.config.ts
81
+ vp dev example/online --config ./example/online/vite.config.ts
66
82
  ```
67
83
 
68
- Run the example dev server:
84
+ Start both demos together with the shared mock OpenAPI server:
69
85
 
70
86
  ```bash
71
- vp example --config ./example/vite.config.ts
87
+ vp run dev:examples
72
88
  ```
73
89
 
74
- After either command, generated files are written to:
90
+ Build either demo shell without running code generation:
75
91
 
76
- ```text
77
- example/src/generated/
78
- api-types.d.ts
79
- types.ts
80
- api.ts
81
- client.ts
92
+ ```bash
93
+ vp build example/local --config ./example/local/vite.config.ts
94
+ vp build example/online --config ./example/online/vite.config.ts
82
95
  ```
83
96
 
97
+ When you run the dev server, generated files are written to `example/<demo>/src/generated/` and are ignored by git.
98
+
84
99
  ## Runtime Contract
85
100
 
86
101
  By default, generated clients import the following symbols from `#/integrations/http`:
@@ -139,7 +154,9 @@ export default defineConfig({
139
154
  Given a spec path like `/api/users/{user_id}`, the plugin generates a path builder:
140
155
 
141
156
  ```ts
142
- export function getUser(params: UserPath): string {
157
+ import type { components } from "./api-types";
158
+
159
+ export function getUser(params: components["schemas"]["UserPath"]): string {
143
160
  return `users/${params.user_id}`;
144
161
  }
145
162
  ```
@@ -147,9 +164,11 @@ export function getUser(params: UserPath): string {
147
164
  And a typed client helper:
148
165
 
149
166
  ```ts
167
+ import type { components } from "./api-types";
168
+
150
169
  export interface GetUserOptions {
151
170
  query?: never;
152
- path: UserPath;
171
+ path: components["schemas"]["UserPath"];
153
172
  body?: never;
154
173
  signal?: AbortSignal;
155
174
  }
@@ -157,8 +176,8 @@ export interface GetUserOptions {
157
176
  export function getUser(
158
177
  options: GetUserOptions,
159
178
  requestOptions: RuntimeRequestOptions = {},
160
- ): Promise<UserResponse> {
161
- return requestJson<UserResponse>(buildGetUserPath(options.path), {
179
+ ): Promise<components["schemas"]["UserResponse"]> {
180
+ return requestJson<components["schemas"]["UserResponse"]>(buildGetUserPath(options.path), {
162
181
  ...requestOptions,
163
182
  method: "GET",
164
183
  signal: options.signal,
@@ -189,13 +208,14 @@ interface Options {
189
208
  requestOptionsType?: string;
190
209
  omitKeys?: string[];
191
210
  };
192
- legacyAliases?: Record<string, string>;
193
211
  }
194
212
  ```
195
213
 
196
214
  ### `input`
197
215
 
198
- Path to the OpenAPI JSON file, relative to the Vite project root.
216
+ 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.
217
+
218
+ In dev mode, generation errors are logged and do not stop Vite from starting. In build mode, the plugin is skipped entirely.
199
219
 
200
220
  ### `output`
201
221
 
@@ -213,10 +233,6 @@ Controls whether the `pathPrefix` is removed from generated path builders. The d
213
233
 
214
234
  Overrides the runtime import path and symbol names used by generated clients.
215
235
 
216
- ### `legacyAliases`
217
-
218
- Adds extra type aliases to `types.ts` so you can preserve older type names during migrations.
219
-
220
236
  ## Programmatic Usage
221
237
 
222
238
  If you want to generate artifacts outside the Vite lifecycle, use `renderGeneratedArtifacts`:
@@ -234,7 +250,6 @@ const files = renderGeneratedArtifacts(spec, {
234
250
 
235
251
  console.log(files.api);
236
252
  console.log(files.client);
237
- console.log(files.types);
238
253
  ```
239
254
 
240
255
  ## Development
@@ -249,5 +264,6 @@ vp pack
249
264
  To validate the real example project as part of local development:
250
265
 
251
266
  ```bash
252
- vp build example --config ./example/vite.config.ts
267
+ vp build example/local --config ./example/local/vite.config.ts
268
+ vp build example/online --config ./example/online/vite.config.ts
253
269
  ```
package/dist/index.d.mts CHANGED
@@ -65,7 +65,7 @@ interface HttpClientConfig {
65
65
  omitKeys?: string[];
66
66
  }
67
67
  interface Options {
68
- /** Path to openapi.json (relative to project root) */
68
+ /** Path or HTTP(S) URL to an OpenAPI JSON/YAML document */
69
69
  input: string;
70
70
  /** Output directory for generated files (relative to project root) */
71
71
  output: string;
@@ -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
@@ -1,45 +1,35 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { parse } from "yaml";
3
5
  import * as ts from "typescript";
4
6
  //#region src/ast.ts
5
7
  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
8
  function renderApiSource(entries, generatedHeader) {
16
9
  const statements = [];
17
10
  const seenGroups = /* @__PURE__ */ new Set();
18
- let needsOperationsImport = false;
19
- const typeAliasImports = /* @__PURE__ */ new Set();
20
11
  for (const entry of entries) {
21
12
  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
13
  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
14
  statements.push(seenGroups.has(entry.group) ? declaration : createGeneratedBannerComment(declaration, capitalize$1(entry.group)));
28
15
  seenGroups.add(entry.group);
29
16
  }
30
17
  const sourceStatements = [];
31
- if (needsOperationsImport) sourceStatements.push(createTypeOnlyImport(["operations"], "./api-types"));
32
- if (typeAliasImports.size > 0) sourceStatements.push(createTypeOnlyImport([...typeAliasImports].sort(), "./types"));
18
+ const apiTypeImports = collectApiTypeImports(entries.map((entry) => entry.pathTypeExpr));
19
+ if (apiTypeImports.length > 0) sourceStatements.push(createTypeOnlyImport(apiTypeImports, "./api-types"));
33
20
  sourceStatements.push(...statements);
34
21
  return printGeneratedFile(sourceStatements, generatedHeader);
35
22
  }
36
23
  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"));
24
+ const statements = [createTypeOnlyImport([httpClient.requestOptionsType], httpClient.module), createValueImport([{ name: httpClient.jsonFunction }, { name: httpClient.voidFunction }], httpClient.module)];
25
+ const apiTypeImports = collectApiTypeImports(model.operations.flatMap((operation) => [
26
+ operation.bodyChannel.typeExpr,
27
+ operation.pathChannel.typeExpr,
28
+ operation.queryChannel.typeExpr,
29
+ operation.responseTypeExpr,
30
+ operation.returnTypeExpr
31
+ ]));
32
+ if (apiTypeImports.length > 0) statements.push(createTypeOnlyImport(apiTypeImports, "./api-types"));
43
33
  statements.push(createValueImport(model.operations.map((operation) => ({
44
34
  alias: operation.builderAlias,
45
35
  name: operation.funcName
@@ -80,13 +70,14 @@ function createExportModifier() {
80
70
  function createGeneratedBannerComment(node, text) {
81
71
  return ts.addSyntheticLeadingComment(node, ts.SyntaxKind.SingleLineCommentTrivia, ` ${text}`, true);
82
72
  }
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;
73
+ function collectApiTypeImports(typeExprs) {
74
+ const names = /* @__PURE__ */ new Set();
75
+ for (const typeExpr of typeExprs) {
76
+ if (!typeExpr) continue;
77
+ if (typeExpr.includes("components[")) names.add("components");
78
+ if (typeExpr.includes("operations[")) names.add("operations");
79
+ }
80
+ return [...names].sort();
90
81
  }
91
82
  function createPathExpression(strippedPath) {
92
83
  const matches = [...strippedPath.matchAll(/\{(\w+)\}/g)];
@@ -151,12 +142,10 @@ function buildClientRenderModelFromOperations(operations, spec, requestFunctionN
151
142
  void: "requestVoid"
152
143
  }) {
153
144
  const context = buildNormalizationContext(spec);
154
- const typeImports = /* @__PURE__ */ new Set();
155
- const normalized = operations.map((entry) => normalizeOperation(entry, context, typeImports, requestFunctionNames));
145
+ const normalized = operations.map((entry) => normalizeOperation(entry, context, requestFunctionNames));
156
146
  return {
157
147
  operations: normalized,
158
- needsSearchParamsHelper: normalized.some((operation) => operation.queryChannel.present),
159
- typeImports: [...typeImports].sort()
148
+ needsSearchParamsHelper: normalized.some((operation) => operation.queryChannel.present)
160
149
  };
161
150
  }
162
151
  function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
@@ -275,24 +264,21 @@ function getTemplateParameterNames(apiPath) {
275
264
  const matches = apiPath.match(/\{(\w+)\}/g) ?? [];
276
265
  return new Set(matches.map((match) => match.slice(1, -1)));
277
266
  }
278
- function resolveParameterTypeExpression(entry, context, location, typeImports) {
267
+ function resolveParameterTypeExpression(entry, context, location) {
279
268
  const effectiveParameters = getEffectiveParametersByLocation(entry, location);
280
269
  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
- }
270
+ const schemaTypeExpr = resolveAlias(context, effectiveParameters.map((parameter) => parameter.name), entry.operation.tags?.[0]);
271
+ if (schemaTypeExpr) return schemaTypeExpr;
286
272
  if (hasSameParameterNames(getParametersByLocation(entry.operation, location), effectiveParameters)) return `operations['${entry.operationId}']['parameters']['${location}']`;
287
273
  return renderInlineParameterObject(effectiveParameters);
288
274
  }
289
- function normalizeOperation(entry, context, typeImports, requestFunctionNames) {
275
+ function normalizeOperation(entry, context, requestFunctionNames) {
290
276
  const successResponse = getSuccessResponseInfo(entry.operation);
291
277
  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);
278
+ const pathChannel = normalizeParameterChannel(entry, context, "path");
279
+ const queryChannel = normalizeParameterChannel(entry, context, "query");
280
+ const bodyChannel = normalizeBodyChannel(entry, context);
281
+ const responseTypeExpr = resolveResponseTypeExpression(entry, context, successResponse);
296
282
  return {
297
283
  bodyChannel,
298
284
  builderAlias,
@@ -306,7 +292,7 @@ function normalizeOperation(entry, context, typeImports, requestFunctionNames) {
306
292
  returnTypeExpr: responseTypeExpr ? `Promise<${responseTypeExpr}>` : "Promise<void>"
307
293
  };
308
294
  }
309
- function normalizeParameterChannel(entry, context, location, typeImports) {
295
+ function normalizeParameterChannel(entry, context, location) {
310
296
  const parameters = getEffectiveParametersByLocation(entry, location);
311
297
  if (parameters.length === 0) return {
312
298
  present: false,
@@ -316,11 +302,11 @@ function normalizeParameterChannel(entry, context, location, typeImports) {
316
302
  return {
317
303
  present: true,
318
304
  required: hasRequiredChannel(parameters),
319
- typeExpr: resolveParameterTypeExpression(entry, context, location, typeImports)
305
+ typeExpr: resolveParameterTypeExpression(entry, context, location)
320
306
  };
321
307
  }
322
- function normalizeBodyChannel(entry, context, typeImports) {
323
- const typeExpr = resolveRequestBodyTypeExpression(entry, context, typeImports);
308
+ function normalizeBodyChannel(entry, context) {
309
+ const typeExpr = resolveRequestBodyTypeExpression(entry, context);
324
310
  if (!typeExpr) return {
325
311
  present: false,
326
312
  required: false,
@@ -332,18 +318,18 @@ function normalizeBodyChannel(entry, context, typeImports) {
332
318
  typeExpr
333
319
  };
334
320
  }
335
- function resolveRequestBodyTypeExpression(entry, context, typeImports) {
321
+ function resolveRequestBodyTypeExpression(entry, context) {
336
322
  const jsonBody = getJsonRequestBody(entry.operation);
337
323
  if (!jsonBody) return null;
338
- const alias = resolveSchemaAlias(context, jsonBody.schema, typeImports);
339
- if (alias) return alias;
324
+ const schemaTypeExpr = resolveSchemaTypeExpression(context, jsonBody.schema);
325
+ if (schemaTypeExpr) return schemaTypeExpr;
340
326
  return `operations['${entry.operationId}']['requestBody']['content']['application/json']`;
341
327
  }
342
- function resolveResponseTypeExpression(entry, context, typeImports, successResponse) {
328
+ function resolveResponseTypeExpression(entry, context, successResponse) {
343
329
  if (!successResponse.hasJsonBody) return null;
344
330
  const jsonContent = (entry.operation.responses?.[successResponse.statusKey])?.content?.["application/json"];
345
- const alias = resolveSchemaAlias(context, jsonContent?.schema, typeImports);
346
- if (alias) return alias;
331
+ const schemaTypeExpr = resolveSchemaTypeExpression(context, jsonContent?.schema);
332
+ if (schemaTypeExpr) return schemaTypeExpr;
347
333
  return `operations['${entry.operationId}']['responses'][${formatStatusKey(successResponse.statusKey)}]['content']['application/json']`;
348
334
  }
349
335
  function hasSameParameterNames(left, right) {
@@ -356,22 +342,24 @@ function resolveAlias(context, parameterNames, tag) {
356
342
  const key = [...parameterNames].sort().join(",");
357
343
  const candidates = context.schemaAliasIndex.get(key);
358
344
  if (!candidates || candidates.length === 0) return;
359
- if (candidates.length === 1) return candidates[0];
345
+ if (candidates.length === 1) return createSchemaTypeExpression(candidates[0]);
360
346
  if (tag) {
361
347
  const singularTag = tag.replace(/s$/, "");
362
348
  const prefix = `${singularTag[0]?.toUpperCase() ?? ""}${singularTag.slice(1)}`;
363
349
  const match = candidates.find((candidate) => candidate.startsWith(prefix));
364
- if (match) return match;
350
+ if (match) return createSchemaTypeExpression(match);
365
351
  }
366
- return candidates[0];
352
+ return createSchemaTypeExpression(candidates[0]);
367
353
  }
368
- function resolveSchemaAlias(context, schema, typeImports) {
354
+ function resolveSchemaTypeExpression(context, schema) {
369
355
  const ref = readSchemaRef(schema);
370
356
  if (!ref) return;
371
357
  const schemaName = ref.split("/").pop();
372
358
  if (!schemaName || !context.schemaNames.has(schemaName)) return;
373
- typeImports.add(schemaName);
374
- return schemaName;
359
+ return createSchemaTypeExpression(schemaName);
360
+ }
361
+ function createSchemaTypeExpression(schemaName) {
362
+ return `components['schemas']['${schemaName}']`;
375
363
  }
376
364
  function readSchemaRef(schema) {
377
365
  if (!schema || typeof schema !== "object") return;
@@ -421,16 +409,11 @@ function resolveHttpClientConfig(config) {
421
409
  };
422
410
  }
423
411
  const GENERATED_HEADER = ["// This file is auto-generated by vite-plugin-openapi-codegen.", "// Do not edit manually. Changes will be overwritten on next build."];
424
- async function generateApiTypes(inputPath, outputDir) {
412
+ async function generateApiTypes(source, outputDir) {
425
413
  const { default: openapiTS, astToString } = await import("openapi-typescript");
426
- const contents = astToString(await openapiTS(new URL(`file://${inputPath}`)));
414
+ const contents = astToString(await openapiTS(source));
427
415
  writeFileSync(resolve(outputDir, "api-types.d.ts"), `${GENERATED_HEADER.join("\n")}\n\n${contents}`);
428
416
  }
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
417
  function createApiEntries(normalizedOps) {
435
418
  return normalizedOps.map((op) => ({
436
419
  funcName: op.entry.funcName,
@@ -455,8 +438,7 @@ function createClientRenderModel(model) {
455
438
  requestFunction: operation.requestFunction,
456
439
  responseTypeExpr: operation.responseTypeExpr,
457
440
  returnTypeExpr: operation.returnTypeExpr
458
- })),
459
- typeImports: model.typeImports
441
+ }))
460
442
  };
461
443
  }
462
444
  function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
@@ -470,46 +452,91 @@ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
470
452
  if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
471
453
  return {
472
454
  api: renderApiSource(createApiEntries(clientModel.operations), GENERATED_HEADER),
473
- client: renderClientSource(createClientRenderModel(clientModel), GENERATED_HEADER, httpClient),
474
- types: renderTypesBarrel(spec, options.legacyAliases)
455
+ client: renderClientSource(createClientRenderModel(clientModel), GENERATED_HEADER, httpClient)
475
456
  };
476
457
  }
458
+ async function loadOpenAPIInput(root, input) {
459
+ if (isHttpUrl(input)) {
460
+ const sourceText = await fetchRemoteOpenAPIInput(input);
461
+ return {
462
+ apiTypesSource: sourceText,
463
+ spec: parseOpenAPISpec(sourceText, input)
464
+ };
465
+ }
466
+ const inputPath = resolve(root, input);
467
+ const sourceText = readFileSync(inputPath, "utf-8");
468
+ return {
469
+ apiTypesSource: pathToFileURL(inputPath),
470
+ spec: parseOpenAPISpec(sourceText, inputPath)
471
+ };
472
+ }
473
+ function isHttpUrl(value) {
474
+ try {
475
+ const url = new URL(value);
476
+ return url.protocol === "http:" || url.protocol === "https:";
477
+ } catch {
478
+ return false;
479
+ }
480
+ }
481
+ async function fetchRemoteOpenAPIInput(input) {
482
+ const response = await fetch(input);
483
+ if (!response.ok) throw new Error(`[openapi-codegen] Failed to fetch OpenAPI document from ${input}: ${response.status} ${response.statusText}`);
484
+ return response.text();
485
+ }
486
+ function parseOpenAPISpec(sourceText, inputLabel) {
487
+ const parsed = parse(sourceText);
488
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`[openapi-codegen] Expected OpenAPI document object from ${inputLabel}`);
489
+ return parsed;
490
+ }
477
491
  async function generate(root, options) {
478
- const inputPath = resolve(root, options.input);
479
492
  const outputDir = resolve(root, options.output);
480
493
  mkdirSync(outputDir, { recursive: true });
481
- const spec = JSON.parse(readFileSync(inputPath, "utf-8"));
494
+ const { apiTypesSource, spec } = await loadOpenAPIInput(root, options.input);
482
495
  const operations = collectOperations(spec, options.pathPrefix ?? "/api/", options.stripPrefix ?? true);
483
496
  const artifacts = renderGeneratedArtifacts(spec, options, operations);
484
497
  warnOnParameterLocationMismatch(operations);
485
- await generateApiTypes(inputPath, outputDir);
486
- writeFileSync(resolve(outputDir, "types.ts"), artifacts.types);
498
+ await generateApiTypes(apiTypesSource, outputDir);
487
499
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
488
500
  writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
489
501
  }
490
502
  function openapiCodegen(options) {
491
503
  let root = process.cwd();
504
+ let command = "build";
492
505
  return {
493
506
  name: "openapi-codegen",
494
507
  enforce: "pre",
495
508
  configResolved(config) {
496
509
  root = config.root;
510
+ command = config.command;
497
511
  },
498
512
  async buildStart() {
499
- await generate(root, options);
513
+ if (command === "serve") {
514
+ runDevelopmentGeneration(root, options);
515
+ return;
516
+ }
500
517
  },
501
518
  configureServer(server) {
519
+ if (isHttpUrl(options.input)) return;
502
520
  const inputPath = resolve(root, options.input);
503
521
  server.watcher.add(inputPath);
504
522
  server.watcher.on("change", async (path) => {
505
523
  if (path === inputPath) {
506
524
  console.log("[openapi-codegen] openapi.json changed, regenerating...");
507
- await generate(root, options);
508
- console.log("[openapi-codegen] regeneration complete.");
525
+ runDevelopmentGeneration(root, options, { onSuccess: () => {
526
+ console.log("[openapi-codegen] regeneration complete.");
527
+ } });
509
528
  }
510
529
  });
511
530
  }
512
531
  };
513
532
  }
533
+ async function runDevelopmentGeneration(root, options, handlers) {
534
+ try {
535
+ await generate(root, options);
536
+ handlers?.onSuccess?.();
537
+ } catch (error) {
538
+ console.error("[openapi-codegen] generation failed during dev mode.", error);
539
+ }
540
+ }
514
541
  //#endregion
515
542
  export { openapiCodegen, renderGeneratedArtifacts };
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.1.0",
4
4
  "description": "Vite plugin that generates typed API clients and route builders from OpenAPI specs",
5
5
  "keywords": [
6
6
  "api-client",
@@ -38,6 +38,7 @@
38
38
  "scripts": {
39
39
  "build": "vp pack",
40
40
  "dev": "vp pack --watch",
41
+ "dev:examples": "node scripts/dev-examples.mjs",
41
42
  "test": "vp test",
42
43
  "check": "vp check",
43
44
  "release": "bumpp --no-verify",
@@ -45,7 +46,8 @@
45
46
  },
46
47
  "dependencies": {
47
48
  "openapi-typescript": "^7.13.0",
48
- "typescript": ">=5.0.0"
49
+ "typescript": ">=5.0.0",
50
+ "yaml": "^2.9.0"
49
51
  },
50
52
  "devDependencies": {
51
53
  "@types/node": "^25.5.0",