Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/react/build-utils/parseVersionRange.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { coerce } from 'semver';

import type { VersionBounds } from '@clerk/shared/versionCheck';
import { coerce } from 'semver';

export type { VersionBounds } from '@clerk/shared/versionCheck';

Expand Down
75 changes: 74 additions & 1 deletion packages/shared/src/__tests__/versionCheck.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { describe, expect, it } from 'vitest';

import { checkVersionAgainstBounds, isVersionCompatible, parseVersion, type VersionBounds } from '../versionCheck';
import {
checkVersionAgainstBounds,
isVersionAtLeast,
isVersionCompatible,
parseVersion,
type VersionBounds,
} from '../versionCheck';

describe('parseVersion', () => {
it('parses standard semver versions', () => {
Expand Down Expand Up @@ -142,3 +148,70 @@ describe('isVersionCompatible', () => {
expect(isVersionCompatible('invalid', bounds)).toBe(false);
});
});

describe('isVersionAtLeast', () => {
describe('returns true when version meets or exceeds minimum', () => {
it('exact match', () => {
expect(isVersionAtLeast('5.100.0', '5.100.0')).toBe(true);
});

it('higher patch', () => {
expect(isVersionAtLeast('5.100.1', '5.100.0')).toBe(true);
});

it('higher minor', () => {
expect(isVersionAtLeast('5.101.0', '5.100.0')).toBe(true);
expect(isVersionAtLeast('5.114.0', '5.100.0')).toBe(true);
});

it('higher major', () => {
expect(isVersionAtLeast('6.0.0', '5.100.0')).toBe(true);
});
});

describe('returns false when version is below minimum', () => {
it('lower patch', () => {
expect(isVersionAtLeast('5.100.0', '5.100.1')).toBe(false);
});

it('lower minor', () => {
expect(isVersionAtLeast('5.99.0', '5.100.0')).toBe(false);
expect(isVersionAtLeast('5.99.999', '5.100.0')).toBe(false);
});

it('lower major', () => {
expect(isVersionAtLeast('4.999.999', '5.100.0')).toBe(false);
});
});

describe('handles pre-release versions', () => {
it('treats pre-release as base version', () => {
expect(isVersionAtLeast('5.100.0-canary.123', '5.100.0')).toBe(true);
expect(isVersionAtLeast('5.114.0-snapshot.456', '5.100.0')).toBe(true);
});

it('compares base versions ignoring pre-release suffix', () => {
expect(isVersionAtLeast('5.99.0-canary.999', '5.100.0')).toBe(false);
});
});

describe('handles edge cases', () => {
it('returns false for null/undefined version', () => {
expect(isVersionAtLeast(null, '5.100.0')).toBe(false);
expect(isVersionAtLeast(undefined, '5.100.0')).toBe(false);
});

it('returns false for empty string', () => {
expect(isVersionAtLeast('', '5.100.0')).toBe(false);
});

it('returns false for invalid version string', () => {
expect(isVersionAtLeast('invalid', '5.100.0')).toBe(false);
expect(isVersionAtLeast('5.100', '5.100.0')).toBe(false);
});

it('returns false if minVersion cannot be parsed', () => {
expect(isVersionAtLeast('5.100.0', 'invalid')).toBe(false);
});
});
});
38 changes: 38 additions & 0 deletions packages/shared/src/versionCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ export function parseVersion(version: string): { major: number; minor: number; p
* Checks if a parsed version satisfies the given version bounds.
*
* @param version - The parsed version to check
* @param version.major
* @param bounds - Array of version bounds to check against
* @param version.minor
* @param version.patch
* @returns true if the version satisfies any of the bounds
*/
export function checkVersionAgainstBounds(
Expand Down Expand Up @@ -69,3 +72,38 @@ export function isVersionCompatible(version: string, bounds: VersionBounds[]): b
}
return checkVersionAgainstBounds(parsed, bounds);
}

/**
* Returns true if the given version is at least the minimum version.
* Both versions are compared by their major.minor.patch components only.
* Pre-release suffixes are ignored (e.g., "5.114.0-canary.123" is treated as "5.114.0").
*
* @param version - The version string to check (e.g., "5.114.0")
* @param minVersion - The minimum required version (e.g., "5.100.0")
* @returns true if version >= minVersion, false otherwise (including if either cannot be parsed)
*
* @example
* isVersionAtLeast("5.114.0", "5.100.0") // true
* isVersionAtLeast("5.99.0", "5.100.0") // false
* isVersionAtLeast("5.100.0-canary.123", "5.100.0") // true
*/
export function isVersionAtLeast(version: string | undefined | null, minVersion: string): boolean {
if (!version) {
return false;
}

const parsed = parseVersion(version);
const minParsed = parseVersion(minVersion);

if (!parsed || !minParsed) {
return false;
}

if (parsed.major !== minParsed.major) {
return parsed.major > minParsed.major;
}
if (parsed.minor !== minParsed.minor) {
return parsed.minor > minParsed.minor;
}
return parsed.patch >= minParsed.patch;
}
31 changes: 31 additions & 0 deletions packages/ui/src/ClerkUi.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ClerkRuntimeError } from '@clerk/shared/error';
import { logger } from '@clerk/shared/logger';
import type { ModuleManager } from '@clerk/shared/moduleManager';
import type { Clerk, ClerkOptions, EnvironmentResource } from '@clerk/shared/types';
import type { ClerkUiInstance, ComponentControls as SharedComponentControls } from '@clerk/shared/ui';
import { isVersionAtLeast, parseVersion } from '@clerk/shared/versionCheck';

import { type MountComponentRenderer, mountComponentRenderer } from './Components';
import { MIN_CLERK_JS_VERSION } from './constants';

export class ClerkUi implements ClerkUiInstance {
static version = __PKG_VERSION__;
Expand All @@ -16,6 +20,33 @@ export class ClerkUi implements ClerkUiInstance {
options: ClerkOptions,
moduleManager: ModuleManager,
) {
const clerk = getClerk();
const clerkVersion = clerk?.version;
const isDevelopmentInstance = clerk?.instanceType === 'development';
const parsedVersion = parseVersion(clerkVersion ?? '');

let incompatibilityMessage: string | null = null;

if (parsedVersion && !isVersionAtLeast(clerkVersion, MIN_CLERK_JS_VERSION)) {
incompatibilityMessage =
`@clerk/ui@${ClerkUi.version} requires @clerk/clerk-js@>=${MIN_CLERK_JS_VERSION}, ` +
`but found @clerk/clerk-js@${clerkVersion}. ` +
`Please upgrade @clerk/clerk-js (or your framework SDK) to a compatible version.`;
} else if (!parsedVersion && !moduleManager) {
incompatibilityMessage =
`@clerk/ui@${ClerkUi.version} requires @clerk/clerk-js@>=${MIN_CLERK_JS_VERSION}, ` +
`but found an incompatible version${clerkVersion ? ` (${clerkVersion})` : ''}. ` +
`Please upgrade @clerk/clerk-js (or your framework SDK) to a compatible version.`;
}
Copy link
Member

Choose a reason for hiding this comment

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

Looking at clerk-js, it always instantiates ClerkUi with new ModuleManager(), and clerkVersion comes from the Clerk instance which should always have its version set. Is there a scenario where ClerkUi would be instantiated without a moduleManager or without knowing the clerkVersion? If this is just defensive coding thats fine, but if theres an actual use case Im curious what it is :)


if (incompatibilityMessage) {
if (isDevelopmentInstance) {
logger.warnOnce(incompatibilityMessage);
} else {
throw new ClerkRuntimeError(incompatibilityMessage, { code: 'clerk_ui_version_mismatch' });
}
Copy link
Member

Choose a reason for hiding this comment

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

Should this be the other way around? Typically we want to fail fast in development (so devs catch issues early) but be more lenient in production (to avoid breaking user-facing apps). The current behavior warns in dev (easy to miss) but throws in prod (could cause outages).

}

this.#componentRenderer = mountComponentRenderer(getClerk, getEnvironment, options, moduleManager);
}

Expand Down
Loading