From 3a93e388c26c2bb335eca4825056e15400eda98c Mon Sep 17 00:00:00 2001 From: Tom Frenken <54979414+tomfrenken@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:40:19 +0200 Subject: [PATCH] feat: Add support for ESM generation in openapi generator (#4883) --- .changeset/five-carrots-whisper.md | 6 + .../src/file-writer/create-file.ts | 4 + packages/generator-common/src/options.ts | 1 - .../__snapshots__/api-file.spec.ts.snap | 126 +++++++ .../__snapshots__/index-file.spec.ts.snap | 28 ++ .../__snapshots__/schema-file.spec.ts.snap | 90 +++++ .../src/file-serializer/api-file.spec.ts | 323 +++++++----------- .../src/file-serializer/api-file.ts | 15 +- .../src/file-serializer/index-file.spec.ts | 95 ++++-- .../src/file-serializer/index-file.ts | 36 +- .../src/file-serializer/schema-file.spec.ts | 306 +++++++---------- .../src/file-serializer/schema-file.ts | 19 +- packages/openapi-generator/src/generator.ts | 11 +- packages/openapi-generator/src/index.ts | 2 +- .../src/options/generator-options.spec.ts | 3 +- .../openapi-generator/src/options/options.ts | 20 +- 16 files changed, 657 insertions(+), 428 deletions(-) create mode 100644 .changeset/five-carrots-whisper.md create mode 100644 packages/openapi-generator/src/file-serializer/__snapshots__/api-file.spec.ts.snap create mode 100644 packages/openapi-generator/src/file-serializer/__snapshots__/index-file.spec.ts.snap create mode 100644 packages/openapi-generator/src/file-serializer/__snapshots__/schema-file.spec.ts.snap diff --git a/.changeset/five-carrots-whisper.md b/.changeset/five-carrots-whisper.md new file mode 100644 index 0000000000..11b3c2c2ff --- /dev/null +++ b/.changeset/five-carrots-whisper.md @@ -0,0 +1,6 @@ +--- +'@sap-cloud-sdk/openapi-generator': minor +'@sap-cloud-sdk/generator-common': minor +--- + +[New Functionality] Introduce option `generateESM` in OpenAPI generator to generate ESM compatible code. diff --git a/packages/generator-common/src/file-writer/create-file.ts b/packages/generator-common/src/file-writer/create-file.ts index 7e5d64de3a..27b1998063 100644 --- a/packages/generator-common/src/file-writer/create-file.ts +++ b/packages/generator-common/src/file-writer/create-file.ts @@ -32,6 +32,10 @@ export interface CreateFileOptions { * Flag to indicate if the file is formatted using prettier - Default is true. */ usePrettier?: boolean; + /** + * Flag to indicate if the file is generated as ESM. + */ + generateESM?: boolean; } /** diff --git a/packages/generator-common/src/options.ts b/packages/generator-common/src/options.ts index fe51bb919d..3cb6dbc790 100644 --- a/packages/generator-common/src/options.ts +++ b/packages/generator-common/src/options.ts @@ -112,7 +112,6 @@ export function getCommonCliOptions(serviceType: ServiceType) { type: 'boolean', default: false }, - readme: { type: 'boolean', describe: getReadmeText(serviceType), diff --git a/packages/openapi-generator/src/file-serializer/__snapshots__/api-file.spec.ts.snap b/packages/openapi-generator/src/file-serializer/__snapshots__/api-file.spec.ts.snap new file mode 100644 index 0000000000..57cb322d05 --- /dev/null +++ b/packages/openapi-generator/src/file-serializer/__snapshots__/api-file.spec.ts.snap @@ -0,0 +1,126 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`api-file creates an api file following the esm pattern 1`] = ` +"import { OpenApiRequestBuilder } from '@sap-cloud-sdk/openapi'; +import type { QueryParameterType, RefType, ResponseType } from './schema/index.js'; +/** + * Representation of the 'TestApi'. + * This API is part of the 'MyServiceName' service. + */ +export const TestApi = { + /** + * Create a request builder for execution of get requests to the 'test/{id}' endpoint. + * @param id - Path parameter. + * @param queryParameters - Object containing the following keys: queryParam. + * @param headerParameters - Object containing the following keys: headerParam. + * @returns The request builder, use the \`execute()\` method to trigger the request. + */ + getFn: (id: string, queryParameters: {'queryParam': QueryParameterType}, headerParameters?: {'headerParam'?: string}) => new OpenApiRequestBuilder( + 'get', + "test/{id}", + { + pathParameters: { id }, + queryParameters, + headerParameters + } + ), + /** + * Create a request builder for execution of post requests to the 'test' endpoint. + * @param body - Request body. + * @returns The request builder, use the \`execute()\` method to trigger the request. + */ + createFn: (body: RefType) => new OpenApiRequestBuilder( + 'post', + "test", + { + body + } + ) +};" +`; + +exports[`api-file creates an api file with documentation 1`] = ` +"import { OpenApiRequestBuilder } from '@sap-cloud-sdk/openapi'; +/** + * Representation of the 'TestApi'. + * This API is part of the 'TestService' service. + */ +export const TestApi = { + /** + * Create a request builder for execution of get requests to the 'test' endpoint. + * @returns The request builder, use the \`execute()\` method to trigger the request. + */ + getFn: () => new OpenApiRequestBuilder( + 'get', + "test" + ) +};" +`; + +exports[`api-file creates documentation for the api 1`] = ` +"/** + * Representation of the 'TestApi'. + * This API is part of the 'TestService' service. + */" +`; + +exports[`api-file serializes api file with multiple operations and references 1`] = ` +"import { OpenApiRequestBuilder } from '@sap-cloud-sdk/openapi'; +import type { QueryParameterType, RefType, ResponseType } from './schema'; +/** + * Representation of the 'TestApi'. + * This API is part of the 'MyServiceName' service. + */ +export const TestApi = { + /** + * Create a request builder for execution of get requests to the 'test/{id}' endpoint. + * @param id - Path parameter. + * @param queryParameters - Object containing the following keys: queryParam. + * @param headerParameters - Object containing the following keys: headerParam. + * @returns The request builder, use the \`execute()\` method to trigger the request. + */ + getFn: (id: string, queryParameters: {'queryParam': QueryParameterType}, headerParameters?: {'headerParam'?: string}) => new OpenApiRequestBuilder( + 'get', + "test/{id}", + { + pathParameters: { id }, + queryParameters, + headerParameters + } + ), + /** + * Create a request builder for execution of post requests to the 'test' endpoint. + * @param body - Request body. + * @returns The request builder, use the \`execute()\` method to trigger the request. + */ + createFn: (body: RefType) => new OpenApiRequestBuilder( + 'post', + "test", + { + body + } + ) +};" +`; + +exports[`api-file serializes api file with one operation and no references 1`] = ` +"import { OpenApiRequestBuilder } from '@sap-cloud-sdk/openapi'; +/** + * Representation of the 'TestApi'. + * This API is part of the 'MyServiceName' service. + */ +export const TestApi = { + /** + * Create a request builder for execution of get requests to the 'test/{id}' endpoint. + * @param id - Path parameter. + * @returns The request builder, use the \`execute()\` method to trigger the request. + */ + getFn: (id: string) => new OpenApiRequestBuilder( + 'get', + "test/{id}", + { + pathParameters: { id } + } + ) +};" +`; diff --git a/packages/openapi-generator/src/file-serializer/__snapshots__/index-file.spec.ts.snap b/packages/openapi-generator/src/file-serializer/__snapshots__/index-file.spec.ts.snap new file mode 100644 index 0000000000..3a5f8a5c7c --- /dev/null +++ b/packages/openapi-generator/src/file-serializer/__snapshots__/index-file.spec.ts.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`index-file apiIndexFile serializes the api index file following the esm format 1`] = ` +" export * from './test-api.js'; + export * from './default-api.js'; + export * from './schema/index.js';" +`; + +exports[`index-file apiIndexFile serializes the api index file with referenced schemas 1`] = ` +" export * from './test-api'; + export * from './default-api'; + export * from './schema';" +`; + +exports[`index-file apiIndexFile serializes the api index file without referenced schemas 1`] = ` +" export * from './test-api'; + export * from './default-api';" +`; + +exports[`index-file schemaIndexFile serializes the schema index file for schemas following the ESM format 1`] = ` +"export * from './my-schema-1.js'; +export * from './some-other-name.js';" +`; + +exports[`index-file schemaIndexFile serializes the schema index file for schemas in a document 1`] = ` +"export * from './my-schema-1'; +export * from './some-other-name';" +`; diff --git a/packages/openapi-generator/src/file-serializer/__snapshots__/schema-file.spec.ts.snap b/packages/openapi-generator/src/file-serializer/__snapshots__/schema-file.spec.ts.snap new file mode 100644 index 0000000000..97f74c2196 --- /dev/null +++ b/packages/openapi-generator/src/file-serializer/__snapshots__/schema-file.spec.ts.snap @@ -0,0 +1,90 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`schemaFile creates a schema property documentation 1`] = ` +"/** + * My property Description. + */" +`; + +exports[`schemaFile creates schema documentation 1`] = ` +"/** + * Representation of the 'mySchema' schema. + */" +`; + +exports[`schemaFile serializes schema file for schema 1`] = ` +" + /** + * Representation of the 'MySchema' schema. + * @deprecated + */ + export type MySchema = { + /** + * Max Length: 10. + */ + 'string-property': string; + };" +`; + +exports[`schemaFile serializes schema file for schema including ESM references 1`] = ` +" import type { OtherSchema1 } from './other-schema-1.js'; + import type { OtherSchema2 } from './other-schema-2.js'; + /** + * Representation of the 'MySchema' schema. + */ + export type MySchema = { + 'otherSchema1': OtherSchema1; + /** + * Description other Schema 2 + */ + 'otherSchema2': OtherSchema2; + };" +`; + +exports[`schemaFile serializes schema file for schema including not schema 1`] = ` +" + /** + * Representation of the 'MySchema' schema. + */ + export type MySchema = any[];" +`; + +exports[`schemaFile serializes schema file for schema including references 1`] = ` +" import type { OtherSchema1 } from './other-schema-1'; + import type { OtherSchema2 } from './other-schema-2'; + /** + * Representation of the 'MySchema' schema. + */ + export type MySchema = { + 'otherSchema1': OtherSchema1; + /** + * Description other Schema 2 + */ + 'otherSchema2': OtherSchema2; + };" +`; + +exports[`schemaFile serializes schema file without imports for schema including only self reference 1`] = ` +" + /** + * Representation of the 'MySchema' schema. + */ + export type MySchema = { + 'property'?: MySchema; + };" +`; + +exports[`schemaFile serializes simple schema file for schema with description 1`] = ` +" + /** + * Representation of the 'MySchema' schema. + */ + export type MySchema = { + /** + * My description + * Min Length: 2. + */ + 'string-property': string; + 'string-property-no-description': string; + };" +`; diff --git a/packages/openapi-generator/src/file-serializer/api-file.spec.ts b/packages/openapi-generator/src/file-serializer/api-file.spec.ts index d08ea69d8e..059bb82c17 100644 --- a/packages/openapi-generator/src/file-serializer/api-file.spec.ts +++ b/packages/openapi-generator/src/file-serializer/api-file.spec.ts @@ -1,210 +1,147 @@ +import { readPrettierConfig } from '@sap-cloud-sdk/generator-common/internal'; import { OpenApiApi, OpenApiReferenceSchema } from '../openapi-types'; import { apiDocumentation, apiFile } from './api-file'; -describe('apiFile', () => { - it('apiFile serializes api file with one operation and no references', () => { - const api: OpenApiApi = { - name: 'TestApi', - operations: [ +const singleOperationApi: OpenApiApi = { + name: 'TestApi', + operations: [ + { + operationId: 'getFn', + method: 'get', + tags: [], + pathParameters: [ { - operationId: 'getFn', - method: 'get', - tags: [], - pathParameters: [ - { - in: 'path', - name: 'id', - originalName: 'id', - schema: { type: 'string' }, - required: true, - schemaProperties: {} - } - ], - queryParameters: [], - headerParameters: [], - response: { type: 'any' }, - responses: { 200: { description: 'some response description' } }, - pathPattern: 'test/{id}' + in: 'path', + name: 'id', + originalName: 'id', + schema: { type: 'string' }, + required: true, + schemaProperties: {} } - ] - }; - expect(apiFile(api, 'MyServiceName')).toMatchInlineSnapshot(` - "import { OpenApiRequestBuilder } from '@sap-cloud-sdk/openapi'; - /** - * Representation of the 'TestApi'. - * This API is part of the 'MyServiceName' service. - */ - export const TestApi = { - /** - * Create a request builder for execution of get requests to the 'test/{id}' endpoint. - * @param id - Path parameter. - * @returns The request builder, use the \`execute()\` method to trigger the request. - */ - getFn: (id: string) => new OpenApiRequestBuilder( - 'get', - "test/{id}", - { - pathParameters: { id } - } - ) - };" - `); - }); + ], + queryParameters: [], + headerParameters: [], + response: { type: 'any' }, + responses: { 200: { description: 'some response description' } }, + pathPattern: 'test/{id}' + } + ] +}; - it('apiFile serializes api file with multiple operations and references', () => { - const api: OpenApiApi = { - name: 'TestApi', - operations: [ +const multipleOperationApi: OpenApiApi = { + name: 'TestApi', + operations: [ + { + operationId: 'getFn', + method: 'get', + responses: { 200: { description: 'some response description' } }, + tags: [], + pathParameters: [ { - operationId: 'getFn', - method: 'get', - responses: { 200: { description: 'some response description' } }, - tags: [], - pathParameters: [ - { - in: 'path', - name: 'id', - originalName: 'id', - schema: { - $ref: '#/components/schemas/PathParameterType', - schemaName: 'PathParameterType' - } as OpenApiReferenceSchema, - required: true, - schemaProperties: {} - } - ], - queryParameters: [ - { - in: 'query', - name: 'queryParam', - originalName: 'queryParam', - schema: { - $ref: '#/components/schemas/QueryParameterType', - schemaName: 'QueryParameterType' - } as OpenApiReferenceSchema, - required: true, - schemaProperties: {} - } - ], - headerParameters: [ - { - in: 'header', - name: 'headerParam', - originalName: 'headerParam', - schema: { type: 'string' }, - schemaProperties: {} - } - ], - pathPattern: 'test/{id}', - response: { type: 'string' } - }, + in: 'path', + name: 'id', + originalName: 'id', + schema: { + $ref: '#/components/schemas/PathParameterType', + schemaName: 'PathParameterType' + } as OpenApiReferenceSchema, + required: true, + schemaProperties: {} + } + ], + queryParameters: [ { - operationId: 'createFn', - method: 'post', - responses: { 201: { description: 'some response description' } }, - tags: [], - pathParameters: [], - queryParameters: [], - headerParameters: [], - requestBody: { - required: true, - schema: { - $ref: '#/components/schemas/RefType', - schemaName: 'RefType' - } as OpenApiReferenceSchema - }, - pathPattern: 'test', - response: { - $ref: '#/components/schemas/ResponseType', - schemaName: 'ResponseType' - } as OpenApiReferenceSchema + in: 'query', + name: 'queryParam', + originalName: 'queryParam', + schema: { + $ref: '#/components/schemas/QueryParameterType', + schemaName: 'QueryParameterType' + } as OpenApiReferenceSchema, + required: true, + schemaProperties: {} } - ] - }; - expect(apiFile(api, 'MyServiceName')).toMatchInlineSnapshot(` - "import { OpenApiRequestBuilder } from '@sap-cloud-sdk/openapi'; - import type { QueryParameterType, RefType, ResponseType } from './schema'; - /** - * Representation of the 'TestApi'. - * This API is part of the 'MyServiceName' service. - */ - export const TestApi = { - /** - * Create a request builder for execution of get requests to the 'test/{id}' endpoint. - * @param id - Path parameter. - * @param queryParameters - Object containing the following keys: queryParam. - * @param headerParameters - Object containing the following keys: headerParam. - * @returns The request builder, use the \`execute()\` method to trigger the request. - */ - getFn: (id: string, queryParameters: {'queryParam': QueryParameterType}, headerParameters?: {'headerParam'?: string}) => new OpenApiRequestBuilder( - 'get', - "test/{id}", - { - pathParameters: { id }, - queryParameters, - headerParameters - } - ), - /** - * Create a request builder for execution of post requests to the 'test' endpoint. - * @param body - Request body. - * @returns The request builder, use the \`execute()\` method to trigger the request. - */ - createFn: (body: RefType) => new OpenApiRequestBuilder( - 'post', - "test", - { - body - } - ) - };" - `); - }); - - it('creates a api File with documentation', () => { - const api: OpenApiApi = { - name: 'TestApi', - operations: [ + ], + headerParameters: [ { - operationId: 'getFn', - method: 'get', - responses: { 200: { description: 'some response description' } }, - tags: [], - pathParameters: [], - queryParameters: [], - headerParameters: [], - response: { type: 'any' }, - pathPattern: 'test' + in: 'header', + name: 'headerParam', + originalName: 'headerParam', + schema: { type: 'string' }, + schemaProperties: {} } - ] - }; + ], + pathPattern: 'test/{id}', + response: { type: 'string' } + }, + { + operationId: 'createFn', + method: 'post', + responses: { 201: { description: 'some response description' } }, + tags: [], + pathParameters: [], + queryParameters: [], + headerParameters: [], + requestBody: { + required: true, + schema: { + $ref: '#/components/schemas/RefType', + schemaName: 'RefType' + } as OpenApiReferenceSchema + }, + pathPattern: 'test', + response: { + $ref: '#/components/schemas/ResponseType', + schemaName: 'ResponseType' + } as OpenApiReferenceSchema + } + ] +}; + +const docsApi: OpenApiApi = { + name: 'TestApi', + operations: [ + { + operationId: 'getFn', + method: 'get', + responses: { 200: { description: 'some response description' } }, + tags: [], + pathParameters: [], + queryParameters: [], + headerParameters: [], + response: { type: 'any' }, + pathPattern: 'test' + } + ] +}; - expect(apiFile(api, 'TestService')).toMatchInlineSnapshot(` - "import { OpenApiRequestBuilder } from '@sap-cloud-sdk/openapi'; - /** - * Representation of the 'TestApi'. - * This API is part of the 'TestService' service. - */ - export const TestApi = { - /** - * Create a request builder for execution of get requests to the 'test' endpoint. - * @returns The request builder, use the \`execute()\` method to trigger the request. - */ - getFn: () => new OpenApiRequestBuilder( - 'get', - "test" - ) - };" - `); +describe('api-file', () => { + it('serializes api file with one operation and no references', () => { + expect(apiFile(singleOperationApi, 'MyServiceName')).toMatchSnapshot(); + }); + + it('serializes api file with multiple operations and references', async () => { + expect(apiFile(multipleOperationApi, 'MyServiceName')).toMatchSnapshot(); + }); + + it('creates an api file with documentation', () => { + expect(apiFile(docsApi, 'TestService')).toMatchSnapshot(); + }); + + it('creates an api file following the esm pattern', async () => { + const createFileOptions = { + generateESM: true, + overwrite: false, + prettierOptions: await readPrettierConfig(undefined) + }; + expect( + apiFile(multipleOperationApi, 'MyServiceName', createFileOptions) + ).toMatchSnapshot(); }); it('creates documentation for the api', () => { - expect(apiDocumentation({ name: 'TestApi' } as any, 'TestService')) - .toMatchInlineSnapshot(` - "/** - * Representation of the 'TestApi'. - * This API is part of the 'TestService' service. - */" - `); + expect( + apiDocumentation({ name: 'TestApi' } as any, 'TestService') + ).toMatchSnapshot(); }); }); diff --git a/packages/openapi-generator/src/file-serializer/api-file.ts b/packages/openapi-generator/src/file-serializer/api-file.ts index d0c3a0f843..ad9d856a22 100644 --- a/packages/openapi-generator/src/file-serializer/api-file.ts +++ b/packages/openapi-generator/src/file-serializer/api-file.ts @@ -1,7 +1,8 @@ import { codeBlock, documentationBlock, unixEOL } from '@sap-cloud-sdk/util'; import { serializeImports, - Import + Import, + CreateFileOptions } from '@sap-cloud-sdk/generator-common/internal'; import { OpenApiApi, @@ -18,8 +19,12 @@ import { serializeOperation } from './operation'; * @returns The serialized API file contents. * @internal */ -export function apiFile(api: OpenApiApi, serviceName: string): string { - const imports = serializeImports(getImports(api)); +export function apiFile( + api: OpenApiApi, + serviceName: string, + options?: CreateFileOptions +): string { + const imports = serializeImports(getImports(api, options)); const apiDoc = apiDocumentation(api, serviceName); const apiContent = codeBlock` export const ${api.name} = { @@ -55,14 +60,14 @@ function collectRefsFromOperations( ); } -function getImports(api: OpenApiApi): Import[] { +function getImports(api: OpenApiApi, options?: CreateFileOptions): Import[] { const refs = collectRefsFromOperations(api.operations).map( requestBodyType => requestBodyType.schemaName ); const refImports = { names: refs, - moduleIdentifier: './schema', + moduleIdentifier: options?.generateESM ? './schema/index.js' : './schema', typeOnly: true }; const openApiImports = { diff --git a/packages/openapi-generator/src/file-serializer/index-file.spec.ts b/packages/openapi-generator/src/file-serializer/index-file.spec.ts index 65992c32d2..5af362e7b8 100644 --- a/packages/openapi-generator/src/file-serializer/index-file.spec.ts +++ b/packages/openapi-generator/src/file-serializer/index-file.spec.ts @@ -1,39 +1,68 @@ +import { + CreateFileOptions, + readPrettierConfig +} from '@sap-cloud-sdk/generator-common/internal'; import { OpenApiDocument } from '../openapi-types'; import { apiIndexFile, schemaIndexFile } from './index-file'; -it('apiIndexFile serializes the api index file with referenced schemas', () => { - const document = { - apis: [{ name: 'TestApi' }, { name: 'DefaultApi' }], - schemas: [{}] - } as OpenApiDocument; - expect(apiIndexFile(document)).toMatchInlineSnapshot(` - " export * from './test-api'; - export * from './default-api'; - export * from './schema';" - `); -}); +describe('index-file', () => { + describe('apiIndexFile', () => { + it('serializes the api index file with referenced schemas', () => { + const document = { + apis: [{ name: 'TestApi' }, { name: 'DefaultApi' }], + schemas: [{}] + } as OpenApiDocument; + expect(apiIndexFile(document)).toMatchSnapshot(); + }); -it('apiIndexFile serializes the api index file without referenced schemas', () => { - const document = { - apis: [{ name: 'TestApi' }, { name: 'DefaultApi' }], - schemas: [] - } as unknown as OpenApiDocument; - expect(apiIndexFile(document)).toMatchInlineSnapshot(` - " export * from './test-api'; - export * from './default-api';" - `); -}); + it('serializes the api index file following the esm format', async () => { + const document = { + apis: [{ name: 'TestApi' }, { name: 'DefaultApi' }], + schemas: [{}] + } as OpenApiDocument; + const createFileOptions: CreateFileOptions = { + generateESM: true, + overwrite: false, + prettierOptions: await readPrettierConfig(undefined) + }; + expect(apiIndexFile(document, createFileOptions)).toMatchSnapshot(); + }); + + it('serializes the api index file without referenced schemas', () => { + const document = { + apis: [{ name: 'TestApi' }, { name: 'DefaultApi' }], + schemas: [] + } as unknown as OpenApiDocument; + expect(apiIndexFile(document)).toMatchSnapshot(); + }); + }); + + describe('schemaIndexFile', () => { + it('serializes the schema index file for schemas in a document', () => { + const document = { + apis: [], + schemas: [ + { schemaName: 'MySchema1', fileName: 'my-schema-1' }, + { schemaName: 'MySchema2', fileName: 'some-other-name' } + ] + } as unknown as OpenApiDocument; + expect(schemaIndexFile(document)).toMatchSnapshot(); + }); -it('schemaIndexFile serializes the schema index file for schemas in a document', () => { - const document = { - apis: [], - schemas: [ - { schemaName: 'MySchema1', fileName: 'my-schema-1' }, - { schemaName: 'MySchema2', fileName: 'some-other-name' } - ] - } as unknown as OpenApiDocument; - expect(schemaIndexFile(document)).toMatchInlineSnapshot(` - "export * from './my-schema-1'; - export * from './some-other-name';" - `); + it('serializes the schema index file for schemas following the ESM format', async () => { + const document = { + apis: [], + schemas: [ + { schemaName: 'MySchema1', fileName: 'my-schema-1' }, + { schemaName: 'MySchema2', fileName: 'some-other-name' } + ] + } as unknown as OpenApiDocument; + const createFileOptions: CreateFileOptions = { + generateESM: true, + overwrite: false, + prettierOptions: await readPrettierConfig(undefined) + }; + expect(schemaIndexFile(document, createFileOptions)).toMatchSnapshot(); + }); + }); }); diff --git a/packages/openapi-generator/src/file-serializer/index-file.ts b/packages/openapi-generator/src/file-serializer/index-file.ts index 15c6dc6741..4014d36d29 100644 --- a/packages/openapi-generator/src/file-serializer/index-file.ts +++ b/packages/openapi-generator/src/file-serializer/index-file.ts @@ -1,4 +1,5 @@ import { codeBlock, kebabCase } from '@sap-cloud-sdk/util'; +import { CreateFileOptions } from '@sap-cloud-sdk/generator-common/internal'; import { OpenApiDocument } from '../openapi-types'; /** @@ -7,13 +8,19 @@ import { OpenApiDocument } from '../openapi-types'; * @returns The serialized index file contents. * @internal */ -export function apiIndexFile(openApiDocument: OpenApiDocument): string { +export function apiIndexFile( + openApiDocument: OpenApiDocument, + options?: CreateFileOptions +): string { const files = [ ...openApiDocument.apis.map(api => api.name), ...(openApiDocument.schemas.length ? ['schema'] : []) ]; return codeBlock` - ${exportAllFiles(files.map(fileName => kebabCase(fileName)))} + ${exportAllFiles( + files.map(fileName => kebabCase(fileName)), + options + )} `; } @@ -23,16 +30,29 @@ export function apiIndexFile(openApiDocument: OpenApiDocument): string { * @returns The serialized index file contents. * @internal */ -export function schemaIndexFile(openApiDocument: OpenApiDocument): string { - return exportAllFiles(openApiDocument.schemas.map(schema => schema.fileName)); +export function schemaIndexFile( + openApiDocument: OpenApiDocument, + options?: CreateFileOptions +): string { + return exportAllFiles( + openApiDocument.schemas.map(schema => schema.fileName), + options + ); } -function exportAllFiles(fileNames: string[]): string { +function exportAllFiles( + fileNames: string[], + options?: CreateFileOptions +): string { return codeBlock`${fileNames - .map(fileName => exportAll(fileName)) + .map(fileName => exportAll(fileName, options)) .join('\n')}`; } -function exportAll(file: string) { - return `export * from './${file}';`; +function exportAll(file: string, options?: CreateFileOptions) { + return options?.generateESM + ? file === 'schema' + ? "export * from './schema/index.js';" + : `export * from './${file}.js';` + : `export * from './${file}';`; } diff --git a/packages/openapi-generator/src/file-serializer/schema-file.spec.ts b/packages/openapi-generator/src/file-serializer/schema-file.spec.ts index 69e69d5494..249780cda7 100644 --- a/packages/openapi-generator/src/file-serializer/schema-file.spec.ts +++ b/packages/openapi-generator/src/file-serializer/schema-file.spec.ts @@ -1,201 +1,163 @@ +import { + CreateFileOptions, + readPrettierConfig +} from '@sap-cloud-sdk/generator-common/internal'; import { OpenApiObjectSchemaProperty, OpenApiPersistedSchema } from '../openapi-types'; import { schemaDocumentation, schemaFile } from './schema-file'; import { schemaPropertyDocumentation } from './schema'; -describe('schemaFile', () => { - it('serializes schema file for schema', () => { - expect( - schemaFile({ - schemaName: 'MySchema', - fileName: 'my-schema', + +const schema = { + schemaName: 'MySchema', + fileName: 'my-schema', + schema: { + properties: [ + { + name: 'string-property', + required: true, + schema: { + type: 'string' + }, + schemaProperties: { + maxLength: 10 + } + } + ] + }, + schemaProperties: { + deprecated: true + } +}; + +const schemaWithReferences = { + schemaName: 'MySchema', + fileName: 'my-schema', + schemaProperties: {}, + schema: { + properties: [ + { + name: 'otherSchema1', + required: true, + schema: { + $ref: '#/components/schema/OtherSchema1', + schemaName: 'OtherSchema1', + fileName: 'other-schema-1' + }, + schemaProperties: {} + }, + { + name: 'otherSchema2', + description: 'Description other Schema 2', + required: true, + schema: { + $ref: '#/components/schema/OtherSchema2', + schemaName: 'OtherSchema2', + fileName: 'other-schema-2' + }, + schemaProperties: {} + } + ] + } +}; + +const schemaWithNotSchema = { + schemaName: 'MySchema', + fileName: 'my-schema', + schema: { + items: { not: { type: 'integer' } } + }, + schemaProperties: {} +}; + +const schemaWithoutImportsIncludingOnlySelfReference = { + schemaName: 'MySchema', + fileName: 'my-schema', + schema: { + properties: [ + { + name: 'property', + required: false, + schema: { + $ref: '#/components/schema/MySchema', + schemaName: 'MySchema', + fileName: 'my-schema' + }, + schemaProperties: {} + } + ] + }, + schemaProperties: {} +}; + +const schemaWithDescription = { + schemaName: 'MySchema', + fileName: 'my-schema', + schema: { + properties: [ + { + name: 'string-property', + description: 'My description', + required: true, schema: { - properties: [ - { - name: 'string-property', - required: true, - schema: { - type: 'string' - }, - schemaProperties: { - maxLength: 10 - } - } - ] + type: 'string' }, schemaProperties: { - deprecated: true + minLength: 2 } - }) - ).toMatchInlineSnapshot(` - " - /** - * Representation of the 'MySchema' schema. - * @deprecated - */ - export type MySchema = { - /** - * Max Length: 10. - */ - 'string-property': string; - };" - `); + }, + { + name: 'string-property-no-description', + required: true, + schema: { + type: 'string' + }, + schemaProperties: {} + } + ] + }, + schemaProperties: {} +}; + +describe('schemaFile', () => { + it('serializes schema file for schema', () => { + expect(schemaFile(schema)).toMatchSnapshot(); }); it('serializes schema file for schema including references', () => { + expect(schemaFile(schemaWithReferences)).toMatchSnapshot(); + }); + + it('serializes schema file for schema including ESM references', async () => { + const createFileOptions: CreateFileOptions = { + generateESM: true, + overwrite: false, + prettierOptions: await readPrettierConfig(undefined) + }; expect( - schemaFile({ - schemaName: 'MySchema', - fileName: 'my-schema', - schemaProperties: {}, - schema: { - properties: [ - { - name: 'otherSchema1', - required: true, - schema: { - $ref: '#/components/schema/OtherSchema1', - schemaName: 'OtherSchema1', - fileName: 'other-schema-1' - }, - schemaProperties: {} - }, - { - name: 'otherSchema2', - description: 'Description other Schema 2', - required: true, - schema: { - $ref: '#/components/schema/OtherSchema2', - schemaName: 'OtherSchema2', - fileName: 'other-schema-2' - }, - schemaProperties: {} - } - ] - } - }) - ).toMatchInlineSnapshot(` - " import type { OtherSchema1 } from './other-schema-1'; - import type { OtherSchema2 } from './other-schema-2'; - /** - * Representation of the 'MySchema' schema. - */ - export type MySchema = { - 'otherSchema1': OtherSchema1; - /** - * Description other Schema 2 - */ - 'otherSchema2': OtherSchema2; - };" - `); + schemaFile(schemaWithReferences, createFileOptions) + ).toMatchSnapshot(); }); it('serializes schema file for schema including not schema', () => { - expect( - schemaFile({ - schemaName: 'MySchema', - fileName: 'my-schema', - schema: { - items: { not: { type: 'integer' } } - }, - schemaProperties: {} - }) - ).toMatchInlineSnapshot(` - " - /** - * Representation of the 'MySchema' schema. - */ - export type MySchema = any[];" - `); + expect(schemaFile(schemaWithNotSchema)).toMatchSnapshot(); }); it('serializes schema file without imports for schema including only self reference', () => { expect( - schemaFile({ - schemaName: 'MySchema', - fileName: 'my-schema', - schema: { - properties: [ - { - name: 'property', - required: false, - schema: { - $ref: '#/components/schema/MySchema', - schemaName: 'MySchema', - fileName: 'my-schema' - }, - schemaProperties: {} - } - ] - }, - schemaProperties: {} - }) - ).toMatchInlineSnapshot(` - " - /** - * Representation of the 'MySchema' schema. - */ - export type MySchema = { - 'property'?: MySchema; - };" - `); + schemaFile(schemaWithoutImportsIncludingOnlySelfReference) + ).toMatchSnapshot(); }); it('serializes simple schema file for schema with description', () => { - expect( - schemaFile({ - schemaName: 'MySchema', - fileName: 'my-schema', - schema: { - properties: [ - { - name: 'string-property', - description: 'My description', - required: true, - schema: { - type: 'string' - }, - schemaProperties: { - minLength: 2 - } - }, - { - name: 'string-property-no-description', - required: true, - schema: { - type: 'string' - }, - schemaProperties: {} - } - ] - }, - schemaProperties: {} - }) - ).toMatchInlineSnapshot(` - " - /** - * Representation of the 'MySchema' schema. - */ - export type MySchema = { - /** - * My description - * Min Length: 2. - */ - 'string-property': string; - 'string-property-no-description': string; - };" - `); + expect(schemaFile(schemaWithDescription)).toMatchSnapshot(); }); it('creates schema documentation', () => { expect( schemaDocumentation({ schemaName: 'mySchema' } as OpenApiPersistedSchema) - ).toMatchInlineSnapshot(` - "/** - * Representation of the 'mySchema' schema. - */" - `); + ).toMatchSnapshot(); }); it('uses the schema description documentation if present', () => { @@ -212,10 +174,6 @@ describe('schemaFile', () => { schemaPropertyDocumentation({ description: 'My property Description.' } as OpenApiObjectSchemaProperty) - ).toMatchInlineSnapshot(` - "/** - * My property Description. - */" - `); + ).toMatchSnapshot(); }); }); diff --git a/packages/openapi-generator/src/file-serializer/schema-file.ts b/packages/openapi-generator/src/file-serializer/schema-file.ts index 5a722ed780..be83e233d6 100644 --- a/packages/openapi-generator/src/file-serializer/schema-file.ts +++ b/packages/openapi-generator/src/file-serializer/schema-file.ts @@ -1,7 +1,8 @@ import { codeBlock, documentationBlock, unixEOL } from '@sap-cloud-sdk/util'; import { serializeImports, - Import + Import, + CreateFileOptions } from '@sap-cloud-sdk/generator-common/internal'; import { OpenApiPersistedSchema } from '../openapi-types'; import { collectRefs, getSchemaPropertiesDocumentation } from '../schema-util'; @@ -13,8 +14,11 @@ import { serializeSchema } from './schema'; * @returns The serialized schema file contents. * @internal */ -export function schemaFile(namedSchema: OpenApiPersistedSchema): string { - const imports = serializeImports(getImports(namedSchema)); +export function schemaFile( + namedSchema: OpenApiPersistedSchema, + options?: CreateFileOptions +): string { + const imports = serializeImports(getImports(namedSchema, options)); return codeBlock` ${imports} @@ -25,13 +29,18 @@ export function schemaFile(namedSchema: OpenApiPersistedSchema): string { `; } -function getImports(namedSchema: OpenApiPersistedSchema): Import[] { +function getImports( + namedSchema: OpenApiPersistedSchema, + options?: CreateFileOptions +): Import[] { return collectRefs(namedSchema.schema) .filter(ref => ref.schemaName !== namedSchema.schemaName) .map(ref => ({ names: [ref.schemaName], typeOnly: true, - moduleIdentifier: `./${ref.fileName}` + moduleIdentifier: options?.generateESM + ? `./${ref.fileName}.js` + : `./${ref.fileName}` })); } diff --git a/packages/openapi-generator/src/generator.ts b/packages/openapi-generator/src/generator.ts index 4e97c5e995..5d63405120 100644 --- a/packages/openapi-generator/src/generator.ts +++ b/packages/openapi-generator/src/generator.ts @@ -131,7 +131,8 @@ async function getFileCreationOptions( ): Promise { return { prettierOptions: await readPrettierConfig(options.prettierConfig), - overwrite: options.overwrite + overwrite: options.overwrite, + generateESM: options.generateESM }; } @@ -197,7 +198,7 @@ async function generateMandatorySources( await createFile( schemaDir, 'index.ts', - schemaIndexFile(openApiDocument), + schemaIndexFile(openApiDocument, createFileOptions), createFileOptions ); } @@ -206,7 +207,7 @@ async function generateMandatorySources( await createFile( serviceDir, 'index.ts', - apiIndexFile(openApiDocument), + apiIndexFile(openApiDocument, createFileOptions), createFileOptions ); } @@ -221,7 +222,7 @@ async function createApis( createFile( serviceDir, `${kebabCase(api.name)}.ts`, - apiFile(api, openApiDocument.serviceName), + apiFile(api, openApiDocument.serviceName, options), options ) ) @@ -239,7 +240,7 @@ async function createSchemaFiles( createFile( dir, `${schema.fileName}.ts`, - schemaFile(schema), + schemaFile(schema, createFileOptions), createFileOptions ) ) diff --git a/packages/openapi-generator/src/index.ts b/packages/openapi-generator/src/index.ts index c2746cfa54..988aeb479d 100644 --- a/packages/openapi-generator/src/index.ts +++ b/packages/openapi-generator/src/index.ts @@ -5,4 +5,4 @@ */ export { generate } from './generator'; -export { GeneratorOptions } from './options'; +export { GeneratorOptions, OpenAPIGeneratorOptions } from './options'; diff --git a/packages/openapi-generator/src/options/generator-options.spec.ts b/packages/openapi-generator/src/options/generator-options.spec.ts index f41754b9cb..0a36265772 100644 --- a/packages/openapi-generator/src/options/generator-options.spec.ts +++ b/packages/openapi-generator/src/options/generator-options.spec.ts @@ -36,7 +36,8 @@ describe('parseGeneratorOptions', () => { prettierConfig: undefined, verbose: false, overwrite: false, - config: undefined + config: undefined, + generateESM: false }; it('gets default options', () => { diff --git a/packages/openapi-generator/src/options/options.ts b/packages/openapi-generator/src/options/options.ts index 377da97a95..296890255a 100644 --- a/packages/openapi-generator/src/options/options.ts +++ b/packages/openapi-generator/src/options/options.ts @@ -9,7 +9,17 @@ import { /** * Options to configure OData client generation when using the generator programmatically. */ -export type GeneratorOptions = CommonGeneratorOptions; +export type GeneratorOptions = CommonGeneratorOptions & OpenAPIGeneratorOptions; + +/** + * Options to configure OpenAPI client generation when using the generator programmatically. + */ +export interface OpenAPIGeneratorOptions { + /** + * Whether to generate ECMAScript modules instead of CommonJS modules. + */ + generateESM?: boolean; +} /** * @internal @@ -21,5 +31,11 @@ export type ParsedGeneratorOptions = ParsedOptions; * @internal */ export const cliOptions = { - ...getCommonCliOptions('OpenApi') + ...getCommonCliOptions('OpenApi'), + generateESM: { + describe: + 'When enabled, all generated files follow the ECMAScript module syntax.', + type: 'boolean', + default: false + } } as const satisfies Options;