Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/create-schemas/src/plugins/types-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
Comment on lines +22 to +27
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new enum filtering logic should have a unit test in tests/plugins.test.ts to supplement the E2E test. Consider adding a test case that passes code with enum declarations to typesPlugin and verifies that type aliases are only generated for non-enum schemas.

Example test structure:

test("typesPlugin with enums", async ({ expect }) => {
    const plugin = typesPlugin();
    const result = await plugin.transform({
        config: await resolveConfig({ input: "openapi.json" }),
        id: openapiTypeScriptId,
        code: `export interface components {
          schemas: {
            Status: string;
            Task: { name: string };
          }
        }
        export enum Status {
          Active = "active",
          Inactive = "inactive"
        }`,
        filename: openapiTypeScriptFilename,
        emitFile: () => void 0
    });
    
    expect(result.code).toContain('export type Task = components["schemas"]["Task"]');
    expect(result.code).not.toContain('export type Status = components["schemas"]["Status"]');
});

Copilot uses AI. Check for mistakes.

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}`);
Expand Down
75 changes: 75 additions & 0 deletions packages/create-schemas/tests/__snapshots__/e2e.test.ts.snap
Original file line number Diff line number Diff line change
@@ -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<string, never>;
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<string, never>;
export enum Status {
active = "active",
inactive = "inactive",
pending = "pending"
}
export enum Priority {
low = "low",
medium = "medium",
high = "high"
}
export type operations = Record<string, never>;

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 {
Expand Down
35 changes: 35 additions & 0 deletions packages/create-schemas/tests/data/enums.yaml
Original file line number Diff line number Diff line change
@@ -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
44 changes: 43 additions & 1 deletion packages/create-schemas/tests/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe.concurrent("e2e", () => {

const configFile = `
import { experimental_openapiFetchPlugin } from "../../../src/plugins";

export default { plugins: [experimental_openapiFetchPlugin()] };
`;

Expand All @@ -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
);
Comment on lines +128 to +168
Copy link

Copilot AI Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The E2E test only covers enum names that are already valid TypeScript identifiers (Status, Priority). Consider adding a test case with enum schema names that contain special characters (e.g., user-status, my.org.Status) to ensure the toSafeName transformation correctly matches between enum declarations and the filtering logic. This would help verify that the enum name transformation in openapi-typescript aligns with the toSafeName function used in the filter.

Copilot uses AI. Check for mistakes.
});
Loading