From f993763e9181fb74b09532d2003367bdec8838e6 Mon Sep 17 00:00:00 2001 From: Andrii Andreiev Date: Wed, 30 Apr 2025 14:57:11 +0300 Subject: [PATCH 1/6] fix: package manager detecting --- .../src/codegen/languages/typescript/index.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/api/src/codegen/languages/typescript/index.ts b/packages/api/src/codegen/languages/typescript/index.ts index 2b0c8d28..72000f85 100644 --- a/packages/api/src/codegen/languages/typescript/index.ts +++ b/packages/api/src/codegen/languages/typescript/index.ts @@ -18,6 +18,7 @@ import type { Options } from 'tsup'; import type { JsonObject, PackageJson, TsConfigJson } from 'type-fest'; import path from 'node:path'; +import fs from 'node:fs'; import corePkg from '@readme/api-core/package.json' with { type: 'json' }; import { execa } from 'execa'; @@ -81,6 +82,32 @@ function handleExecFailure(err: Error, opts: InstallerOptions = {}) { } async function detectPackageManager(installDir: string) { + // Check for packageManager field in package.json + const packageJsonPath = path.join(installDir, 'package.json'); + if (fs.existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + if (packageJson.packageManager) { + const [pm] = packageJson.packageManager.split('@'); + // If the package manager is `npm` or `yarn` we should use that. + return pm; + } + } catch (err) { + logger(`Failed to read package.json: ${err.message}`); + } + + if (fs.existsSync(path.join(installDir, 'yarn.lock'))) { + return 'yarn'; + } + if (fs.existsSync(path.join(installDir, 'package-lock.json'))) { + return 'npm'; + } + if (fs.existsSync(path.join(installDir, 'pnpm-lock.yaml'))) { + return 'pnpm'; + } + } + + // Fallback to preferredPM const pm = await preferredPM(installDir); if (pm) { return pm.name; From 393202a31a708a856fb7a329c74cec5356820a07 Mon Sep 17 00:00:00 2001 From: Andrii Andreiev Date: Mon, 5 May 2025 15:35:09 +0300 Subject: [PATCH 2/6] fix: change install command based on pm --- .../api/src/codegen/languages/typescript/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/api/src/codegen/languages/typescript/index.ts b/packages/api/src/codegen/languages/typescript/index.ts index 72000f85..1a83cda6 100644 --- a/packages/api/src/codegen/languages/typescript/index.ts +++ b/packages/api/src/codegen/languages/typescript/index.ts @@ -191,7 +191,16 @@ export default class TSGenerator extends CodeGenerator { const installDir = storage.getIdentifierStorageDir(); const packageManager = await detectPackageManager(installDir); - const installCommand = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean); + let installCommand: string[]; + if (packageManager === 'yarn') { + installCommand = ['add', opts.dryRun ? '--dry-run' : ''].filter(Boolean); + } else if (packageManager === 'pnpm') { + installCommand = ['add', opts.dryRun ? '--dry-run' : ''].filter(Boolean); + } else if (packageManager === 'bun') { + installCommand = ['add', opts.dryRun ? '--dry-run' : ''].filter(Boolean); + } else { + installCommand = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean); + } // This will install the installed SDK as a dependency within the current working directory, // adding `@api/` as a dependency there so you can load it with From c8672e8aa9d62acdf337f9e358751d7153a850d5 Mon Sep 17 00:00:00 2001 From: Andrii Andreiev Date: Mon, 5 May 2025 15:56:23 +0300 Subject: [PATCH 3/6] chore: add logging in case info.version missing --- packages/api/src/codegen/languages/typescript/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/api/src/codegen/languages/typescript/index.ts b/packages/api/src/codegen/languages/typescript/index.ts index 1a83cda6..c009ebc4 100644 --- a/packages/api/src/codegen/languages/typescript/index.ts +++ b/packages/api/src/codegen/languages/typescript/index.ts @@ -656,6 +656,7 @@ dist/ // If the version that's in `info.version` isn't compatible with semver NPM won't be able to // handle it properly so we need to fallback to something it can. pkgVersion = semver.coerce('0.0.0') as SemVer; + logger(`Warning: OpenAPI info.version is missing or invalid. Defaulting to ${pkgVersion.version}`); } const tsupOptions: Options = { From fca64854b52dbcd11c5e85c5f00ba68fc0934a18 Mon Sep 17 00:00:00 2001 From: Andrii Andreiev Date: Tue, 6 May 2025 14:38:47 +0300 Subject: [PATCH 4/6] refactor: pm detecting via project dir --- .../src/codegen/languages/typescript/index.ts | 49 +++---------------- packages/api/src/storage.ts | 15 ++++++ 2 files changed, 22 insertions(+), 42 deletions(-) diff --git a/packages/api/src/codegen/languages/typescript/index.ts b/packages/api/src/codegen/languages/typescript/index.ts index c009ebc4..b5474baa 100644 --- a/packages/api/src/codegen/languages/typescript/index.ts +++ b/packages/api/src/codegen/languages/typescript/index.ts @@ -18,7 +18,6 @@ import type { Options } from 'tsup'; import type { JsonObject, PackageJson, TsConfigJson } from 'type-fest'; import path from 'node:path'; -import fs from 'node:fs'; import corePkg from '@readme/api-core/package.json' with { type: 'json' }; import { execa } from 'execa'; @@ -81,34 +80,10 @@ function handleExecFailure(err: Error, opts: InstallerOptions = {}) { throw err; } -async function detectPackageManager(installDir: string) { - // Check for packageManager field in package.json - const packageJsonPath = path.join(installDir, 'package.json'); - if (fs.existsSync(packageJsonPath)) { - try { - const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); - if (packageJson.packageManager) { - const [pm] = packageJson.packageManager.split('@'); - // If the package manager is `npm` or `yarn` we should use that. - return pm; - } - } catch (err) { - logger(`Failed to read package.json: ${err.message}`); - } +async function detectPackageManager() { + const projectDir = Storage.getProjectDir(); - if (fs.existsSync(path.join(installDir, 'yarn.lock'))) { - return 'yarn'; - } - if (fs.existsSync(path.join(installDir, 'package-lock.json'))) { - return 'npm'; - } - if (fs.existsSync(path.join(installDir, 'pnpm-lock.yaml'))) { - return 'pnpm'; - } - } - - // Fallback to preferredPM - const pm = await preferredPM(installDir); + const pm = await preferredPM(projectDir); if (pm) { return pm.name; } @@ -189,18 +164,9 @@ export default class TSGenerator extends CodeGenerator { // eslint-disable-next-line class-methods-use-this async install(storage: Storage, opts: InstallerOptions = {}): Promise { const installDir = storage.getIdentifierStorageDir(); - const packageManager = await detectPackageManager(installDir); - - let installCommand: string[]; - if (packageManager === 'yarn') { - installCommand = ['add', opts.dryRun ? '--dry-run' : ''].filter(Boolean); - } else if (packageManager === 'pnpm') { - installCommand = ['add', opts.dryRun ? '--dry-run' : ''].filter(Boolean); - } else if (packageManager === 'bun') { - installCommand = ['add', opts.dryRun ? '--dry-run' : ''].filter(Boolean); - } else { - installCommand = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean); - } + const packageManager = await detectPackageManager(); + + const installCommand = ['install', '--save', opts.dryRun ? '--dry-run' : ''].filter(Boolean); // This will install the installed SDK as a dependency within the current working directory, // adding `@api/` as a dependency there so you can load it with @@ -225,8 +191,7 @@ export default class TSGenerator extends CodeGenerator { static async uninstall(storage: Storage, opts: InstallerOptions = {}): Promise { const pkgName = storage.getPackageName() as string; - const installDir = storage.getIdentifierStorageDir(); - const packageManager = await detectPackageManager(installDir); + const packageManager = await detectPackageManager(); const args = ['uninstall', pkgName, opts.dryRun ? '--dry-run' : ''].filter(Boolean); return execa(packageManager, args) diff --git a/packages/api/src/storage.ts b/packages/api/src/storage.ts index 7d091a7e..56f6457b 100644 --- a/packages/api/src/storage.ts +++ b/packages/api/src/storage.ts @@ -79,6 +79,21 @@ export default class Storage { fs.mkdirSync(Storage.getAPIsDir(), { recursive: true }); } + /** + * Retrieves the project's root directory path. + * + * If a storage directory has not been explicitly set, this method will default it to + * the current working directory. It then returns the directory name of the storage path. + * + */ + static getProjectDir() { + if (!Storage.dir) { + Storage.setStorageDir(); + } + + return path.dirname(Storage.dir); + } + /** * Reset the state of the entire storage system. * From b06db223ea7fb9d3f102d189dd760cc26bc8f64f Mon Sep 17 00:00:00 2001 From: Andrii Andreiev <129078694+AndriiAndreiev@users.noreply.github.com> Date: Fri, 30 May 2025 17:23:32 +0300 Subject: [PATCH 5/6] chore: remove redundant log --- packages/api/src/codegen/languages/typescript/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/api/src/codegen/languages/typescript/index.ts b/packages/api/src/codegen/languages/typescript/index.ts index b5474baa..af1ab537 100644 --- a/packages/api/src/codegen/languages/typescript/index.ts +++ b/packages/api/src/codegen/languages/typescript/index.ts @@ -621,7 +621,6 @@ dist/ // If the version that's in `info.version` isn't compatible with semver NPM won't be able to // handle it properly so we need to fallback to something it can. pkgVersion = semver.coerce('0.0.0') as SemVer; - logger(`Warning: OpenAPI info.version is missing or invalid. Defaulting to ${pkgVersion.version}`); } const tsupOptions: Options = { From 8903227e778bb16e52464bc0d798e4c6d589178d Mon Sep 17 00:00:00 2001 From: Andrii Andreiev <129078694+AndriiAndreiev@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:20:18 +0300 Subject: [PATCH 6/6] test: getProjectDir tests --- packages/api/test/storage.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/api/test/storage.test.ts b/packages/api/test/storage.test.ts index c9c0989d..7abdc1bc 100644 --- a/packages/api/test/storage.test.ts +++ b/packages/api/test/storage.test.ts @@ -64,6 +64,22 @@ describe('storage', () => { }); }); + describe('#getProjectDir', () => { + it('should return the parent directory of the storage directory', () => { + Storage.setStorageDir(uniqueTempDir()); + const projectDir = Storage.getProjectDir(); + expect(projectDir).toBe(path.dirname(Storage.dir)); + }); + + it('should set and return default storage directory if none is set', () => { + Storage.dir = ''; + const projectDir = Storage.getProjectDir(); + expect(Storage.dir).toBe(path.join(process.cwd(), '.api')); + expect(projectDir).toBe(path.dirname(Storage.dir)); + expect(projectDir).toBe(process.cwd()); + }); + }); + describe('#generateIntegrityHash', () => { it('should generate an integrity hash for an API definition', () => { expect(Storage.generateIntegrityHash(petstoreSimple as OASDocument)).toBe(