Skip to content

Commit ab9efa2

Browse files
authored
fix(react): make __internal_* URL props take precedence over bundled constructors (#7919)
1 parent 439365e commit ab9efa2

3 files changed

Lines changed: 96 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/react': patch
3+
---
4+
5+
Fix `__internal_clerkJSUrl` and `__internal_clerkUIUrl` being silently ignored when bundled `Clerk` or `ui.ClerkUI` constructors are provided. Internal URL overrides now take precedence.

packages/react/src/__tests__/isomorphicClerk.test.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { loadClerkJSScript, loadClerkUIScript } from '@clerk/shared/loadClerkJsScript';
12
import type { Resources, UnsubscribeCallback } from '@clerk/shared/types';
2-
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
3+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
34

45
import { IsomorphicClerk } from '../isomorphicClerk';
56

@@ -12,7 +13,9 @@ vi.mock('@clerk/shared/loadClerkJsScript', () => ({
1213
describe('isomorphicClerk', () => {
1314
beforeAll(() => {
1415
vi.useFakeTimers();
16+
});
1517

18+
beforeEach(() => {
1619
// Set up minimal global Clerk objects to prevent errors during initialization
1720
(global as any).Clerk = {
1821
load: vi.fn().mockResolvedValue(undefined),
@@ -21,13 +24,18 @@ describe('isomorphicClerk', () => {
2124
(global as any).__internal_ClerkUICtor = vi.fn();
2225
});
2326

24-
afterAll(() => {
25-
vi.useRealTimers();
27+
afterEach(() => {
28+
vi.mocked(loadClerkJSScript).mockClear();
29+
vi.mocked(loadClerkUIScript).mockClear();
2630
// Clean up globals
2731
delete (global as any).Clerk;
2832
delete (global as any).__internal_ClerkUICtor;
2933
});
3034

35+
afterAll(() => {
36+
vi.useRealTimers();
37+
});
38+
3139
it('instantiates a IsomorphicClerk instance', () => {
3240
expect(() => {
3341
new IsomorphicClerk({ publishableKey: 'pk_test_XXX' });
@@ -117,4 +125,79 @@ describe('isomorphicClerk', () => {
117125
expect(listenerCallHistory).toEqual([]);
118126
expect(listenerCallHistory.length).toBe(0);
119127
});
128+
129+
describe('__internal_* URL precedence', () => {
130+
it('__internal_clerkJSUrl causes script loading even when Clerk prop is provided', async () => {
131+
const mockClerkCtor = vi.fn().mockImplementation(() => ({
132+
load: vi.fn().mockResolvedValue(undefined),
133+
loaded: false,
134+
}));
135+
// Make the mock pass the isConstructor check
136+
mockClerkCtor.prototype = {};
137+
138+
const clerk = new IsomorphicClerk({
139+
publishableKey: 'pk_test_XXX',
140+
Clerk: mockClerkCtor as any,
141+
__internal_clerkJSUrl: 'https://staging.clerk.com/clerk.js',
142+
});
143+
144+
// Trigger loading by accessing the private method
145+
await (clerk as any).getClerkJsEntryChunk();
146+
147+
// Should load from URL, not use the bundled constructor
148+
expect(loadClerkJSScript).toHaveBeenCalled();
149+
expect(mockClerkCtor).not.toHaveBeenCalled();
150+
});
151+
152+
it('__internal_clerkUIUrl causes script loading even when ui.ClerkUI prop is provided', async () => {
153+
const mockClerkUI = vi.fn();
154+
155+
const clerk = new IsomorphicClerk({
156+
publishableKey: 'pk_test_XXX',
157+
ui: { ClerkUI: mockClerkUI } as any,
158+
__internal_clerkUIUrl: 'https://staging.clerk.com/clerk-ui.js',
159+
});
160+
161+
const result = await (clerk as any).getClerkUIEntryChunk();
162+
163+
// Should load from URL, not return the bundled ClerkUI
164+
expect(loadClerkUIScript).toHaveBeenCalled();
165+
expect(result).not.toBe(mockClerkUI);
166+
});
167+
168+
it('Clerk prop is used when no __internal_clerkJSUrl is set', async () => {
169+
const mockInstance = {
170+
load: vi.fn().mockResolvedValue(undefined),
171+
loaded: false,
172+
};
173+
const mockClerkCtor = vi.fn().mockImplementation(() => mockInstance);
174+
mockClerkCtor.prototype = {};
175+
176+
const clerk = new IsomorphicClerk({
177+
publishableKey: 'pk_test_XXX',
178+
Clerk: mockClerkCtor as any,
179+
});
180+
181+
await (clerk as any).getClerkJsEntryChunk();
182+
183+
// Should use the bundled constructor, not load from URL
184+
expect(loadClerkJSScript).not.toHaveBeenCalled();
185+
expect(mockClerkCtor).toHaveBeenCalled();
186+
});
187+
188+
it('ui.ClerkUI is used when no __internal_clerkUIUrl is set', async () => {
189+
const mockClerkUI = vi.fn();
190+
191+
const clerk = new IsomorphicClerk({
192+
publishableKey: 'pk_test_XXX',
193+
ui: { ClerkUI: mockClerkUI } as any,
194+
});
195+
196+
const result = await (clerk as any).getClerkUIEntryChunk();
197+
198+
// Should return the bundled ClerkUI, not load from URL
199+
expect(loadClerkUIScript).not.toHaveBeenCalled();
200+
expect(result).toBe(mockClerkUI);
201+
});
202+
});
120203
});

packages/react/src/isomorphicClerk.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
488488

489489
private async getClerkJsEntryChunk(): Promise<HeadlessBrowserClerk | BrowserClerk> {
490490
// Hotload bundle
491-
if (!this.options.Clerk && !__BUILD_DISABLE_RHC__) {
491+
if ((!this.options.Clerk || this.options.__internal_clerkJSUrl) && !__BUILD_DISABLE_RHC__) {
492492
// the UMD script sets the global.Clerk instance
493493
// we do not want to await here as we
494494
await loadClerkJSScript({
@@ -501,7 +501,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
501501
}
502502

503503
// Otherwise, set global.Clerk to the bundled ctor or instance
504-
if (this.options.Clerk) {
504+
if (this.options.Clerk && !this.options.__internal_clerkJSUrl) {
505505
global.Clerk = isConstructor<BrowserClerkConstructor | HeadlessBrowserClerkConstructor>(this.options.Clerk)
506506
? new this.options.Clerk(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain })
507507
: this.options.Clerk;
@@ -518,12 +518,13 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
518518
private async getClerkUIEntryChunk(): Promise<ClerkUIConstructor | undefined> {
519519
// Support bundled UI via ui.ClerkUI prop
520520
const uiProp = (this.options as { ui?: { __brand?: string; ClerkUI?: ClerkUIConstructor } }).ui;
521-
if (uiProp?.ClerkUI) {
521+
const hasInternalUrl = !!this.options.__internal_clerkUIUrl;
522+
if (uiProp?.ClerkUI && !hasInternalUrl) {
522523
return uiProp.ClerkUI;
523524
}
524525

525526
// Skip CDN prefetch when ui prop is passed (bundled UI) or prefetchUI is false
526-
if (uiProp || this.options.prefetchUI === false) {
527+
if ((uiProp || this.options.prefetchUI === false) && !hasInternalUrl) {
527528
return undefined;
528529
}
529530

0 commit comments

Comments
 (0)