Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c5ff0d1
feat(react,vue,astro): Add ui prop for version metadata
jacekradko Jan 23, 2026
d8370e0
chore: add changeset for ui prop
jacekradko Jan 23, 2026
67fdc89
refactor: use ui prop instead of clerkUiCtor in chrome-extension
jacekradko Jan 23, 2026
2399eff
refactor: remove clerkUiCtor from public API, add __internal_forceBun…
jacekradko Jan 23, 2026
89bb1ff
chore: update changeset description
jacekradko Jan 23, 2026
ccac639
refactor: rename clerkUiCtor to ClerkUI and ClerkUiConstructor to Cle…
jacekradko Jan 23, 2026
cf2a354
refactor: rename __internal_ClerkUiCtor to __internal_ClerkUICtor
jacekradko Jan 23, 2026
1d2c8cf
chore: simplify changeset
jacekradko Jan 24, 2026
8f705a8
refactor: rename ui.ctor to ui.ClerkUI
jacekradko Jan 24, 2026
478a0b3
Merge branch 'main' into jrad/ui-prop-cleanup
jacekradko Jan 24, 2026
9517d40
refactor: move ClerkUI inside ui object in ClerkOptions
jacekradko Jan 24, 2026
890cd43
fix: rename ctor to ClerkUI in ui export
jacekradko Jan 24, 2026
7f7665f
chore: fix formatting
jacekradko Jan 24, 2026
a3014de
chore: fix es-ES.ts formatting
jacekradko Jan 24, 2026
9774008
fix(astro): Remove unnecessary eslint-disable directive
jacekradko Jan 24, 2026
e81f20a
test(vue): add unit tests for CDN UI loading with version pinning
jacekradko Jan 24, 2026
d3c96f3
fix(astro): honor bundled UI constructor in getClerkUiEntryChunk
jacekradko Jan 24, 2026
aa0c84b
fix(astro): preserve clerkUiUrl fallback in loadClerkUiScript call
jacekradko Jan 24, 2026
64ff3dc
fix: standardize casing for clerkUIUrl and clerkUIVersion properties
jacekradko Jan 24, 2026
76d7c96
fix(vue): preserve clerkUIUrl fallback when ui.url is not set
jacekradko Jan 24, 2026
c0d3b6f
fix(vue): use type cast for clerkUIUrl/clerkUIVersion fallback
jacekradko Jan 24, 2026
61655b1
fix: update integration templates to use clerkUIUrl casing
jacekradko Jan 24, 2026
978a0d8
Merge branch 'main' into jrad/ui-prop-cleanup
jacekradko Jan 26, 2026
18a45b8
test(react-router): Add ClerkProvider clerkUIUrl prop tests
jacekradko Jan 26, 2026
a9f4cca
fix(react): Use bundled ClerkUI by default, add __internal_preferCDN …
jacekradko Jan 26, 2026
08064ce
fix(react): Await getClerkUiEntryChunk to ensure ClerkUI is resolved
jacekradko Jan 26, 2026
f44ff91
Merge branch 'main' into jrad/ui-prop-cleanup
jacekradko Jan 27, 2026
aaecdf7
fix(integration): Wait for Clerk to load before signOut in component …
jacekradko Jan 27, 2026
b9a490f
test(react): Add unit tests for bundled vs CDN UI loading
jacekradko Jan 27, 2026
21895c2
Merge main into jrad/ui-prop-cleanup
jacekradko Jan 30, 2026
68a17c9
fix(vue): Handle ui prop in clerkPlugin and fix test mocks
jacekradko Jan 30, 2026
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
19 changes: 19 additions & 0 deletions .changeset/shiny-owls-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@clerk/ui': minor
'@clerk/react': minor
'@clerk/vue': minor
'@clerk/astro': minor
'@clerk/chrome-extension': minor
'@clerk/shared': minor
---

Add `ui` prop to ClerkProvider for passing `@clerk/ui`

