From 1600f29fe36e89c336af1b1de91b0095a7f41cd6 Mon Sep 17 00:00:00 2001 From: jgclement91 <8974093+jgclement91@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:34:33 -0500 Subject: [PATCH] Fix duplicate type alias generation for enum declarations --- .../src/plugins/types-plugin.ts | 10 ++- .../tests/__snapshots__/e2e.test.ts.snap | 75 +++++++++++++++++++ packages/create-schemas/tests/data/enums.yaml | 35 +++++++++ packages/create-schemas/tests/e2e.test.ts | 44 ++++++++++- 4 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 packages/create-schemas/tests/data/enums.yaml diff --git a/packages/create-schemas/src/plugins/types-plugin.ts b/packages/create-schemas/src/plugins/types-plugin.ts index 9085ef0..400cdf7 100644 --- a/packages/create-schemas/src/plugins/types-plugin.ts +++ b/packages/create-schemas/src/plugins/types-plugin.ts @@ -19,18 +19,26 @@ export function typesPlugin(): Plugin { const ast = stringToAST(code) as ts.Node[]; + // Collect names of all enum declarations to avoid generating duplicate type aliases + const existingEnums = new Set( + ast + .filter(node => ts.isEnumDeclaration(node)) + .map(node => (node as ts.EnumDeclaration).name.text) + ); + const componentsDeclaration = ast.find(isComponentsInterfaceDeclaration); assert(componentsDeclaration, "Missing components declaration"); const schema = componentsDeclaration.members.find(isComponentsSchema); assert(schema, "Missing components declaration"); assert(schema.type && ts.isTypeLiteralNode(schema.type), "Invalid schema type"); - + const typeNodes = schema.type.members .map(member => member.name) .filter(name => name !== undefined) .filter(name => ts.isStringLiteral(name) || ts.isIdentifier(name)) .map(name => name.text) + .filter(name => !existingEnums.has(toSafeName(name))) .map(name => { if (RESERVED_IDENTIFIERS.has(name)) { throw new Error(`Invalid schema name: ${name}`); diff --git a/packages/create-schemas/tests/__snapshots__/e2e.test.ts.snap b/packages/create-schemas/tests/__snapshots__/e2e.test.ts.snap index 03b092a..87f75cd 100644 --- a/packages/create-schemas/tests/__snapshots__/e2e.test.ts.snap +++ b/packages/create-schemas/tests/__snapshots__/e2e.test.ts.snap @@ -1,5 +1,80 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`e2e > enums.yaml / enum generation without duplicate type aliases 1`] = ` +"/** This file has been generated by @workleap/create-schemas (https://github.com/workleap/wl-openapi-typescript). Do not modify manually. */ +export interface paths { + "/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Success */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** @enum {string} */ + Status: Status; + /** @enum {string} */ + Priority: Priority; + Task: { + id?: string; + status?: components["schemas"]["Status"]; + priority?: components["schemas"]["Priority"]; + name?: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export enum Status { + active = "active", + inactive = "inactive", + pending = "pending" +} +export enum Priority { + low = "low", + medium = "medium", + high = "high" +} +export type operations = Record; + +export type Task = components["schemas"]["Task"]; +export type Endpoints = keyof paths; +" +`; + exports[`e2e > officevice.yaml / file URLs 1`] = ` "/** This file has been generated by @workleap/create-schemas (https://github.com/workleap/wl-openapi-typescript). Do not modify manually. */ export interface paths { diff --git a/packages/create-schemas/tests/data/enums.yaml b/packages/create-schemas/tests/data/enums.yaml new file mode 100644 index 0000000..b44a9c4 --- /dev/null +++ b/packages/create-schemas/tests/data/enums.yaml @@ -0,0 +1,35 @@ +openapi: 3.0.0 +info: + title: Enum Test API + version: 1.0.0 +paths: + /test: + get: + responses: + '200': + description: Success +components: + schemas: + Status: + type: string + enum: + - active + - inactive + - pending + Priority: + type: string + enum: + - low + - medium + - high + Task: + type: object + properties: + id: + type: string + status: + $ref: '#/components/schemas/Status' + priority: + $ref: '#/components/schemas/Priority' + name: + type: string diff --git a/packages/create-schemas/tests/e2e.test.ts b/packages/create-schemas/tests/e2e.test.ts index 2197bd0..c2a0185 100644 --- a/packages/create-schemas/tests/e2e.test.ts +++ b/packages/create-schemas/tests/e2e.test.ts @@ -105,7 +105,7 @@ describe.concurrent("e2e", () => { const configFile = ` import { experimental_openapiFetchPlugin } from "../../../src/plugins"; - + export default { plugins: [experimental_openapiFetchPlugin()] }; `; @@ -124,4 +124,46 @@ describe.concurrent("e2e", () => { }, timeout ); + + test( + "enums.yaml / enum generation without duplicate type aliases", + async ({ expect, onTestFinished }) => { + const tempFolder = await createTemporaryFolder({ onTestFinished }); + + const configFile = ` + export default { + openApiTsOptions: { + enum: true + } + }; + `; + + await writeFile(join(tempFolder, "create-schemas.config.ts"), configFile); + + const result = await runCompiledBin({ + source: join(dataFolder, "enums.yaml"), + outdir: join(tempFolder, "dist"), + cwd: tempFolder + }); + + const typesFile = result.find(file => file.filename === openapiTypeScriptFilename); + assert(typesFile); + + // Verify enums are generated + expect(typesFile.code).toContain("export enum Status"); + expect(typesFile.code).toContain("export enum Priority"); + + // Verify NO duplicate type aliases for enums + const statusTypeAliasRegex = /export type Status = components\["schemas"\]\["Status"\];/; + const priorityTypeAliasRegex = /export type Priority = components\["schemas"\]\["Priority"\];/; + expect(typesFile.code).not.toMatch(statusTypeAliasRegex); + expect(typesFile.code).not.toMatch(priorityTypeAliasRegex); + + // Verify non-enum types still get type aliases + expect(typesFile.code).toContain('export type Task = components["schemas"]["Task"];'); + + expect(typesFile.code).toMatchSnapshot(); + }, + timeout + ); });