Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser): generate enums into their own file #358

Merged
merged 3 commits into from
Apr 12, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/modern-dots-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hey-api/openapi-ts": minor
---

fix: generate enums into their own file
5 changes: 5 additions & 0 deletions .changeset/young-moles-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"openapi-ts-docs": patch
---

docs: add enums migration
34 changes: 21 additions & 13 deletions docs/openapi-ts/migrating.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Migrating to @hey-api/openapi-ts.

# Migrating

While we try to avoid breaking changes, sometimes it's unavoidable in order to offer you the latest features. This page lists changes that require updates to your code.
While we try to avoid breaking changes, sometimes it's unavoidable in order to offer you the latest features. This page lists changes that require updates to your code. If you run into an issue with migration, please [open an issue](https://github.com/hey-api/openapi-ts/issues).

## @next

Expand Down Expand Up @@ -50,6 +50,25 @@ This config option is deprecated and will be removed.

This config option is deprecated and will be removed.

## v0.39.0

### Single `enums.gen.ts` file

Enums are now exported from a single file. If you used imports from `model.ts`, you can change it to `enums.gen.ts`.

```js
import { Enum } from 'client/models' // [!code --]
import { Enum } from 'client/enums.gen.ts' // [!code ++]
```

Enums are no longer exported from `index.ts`. If you used imports from index file, you will need to move enums into their own import statement.

```js
import { Enum, DefaultService } from 'client' // [!code --]
import { Enum } from 'client/enums.gen.ts' // [!code ++]
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should probably work on renaming all files to .gen.ts for consistency

import { DefaultService } from 'client/services' // [!code ++]
```

## v0.38.0

