Skip to content

Commit b1bbe62

Browse files
authored
feat: add dedupeEnums option (#1775)
* feat: add dedupeEnums option * Create wet-foxes-watch.md * Create olive-rats-occur.md * chore: lint code * chore: changeset
1 parent 8e5fa3a commit b1bbe62

File tree

11 files changed

+131
-20
lines changed

11 files changed

+131
-20
lines changed

.changeset/wet-foxes-watch.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"openapi-typescript": minor
3+
---
4+
5+
feat: add dedupeEnums option

docs/cli.md

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ The following flags are supported in the CLI:
113113
| `--empty-objects-unknown` | | `false` | Allow arbitrary properties for schema objects with no specified properties, and no specified `additionalProperties` |
114114
| `--enum` | | `false` | Generate true [TS enums](https://www.typescriptlang.org/docs/handbook/enums.html) rather than string unions. |
115115
| `--enum-values` | | `false` | Export enum values as arrays. |
116+
| `--dedupe-enums` | | `false` | Dedupe enum types when `--enum=true` is set |
116117
| `--check` | | `false` | Check that the generated types are up-to-date. |
117118
| `--exclude-deprecated` | | `false` | Exclude deprecated fields from types |
118119
| `--export-type` | `-t` | `false` | Export `type` instead of `interface` |

docs/zh/cli.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,8 @@ CLI 支持以下参数:
112112
| `--empty-objects-unknown` | | `false` | 允许在未指定属性和未指定 `additionalProperties` 的情况下,为模式对象设置任意属性 |
113113
| `--enum` | | `false` | 生成真实的 [TS 枚举](https://www.typescriptlang.org/docs/handbook/enums.html),而不是字符串联合。 |
114114
| `--enum-values` | | `false` | 将 enum 值导出为数组 |
115-
| `--check` | | `false` | 检查生成的类型是否是最新的 |
115+
| `--dedupe-enums` | | `false` | 将 enum 值导出为数组 |
116+
| `--check` | | `false` | 当 --enum=true 时,去除重复定义的枚举类型 |
116117
| `--exclude-deprecated` | | `false` | 从类型中排除已弃用的字段 |
117118
| `--export-type` | `-t` | `false` | 导出 `type` 而不是 `interface` |
118119
| `--immutable` | | `false` | 生成不可变类型(只读属性和只读数组) |

packages/openapi-typescript-helpers/package.json

+1-6
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,7 @@
2323
},
2424
"./*": "./*"
2525
},
26-
"files": [
27-
"index.js",
28-
"index.cjs",
29-
"index.d.ts",
30-
"index.d.cts"
31-
],
26+
"files": ["index.js", "index.cjs", "index.d.ts", "index.d.cts"],
3227
"homepage": "https://openapi-ts.dev",
3328
"repository": {
3429
"type": "git",

packages/openapi-typescript/bin/cli.js

+3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Options
1717
--output, -o Specify output file (if not specified in redocly.yaml)
1818
--enum Export true TS enums instead of unions
1919
--enum-values Export enum values as arrays
20+
--dedupe-enums Dedupe enum types when \`--enum=true\` is set
2021
--check Check that the generated types are up-to-date. (default: false)
2122
--export-type, -t Export top-level \`type\` instead of \`interface\`
2223
--immutable Generate readonly types
@@ -66,6 +67,7 @@ const flags = parser(args, {
6667
"emptyObjectsUnknown",
6768
"enum",
6869
"enumValues",
70+
"dedupeEnums",
6971
"check",
7072
"excludeDeprecated",
7173
"exportType",
@@ -126,6 +128,7 @@ async function generateSchema(schema, { redocly, silent = false }) {
126128
emptyObjectsUnknown: flags.emptyObjectsUnknown,
127129
enum: flags.enum,
128130
enumValues: flags.enumValues,
131+
dedupeEnums: flags.dedupeEnums,
129132
excludeDeprecated: flags.excludeDeprecated,
130133
exportType: flags.exportType,
131134
immutable: flags.immutable,

packages/openapi-typescript/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export default async function openapiTS(
7575
emptyObjectsUnknown: options.emptyObjectsUnknown ?? false,
7676
enum: options.enum ?? false,
7777
enumValues: options.enumValues ?? false,
78+
dedupeEnums: options.dedupeEnums ?? false,
7879
excludeDeprecated: options.excludeDeprecated ?? false,
7980
exportType: options.exportType ?? false,
8081
immutable: options.immutable ?? false,

packages/openapi-typescript/src/lib/ts.ts

+19-2
Original file line numberDiff line numberDiff line change
@@ -204,20 +204,37 @@ export function tsDedupe(types: ts.TypeNode[]): ts.TypeNode[] {
204204
return filteredTypes;
205205
}
206206

207+
export const enumCache = new Map<string, ts.EnumDeclaration>();
208+
207209
/** Create a TS enum (with sanitized name and members) */
208210
export function tsEnum(
209211
name: string,
210212
members: (string | number)[],
211213
metadata?: { name?: string; description?: string }[],
212-
options?: { export?: boolean },
214+
options?: { export?: boolean; shouldCache?: boolean },
213215
) {
214216
let enumName = sanitizeMemberName(name);
215217
enumName = `${enumName[0].toUpperCase()}${enumName.substring(1)}`;
216-
return ts.factory.createEnumDeclaration(
218+
let key = "";
219+
if (options?.shouldCache) {
220+
key = `${members
221+
.slice(0)
222+
.sort()
223+
.map((v, i) => {
224+
return `${metadata?.[i]?.name ?? String(v)}:${metadata?.[i]?.description || ""}`;
225+
})
226+
.join(",")}`;
227+
if (enumCache.has(key)) {
228+
return enumCache.get(key) as ts.EnumDeclaration;
229+
}
230+
}
231+
const enumDeclaration = ts.factory.createEnumDeclaration(
217232
/* modifiers */ options ? tsModifiers({ export: options.export ?? false }) : undefined,
218233
/* name */ enumName,
219234
/* members */ members.map((value, i) => tsEnumMember(value, metadata?.[i])),
220235
);
236+
options?.shouldCache && enumCache.set(key, enumDeclaration);
237+
return enumDeclaration;
221238
}
222239

223240
/** Create an exported TS array literal expression */

packages/openapi-typescript/src/transform/schema-object.ts

+8-11
Original file line numberDiff line numberDiff line change
@@ -102,17 +102,14 @@ export function transformSchemaObjectWithComposition(
102102
name: schemaObject["x-enum-varnames"]?.[i] ?? schemaObject["x-enumNames"]?.[i],
103103
description: schemaObject["x-enum-descriptions"]?.[i] ?? schemaObject["x-enumDescriptions"]?.[i],
104104
}));
105-
const enumType = tsEnum(
106-
enumName,
107-
schemaObject.enum as (string | number)[],
108-
metadata,
109-
110-
{
111-
export: true,
112-
// readonly: TS enum do not support the readonly modifier
113-
},
114-
);
115-
options.ctx.injectFooter.push(enumType);
105+
const enumType = tsEnum(enumName, schemaObject.enum as (string | number)[], metadata, {
106+
shouldCache: options.ctx.dedupeEnums,
107+
export: true,
108+
// readonly: TS enum do not support the readonly modifier
109+
});
110+
if (!options.ctx.injectFooter.includes(enumType)) {
111+
options.ctx.injectFooter.push(enumType);
112+
}
116113
return ts.factory.createTypeReferenceNode(enumType.name);
117114
}
118115
const enumType = schemaObject.enum.map(tsLiteral);

packages/openapi-typescript/src/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,8 @@ export interface OpenAPITSOptions {
651651
enum?: boolean;
652652
/** Export union values as arrays */
653653
enumValues?: boolean;
654+
/** Dedupe enum values */
655+
dedupeEnums?: boolean;
654656
/** (optional) Substitute path parameter names with their respective types */
655657
pathParamsAsTypes?: boolean;
656658
/** Treat all objects as if they have \`required\` set to all properties by default (default: false) */
@@ -678,6 +680,7 @@ export interface GlobalContext {
678680
emptyObjectsUnknown: boolean;
679681
enum: boolean;
680682
enumValues: boolean;
683+
dedupeEnums: boolean;
681684
excludeDeprecated: boolean;
682685
exportType: boolean;
683686
immutable: boolean;

packages/openapi-typescript/test/node-api.test.ts

+87
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,93 @@ export type operations = Record<string, never>;`,
804804
options: { enumValues: true },
805805
},
806806
],
807+
[
808+
"options > dedupeEnums",
809+
{
810+
given: {
811+
openapi: "3.1",
812+
info: { title: "Test", version: "1.0" },
813+
paths: {
814+
"/url": {
815+
get: {
816+
parameters: [
817+
{
818+
name: "status",
819+
in: "query",
820+
schema: {
821+
type: "string",
822+
enum: ["active", "inactive"],
823+
},
824+
},
825+
],
826+
},
827+
},
828+
},
829+
components: {
830+
schemas: {
831+
Status: {
832+
type: "string",
833+
enum: ["active", "inactive"],
834+
},
835+
StatusReverse: {
836+
type: "string",
837+
enum: ["inactive", "active"],
838+
},
839+
},
840+
},
841+
},
842+
want: `export interface paths {
843+
"/url": {
844+
parameters: {
845+
query?: never;
846+
header?: never;
847+
path?: never;
848+
cookie?: never;
849+
};
850+
get: {
851+
parameters: {
852+
query?: {
853+
status?: PathsUrlGetParametersQueryStatus;
854+
};
855+
header?: never;
856+
path?: never;
857+
cookie?: never;
858+
};
859+
requestBody?: never;
860+
responses: never;
861+
};
862+
put?: never;
863+
post?: never;
864+
delete?: never;
865+
options?: never;
866+
head?: never;
867+
patch?: never;
868+
trace?: never;
869+
};
870+
}
871+
export type webhooks = Record<string, never>;
872+
export interface components {
873+
schemas: {
874+
/** @enum {string} */
875+
Status: PathsUrlGetParametersQueryStatus;
876+
/** @enum {string} */
877+
StatusReverse: PathsUrlGetParametersQueryStatus;
878+
};
879+
responses: never;
880+
parameters: never;
881+
requestBodies: never;
882+
headers: never;
883+
pathItems: never;
884+
}
885+
export type $defs = Record<string, never>;
886+
export enum PathsUrlGetParametersQueryStatus {
887+
active = "active",
888+
inactive = "inactive"
889+
}
890+
export type operations = Record<string, never>;`,
891+
options: { enum: true, dedupeEnums: true },
892+
},
893+
],
807894
[
808895
"snapshot > GitHub",
809896
{

packages/openapi-typescript/test/test-helpers.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const DEFAULT_CTX: GlobalContext = {
1515
emptyObjectsUnknown: false,
1616
enum: false,
1717
enumValues: false,
18+
dedupeEnums: false,
1819
excludeDeprecated: false,
1920
exportType: false,
2021
immutable: false,

0 commit comments

Comments
 (0)