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 +84 -30
- package/dist/index.mjs +99 -35
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -268,7 +268,81 @@ function collectOperations(spec, pathPrefix = "/api/", stripPrefix = true) {
|
|
|
268
268
|
});
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
-
|
|
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(
|
|
681
|
+
async function generateApiTypes(input, outputDir, useTypeAliases) {
|
|
608
682
|
const { default: openapiTS, astToString } = await import("openapi-typescript");
|
|
609
|
-
const
|
|
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
|
|
754
|
-
const
|
|
755
|
-
const
|
|
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(
|
|
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
|
-
|
|
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(
|
|
679
|
+
async function generateApiTypes(input, outputDir, useTypeAliases) {
|
|
606
680
|
const { default: openapiTS, astToString } = await import("openapi-typescript");
|
|
607
|
-
const
|
|
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
|
|
752
|
-
const
|
|
753
|
-
const
|
|
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(
|
|
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, {
|
|
785
|
-
|
|
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
|
-
|
|
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 };
|