Usage:
```tsx
import { ui } from '@clerk/ui';

<ClerkProvider ui={ui}>
...
</ClerkProvider>
```
3 changes: 2 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@
"lint": "eslint src env.d.ts",
"lint:attw": "attw --pack . --profile esm-only --ignore-rules internal-resolution-error",
"lint:publint": "pnpm copy:components && publint",
"publish:local": "pnpm yalc push --replace --sig"
"publish:local": "pnpm yalc push --replace --sig",
"test": "vitest run"
},
"dependencies": {
"@clerk/backend": "workspace:^",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const mockLoadClerkUIScript = vi.fn();
const mockLoadClerkJSScript = vi.fn();

vi.mock('@clerk/shared/loadClerkJsScript', () => ({
loadClerkJSScript: (...args: unknown[]) => mockLoadClerkJSScript(...args),
loadClerkUIScript: (...args: unknown[]) => mockLoadClerkUIScript(...args),
setClerkJSLoadingErrorPackageName: vi.fn(),
}));

// Mock nanostores
vi.mock('../../stores/external', () => ({
$clerkStore: { notify: vi.fn() },
}));

vi.mock('../../stores/internal', () => ({
$clerk: { get: vi.fn(), set: vi.fn() },
$csrState: { setKey: vi.fn() },
}));

vi.mock('../invoke-clerk-astro-js-functions', () => ({
invokeClerkAstroJSFunctions: vi.fn(),
}));

vi.mock('../mount-clerk-astro-js-components', () => ({
mountAllClerkAstroJSComponents: vi.fn(),
}));

const mockClerkUICtor = vi.fn();

describe('getClerkUIEntryChunk', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.resetModules();
(window as any).__internal_ClerkUICtor = undefined;
(window as any).Clerk = undefined;
});

afterEach(() => {
(window as any).__internal_ClerkUICtor = undefined;
(window as any).Clerk = undefined;
});

it('preserves clerkUIUrl from options', async () => {
mockLoadClerkUIScript.mockImplementation(async () => {
(window as any).__internal_ClerkUICtor = mockClerkUICtor;
return null;
});

mockLoadClerkJSScript.mockImplementation(async () => {
(window as any).Clerk = {
load: vi.fn().mockResolvedValue(undefined),
addListener: vi.fn(),
};
return null;
});

// Dynamically import to get fresh module with mocks
const { createClerkInstance } = await import('../create-clerk-instance');

// Call createClerkInstance with clerkUIUrl
await createClerkInstance({
publishableKey: 'pk_test_xxx',
clerkUIUrl: 'https://custom.selfhosted.example.com/ui.js',
});

expect(mockLoadClerkUIScript).toHaveBeenCalled();
const loadClerkUIScriptCall = mockLoadClerkUIScript.mock.calls[0]?.[0] as Record<string, unknown>;
expect(loadClerkUIScriptCall?.clerkUIUrl).toBe('https://custom.selfhosted.example.com/ui.js');
});

it('does not set clerkUIUrl when not provided', async () => {
mockLoadClerkUIScript.mockImplementation(async () => {
(window as any).__internal_ClerkUICtor = mockClerkUICtor;
return null;
});

mockLoadClerkJSScript.mockImplementation(async () => {
(window as any).Clerk = {
load: vi.fn().mockResolvedValue(undefined),
addListener: vi.fn(),
};
return null;
});

const { createClerkInstance } = await import('../create-clerk-instance');

await createClerkInstance({
publishableKey: 'pk_test_xxx',
});

expect(mockLoadClerkUIScript).toHaveBeenCalled();
const loadClerkUIScriptCall = mockLoadClerkUIScript.mock.calls[0]?.[0] as Record<string, unknown>;
expect(loadClerkUIScriptCall?.clerkUIUrl).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ function createInjectionScriptRunner(creator: CreateClerkInstanceInternalFn) {
clientSafeVars = JSON.parse(clientSafeVarsContainer.textContent || '{}');
}

await creator(mergeEnvVarsWithParams({ ...astroClerkOptions, ...clientSafeVars }));
await creator({
...mergeEnvVarsWithParams({ ...astroClerkOptions, ...clientSafeVars }),
});
}

return runner;
Expand Down
9 changes: 9 additions & 0 deletions packages/astro/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
},
});
6 changes: 6 additions & 0 deletions packages/astro/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { vi } from 'vitest';