### Renamed `write`
Expand Down Expand Up @@ -140,15 +159,4 @@ This config option has been removed. Generated types will behave the same as `us

## OpenAPI TypeScript Codegen

`openapi-ts` was originally forked from Ferdi Koomen's [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen). Therefore, we want you to be able to migrate your openapi-typescript-codegen projects. Migration should be relatively straightforward if you follow the release notes on this page. If you run into an issue with migration, please [open an issue](https://github.com/hey-api/openapi-ts/issues).

### Changed

- `exportSchemas` is `true` by default (see [v0.27.36](#v0-27-36))
- `useOptions` is `true` by default (see [v0.27.38](#v0-27-38))

### Removed

- `useUnionTypes` has been removed (see [v0.27.24](#v0-27-24))
- `indent` has been removed (see [v0.27.26](#v0-27-26))
- `postfixModels` has been removed (see [v0.35.0](#v0-35-0))
`openapi-ts` was originally forked from Ferdi Koomen's [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen). Therefore, we want you to be able to migrate your projects. Migration should be relatively straightforward if you follow the release notes on this page. Start here and scroll up to the release you're migrating to.
15 changes: 11 additions & 4 deletions packages/openapi-ts/src/compiler/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type PathOrFileDescriptor, writeFileSync } from 'node:fs';
import { writeFileSync } from 'node:fs';

import ts from 'typescript';

Expand All @@ -15,6 +15,7 @@ export class TypeScriptFile {
private _headers: Array<string> = [];
private _imports: Array<ts.Node> = [];
private _items: Array<ts.Node | string> = [];
private _path: string = '';

public add(...nodes: Array<ts.Node | string>): void {
this._items = [...this._items, ...nodes];
Expand All @@ -31,6 +32,11 @@ export class TypeScriptFile {
this._imports = [...this._imports, compiler.import.named(...params)];
}

public setPath(path: string) {
this._path = path;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we pass the path in the class contructor (same with a header)? Its probably cleaner than doing this. And they can be optional

return this;
}

public toString(seperator: string = '\n') {
let output: string[] = [];
if (this._headers.length) {
Expand All @@ -43,11 +49,12 @@ export class TypeScriptFile {
return output.join(seperator);
}

public write(file: PathOrFileDescriptor, seperator: string = '\n') {
if (!this._items.length) {
public write(seperator = '\n') {
// TODO: throw if path is not set. do not throw if items are empty
if (!this._items.length || !this._path) {
return;
}
writeFileSync(file, this.toString(seperator));
writeFileSync(this._path, this.toString(seperator));
}
}

Expand Down
97 changes: 52 additions & 45 deletions packages/openapi-ts/src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
*/
export const toExpression = (value: unknown, unescape = false): ts.Expression | undefined => {
if (Array.isArray(value)) {
return createArrayType(value);
return createArrayType({ arr: value });

Check warning on line 12 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L12

Added line #L12 was not covered by tests
}

if (typeof value === 'object' && value !== null) {
return createObjectType(value);
return createObjectType({ obj: value });

Check warning on line 16 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L16

Added line #L16 was not covered by tests
}

if (typeof value === 'number') {
Expand All @@ -39,7 +39,13 @@
* @param multiLine - if the array should be multiline.
* @returns ts.ArrayLiteralExpression
*/
export const createArrayType = <T>(arr: T[], multiLine: boolean = false): ts.ArrayLiteralExpression =>
export const createArrayType = <T>({
arr,
multiLine = false,
}: {
arr: T[];
multiLine?: boolean;
}): ts.ArrayLiteralExpression =>

Check warning on line 48 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L43-L48

Added lines #L43 - L48 were not covered by tests
ts.factory.createArrayLiteralExpression(
arr.map(v => toExpression(v)).filter(isType<ts.Expression>),
// Multiline if the array contains objects, or if specified by the user.
Expand All @@ -48,59 +54,60 @@

/**
* Create Object type expression.
* @param obj - the object to create.
* @param options - options to use when creating type.
* @returns ts.ObjectLiteralExpression
*/
export const createObjectType = <T extends object>(
obj: T,
options: {
multiLine?: boolean;
unescape?: boolean;
comments?: Record<string | number, Comments>;
} = {
comments: {},
multiLine: true,
unescape: false,
}
): ts.ObjectLiteralExpression => {
const expression = ts.factory.createObjectLiteralExpression(
Object.entries(obj)
.map(([key, value]) => {
const initializer = toExpression(value, options.unescape);
if (!initializer) {
return undefined;
}
const c = options.comments?.[key];
if (key.match(/\W/g) && !key.startsWith("'") && !key.endsWith("'")) {
key = `'${key}'`;
}
const assignment = ts.factory.createPropertyAssignment(key, initializer);
if (c?.length) {
addLeadingComment(assignment, c);
}
return assignment;
})
.filter(isType<ts.PropertyAssignment>),
options.multiLine
);
export const createObjectType = <T extends object>({
comments = {},
multiLine = true,
obj,
unescape = false,
}: {
obj: T;
multiLine?: boolean;
unescape?: boolean;
comments?: Record<string | number, Comments>;
}): ts.ObjectLiteralExpression => {
const properties = Object.entries(obj)
.map(([key, value]) => {
const initializer = toExpression(value, unescape);
if (!initializer) {
return undefined;
}

Check warning on line 76 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L75-L76

Added lines #L75 - L76 were not covered by tests
if (key.match(/\W/g) && !key.startsWith("'") && !key.endsWith("'")) {
key = `'${key}'`;
}

Check warning on line 79 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L78-L79

Added lines #L78 - L79 were not covered by tests
const assignment = ts.factory.createPropertyAssignment(key, initializer);
const c = comments?.[key];
if (c?.length) {
addLeadingComment(assignment, c);
}

Check warning on line 84 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L83-L84

Added lines #L83 - L84 were not covered by tests
return assignment;
})
.filter(isType<ts.PropertyAssignment>);
const expression = ts.factory.createObjectLiteralExpression(properties, multiLine);
return expression;
};

/**
* Create enum declaration. Example `export enum T = { X, Y };`
* @param name - the name of the enum.
* @param obj - the object representing the enum.
* @param comment - comment to add to enum.
* @param leadingComment - leading comment to add to enum.
* @param comments - comments to add to each property of enum.
* @returns
*/
export const createEnumDeclaration = <T extends object>(
name: string,
obj: T,
comment: Comments = [],
comments: Record<string | number, Comments> = {}
): ts.EnumDeclaration => {
export const createEnumDeclaration = <T extends object>({
name,
obj,
leadingComment = [],
comments = {},
}: {
name: string;
obj: T;
leadingComment: Comments;
comments: Record<string | number, Comments>;
}): ts.EnumDeclaration => {

Check warning on line 110 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L101-L110

Added lines #L101 - L110 were not covered by tests
const declaration = ts.factory.createEnumDeclaration(
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier(name),
Expand All @@ -114,8 +121,8 @@
return assignment;
})
);
if (comment.length) {
addLeadingComment(declaration, comment);
if (leadingComment.length) {
addLeadingComment(declaration, leadingComment);

Check warning on line 125 in packages/openapi-ts/src/compiler/types.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/compiler/types.ts#L124-L125

Added lines #L124 - L125 were not covered by tests
}
return declaration;
};
13 changes: 4 additions & 9 deletions packages/openapi-ts/src/utils/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,12 @@
*/
export const enumName = (client: Client, name?: string) => {
if (!name) {
return name;
return null;

Check warning on line 46 in packages/openapi-ts/src/utils/enum.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/utils/enum.ts#L46

Added line #L46 was not covered by tests
}
const escapedName = unescapeName(name).replace(/[-_]([a-z])/gi, ($0, $1: string) => $1.toLocaleUpperCase());
let result = `${escapedName.charAt(0).toLocaleUpperCase() + escapedName.slice(1)}Enum`;
let index = 1;
while (client.enumNames.includes(result)) {
if (result.endsWith(index.toString())) {
result = result.slice(0, result.length - index.toString().length);
}
index += 1;
result = result + index.toString();
const result = `${escapedName.charAt(0).toLocaleUpperCase() + escapedName.slice(1)}Enum`;
if (client.enumNames.includes(result)) {
return null;

Check warning on line 51 in packages/openapi-ts/src/utils/enum.ts

View check run for this annotation

Codecov / codecov/patch

packages/openapi-ts/src/utils/enum.ts#L49-L51

Added lines #L49 - L51 were not covered by tests
}
client.enumNames = [...client.enumNames, result];
return result;
Expand Down
18 changes: 9 additions & 9 deletions packages/openapi-ts/src/utils/write/__tests__/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@ import path from 'node:path';
import { beforeEach, describe, expect, it, vi } from 'vitest';

import { setConfig } from '../../config';
import { writeClientCore } from '../core';
import { writeCore } from '../core';
import { mockTemplates } from './mocks';
import { openApi } from './models';

vi.mock('node:fs');

describe('writeClientCore', () => {
let templates: Parameters<typeof writeClientCore>[3];
describe('writeCore', () => {
let templates: Parameters<typeof writeCore>[3];
beforeEach(() => {
templates = mockTemplates;
});

it('writes to filesystem', async () => {
const client: Parameters<typeof writeClientCore>[2] = {
const client: Parameters<typeof writeCore>[2] = {
enumNames: [],
models: [],
server: 'http://localhost:8080',
Expand Down Expand Up @@ -46,7 +46,7 @@ describe('writeClientCore', () => {
useOptions: true,
});

await writeClientCore(openApi, '/', client, templates);
await writeCore(openApi, '/', client, templates);

expect(writeFileSync).toHaveBeenCalledWith(path.resolve('/', '/OpenAPI.ts'), 'settings');
expect(writeFileSync).toHaveBeenCalledWith(path.resolve('/', '/ApiError.ts'), 'apiError');
Expand All @@ -57,7 +57,7 @@ describe('writeClientCore', () => {
});

it('uses client server value for base', async () => {
const client: Parameters<typeof writeClientCore>[2] = {
const client: Parameters<typeof writeCore>[2] = {
enumNames: [],
models: [],
server: 'http://localhost:8080',
Expand Down Expand Up @@ -86,7 +86,7 @@ describe('writeClientCore', () => {
useOptions: true,
});

await writeClientCore(openApi, '/', client, templates);
await writeCore(openApi, '/', client, templates);

expect(templates.core.settings).toHaveBeenCalledWith({
$config: config,
Expand All @@ -97,7 +97,7 @@ describe('writeClientCore', () => {
});

it('uses custom value for base', async () => {
const client: Parameters<typeof writeClientCore>[2] = {
const client: Parameters<typeof writeCore>[2] = {
enumNames: [],
models: [],
server: 'http://localhost:8080',
Expand Down Expand Up @@ -127,7 +127,7 @@ describe('writeClientCore', () => {
useOptions: true,
});

await writeClientCore(openApi, '/', client, templates);
await writeCore(openApi, '/', client, templates);

expect(templates.core.settings).toHaveBeenCalledWith({
$config: config,
Expand Down
8 changes: 4 additions & 4 deletions packages/openapi-ts/src/utils/write/__tests__/models.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import path from 'node:path';
import { describe, expect, it, vi } from 'vitest';

import { setConfig } from '../../config';
import { writeClientModels } from '../models';
import { writeTypesAndEnums } from '../models';
import { openApi } from './models';

vi.mock('node:fs');

describe('writeClientModels', () => {
describe('writeTypesAndEnums', () => {
it('writes to filesystem', async () => {
setConfig({
client: 'fetch',
Expand All @@ -32,7 +32,7 @@ describe('writeClientModels', () => {
useOptions: true,
});

const client: Parameters<typeof writeClientModels>[2] = {
const client: Parameters<typeof writeTypesAndEnums>[2] = {
enumNames: [],
models: [
{
Expand All @@ -59,7 +59,7 @@ describe('writeClientModels', () => {
version: 'v1',
};

await writeClientModels(openApi, '/', client);
await writeTypesAndEnums(openApi, '/', client);

expect(writeFileSync).toHaveBeenCalledWith(path.resolve('/', '/models.ts'), expect.anything());
});
Expand Down
Loading