vite-plugin-openapi-codegen 3.2.1 → 4.0.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
@@ -57,11 +57,11 @@ function renderAccessPoliciesSource(entries, generatedHeader) {
57
57
  const lines = [
58
58
  ...generatedHeader,
59
59
  "",
60
- "export type AccessPolicyKind = \"authenticated\" | \"internal\" | \"public\" | \"role\";",
60
+ "export type AccessPolicyKind = \"authenticated\" | \"internal\" | \"public\" | \"permission\";",
61
61
  "",
62
62
  "export interface AccessPolicy {",
63
63
  " kind: AccessPolicyKind;",
64
- " roles?: readonly string[];",
64
+ " permissions?: readonly string[];",
65
65
  "}",
66
66
  "",
67
67
  "export interface OperationAccessPolicy extends AccessPolicy {",
@@ -80,7 +80,7 @@ function renderAccessPoliciesSource(entries, generatedHeader) {
80
80
  lines.push(` method: ${JSON.stringify(entry.methodUpper)},`);
81
81
  lines.push(` operationId: ${JSON.stringify(entry.operationId)},`);
82
82
  lines.push(` path: ${JSON.stringify(entry.strippedPath)},`);
83
- if (entry.roles.length > 0) lines.push(` roles: [${entry.roles.map((role) => JSON.stringify(role)).join(", ")}],`);
83
+ if (entry.permissions.length > 0) lines.push(` permissions: [${entry.permissions.map((permission) => JSON.stringify(permission)).join(", ")}],`);
84
84
  lines.push(" },");
85
85
  }
86
86
  lines.push("} as const satisfies Record<string, OperationAccessPolicy>;", "", "export type AccessPolicyKey = keyof typeof accessPolicies;", "");
@@ -268,13 +268,45 @@ function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
268
268
  });
269
269
  }
270
270
  }
271
- return excludeInternalOperations(entries, spec);
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;
272
276
  }
273
277
  function excludeInternalOperations(entries, spec) {
274
278
  const securitySchemes = spec.components?.securitySchemes;
275
279
  if (!securitySchemes) return entries;
276
280
  return entries.filter((entry) => readSecurityRequirement(entry, securitySchemes, spec.security)?.kind !== "internal");
277
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
+ }
278
310
  function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
279
311
  const security = entry.operation.security ?? topLevelSecurity;
280
312
  if (security === void 0) return null;
@@ -291,10 +323,10 @@ function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
291
323
  const scheme = securitySchemes?.[schemeName];
292
324
  if (scheme?.type === "apiKey" && scheme.in === "header") return { kind: "internal" };
293
325
  if (scheme?.type === "http" || scheme?.type === "apiKey" && scheme.in === "cookie") {
294
- const roles = readSecurityScopes(entry, schemeName, scopes);
295
- return roles.length === 0 ? { kind: "authenticated" } : {
296
- kind: "role",
297
- roles
326
+ const permissions = readSecurityScopes(entry, schemeName, scopes);
327
+ return permissions.length === 0 ? { kind: "authenticated" } : {
328
+ kind: "permission",
329
+ permissions
298
330
  };
299
331
  }
300
332
  }
@@ -302,12 +334,12 @@ function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
302
334
  }
303
335
  function readSecurityScopes(entry, schemeName, scopes) {
304
336
  if (!Array.isArray(scopes)) throw new Error(`Operation "${entry.operationId}" has non-array scopes for security scheme "${schemeName}"`);
305
- const roles = [];
337
+ const permissions = [];
306
338
  for (const scope of scopes) {
307
339
  if (typeof scope !== "string") throw new Error(`Operation "${entry.operationId}" has a non-string scope for security scheme "${schemeName}"`);
308
- roles.push(scope);
340
+ permissions.push(scope);
309
341
  }
310
- return roles;
342
+ return permissions;
311
343
  }
312
344
  function isRecord(value) {
313
345
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -646,9 +678,10 @@ function resolveHttpClientConfig(config) {
646
678
  };
647
679
  }