import packageJson from './package.json';

vi.stubGlobal('PACKAGE_NAME', packageJson.name);
vi.stubGlobal('PACKAGE_VERSION', packageJson.version);
92 changes: 92 additions & 0 deletions packages/react-router/src/client/__tests__/ClerkProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { render } from '@testing-library/react';
import React from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

const mockClerkProvider = vi.fn(({ children }: { children: React.ReactNode }) => <div>{children}</div>);

vi.mock('@clerk/react', () => ({
ClerkProvider: (props: any) => mockClerkProvider(props),
}));

vi.mock('react-router', () => ({
useNavigate: () => vi.fn(),
useLocation: () => ({ pathname: '/' }),
UNSAFE_DataRouterContext: React.createContext(null),
}));

vi.mock('../../utils/assert', () => ({
assertPublishableKeyInSpaMode: vi.fn(),
assertValidClerkState: vi.fn(),
isSpaMode: () => true,
warnForSsr: vi.fn(),
}));

describe('ClerkProvider clerkUIUrl prop', () => {
beforeEach(() => {
vi.clearAllMocks();
});

afterEach(() => {
vi.clearAllMocks();
});

it('passes clerkUIUrl prop to the underlying ClerkProvider', async () => {
const { ClerkProvider } = await import('../ReactRouterClerkProvider');

render(
<ClerkProvider
publishableKey='pk_test_xxx'
clerkUIUrl='https://custom.clerk.ui/ui.js'
>
<div>Test</div>
</ClerkProvider>,
);

expect(mockClerkProvider).toHaveBeenCalledWith(
expect.objectContaining({
clerkUIUrl: 'https://custom.clerk.ui/ui.js',
}),
);
});

it('passes clerkUIUrl as undefined when not provided', async () => {
const { ClerkProvider } = await import('../ReactRouterClerkProvider');

render(
<ClerkProvider publishableKey='pk_test_xxx'>
<div>Test</div>
</ClerkProvider>,
);

expect(mockClerkProvider).toHaveBeenCalledWith(
expect.objectContaining({
clerkUIUrl: undefined,
}),
);
});

it('passes clerkUIUrl alongside other props', async () => {
const { ClerkProvider } = await import('../ReactRouterClerkProvider');

render(
<ClerkProvider
publishableKey='pk_test_xxx'
clerkUIUrl='https://custom.clerk.ui/ui.js'
clerkJSUrl='https://custom.clerk.js/clerk.js'
signInUrl='/sign-in'
signUpUrl='/sign-up'
>
<div>Test</div>
</ClerkProvider>,
);

expect(mockClerkProvider).toHaveBeenCalledWith(
expect.objectContaining({
clerkUIUrl: 'https://custom.clerk.ui/ui.js',
clerkJSUrl: 'https://custom.clerk.js/clerk.js',
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
}),
);
});
});
5 changes: 4 additions & 1 deletion packages/shared/src/ui/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface ClerkUiInstance {
}

// Constructor type
export interface ClerkUiConstructor {
export interface ClerkUIConstructor {
new (
getClerk: () => Clerk,
getEnvironment: () => EnvironmentResource | null | undefined,
Expand All @@ -41,3 +41,6 @@ export interface ClerkUiConstructor {
}

export type ClerkUi = ClerkUiInstance;

// Alias for compatibility with main branch naming convention
export type ClerkUiConstructor = ClerkUIConstructor;
16 changes: 14 additions & 2 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import type { Ui } from './internal';
import type { Appearance } from './internal/appearance';

import { ClerkUi } from './ClerkUi';

declare const PACKAGE_VERSION: string;

/**
* Default ui object for Clerk UI components
* Tagged with the internal Appearance type for type-safe appearance prop inference
* UI object for Clerk UI components.
* Pass this to ClerkProvider to use the bundled UI.
*
* @example
* ```tsx
* import { ui } from '@clerk/ui';
*
* <ClerkProvider ui={ui}>
* ...
* </ClerkProvider>
* ```
*/
export const ui = {
version: PACKAGE_VERSION,
ClerkUI: ClerkUi,
} as Ui<Appearance>;
Loading
Loading