vite-plugin-openapi-codegen 3.2.0 → 3.3.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/dist/cli.mjs CHANGED
@@ -268,7 +268,81 @@ function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
268
268
  });
269
269
  }
270
270
  }
271
- return entries;
271
+ const publicEntries = excludeInternalOperations(entries, spec);
272
+ if (entries.length > 0 && publicEntries.length === 0) throw new Error(`All ${entries.length} operation(s) matching prefix "${pathPrefix}" are internal-only and were excluded`);
273
+ const excludedCount = entries.length - publicEntries.length;
274
+ if (excludedCount > 0) console.info(`[openapi-codegen] excluded ${excludedCount} internal-only operation(s) from generated artifacts.`);
275
+ return publicEntries;
276
+ }
277
+ function excludeInternalOperations(entries, spec) {
278
+ const securitySchemes = spec.components?.securitySchemes;
279
+ if (!securitySchemes) return entries;
280
+ return entries.filter((entry) => readSecurityRequirement(entry, securitySchemes, spec.security)?.kind !== "internal");
281
+ }
282
+ function excludeInternalOperationsFromSpec(spec) {
283
+ const securitySchemes = spec.components?.securitySchemes;
284
+ if (!securitySchemes || !spec.paths) return {
285
+ excludedCount: 0,
286
+ spec
287
+ };
288
+ const clone = structuredClone(spec);
289
+ let excludedCount = 0;
290
+ for (const [path, pathItem] of Object.entries(clone.paths ?? {})) {
291
+ let removedFromPath = 0;
292
+ for (const method of HTTP_METHODS) {
293
+ const operation = pathItem[method];
294
+ if (!operation) continue;
295
+ if (readSecurityRequirement({
296
+ operation,
297
+ operationId: operation.operationId ?? `${method.toUpperCase()} ${path}`
298
+ }, securitySchemes, clone.security)?.kind !== "internal") continue;
299
+ delete pathItem[method];
300
+ removedFromPath += 1;
301
+ }
302
+ excludedCount += removedFromPath;
303
+ if (removedFromPath > 0 && !HTTP_METHODS.some((method) => pathItem[method])) delete clone.paths?.[path];
304
+ }
305
+ return {
306
+ excludedCount,
307
+ spec: clone
308
+ };
309
+ }
310
+ function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
311
+ const security = entry.operation.security ?? topLevelSecurity;
312
+ if (security === void 0) return null;
313
+ if (!Array.isArray(security)) throw new Error(`Operation "${entry.operationId}" has invalid security`);
314
+ if (security.length === 0) return { kind: "public" };
315
+ if (security.length > 1) throw new Error(`Operation "${entry.operationId}" declares ${security.length} security requirements; the backend contract emits at most one`);
316
+ const requirement = security[0];
317
+ if (!isRecord(requirement)) throw new Error(`Operation "${entry.operationId}" has an invalid security requirement`);
318
+ const schemeEntries = Object.entries(requirement);
319
+ if (schemeEntries.length > 1) throw new Error(`Operation "${entry.operationId}" declares ${schemeEntries.length} schemes in one security requirement; the backend contract emits exactly one`);
320
+ const schemeEntry = schemeEntries[0];
321
+ if (schemeEntry) {
322
+ const [schemeName, scopes] = schemeEntry;
323
+ const scheme = securitySchemes?.[schemeName];
324
+ if (scheme?.type === "apiKey" && scheme.in === "header") return { kind: "internal" };
325
+ if (scheme?.type === "http" || scheme?.type === "apiKey" && scheme.in === "cookie") {
326
+ const roles = readSecurityScopes(entry, schemeName, scopes);
327
+ return roles.length === 0 ? { kind: "authenticated" } : {
328
+ kind: "role",
329
+ roles
330
+ };
331
+ }
332
+ }
333
+ throw new Error(`Operation "${entry.operationId}" has an unrecognized security scheme`);
334
+ }
335
+ function readSecurityScopes(entry, schemeName, scopes) {
336
+ if (!Array.isArray(scopes)) throw new Error(`Operation "${entry.operationId}" has non-array scopes for security scheme "${schemeName}"`);
337
+ const roles = [];
338
+ for (const scope of scopes) {
339
+ if (typeof scope !== "string") throw new Error(`Operation "${entry.operationId}" has a non-string scope for security scheme "${schemeName}"`);
340
+ roles.push(scope);
341
+ }
342
+ return roles;
343
+ }
344
+ function isRecord(value) {
345
+ return typeof value === "object" && value !== null && !Array.isArray(value);
272
346
  }