648
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."];
649
- async function generateApiTypes(source, outputDir, useTypeAliases) {
681
+ async function generateApiTypes(input, outputDir, useTypeAliases) {
650
682
  const { default: openapiTS, astToString } = await import("openapi-typescript");
651
- 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 ? {
652
685
  rootTypes: true,
653
686
  rootTypesKeepCasing: true,
654
687
  rootTypesNoSchemaPrefix: true
@@ -699,7 +732,7 @@ function createAccessPolicyEntries(operations, securitySchemes, topLevelSecurity
699
732
  kind: accessPolicy.kind,
700
733
  methodUpper: entry.method.toUpperCase(),
701
734
  operationId: entry.operationId,
702
- roles: accessPolicy.roles ?? [],
735
+ permissions: accessPolicy.permissions ?? [],
703
736
  strippedPath: entry.strippedPath
704
737
  }];
705
738
  });
@@ -769,11 +802,13 @@ function parseOpenAPISpec(sourceText, inputLabel) {
769
802
  async function generateOpenAPIArtifacts(root, options) {
770
803
  const outputDir = resolve(root, options.output);
771
804
  mkdirSync(outputDir, { recursive: true });
772
- const { apiTypesSource, spec } = await loadOpenAPIInput(root, options.input);
773
- const operations = collectOperations(spec, options.pathPrefix ?? "/api/", options.stripPrefix ?? true);
774
- 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);
775
810
  warnOnParameterLocationMismatch(operations);
776
- await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
811
+ await generateApiTypes(input, outputDir, options.typeAliases ?? false);
777
812
  if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
778
813
  if (artifacts.accessPolicies) writeFileSync(resolve(outputDir, "access-policies.ts"), artifacts.accessPolicies);
779
814
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
package/dist/index.mjs CHANGED
@@ -55,11 +55,11 @@ function renderAccessPoliciesSource(entries, generatedHeader) {
55
55
  const lines = [
56
56
  ...generatedHeader,
57
57
  "",
58
- "export type AccessPolicyKind = \"authenticated\" | \"internal\" | \"public\" | \"role\";",
58
+ "export type AccessPolicyKind = \"authenticated\" | \"internal\" | \"public\" | \"permission\";",
59
59
  "",
60
60
  "export interface AccessPolicy {",
61
61
  " kind: AccessPolicyKind;",
62
- " roles?: readonly string[];",
62
+ " permissions?: readonly string[];",
63
63
  "}",
64
64
  "",
65
65
  "export interface OperationAccessPolicy extends AccessPolicy {",
@@ -78,7 +78,7 @@ function renderAccessPoliciesSource(entries, generatedHeader) {
78
78
  lines.push(` method: ${JSON.stringify(entry.methodUpper)},`);
79
79
  lines.push(` operationId: ${JSON.stringify(entry.operationId)},`);
80
80
  lines.push(` path: ${JSON.stringify(entry.strippedPath)},`);
81
- if (entry.roles.length > 0) lines.push(` roles: [${entry.roles.map((role) => JSON.stringify(role)).join(", ")}],`);
81
+ if (entry.permissions.length > 0) lines.push(` permissions: [${entry.permissions.map((permission) => JSON.stringify(permission)).join(", ")}],`);
82
82
  lines.push(" },");
83
83
  }
84
84
  lines.push("} as const satisfies Record<string, OperationAccessPolicy>;", "", "export type AccessPolicyKey = keyof typeof accessPolicies;", "");
@@ -266,13 +266,45 @@ function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
266
266
  });
267
267
  }
268
268
  }
269
- return excludeInternalOperations(entries, spec);
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;
270
274
  }
271
275
  function excludeInternalOperations(entries, spec) {
272
276
  const securitySchemes = spec.components?.securitySchemes;
273
277
  if (!securitySchemes) return entries;
274
278
  return entries.filter((entry) => readSecurityRequirement(entry, securitySchemes, spec.security)?.kind !== "internal");
275
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
+ }
276
308
  function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
277
309
  const security = entry.operation.security ?? topLevelSecurity;
278
310
  if (security === void 0) return null;
