Skip to content

Commit fd69edb

Browse files
authored
feat(ui): enforce minimum clerk-js version compatibility (#7667)
1 parent 6d960a8 commit fd69edb

File tree

8 files changed

+370
-4
lines changed

8 files changed

+370
-4
lines changed

.changeset/ripe-bobcats-smile.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/shared': minor
3+
'@clerk/ui': minor
4+
'@clerk/react': patch
5+
---
6+
7+
Add runtime version check in ClerkUi constructor to detect incompatible @clerk/clerk-js versions

packages/react/build-utils/parseVersionRange.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { coerce } from 'semver';
2-
31
import type { VersionBounds } from '@clerk/shared/versionCheck';
2+
import { coerce } from 'semver';
43

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

packages/shared/src/__tests__/versionCheck.spec.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { checkVersionAgainstBounds, isVersionCompatible, parseVersion, type VersionBounds } from '../versionCheck';
3+
import {
4+
checkVersionAgainstBounds,
5+
isVersionAtLeast,
6+
isVersionCompatible,
7+
parseVersion,
8+
type VersionBounds,
9+
} from '../versionCheck';
410

511
describe('parseVersion', () => {
612
it('parses standard semver versions', () => {
@@ -142,3 +148,70 @@ describe('isVersionCompatible', () => {
142148
expect(isVersionCompatible('invalid', bounds)).toBe(false);
143149
});
144150
});
151+
152+
describe('isVersionAtLeast', () => {
153+
describe('returns true when version meets or exceeds minimum', () => {
154+
it('exact match', () => {
155+
expect(isVersionAtLeast('5.100.0', '5.100.0')).toBe(true);
156+
});
157+
158+
it('higher patch', () => {
159+
expect(isVersionAtLeast('5.100.1', '5.100.0')).toBe(true);
160+
});
161+
162+
it('higher minor', () => {
163+
expect(isVersionAtLeast('5.101.0', '5.100.0')).toBe(true);
164+
expect(isVersionAtLeast('5.114.0', '5.100.0')).toBe(true);
165+
});
166+
167+
it('higher major', () => {
168+
expect(isVersionAtLeast('6.0.0', '5.100.0')).toBe(true);
169+
});
170+
});
171+
172+
describe('returns false when version is below minimum', () => {
173+
it('lower patch', () => {
174+
expect(isVersionAtLeast('5.100.0', '5.100.1')).toBe(false);
175+
});
176+
177+
it('lower minor', () => {
178+
expect(isVersionAtLeast('5.99.0', '5.100.0')).toBe(false);
179+
expect(isVersionAtLeast('5.99.999', '5.100.0')).toBe(false);
180+
});
181+
182+
it('lower major', () => {
183+
expect(isVersionAtLeast('4.999.999', '5.100.0')).toBe(false);
184+
});
185+
});
186+
187+
describe('handles pre-release versions', () => {
188+
it('treats pre-release as base version', () => {
189+
expect(isVersionAtLeast('5.100.0-canary.123', '5.100.0')).toBe(true);
190+
expect(isVersionAtLeast('5.114.0-snapshot.456', '5.100.0')).toBe(true);
191+
});
192+
193+
it('compares base versions ignoring pre-release suffix', () => {
194+
expect(isVersionAtLeast('5.99.0-canary.999', '5.100.0')).toBe(false);
195+
});
196+
});
197+
198+
describe('handles edge cases', () => {
199+
it('returns false for null/undefined version', () => {
200+
expect(isVersionAtLeast(null, '5.100.0')).toBe(false);
201+
expect(isVersionAtLeast(undefined, '5.100.0')).toBe(false);
202+
});
203+
204+
it('returns false for empty string', () => {
205+
expect(isVersionAtLeast('', '5.100.0')).toBe(false);
206+
});
207+
208+
it('returns false for invalid version string', () => {
209+
expect(isVersionAtLeast('invalid', '5.100.0')).toBe(false);
210+
expect(isVersionAtLeast('5.100', '5.100.0')).toBe(false);
211+
});
212+
213+
it('returns false if minVersion cannot be parsed', () => {
214+
expect(isVersionAtLeast('5.100.0', 'invalid')).toBe(false);
215+
});
216+
});
217+
});

packages/shared/src/versionCheck.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ export function parseVersion(version: string): { major: number; minor: number; p
3030
* Checks if a parsed version satisfies the given version bounds.
3131
*
3232
* @param version - The parsed version to check
33+
* @param version.major
3334
* @param bounds - Array of version bounds to check against
35+
* @param version.minor
36+
* @param version.patch
3437
* @returns true if the version satisfies any of the bounds
3538
*/
3639
export function checkVersionAgainstBounds(
@@ -69,3 +72,38 @@ export function isVersionCompatible(version: string, bounds: VersionBounds[]): b
6972
}
7073
return checkVersionAgainstBounds(parsed, bounds);
7174
}
75+
76+
/**
77+
* Returns true if the given version is at least the minimum version.
78+
* Both versions are compared by their major.minor.patch components only.
79+
* Pre-release suffixes are ignored (e.g., "5.114.0-canary.123" is treated as "5.114.0").
80+
*
81+
* @param version - The version string to check (e.g., "5.114.0")
82+
* @param minVersion - The minimum required version (e.g., "5.100.0")
83+
* @returns true if version >= minVersion, false otherwise (including if either cannot be parsed)
84+
*
85+
* @example
86+
* isVersionAtLeast("5.114.0", "5.100.0") // true
87+
* isVersionAtLeast("5.99.0", "5.100.0") // false
88+
* isVersionAtLeast("5.100.0-canary.123", "5.100.0") // true
89+
*/
90+
export function isVersionAtLeast(version: string | undefined | null, minVersion: string): boolean {
91+
if (!version) {
92+
return false;
93+
}
94+
95+
const parsed = parseVersion(version);
96+
const minParsed = parseVersion(minVersion);
97+
98+
if (!parsed || !minParsed) {
99+
return false;
100+
}
101+
102+
if (parsed.major !== minParsed.major) {
103+
return parsed.major > minParsed.major;
104+
}
105+
if (parsed.minor !== minParsed.minor) {
106+
return parsed.minor > minParsed.minor;
107+
}
108+
return parsed.patch >= minParsed.patch;
109+
}

packages/ui/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"files": [
33
{ "path": "./dist/ui.browser.js", "maxSize": "36KB" },
4-
{ "path": "./dist/ui.legacy.browser.js", "maxSize": "74KB" },
4+
{ "path": "./dist/ui.legacy.browser.js", "maxSize": "76KB" },
55
{ "path": "./dist/framework*.js", "maxSize": "44KB" },
66
{ "path": "./dist/vendors*.js", "maxSize": "73KB" },
77
{ "path": "./dist/ui-common*.js", "maxSize": "130KB" },

packages/ui/src/ClerkUi.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
import { ClerkRuntimeError } from '@clerk/shared/error';
2+
import { logger } from '@clerk/shared/logger';
13
import type { ModuleManager } from '@clerk/shared/moduleManager';
24
import type { Clerk, ClerkOptions, EnvironmentResource } from '@clerk/shared/types';
35
import type { ClerkUiInstance, ComponentControls as SharedComponentControls } from '@clerk/shared/ui';
6+
import { isVersionAtLeast, parseVersion } from '@clerk/shared/versionCheck';
47

58
import { type MountComponentRenderer, mountComponentRenderer } from './Components';
9+
import { MIN_CLERK_JS_VERSION } from './constants';
610

711
export class ClerkUi implements ClerkUiInstance {
812
static version = __PKG_VERSION__;
@@ -16,6 +20,33 @@ export class ClerkUi implements ClerkUiInstance {
1620
options: ClerkOptions,
1721
moduleManager: ModuleManager,
1822
) {
23+
const clerk = getClerk();
24+
const clerkVersion = clerk?.version;
25+
const isDevelopmentInstance = clerk?.instanceType === 'development';
26+
const parsedVersion = parseVersion(clerkVersion ?? '');
27+
28+
let incompatibilityMessage: string | null = null;
29+
30+
if (parsedVersion && !isVersionAtLeast(clerkVersion, MIN_CLERK_JS_VERSION)) {
31+
incompatibilityMessage =
32+
`@clerk/ui@${ClerkUi.version} requires @clerk/clerk-js@>=${MIN_CLERK_JS_VERSION}, ` +
33+
`but found @clerk/clerk-js@${clerkVersion}. ` +
34+
`Please upgrade @clerk/clerk-js (or your framework SDK) to a compatible version.`;
35+
} else if (!parsedVersion && !moduleManager) {
36+
incompatibilityMessage =
37+
`@clerk/ui@${ClerkUi.version} requires @clerk/clerk-js@>=${MIN_CLERK_JS_VERSION}, ` +
38+
`but found an incompatible version${clerkVersion ? ` (${clerkVersion})` : ''}. ` +
39+
`Please upgrade @clerk/clerk-js (or your framework SDK) to a compatible version.`;
40+
}
41+
42+
if (incompatibilityMessage) {
43+
if (isDevelopmentInstance) {
44+
throw new ClerkRuntimeError(incompatibilityMessage, { code: 'clerk_ui_version_mismatch' });
45+
} else {
46+
logger.warnOnce(incompatibilityMessage);
47+
}
48+
}
49+
1950
this.#componentRenderer = mountComponentRenderer(getClerk, getEnvironment, options, moduleManager);
2051
}
2152

0 commit comments

Comments
 (0)