273
347
  function getEffectiveParametersByLocation(entry, location) {
274
348
  return (entry.operation.parameters ?? []).filter((parameter) => getEffectiveParameterLocation(entry.apiPath, parameter) === location);
@@ -604,9 +678,10 @@ function resolveHttpClientConfig(config) {
604
678
  };
605
679
  }
606
680
  const GENERATED_HEADER = ["// This file is auto-generated by vite-plugin-openapi-codegen.", "// Do not edit manually. Changes will be overwritten on next build."];
607
- async function generateApiTypes(source, outputDir, useTypeAliases) {
681
+ async function generateApiTypes(input, outputDir, useTypeAliases) {
608
682
  const { default: openapiTS, astToString } = await import("openapi-typescript");
609
- const contents = astToString(await openapiTS(source, useTypeAliases ? {
683
+ const { excludedCount, spec } = excludeInternalOperationsFromSpec(input.spec);
684
+ const contents = astToString(await openapiTS(excludedCount > 0 ? spec : input.apiTypesSource, useTypeAliases ? {
610
685
  rootTypes: true,
611
686
  rootTypesKeepCasing: true,
612
687
  rootTypesNoSchemaPrefix: true
@@ -662,29 +737,6 @@ function createAccessPolicyEntries(operations, securitySchemes, topLevelSecurity
662
737
  }];
663
738
  });
664
739
  }
665
- function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
666
- const security = entry.operation.security ?? topLevelSecurity;
667
- if (security === void 0) return null;
668
- if (!Array.isArray(security)) throw new Error(`Operation "${entry.operationId}" has invalid security`);
669
- if (security.length === 0) return { kind: "public" };
670
- const requirement = security[0];
671
- if (!isRecord(requirement)) throw new Error(`Operation "${entry.operationId}" has an invalid security requirement`);
672
- for (const [schemeName, scopes] of Object.entries(requirement)) {
673
- const scheme = securitySchemes?.[schemeName];
674
- if (scheme?.type === "apiKey" && scheme.in === "header") return { kind: "internal" };
675
- if (scheme?.type === "http" || scheme?.type === "apiKey" && scheme.in === "cookie") {
676
- const roles = Array.isArray(scopes) ? scopes.filter((scope) => typeof scope === "string") : [];
677
- return roles.length === 0 ? { kind: "authenticated" } : {
678
- kind: "role",
679
- roles
680
- };
681
- }
682
- }
683
- throw new Error(`Operation "${entry.operationId}" has an unrecognized security scheme`);
684
- }
685
- function isRecord(value) {
686
- return typeof value === "object" && value !== null && !Array.isArray(value);
687
- }
688
740
  function resolveChannel(channel, useTypeAliases) {
689
741
  if (!channel.typeRef) return channel;
690
742
  return {
@@ -750,11 +802,13 @@ function parseOpenAPISpec(sourceText, inputLabel) {
750
802
  async function generateOpenAPIArtifacts(root, options) {
751
803
  const outputDir = resolve(root, options.output);
752
804
  mkdirSync(outputDir, { recursive: true });
753
- const { apiTypesSource, spec } = await loadOpenAPIInput(root, options.input);
754
- const operations = collectOperations(spec, options.pathPrefix ?? "/api/", options.stripPrefix ?? true);
755
- const artifacts = renderGeneratedArtifacts(spec, options, operations);
805
+ const input = await loadOpenAPIInput(root, options.input);
806
+ const pathPrefix = options.pathPrefix ?? "/api/";
807
+ const stripPrefix = options.stripPrefix ?? true;
808
+ const operations = collectOperations(input.spec, pathPrefix, stripPrefix);
809
+ const artifacts = renderGeneratedArtifacts(input.spec, options, operations);
756
810
  warnOnParameterLocationMismatch(operations);
757
- await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
811
+ await generateApiTypes(input, outputDir, options.typeAliases ?? false);
758
812
  if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
759
813
  if (artifacts.accessPolicies) writeFileSync(resolve(outputDir, "access-policies.ts"), artifacts.accessPolicies);
760
814
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
package/dist/index.mjs CHANGED
@@ -266,7 +266,81 @@ function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
266
266
  });
267
267
  }
268
268
  }
269
- return entries;
269
+ const publicEntries = excludeInternalOperations(entries, spec);
270
+ if (entries.length > 0 && publicEntries.length === 0) throw new Error(`All ${entries.length} operation(s) matching prefix "${pathPrefix}" are internal-only and were excluded`);
271
+ const excludedCount = entries.length - publicEntries.length;
272
+ if (excludedCount > 0) console.info(`[openapi-codegen] excluded ${excludedCount} internal-only operation(s) from generated artifacts.`);
273
+ return publicEntries;
274
+ }
275
+ function excludeInternalOperations(entries, spec) {
276
+ const securitySchemes = spec.components?.securitySchemes;
277
+ if (!securitySchemes) return entries;
278
+ return entries.filter((entry) => readSecurityRequirement(entry, securitySchemes, spec.security)?.kind !== "internal");
279
+ }
280
+ function excludeInternalOperationsFromSpec(spec) {
281
+ const securitySchemes = spec.components?.securitySchemes;
282
+ if (!securitySchemes || !spec.paths) return {
283
+ excludedCount: 0,
284
+ spec
285
+ };
286
+ const clone = structuredClone(spec);
287
+ let excludedCount = 0;
288
+ for (const [path, pathItem] of Object.entries(clone.paths ?? {})) {
289
+ let removedFromPath = 0;
290
+ for (const method of HTTP_METHODS) {
291
+ const operation = pathItem[method];
292
+ if (!operation) continue;
293
+ if (readSecurityRequirement({
294
+ operation,
295
+ operationId: operation.operationId ?? `${method.toUpperCase()} ${path}`
296
+ }, securitySchemes, clone.security)?.kind !== "internal") continue;
297
+ delete pathItem[method];
298
+ removedFromPath += 1;
299
+ }
300
+ excludedCount += removedFromPath;
301
+ if (removedFromPath > 0 && !HTTP_METHODS.some((method) => pathItem[method])) delete clone.paths?.[path];
302
+ }
303
+ return {
304
+ excludedCount,
305
+ spec: clone
306
+ };
307
+ }
308
+ function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
309
+ const security = entry.operation.security ?? topLevelSecurity;
310
+ if (security === void 0) return null;
311
+ if (!Array.isArray(security)) throw new Error(`Operation "${entry.operationId}" has invalid security`);
312
+ if (security.length === 0) return { kind: "public" };
313
+ if (security.length > 1) throw new Error(`Operation "${entry.operationId}" declares ${security.length} security requirements; the backend contract emits at most one`);
314
+ const requirement = security[0];
315
+ if (!isRecord(requirement)) throw new Error(`Operation "${entry.operationId}" has an invalid security requirement`);
316
+ const schemeEntries = Object.entries(requirement);
317
+ if (schemeEntries.length > 1) throw new Error(`Operation "${entry.operationId}" declares ${schemeEntries.length} schemes in one security requirement; the backend contract emits exactly one`);
318
+ const schemeEntry = schemeEntries[0];
319
+ if (schemeEntry) {
320
+ const [schemeName, scopes] = schemeEntry;
321
+ const scheme = securitySchemes?.[schemeName];
322
+ if (scheme?.type === "apiKey" && scheme.in === "header") return { kind: "internal" };
323
+ if (scheme?.type === "http" || scheme?.type === "apiKey" && scheme.in === "cookie") {
324
+ const roles = readSecurityScopes(entry, schemeName, scopes);
325
+ return roles.length === 0 ? { kind: "authenticated" } : {
326
+ kind: "role",
327
+ roles
328
+ };
329
+ }
330
+ }
331
+ throw new Error(`Operation "${entry.operationId}" has an unrecognized security scheme`);
332
+ }
333
+ function readSecurityScopes(entry, schemeName, scopes) {
334
+ if (!Array.isArray(scopes)) throw new Error(`Operation "${entry.operationId}" has non-array scopes for security scheme "${schemeName}"`);
335
+ const roles = [];
336
+ for (const scope of scopes) {
337
+ if (typeof scope !== "string") throw new Error(`Operation "${entry.operationId}" has a non-string scope for security scheme "${schemeName}"`);
338
+ roles.push(scope);
339
+ }
340
+ return roles;
341
+ }
342
+ function isRecord(value) {
343
+ return typeof value === "object" && value !== null && !Array.isArray(value);
270
344
  }
271
345
  function getEffectiveParametersByLocation(entry, location) {
272
346
  return (entry.operation.parameters ?? []).filter((parameter) => getEffectiveParameterLocation(entry.apiPath, parameter) === location);
@@ -602,9 +676,10 @@ function resolveHttpClientConfig(config) {
602
676
  };
603
677
  }
604
678
  const GENERATED_HEADER = ["// This file is auto-generated by vite-plugin-openapi-codegen.", "// Do not edit manually. Changes will be overwritten on next build."];
605
- async function generateApiTypes(source, outputDir, useTypeAliases) {
679
+ async function generateApiTypes(input, outputDir, useTypeAliases) {
606
680
  const { default: openapiTS, astToString } = await import("openapi-typescript");
607
- const contents = astToString(await openapiTS(source, useTypeAliases ? {
681
+ const { excludedCount, spec } = excludeInternalOperationsFromSpec(input.spec);
682
+ const contents = astToString(await openapiTS(excludedCount > 0 ? spec : input.apiTypesSource, useTypeAliases ? {
608
683
  rootTypes: true,
609
684
  rootTypesKeepCasing: true,
610
685
  rootTypesNoSchemaPrefix: true
@@ -660,29 +735,6 @@ function createAccessPolicyEntries(operations, securitySchemes, topLevelSecurity
660
735
  }];
661
736
  });
662
737
  }
663
- function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
664
- const security = entry.operation.security ?? topLevelSecurity;
665
- if (security === void 0) return null;
666
- if (!Array.isArray(security)) throw new Error(`Operation "${entry.operationId}" has invalid security`);
667
- if (security.length === 0) return { kind: "public" };
668
- const requirement = security[0];
669
- if (!isRecord(requirement)) throw new Error(`Operation "${entry.operationId}" has an invalid security requirement`);
670
- for (const [schemeName, scopes] of Object.entries(requirement)) {
671
- const scheme = securitySchemes?.[schemeName];
672
- if (scheme?.type === "apiKey" && scheme.in === "header") return { kind: "internal" };
673
- if (scheme?.type === "http" || scheme?.type === "apiKey" && scheme.in === "cookie") {
674
- const roles = Array.isArray(scopes) ? scopes.filter((scope) => typeof scope === "string") : [];
675
- return roles.length === 0 ? { kind: "authenticated" } : {
676
- kind: "role",
677
- roles
678
- };
679
- }
680
- }
681
- throw new Error(`Operation "${entry.operationId}" has an unrecognized security scheme`);
682
- }
683
- function isRecord(value) {
684
- return typeof value === "object" && value !== null && !Array.isArray(value);
685
- }
686
738
  function resolveChannel(channel, useTypeAliases) {
687
739
  if (!channel.typeRef) return channel;
688
740
  return {
@@ -748,11 +800,13 @@ function parseOpenAPISpec(sourceText, inputLabel) {
748
800
  async function generateOpenAPIArtifacts(root, options) {
749
801
  const outputDir = resolve(root, options.output);
750
802
  mkdirSync(outputDir, { recursive: true });
751
- const { apiTypesSource, spec } = await loadOpenAPIInput(root, options.input);
752
- const operations = collectOperations(spec, options.pathPrefix ?? "/api/", options.stripPrefix ?? true);
753
- const artifacts = renderGeneratedArtifacts(spec, options, operations);
803
+ const input = await loadOpenAPIInput(root, options.input);
804
+ const pathPrefix = options.pathPrefix ?? "/api/";
805
+ const stripPrefix = options.stripPrefix ?? true;
806
+ const operations = collectOperations(input.spec, pathPrefix, stripPrefix);
807
+ const artifacts = renderGeneratedArtifacts(input.spec, options, operations);
754
808
  warnOnParameterLocationMismatch(operations);
755
- await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
809
+ await generateApiTypes(input, outputDir, options.typeAliases ?? false);
756
810
  if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
757
811
  if (artifacts.accessPolicies) writeFileSync(resolve(outputDir, "access-policies.ts"), artifacts.accessPolicies);
758
812
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
@@ -771,7 +825,7 @@ function openapiCodegen(options) {
771
825
  },
772
826
  async buildStart() {
773
827
  if (command === "serve" && options.generateOnDev !== false) {
774
- runDevelopmentGeneration(root, options);
828
+ runDevelopmentGeneration(root, options, { onError: resolvePluginErrorRaiser(this) }).catch(() => {});
775
829
  return;
776
830
  }
777
831
  },
@@ -781,9 +835,12 @@ function openapiCodegen(options) {
781
835
  const inputPath = resolve(root, options.input);
782
836
  if (resolve(ctx.file) !== inputPath) return;
783
837
  console.log("[openapi-codegen] openapi.json changed, regenerating...");
784
- await runDevelopmentGeneration(root, options, { onSuccess: () => {
785
- console.log("[openapi-codegen] regeneration complete.");
786
- } });
838
+ await runDevelopmentGeneration(root, options, {
839
+ onError: resolvePluginErrorRaiser(this),
840
+ onSuccess: () => {
841
+ console.log("[openapi-codegen] regeneration complete.");
842
+ }
843
+ });
787
844
  }
788
845
  };
789
846
  }
@@ -792,8 +849,15 @@ async function runDevelopmentGeneration(root, options, handlers) {
792
849
  await generateOpenAPIArtifacts(root, options);
793
850
  handlers?.onSuccess?.();
794
851
  } catch (error) {
795
- console.error("[openapi-codegen] generation failed during dev mode.", error);
852
+ const failure = error instanceof Error ? error : new Error(String(error));
853
+ console.error("[openapi-codegen] generation failed during dev mode.", failure);
854
+ handlers?.onError?.(failure);
796
855
  }
797
856
  }
857
+ function resolvePluginErrorRaiser(context) {
858
+ if (typeof context !== "object" || context === null || typeof context.error !== "function") return;
859
+ const pluginContext = context;
860
+ return (error) => pluginContext.error(error);
861
+ }
798
862
  //#endregion
799
863
  export { openapiCodegen, renderGeneratedArtifacts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-openapi-codegen",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Vite plugin that generates typed API clients and route builders from OpenAPI specs",
5
5
  "keywords": [
6
6
  "api-client",