@@ -289,10 +321,10 @@ function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
289
321
  const scheme = securitySchemes?.[schemeName];
290
322
  if (scheme?.type === "apiKey" && scheme.in === "header") return { kind: "internal" };
291
323
  if (scheme?.type === "http" || scheme?.type === "apiKey" && scheme.in === "cookie") {
292
- const roles = readSecurityScopes(entry, schemeName, scopes);
293
- return roles.length === 0 ? { kind: "authenticated" } : {
294
- kind: "role",
295
- roles
324
+ const permissions = readSecurityScopes(entry, schemeName, scopes);
325
+ return permissions.length === 0 ? { kind: "authenticated" } : {
326
+ kind: "permission",
327
+ permissions
296
328
  };
297
329
  }
298
330
  }
@@ -300,12 +332,12 @@ function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
300
332
  }
301
333
  function readSecurityScopes(entry, schemeName, scopes) {
302
334
  if (!Array.isArray(scopes)) throw new Error(`Operation "${entry.operationId}" has non-array scopes for security scheme "${schemeName}"`);
303
- const roles = [];
335
+ const permissions = [];
304
336
  for (const scope of scopes) {
305
337
  if (typeof scope !== "string") throw new Error(`Operation "${entry.operationId}" has a non-string scope for security scheme "${schemeName}"`);
306
- roles.push(scope);
338
+ permissions.push(scope);
307
339
  }
308
- return roles;
340
+ return permissions;
309
341
  }
310
342
  function isRecord(value) {
311
343
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -644,9 +676,10 @@ function resolveHttpClientConfig(config) {
644
676
  };
645
677
  }
646
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."];
647
- async function generateApiTypes(source, outputDir, useTypeAliases) {
679
+ async function generateApiTypes(input, outputDir, useTypeAliases) {
648
680
  const { default: openapiTS, astToString } = await import("openapi-typescript");
649
- 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 ? {
650
683
  rootTypes: true,
651
684
  rootTypesKeepCasing: true,
652
685
  rootTypesNoSchemaPrefix: true
@@ -697,7 +730,7 @@ function createAccessPolicyEntries(operations, securitySchemes, topLevelSecurity
697
730
  kind: accessPolicy.kind,
698
731
  methodUpper: entry.method.toUpperCase(),
699
732
  operationId: entry.operationId,
700
- roles: accessPolicy.roles ?? [],
733
+ permissions: accessPolicy.permissions ?? [],
701
734
  strippedPath: entry.strippedPath
702
735
  }];
703
736
  });
@@ -767,11 +800,13 @@ function parseOpenAPISpec(sourceText, inputLabel) {
767
800
  async function generateOpenAPIArtifacts(root, options) {
768
801
  const outputDir = resolve(root, options.output);
769
802
  mkdirSync(outputDir, { recursive: true });
770
- const { apiTypesSource, spec } = await loadOpenAPIInput(root, options.input);
771
- const operations = collectOperations(spec, options.pathPrefix ?? "/api/", options.stripPrefix ?? true);
772
- 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);
773
808
  warnOnParameterLocationMismatch(operations);
774
- await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
809
+ await generateApiTypes(input, outputDir, options.typeAliases ?? false);
775
810
  if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
776
811
  if (artifacts.accessPolicies) writeFileSync(resolve(outputDir, "access-policies.ts"), artifacts.accessPolicies);
777
812
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
@@ -790,7 +825,7 @@ function openapiCodegen(options) {
790
825
  },
791
826
  async buildStart() {
792
827
  if (command === "serve" && options.generateOnDev !== false) {
793
- runDevelopmentGeneration(root, options, { onError: resolvePluginErrorRaiser(this) });
828
+ runDevelopmentGeneration(root, options, { onError: resolvePluginErrorRaiser(this) }).catch(() => {});
794
829
  return;
795
830
  }
796
831
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-openapi-codegen",
3
- "version": "3.2.1",
3
+ "version": "4.0.0",
4
4
  "description": "Vite plugin that generates typed API clients and route builders from OpenAPI specs",
5
5
  "keywords": [
6
6
  "api-client",