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 +53 -18
- package/dist/index.mjs +54 -19
- package/package.json +1 -1
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\" | \"
|
|
60
|
+
"export type AccessPolicyKind = \"authenticated\" | \"internal\" | \"public\" | \"permission\";",
|
|
61
61
|
"",
|
|
62
62
|
"export interface AccessPolicy {",
|
|
63
63
|
" kind: AccessPolicyKind;",
|
|
64
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
|
295
|
-
return
|
|
296
|
-
kind: "
|
|
297
|
-
|
|
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
|
|
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
|
-
|
|
340
|
+
permissions.push(scope);
|
|
309
341
|
}
|
|
310
|
-
return
|
|
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(
|
|
681
|
+
async function generateApiTypes(input, outputDir, useTypeAliases) {
|
|
650
682
|
const { default: openapiTS, astToString } = await import("openapi-typescript");
|
|
651
|
-
const
|
|
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
|
-
|
|
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
|
|
773
|
-
const
|
|
774
|
-
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);
|
|
775
810
|
warnOnParameterLocationMismatch(operations);
|
|
776
|
-
await generateApiTypes(
|
|
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\" | \"
|
|
58
|
+
"export type AccessPolicyKind = \"authenticated\" | \"internal\" | \"public\" | \"permission\";",
|
|
59
59
|
"",
|
|
60
60
|
"export interface AccessPolicy {",
|
|
61
61
|
" kind: AccessPolicyKind;",
|
|
62
|
-
"
|
|
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.
|
|
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
|
-
|
|
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
|
|
293
|
-
return
|
|
294
|
-
kind: "
|
|
295
|
-
|
|
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
|
|
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
|
-
|
|
338
|
+
permissions.push(scope);
|
|
307
339
|
}
|
|
308
|
-
return
|
|
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(
|
|
679
|
+
async function generateApiTypes(input, outputDir, useTypeAliases) {
|
|
648
680
|
const { default: openapiTS, astToString } = await import("openapi-typescript");
|
|
649
|
-
const
|
|
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
|
-
|
|
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
|
|
771
|
-
const
|
|
772
|
-
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);
|
|
773
808
|
warnOnParameterLocationMismatch(operations);
|
|
774
|
-
await generateApiTypes(
|
|
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
|
},
|