diff --git a/packages/api/src/codegen/language.ts b/packages/api/src/codegen/codegenerator.ts similarity index 91% rename from packages/api/src/codegen/language.ts rename to packages/api/src/codegen/codegenerator.ts index fe60186d..2d8a52ca 100644 --- a/packages/api/src/codegen/language.ts +++ b/packages/api/src/codegen/codegenerator.ts @@ -16,7 +16,7 @@ export interface InstallerOptions { logger?: (msg: string) => void; } -export default abstract class CodeGeneratorLanguage { +export default abstract class CodeGenerator { spec: Oas; specPath: string; @@ -55,9 +55,9 @@ export default abstract class CodeGeneratorLanguage { } } - abstract generator(): Promise>; + abstract compile(): Promise>; - abstract installer(storage: Storage, opts?: InstallerOptions): Promise; + abstract install(storage: Storage, opts?: InstallerOptions): Promise; hasRequiredPackages() { return Boolean(Object.keys(this.requiredPackages)); diff --git a/packages/api/src/codegen/index.ts b/packages/api/src/codegen/factory.ts similarity index 81% rename from packages/api/src/codegen/index.ts rename to packages/api/src/codegen/factory.ts index 6194d1e6..237ad0ab 100644 --- a/packages/api/src/codegen/index.ts +++ b/packages/api/src/codegen/factory.ts @@ -1,16 +1,16 @@ -import type CodeGeneratorLanguage from './language.js'; +import type CodeGenerator from './codegenerator.js'; import type Oas from 'oas'; -import TSGenerator from './languages/typescript.js'; +import TSGenerator from './languages/typescript/index.js'; export type SupportedLanguages = 'js' | 'js-cjs' | 'js-esm' | 'ts'; -export default function codegen( +export default function codegenFactory( language: SupportedLanguages, spec: Oas, specPath: string, identifier: string, -): CodeGeneratorLanguage { +): CodeGenerator { switch (language) { case 'js': throw new TypeError('An export format of CommonJS or ECMAScript is required for JavaScript compilation.'); diff --git a/packages/api/src/codegen/languages/typescript.ts b/packages/api/src/codegen/languages/typescript/index.ts similarity index 96% rename from packages/api/src/codegen/languages/typescript.ts rename to packages/api/src/codegen/languages/typescript/index.ts index c2ba92db..d22b2a93 100644 --- a/packages/api/src/codegen/languages/typescript.ts +++ b/packages/api/src/codegen/languages/typescript/index.ts @@ -1,5 +1,5 @@ -import type Storage from '../../storage.js'; -import type { InstallerOptions } from '../language.js'; +import type Storage from '../../../storage.js'; +import type { InstallerOptions } from '../../codegenerator.js'; import type Oas from 'oas'; import type Operation from 'oas/operation'; import type { HttpMethods, SchemaObject } from 'oas/rmoas.types'; @@ -21,10 +21,10 @@ import setWith from 'lodash.setwith'; import semver from 'semver'; import { IndentationText, Project, QuoteKind, ScriptTarget, VariableDeclarationKind } from 'ts-morph'; -import logger from '../../logger.js'; -import CodeGeneratorLanguage from '../language.js'; +import logger from '../../../logger.js'; +import CodeGenerator from '../../codegenerator.js'; -import { docblockEscape, generateTypeName, wordWrap } from './typescript/util.js'; +import { docblockEscape, generateTypeName, wordWrap } from './util.js'; export interface TSGeneratorOptions { compilerTarget?: 'cjs' | 'esm'; @@ -45,7 +45,7 @@ interface OperationTypeHousing { }; } -export default class TSGenerator extends CodeGeneratorLanguage { +export default class TSGenerator extends CodeGenerator { project: Project; outputJS: boolean; @@ -129,7 +129,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { this.schemas = {}; } - async installer(storage: Storage, opts: InstallerOptions = {}): Promise { + async install(storage: Storage, opts: InstallerOptions = {}): Promise { const installDir = storage.getIdentifierStorageDir(); const info = this.spec.getDefinition().info; @@ -185,7 +185,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { * Compile the current OpenAPI definition into a TypeScript library. * */ - async generator() { + async compile() { const sdkSource = this.createSourceFile(); if (Object.keys(this.schemas).length) { @@ -289,7 +289,7 @@ export default class TSGenerator extends CodeGeneratorLanguage { * Create our main SDK source file. * */ - createSourceFile() { + private createSourceFile() { const { operations } = this.loadOperationsAndMethods(); const sourceFile = this.project.createSourceFile('index.ts', ''); @@ -450,7 +450,7 @@ sdk.server('https://eu.api.example.com/v14');`), * infrastructure sources its data from. Without this there are no types. * */ - createSchemasFile() { + private createSchemasFile() { const sourceFile = this.project.createSourceFile('schemas.ts', ''); const sortedSchemas = new Map(Array.from(Object.entries(this.schemas)).sort()); @@ -498,7 +498,7 @@ sdk.server('https://eu.api.example.com/v14');`), * * @see {@link https://npm.im/json-schema-to-ts} */ - createTypesFile() { + private createTypesFile() { const sourceFile = this.project.createSourceFile('types.ts', ''); sourceFile.addImportDeclarations([ @@ -513,25 +513,11 @@ sdk.server('https://eu.api.example.com/v14');`), return sourceFile; } - /** - * Add a new JSDoc `@tag` to an existing docblock. - * - */ - static addTagToDocblock(docblock: OptionalKind, tag: OptionalKind) { - const tags = docblock.tags ?? []; - tags.push(tag); - - return { - ...docblock, - tags, - }; - } - /** * Create operation accessors on the SDK. * */ - createOperationAccessor( + private createOperationAccessor( operation: Operation, operationId: string, paramTypes?: OperationTypeHousing['types']['params'], @@ -556,7 +542,7 @@ sdk.server('https://eu.api.example.com/v14');`), }; if (summary && description) { - docblock = TSGenerator.addTagToDocblock(docblock, { + docblock = TSGenerator.#addTagToDocblock(docblock, { tagName: 'summary', text: docblockEscape(wordWrap(summary)), }); @@ -609,7 +595,7 @@ sdk.server('https://eu.api.example.com/v14');`), } if (Number(statusPrefix) >= 4) { - docblock = TSGenerator.addTagToDocblock(docblock, { + docblock = TSGenerator.#addTagToDocblock(docblock, { tagName: 'throws', text: `FetchError<${status}, ${responseType}>${ responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : '' @@ -626,7 +612,7 @@ sdk.server('https://eu.api.example.com/v14');`), // 400 and 500 status code families are thrown as exceptions so adding them as a possible // return type isn't valid. if (Number(status) >= 400) { - docblock = TSGenerator.addTagToDocblock(docblock, { + docblock = TSGenerator.#addTagToDocblock(docblock, { tagName: 'throws', text: `FetchError<${status}, ${responseType}>${ responseDescription ? docblockEscape(wordWrap(` ${responseDescription}`)) : '' @@ -736,7 +722,7 @@ sdk.server('https://eu.api.example.com/v14');`), * along with every HTTP method that's in use. * */ - loadOperationsAndMethods() { + private loadOperationsAndMethods() { const operations: Record = {}; const methods = new Set(); @@ -777,7 +763,7 @@ sdk.server('https://eu.api.example.com/v14');`), * usable TypeScript types. * */ - prepareParameterTypesForOperation(operation: Operation, operationId: string) { + private prepareParameterTypesForOperation(operation: Operation, operationId: string) { const schemas = operation.getParametersAsJSONSchema({ includeDiscriminatorMappingRefs: false, mergeIntoBodyAndMetadata: true, @@ -830,7 +816,7 @@ sdk.server('https://eu.api.example.com/v14');`), * Compile the response schemas for an API operation into usable TypeScript types. * */ - prepareResponseTypesForOperation(operation: Operation, operationId: string) { + private prepareResponseTypesForOperation(operation: Operation, operationId: string) { const responseStatusCodes = operation.getResponseStatusCodes(); if (!responseStatusCodes.length) { return undefined; @@ -899,7 +885,7 @@ sdk.server('https://eu.api.example.com/v14');`), * Add a given schema into our schema dataset that we'll be be exporting as types. * */ - addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) { + private addSchemaToExport(schema: SchemaObject, typeName: string, pointer: string) { if (this.types.has(typeName)) { return; } @@ -907,4 +893,18 @@ sdk.server('https://eu.api.example.com/v14');`), setWith(this.schemas, pointer, schema, Object); this.types.set(typeName, `FromSchema`); } + + /** + * Add a new JSDoc `@tag` to an existing docblock. + * + */ + static #addTagToDocblock(docblock: OptionalKind, tag: OptionalKind) { + const tags = docblock.tags ?? []; + tags.push(tag); + + return { + ...docblock, + tags, + }; + } } diff --git a/packages/api/src/commands/install.ts b/packages/api/src/commands/install.ts index c6233a06..348de8fc 100644 --- a/packages/api/src/commands/install.ts +++ b/packages/api/src/commands/install.ts @@ -1,11 +1,11 @@ -import type { SupportedLanguages } from '../codegen/index.js'; +import type { SupportedLanguages } from '../codegen/factory.js'; import { Command, Option } from 'commander'; import figures from 'figures'; import Oas from 'oas'; import ora from 'ora'; -import codegen from '../codegen/index.js'; +import codegenFactory from '../codegen/factory.js'; import Fetcher from '../fetcher.js'; import promptTerminal from '../lib/prompt.js'; import logger from '../logger.js'; @@ -120,9 +120,9 @@ cmd // @todo look for a prettier config and if we find one ask them if we should use it spinner = ora('Generating your SDK').start(); - const generator = codegen(language, oas, './openapi.json', identifier); + const generator = codegenFactory(language, oas, './openapi.json', identifier); const sdkSource = await generator - .generator() + .compile() .then(res => { spinner.succeed(spinner.text); return res; @@ -170,7 +170,7 @@ cmd spinner = ora('Installing required packages').start(); try { - await generator.installer(storage); + await generator.install(storage); spinner.succeed(spinner.text); } catch (err) { // @todo cleanup installed files diff --git a/packages/api/test/codegen/languages/typescript.test.ts b/packages/api/test/codegen/languages/typescript/index.test.ts similarity index 84% rename from packages/api/test/codegen/languages/typescript.test.ts rename to packages/api/test/codegen/languages/typescript/index.test.ts index c869d1f0..9992be81 100644 --- a/packages/api/test/codegen/languages/typescript.test.ts +++ b/packages/api/test/codegen/languages/typescript/index.test.ts @@ -1,4 +1,4 @@ -import type { TSGeneratorOptions } from '../../../src/codegen/languages/typescript.js'; +import type { TSGeneratorOptions } from '../../../../src/codegen/languages/typescript/index.js'; import fs from 'node:fs/promises'; import path from 'node:path'; @@ -9,9 +9,9 @@ import Oas from 'oas'; import uniqueTempDir from 'unique-temp-dir'; import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; -import TSGenerator from '../../../src/codegen/languages/typescript.js'; -import * as packageInfo from '../../../src/packageInfo.js'; -import Storage from '../../../src/storage.js'; +import TSGenerator from '../../../../src/codegen/languages/typescript/index.js'; +import * as packageInfo from '../../../../src/packageInfo.js'; +import Storage from '../../../../src/storage.js'; function assertSDKFixture(file: string, fixture: string, opts: TSGeneratorOptions = {}) { return async () => { @@ -19,10 +19,10 @@ function assertSDKFixture(file: string, fixture: string, opts: TSGeneratorOption await oas.dereference({ preserveRefAsJSONSchemaTitle: true }); const ts = new TSGenerator(oas, file, fixture, opts); - const actualFiles = await ts.generator(); + const actualFiles = await ts.compile(); // Determine if the generated code matches what we've got in our fixture. - const dir = path.resolve(path.join(__dirname, '..', '..', '__fixtures__', 'sdk', fixture)); + const dir = path.resolve(path.join(__dirname, '..', '..', '..', '__fixtures__', 'sdk', fixture)); let expectedFiles: string[]; try { @@ -62,7 +62,7 @@ function assertSDKFixture(file: string, fixture: string, opts: TSGeneratorOption ); // Make sure that we can load the SDK without any TS compilation errors. - const sdk = await import(`../../__fixtures__/sdk/${fixture}`).then(r => r.default); + const sdk = await import(`../../../__fixtures__/sdk/${fixture}`).then(r => r.default); expect(sdk.constructor.name).toBe('SDK'); }; } @@ -88,7 +88,7 @@ describe('typescript', () => { await storage.load(); const ts = new TSGenerator(oas, './openapi.json', 'petstore', { compilerTarget: 'cjs' }); - await ts.installer(storage, { logger, dryRun: true }); + await ts.install(storage, { logger, dryRun: true }); const pkgJson = await fs .readFile(path.join(storage.getIdentifierStorageDir(), 'package.json'), 'utf-8') @@ -171,7 +171,7 @@ describe('typescript', () => { }); it('should be able to make an API request (TS)', async () => { - const sdk = await import('../../__fixtures__/sdk/simple-ts/index.js').then(r => r.default); + const sdk = await import('../../../__fixtures__/sdk/simple-ts/index.js').then(r => r.default); fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.searchParams); await sdk.findPetsByStatus({ status: ['available'] }).then(({ data, status, headers, res }) => { @@ -183,7 +183,7 @@ describe('typescript', () => { }); it('should be able to make an API request with an `accept` header`', async () => { - const sdk = await import('../../__fixtures__/sdk/simple-ts/index.js').then(r => r.default); + const sdk = await import('../../../__fixtures__/sdk/simple-ts/index.js').then(r => r.default); fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.headers); await sdk @@ -203,20 +203,20 @@ describe('typescript', () => { * * @see {@link https://github.com/readmeio/api/pull/734} */ - it.skip('should be able to make an API request (JS + CommonJS)', async () => { - const sdk = await import('../../__fixtures__/sdk/simple-js-cjs/index.js').then(r => r.default); - fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.searchParams); - - await sdk.findPetsByStatus({ status: ['available'] }).then(({ data, status, headers, res }) => { - expect(data).toBe('/v2/pet/findByStatus?status=available'); - expect(status).toBe(200); - expect(headers.constructor.name).toBe('Headers'); - expect(res.constructor.name).toBe('Response'); - }); - }); + // it.skip('should be able to make an API request (JS + CommonJS)', async () => { + // const sdk = await import('../../../__fixtures__/sdk/simple-js-cjs/index.js').then(r => r.default); + // fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.searchParams); + + // await sdk.findPetsByStatus({ status: ['available'] }).then(({ data, status, headers, res }) => { + // expect(data).toBe('/v2/pet/findByStatus?status=available'); + // expect(status).toBe(200); + // expect(headers.constructor.name).toBe('Headers'); + // expect(res.constructor.name).toBe('Response'); + // }); + // }); it('should be able to make an API request (JS + ESM)', async () => { - const sdk = await import('../../__fixtures__/sdk/simple-js-esm/index.js').then(r => r.default); + const sdk = await import('../../../__fixtures__/sdk/simple-js-esm/index.js').then(r => r.default); fetchMock.get('http://petstore.swagger.io/v2/pet/findByStatus?status=available', mockResponse.searchParams); await sdk.findPetsByStatus({ status: ['available'] }).then(({ data, status, headers, res }) => { @@ -240,7 +240,7 @@ describe('typescript', () => { }); const ts = new TSGenerator(oas, 'no-paths', './no-paths.json'); - await expect(ts.generator()).rejects.toThrow( + await expect(ts.compile()).rejects.toThrow( 'Sorry, this OpenAPI definition does not have any operation paths to generate an SDK for.', ); }); diff --git a/packages/api/test/codegen/languages/typescript/smoketest.test.ts b/packages/api/test/codegen/languages/typescript/smoketest.test.ts index ef7b0d86..ea955940 100644 --- a/packages/api/test/codegen/languages/typescript/smoketest.test.ts +++ b/packages/api/test/codegen/languages/typescript/smoketest.test.ts @@ -20,7 +20,7 @@ import Oas from 'oas'; import OASNormalize from 'oas-normalize'; import { describe, it, expect } from 'vitest'; -import TSGenerator from '../../../../src/codegen/languages/typescript.js'; +import TSGenerator from '../../../../src/codegen/languages/typescript/index.js'; // These APIs don't have any schemas so they should only be generating an `index.ts`. const APIS_WITHOUT_SCHEMAS = ['poemist.com']; @@ -70,7 +70,7 @@ describe('typescript smoketest', () => { await oas.dereference({ preserveRefAsJSONSchemaTitle: true }); const ts = new TSGenerator(oas, './path/not/needed.json', name); - const res = await ts.generator(); + const res = await ts.compile(); if (APIS_WITHOUT_SCHEMAS.includes(name)) { expect(Object.keys(res)).toStrictEqual(['index.ts']);