From 202bc121e288b0616458746a7e94ceaca61b2f22 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 19:31:36 +0000 Subject: [PATCH 1/2] feat(plugin-react-query,plugin-vue-query,plugin-swr): error on a contract client that predates unwrap() Generated hooks now call unwrap() on the client's result, added to plugin-fetch and plugin-axios in 5.2.0. An older client plugin generated a client without unwrap(), so the hooks failed at runtime with no clear cause. resolveContractClient now checks the registered client plugin's installed version and throws a diagnostic naming the required version during setup instead. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018r9FQxbCd8tL7Npvu8khvL --- .changeset/contract-client-version-check.md | 16 +++++ internals/client/src/index.ts | 1 + internals/client/src/resolveClient.test.ts | 60 ++++++++++++++++++- internals/client/src/resolveClient.ts | 38 +++++++++++- .../client/src/resolvePackageVersion.test.ts | 52 ++++++++++++++++ internals/client/src/resolvePackageVersion.ts | 35 +++++++++++ packages/plugin-mcp/src/plugin.ts | 2 +- packages/plugin-react-query/src/plugin.ts | 2 +- packages/plugin-swr/src/plugin.ts | 2 +- packages/plugin-vue-query/src/plugin.ts | 2 +- 10 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 .changeset/contract-client-version-check.md create mode 100644 internals/client/src/resolvePackageVersion.test.ts create mode 100644 internals/client/src/resolvePackageVersion.ts diff --git a/.changeset/contract-client-version-check.md b/.changeset/contract-client-version-check.md new file mode 100644 index 000000000..96ea3ff7b --- /dev/null +++ b/.changeset/contract-client-version-check.md @@ -0,0 +1,16 @@ +--- +'@kubb/plugin-react-query': patch +'@kubb/plugin-vue-query': patch +'@kubb/plugin-swr': patch +--- + +Setup now throws a clear error when the registered `@kubb/plugin-fetch` or `@kubb/plugin-axios` +predates the `unwrap()` method these hooks call, instead of generating hooks that fail at runtime. + +``` +`@kubb/plugin-fetch` is registered at version 5.1.2, but this plugin needs `@kubb/plugin-fetch@5.2.0` or newer. +Generated hooks call `unwrap()` on the client's result, which `@kubb/plugin-fetch` only added in 5.2.0. +Upgrade `@kubb/plugin-fetch` to 5.2.0 or later. +``` + +Upgrade `@kubb/plugin-fetch` or `@kubb/plugin-axios` to `5.2.0` or later to clear the error. diff --git a/internals/client/src/index.ts b/internals/client/src/index.ts index 04590d99e..577e5ce88 100644 --- a/internals/client/src/index.ts +++ b/internals/client/src/index.ts @@ -22,5 +22,6 @@ export { resolveClientOperation } from './resolveClientOperation.ts' export type { ClientOperation } from './resolveClientOperation.ts' export { resolveOperationTypes } from './resolveOperationTypes.ts' export type { OperationTypeNames, OperationTypeSource } from './resolveOperationTypes.ts' +export { isVersionAtLeast, resolvePackageVersion } from './resolvePackageVersion.ts' export { resolverClient } from './resolver.ts' export type { ContractClientFactory, Mode, Options, ValidatorOptions, ResolvedOptions, ResolverClient } from './types.ts' diff --git a/internals/client/src/resolveClient.test.ts b/internals/client/src/resolveClient.test.ts index 989dd3534..53d0c6f4a 100644 --- a/internals/client/src/resolveClient.test.ts +++ b/internals/client/src/resolveClient.test.ts @@ -1,5 +1,8 @@ -import { describe, expect, test } from 'vitest' -import { resolveClient } from './resolveClient.ts' +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { afterEach, beforeEach, describe, expect, test } from 'vitest' +import { resolveClient, resolveContractClient } from './resolveClient.ts' describe('resolveClient', () => { test("client: 'fetch' selects plugin-fetch when it is registered", () => { @@ -36,3 +39,56 @@ describe('resolveClient', () => { } }) }) + +describe('resolveContractClient', () => { + let root: string + + function installPackage(name: string, version: string) { + const pkgDir = path.join(root, 'node_modules', ...name.split('/')) + fs.mkdirSync(pkgDir, { recursive: true }) + fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name, version })) + } + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'kubb-resolve-contract-client-')) + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'fixture', version: '0.0.0' })) + }) + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }) + }) + + test('a consumer without requireUnwrap ignores an old contract client version', () => { + installPackage('@kubb/plugin-fetch', '5.1.2') + + expect(resolveContractClient({ client: 'fetch', plugins: [{ name: 'plugin-fetch' }], root })).toStrictEqual({ + kind: 'contract', + pluginName: 'plugin-fetch', + }) + }) + + test('requireUnwrap throws when plugin-fetch predates unwrap()', () => { + installPackage('@kubb/plugin-fetch', '5.1.2') + + expect(() => resolveContractClient({ client: 'fetch', plugins: [{ name: 'plugin-fetch' }], root, requireUnwrap: true })).toThrowError( + /@kubb\/plugin-fetch.*5\.1\.2.*@kubb\/plugin-fetch@5\.2\.0/s, + ) + }) + + test('requireUnwrap throws when plugin-axios predates unwrap()', () => { + installPackage('@kubb/plugin-axios', '5.1.0') + + expect(() => resolveContractClient({ client: 'axios', plugins: [{ name: 'plugin-axios' }], root, requireUnwrap: true })).toThrowError( + /@kubb\/plugin-axios.*5\.1\.0.*@kubb\/plugin-axios@5\.2\.0/s, + ) + }) + + test('requireUnwrap passes when the contract client is new enough', () => { + installPackage('@kubb/plugin-fetch', '5.2.0') + + expect(resolveContractClient({ client: 'fetch', plugins: [{ name: 'plugin-fetch' }], root, requireUnwrap: true })).toStrictEqual({ + kind: 'contract', + pluginName: 'plugin-fetch', + }) + }) +}) diff --git a/internals/client/src/resolveClient.ts b/internals/client/src/resolveClient.ts index 822b2e121..19dac2f1e 100644 --- a/internals/client/src/resolveClient.ts +++ b/internals/client/src/resolveClient.ts @@ -14,11 +14,22 @@ * contract client plugin is picked up automatically. */ +import { isVersionAtLeast, resolvePackageVersion } from './resolvePackageVersion.ts' + // Canonical plugin names. They mirror the `pluginFetchName` / `pluginAxiosName` consts the plugins // export, kept as literals so this internal needs no plugin install deps. const pluginFetchName = 'plugin-fetch' const pluginAxiosName = 'plugin-axios' +// Consumers (react-query, vue-query, swr) build their generated hooks on `unwrap()`, which the +// contract client plugins only started attaching to their `RequestResult` promise at this version. +// An older client plugin generates a client without `unwrap()`, so the hooks would call a method +// that does not exist at runtime. +const MIN_CONTRACT_CLIENT_VERSION: Record = { + [pluginFetchName]: '5.2.0', + [pluginAxiosName]: '5.2.0', +} + /** * The client selector accepted by a consumer's `client` option. Both call a registered contract * client plugin. @@ -100,13 +111,36 @@ export function resolveClient(options: { client: ClientSelector | undefined; plu * Resolves the contract client during a consumer plugin's setup hook. Extracts the plugin names * from the raw `plugins` config, applies {@link resolveClient}, and throws the diagnostic on a * misconfiguration so every consumer fails fast with the same message. + * + * Pass `requireUnwrap: true` for a consumer (react-query, vue-query, swr) whose generated hooks + * call `unwrap()` on the client's result. This checks the resolved contract client plugin's + * installed version against {@link MIN_CONTRACT_CLIENT_VERSION} and throws when it predates + * `unwrap()`, so the setup fails with a clear diagnostic instead of the generated hooks calling a + * method that does not exist at runtime. */ -export function resolveContractClient(options: { client: ClientSelector | undefined; plugins?: ReadonlyArray }): ResolvedContractClient { - const { client, plugins = [] } = options +export function resolveContractClient(options: { + client: ClientSelector | undefined + plugins?: ReadonlyArray + root: string + requireUnwrap?: boolean +}): ResolvedContractClient { + const { client, plugins = [], root, requireUnwrap = false } = options const pluginNames = plugins.map((plugin) => (plugin as { name?: string }).name).filter((name): name is string => Boolean(name)) const resolved = resolveClient({ client, pluginNames }) if (resolved.kind === 'error') { throw new Error(resolved.message) } + + const minVersion = requireUnwrap ? MIN_CONTRACT_CLIENT_VERSION[resolved.pluginName] : undefined + const packageName = `@kubb/${resolved.pluginName}` + const installedVersion = minVersion ? resolvePackageVersion(packageName, root) : undefined + if (minVersion && installedVersion && !isVersionAtLeast(installedVersion, minVersion)) { + throw new Error( + `\`${packageName}\` is registered at version ${installedVersion}, but this plugin needs \`${packageName}@${minVersion}\` or newer. ` + + `Generated hooks call \`unwrap()\` on the client's result, which \`${packageName}\` only added in ${minVersion}. ` + + `Upgrade \`${packageName}\` to ${minVersion} or later.`, + ) + } + return resolved } diff --git a/internals/client/src/resolvePackageVersion.test.ts b/internals/client/src/resolvePackageVersion.test.ts new file mode 100644 index 000000000..c7744f52b --- /dev/null +++ b/internals/client/src/resolvePackageVersion.test.ts @@ -0,0 +1,52 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { afterEach, beforeEach, describe, expect, test } from 'vitest' +import { isVersionAtLeast, resolvePackageVersion } from './resolvePackageVersion.ts' + +describe('isVersionAtLeast', () => { + test('true when equal', () => { + expect(isVersionAtLeast('5.2.0', '5.2.0')).toBe(true) + }) + + test('true when the patch is newer', () => { + expect(isVersionAtLeast('5.2.1', '5.2.0')).toBe(true) + }) + + test('true when the minor is newer', () => { + expect(isVersionAtLeast('5.3.0', '5.2.0')).toBe(true) + }) + + test('false when older', () => { + expect(isVersionAtLeast('5.1.2', '5.2.0')).toBe(false) + }) + + test('ignores a prerelease suffix', () => { + expect(isVersionAtLeast('5.2.0-beta.1', '5.2.0')).toBe(true) + }) +}) + +describe('resolvePackageVersion', () => { + let root: string + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'kubb-resolve-package-version-')) + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'fixture', version: '0.0.0' })) + }) + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }) + }) + + test('reads the version from an installed package under the given root', () => { + const pkgDir = path.join(root, 'node_modules', '@kubb', 'plugin-fetch') + fs.mkdirSync(pkgDir, { recursive: true }) + fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@kubb/plugin-fetch', version: '5.2.0' })) + + expect(resolvePackageVersion('@kubb/plugin-fetch', root)).toBe('5.2.0') + }) + + test('returns undefined when the package is not installed', () => { + expect(resolvePackageVersion('@kubb-fixture/does-not-exist', root)).toBeUndefined() + }) +}) diff --git a/internals/client/src/resolvePackageVersion.ts b/internals/client/src/resolvePackageVersion.ts new file mode 100644 index 000000000..ccafb61bf --- /dev/null +++ b/internals/client/src/resolvePackageVersion.ts @@ -0,0 +1,35 @@ +import { createRequire } from 'node:module' +import path from 'node:path' + +/** + * Reads the `version` field of an installed package's `package.json`, resolved from the user's + * project root so it reflects what is actually installed rather than a workspace dependency. + * Returns `undefined` when the package cannot be resolved (not installed, or no `version` field). + */ +export function resolvePackageVersion(packageName: string, root: string): string | undefined { + try { + const require = createRequire(path.join(root, 'package.json')) + const pkg = require(`${packageName}/package.json`) as { version?: string } + return pkg.version + } catch { + return undefined + } +} + +/** + * Compares two dot-separated version strings (`major.minor.patch`, prerelease suffixes ignored). + * Missing or non-numeric parts count as `0`. + */ +export function isVersionAtLeast(version: string, minVersion: string): boolean { + const parse = (value: string) => + value + .split('-')[0]! + .split('.') + .map((part) => Number.parseInt(part, 10) || 0) + const [major = 0, minor = 0, patch = 0] = parse(version) + const [minMajor = 0, minMinor = 0, minPatch = 0] = parse(minVersion) + + if (major !== minMajor) return major > minMajor + if (minor !== minMinor) return minor > minMinor + return patch >= minPatch +} diff --git a/packages/plugin-mcp/src/plugin.ts b/packages/plugin-mcp/src/plugin.ts index 88526aa13..94789d432 100644 --- a/packages/plugin-mcp/src/plugin.ts +++ b/packages/plugin-mcp/src/plugin.ts @@ -70,7 +70,7 @@ export const pluginMcp = definePlugin((options) => { include, override, group: groupConfig, - client: resolveContractClient({ client, plugins: ctx.config.plugins }), + client: resolveContractClient({ client, plugins: ctx.config.plugins, root: ctx.config.root }), resolver, }) ctx.setResolver(resolver) diff --git a/packages/plugin-react-query/src/plugin.ts b/packages/plugin-react-query/src/plugin.ts index 29198fe5a..091c4ee8a 100644 --- a/packages/plugin-react-query/src/plugin.ts +++ b/packages/plugin-react-query/src/plugin.ts @@ -88,7 +88,7 @@ export const pluginReactQuery = definePlugin((options) => { ctx.setOptions({ output, - client: resolveContractClient({ client, plugins: ctx.config.plugins }), + client: resolveContractClient({ client, plugins: ctx.config.plugins, root: ctx.config.root, requireUnwrap: true }), queryKey, query: resolveQueryConfig(query, { importPath: '@tanstack/react-query' }), mutationKey, diff --git a/packages/plugin-swr/src/plugin.ts b/packages/plugin-swr/src/plugin.ts index a282a3cd5..347c5a40e 100644 --- a/packages/plugin-swr/src/plugin.ts +++ b/packages/plugin-swr/src/plugin.ts @@ -64,7 +64,7 @@ export const pluginSwr = definePlugin((options) => { ctx.setOptions({ output, - client: resolveContractClient({ client, plugins: ctx.config.plugins }), + client: resolveContractClient({ client, plugins: ctx.config.plugins, root: ctx.config.root, requireUnwrap: true }), queryKey, query: resolveQueryConfig(query, { importPath: 'swr' }), mutationKey, diff --git a/packages/plugin-vue-query/src/plugin.ts b/packages/plugin-vue-query/src/plugin.ts index f6d557ccb..fca986f10 100644 --- a/packages/plugin-vue-query/src/plugin.ts +++ b/packages/plugin-vue-query/src/plugin.ts @@ -69,7 +69,7 @@ export const pluginVueQuery = definePlugin((options) => { ctx.setOptions({ output, - client: resolveContractClient({ client, plugins: ctx.config.plugins }), + client: resolveContractClient({ client, plugins: ctx.config.plugins, root: ctx.config.root, requireUnwrap: true }), queryKey, query: resolveQueryConfig(query, { importPath: '@tanstack/vue-query' }), mutationKey, From 4aeaaac84955fb0b159bbc48622914b91bcc917e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 19:34:12 +0000 Subject: [PATCH 2/2] refactor(internals/client): inline the version check into resolveClient Folds the package-version lookup and comparison directly into resolveClient.ts instead of a separate module, and drops the plugin-mcp change beyond the required root parameter, since it does not call unwrap(). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018r9FQxbCd8tL7Npvu8khvL --- internals/client/src/index.ts | 1 - internals/client/src/resolveClient.ts | 57 ++++++++++++------- .../client/src/resolvePackageVersion.test.ts | 52 ----------------- internals/client/src/resolvePackageVersion.ts | 35 ------------ 4 files changed, 38 insertions(+), 107 deletions(-) delete mode 100644 internals/client/src/resolvePackageVersion.test.ts delete mode 100644 internals/client/src/resolvePackageVersion.ts diff --git a/internals/client/src/index.ts b/internals/client/src/index.ts index 577e5ce88..04590d99e 100644 --- a/internals/client/src/index.ts +++ b/internals/client/src/index.ts @@ -22,6 +22,5 @@ export { resolveClientOperation } from './resolveClientOperation.ts' export type { ClientOperation } from './resolveClientOperation.ts' export { resolveOperationTypes } from './resolveOperationTypes.ts' export type { OperationTypeNames, OperationTypeSource } from './resolveOperationTypes.ts' -export { isVersionAtLeast, resolvePackageVersion } from './resolvePackageVersion.ts' export { resolverClient } from './resolver.ts' export type { ContractClientFactory, Mode, Options, ValidatorOptions, ResolvedOptions, ResolverClient } from './types.ts' diff --git a/internals/client/src/resolveClient.ts b/internals/client/src/resolveClient.ts index 19dac2f1e..a98252dc2 100644 --- a/internals/client/src/resolveClient.ts +++ b/internals/client/src/resolveClient.ts @@ -14,21 +14,19 @@ * contract client plugin is picked up automatically. */ -import { isVersionAtLeast, resolvePackageVersion } from './resolvePackageVersion.ts' +import { createRequire } from 'node:module' +import path from 'node:path' // Canonical plugin names. They mirror the `pluginFetchName` / `pluginAxiosName` consts the plugins // export, kept as literals so this internal needs no plugin install deps. const pluginFetchName = 'plugin-fetch' const pluginAxiosName = 'plugin-axios' -// Consumers (react-query, vue-query, swr) build their generated hooks on `unwrap()`, which the -// contract client plugins only started attaching to their `RequestResult` promise at this version. -// An older client plugin generates a client without `unwrap()`, so the hooks would call a method -// that does not exist at runtime. -const MIN_CONTRACT_CLIENT_VERSION: Record = { - [pluginFetchName]: '5.2.0', - [pluginAxiosName]: '5.2.0', -} +// react-query, vue-query, and swr build their generated hooks on `unwrap()`, which plugin-fetch +// and plugin-axios only started attaching to their result at this version. An older client plugin +// would generate a client without `unwrap()`, so the hooks would call a method that does not +// exist at runtime. +const MIN_CLIENT_VERSION_FOR_UNWRAP = '5.2.0' /** * The client selector accepted by a consumer's `client` option. Both call a registered contract @@ -107,16 +105,29 @@ export function resolveClient(options: { client: ClientSelector | undefined; plu } } +/** + * Compares dot-separated `major.minor.patch` version strings. Missing or non-numeric parts count + * as `0`, and a prerelease suffix (`-beta.1`) is ignored. + */ +function isVersionAtLeast(version: string, minVersion: string): boolean { + const parts = (value: string) => value.split('-')[0]!.split('.').map(Number) + const [major = 0, minor = 0, patch = 0] = parts(version) + const [minMajor = 0, minMinor = 0, minPatch = 0] = parts(minVersion) + + if (major !== minMajor) return major > minMajor + if (minor !== minMinor) return minor > minMinor + return patch >= minPatch +} + /** * Resolves the contract client during a consumer plugin's setup hook. Extracts the plugin names * from the raw `plugins` config, applies {@link resolveClient}, and throws the diagnostic on a * misconfiguration so every consumer fails fast with the same message. * * Pass `requireUnwrap: true` for a consumer (react-query, vue-query, swr) whose generated hooks - * call `unwrap()` on the client's result. This checks the resolved contract client plugin's - * installed version against {@link MIN_CONTRACT_CLIENT_VERSION} and throws when it predates - * `unwrap()`, so the setup fails with a clear diagnostic instead of the generated hooks calling a - * method that does not exist at runtime. + * call `unwrap()` on the client's result. This reads the resolved client plugin's installed + * version from the user's project and throws when it predates `unwrap()`, so setup fails with a + * clear diagnostic instead of the generated hooks calling a method that does not exist. */ export function resolveContractClient(options: { client: ClientSelector | undefined @@ -130,15 +141,23 @@ export function resolveContractClient(options: { if (resolved.kind === 'error') { throw new Error(resolved.message) } + if (!requireUnwrap) { + return resolved + } - const minVersion = requireUnwrap ? MIN_CONTRACT_CLIENT_VERSION[resolved.pluginName] : undefined const packageName = `@kubb/${resolved.pluginName}` - const installedVersion = minVersion ? resolvePackageVersion(packageName, root) : undefined - if (minVersion && installedVersion && !isVersionAtLeast(installedVersion, minVersion)) { + let installedVersion: string | undefined + try { + installedVersion = (createRequire(path.join(root, 'package.json'))(`${packageName}/package.json`) as { version?: string }).version + } catch { + installedVersion = undefined + } + + if (installedVersion && !isVersionAtLeast(installedVersion, MIN_CLIENT_VERSION_FOR_UNWRAP)) { throw new Error( - `\`${packageName}\` is registered at version ${installedVersion}, but this plugin needs \`${packageName}@${minVersion}\` or newer. ` + - `Generated hooks call \`unwrap()\` on the client's result, which \`${packageName}\` only added in ${minVersion}. ` + - `Upgrade \`${packageName}\` to ${minVersion} or later.`, + `\`${packageName}\` is registered at version ${installedVersion}, but this plugin needs \`${packageName}@${MIN_CLIENT_VERSION_FOR_UNWRAP}\` or newer. ` + + `Generated hooks call \`unwrap()\` on the client's result, which \`${packageName}\` only added in ${MIN_CLIENT_VERSION_FOR_UNWRAP}. ` + + `Upgrade \`${packageName}\` to ${MIN_CLIENT_VERSION_FOR_UNWRAP} or later.`, ) } diff --git a/internals/client/src/resolvePackageVersion.test.ts b/internals/client/src/resolvePackageVersion.test.ts deleted file mode 100644 index c7744f52b..000000000 --- a/internals/client/src/resolvePackageVersion.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import fs from 'node:fs' -import os from 'node:os' -import path from 'node:path' -import { afterEach, beforeEach, describe, expect, test } from 'vitest' -import { isVersionAtLeast, resolvePackageVersion } from './resolvePackageVersion.ts' - -describe('isVersionAtLeast', () => { - test('true when equal', () => { - expect(isVersionAtLeast('5.2.0', '5.2.0')).toBe(true) - }) - - test('true when the patch is newer', () => { - expect(isVersionAtLeast('5.2.1', '5.2.0')).toBe(true) - }) - - test('true when the minor is newer', () => { - expect(isVersionAtLeast('5.3.0', '5.2.0')).toBe(true) - }) - - test('false when older', () => { - expect(isVersionAtLeast('5.1.2', '5.2.0')).toBe(false) - }) - - test('ignores a prerelease suffix', () => { - expect(isVersionAtLeast('5.2.0-beta.1', '5.2.0')).toBe(true) - }) -}) - -describe('resolvePackageVersion', () => { - let root: string - - beforeEach(() => { - root = fs.mkdtempSync(path.join(os.tmpdir(), 'kubb-resolve-package-version-')) - fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'fixture', version: '0.0.0' })) - }) - - afterEach(() => { - fs.rmSync(root, { recursive: true, force: true }) - }) - - test('reads the version from an installed package under the given root', () => { - const pkgDir = path.join(root, 'node_modules', '@kubb', 'plugin-fetch') - fs.mkdirSync(pkgDir, { recursive: true }) - fs.writeFileSync(path.join(pkgDir, 'package.json'), JSON.stringify({ name: '@kubb/plugin-fetch', version: '5.2.0' })) - - expect(resolvePackageVersion('@kubb/plugin-fetch', root)).toBe('5.2.0') - }) - - test('returns undefined when the package is not installed', () => { - expect(resolvePackageVersion('@kubb-fixture/does-not-exist', root)).toBeUndefined() - }) -}) diff --git a/internals/client/src/resolvePackageVersion.ts b/internals/client/src/resolvePackageVersion.ts deleted file mode 100644 index ccafb61bf..000000000 --- a/internals/client/src/resolvePackageVersion.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { createRequire } from 'node:module' -import path from 'node:path' - -/** - * Reads the `version` field of an installed package's `package.json`, resolved from the user's - * project root so it reflects what is actually installed rather than a workspace dependency. - * Returns `undefined` when the package cannot be resolved (not installed, or no `version` field). - */ -export function resolvePackageVersion(packageName: string, root: string): string | undefined { - try { - const require = createRequire(path.join(root, 'package.json')) - const pkg = require(`${packageName}/package.json`) as { version?: string } - return pkg.version - } catch { - return undefined - } -} - -/** - * Compares two dot-separated version strings (`major.minor.patch`, prerelease suffixes ignored). - * Missing or non-numeric parts count as `0`. - */ -export function isVersionAtLeast(version: string, minVersion: string): boolean { - const parse = (value: string) => - value - .split('-')[0]! - .split('.') - .map((part) => Number.parseInt(part, 10) || 0) - const [major = 0, minor = 0, patch = 0] = parse(version) - const [minMajor = 0, minMinor = 0, minPatch = 0] = parse(minVersion) - - if (major !== minMajor) return major > minMajor - if (minor !== minMinor) return minor > minMinor - return patch >= minPatch -}