Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
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>
```
2 changes: 1 addition & 1 deletion integration/templates/custom-flows-react-vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ createRoot(document.getElementById('root')!).render(
<div className='flex w-full max-w-sm flex-col gap-6'>
<ClerkProvider
clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL as string}
clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL as string}
clerkUIUrl={import.meta.env.VITE_CLERK_UI_URL as string}
appearance={{
options: {
showOptionalFields: true,
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/expo-web/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function RootLayout() {
routerPush={(to: string) => router.push(to)}
routerReplace={to => router.replace(to)}
clerkJSUrl={process.env.EXPO_PUBLIC_CLERK_JS_URL}
clerkUiUrl={process.env.EXPO_PUBLIC_CLERK_UI_URL}
clerkUIUrl={process.env.EXPO_PUBLIC_CLERK_UI_URL}
appearance={{
options: {
showOptionalFields: true,
Expand Down
6 changes: 2 additions & 4 deletions integration/templates/express-vite/src/client/main.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { Clerk } from '@clerk/clerk-js';
import { ClerkUi } from '@clerk/ui/entry';
import { ui } from '@clerk/ui';

const publishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;

document.addEventListener('DOMContentLoaded', async function () {
const clerk = new Clerk(publishableKey);

await clerk.load({
clerkUiCtor: ClerkUi,
});
await clerk.load({ ui });

if (clerk.isSignedIn) {
document.getElementById('app')!.innerHTML = `
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/react-cra/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ root.render(
<ClerkProvider
publishableKey={process.env.REACT_APP_CLERK_PUBLISHABLE_KEY as string}
clerkJSUrl={process.env.REACT_APP_CLERK_JS as string}
clerkUiUrl={process.env.REACT_APP_CLERK_UI as string}
clerkUIUrl={process.env.REACT_APP_CLERK_UI as string}
appearance={{
options: {
showOptionalFields: true,
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/react-router-library/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ createRoot(document.getElementById('root')!).render(
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
clerkJSUrl={CLERK_JS_URL}
clerkUiUrl={CLERK_UI_URL}
clerkUIUrl={CLERK_UI_URL}
appearance={{
options: {
showOptionalFields: true,
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/react-router-node/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export default function App({ loaderData }: Route.ComponentProps) {
<ClerkProvider
loaderData={loaderData}
clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL}
clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL}
clerkUIUrl={import.meta.env.VITE_CLERK_UI_URL}
appearance={{
options: {
showOptionalFields: true,
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/react-vite/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const Root = () => {
return (
<ClerkProvider
clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL as string}
clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL as string}
clerkUIUrl={import.meta.env.VITE_CLERK_UI_URL as string}
routerPush={(to: string) => navigate(to)}
routerReplace={(to: string) => navigate(to, { replace: true })}
appearance={{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
<body>
<ClerkProvider
clerkJSUrl={import.meta.env.VITE_CLERK_JS_URL}
clerkUiUrl={import.meta.env.VITE_CLERK_UI_URL}
clerkUIUrl={import.meta.env.VITE_CLERK_UI_URL}
appearance={{
options: {
showOptionalFields: true,
Expand Down
2 changes: 1 addition & 1 deletion integration/templates/vue-vite/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const app = createApp(App);
app.use(clerkPlugin, {
publishableKey: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY,
clerkJSUrl: import.meta.env.VITE_CLERK_JS_URL,
clerkUiUrl: import.meta.env.VITE_CLERK_UI_URL,
clerkUIUrl: import.meta.env.VITE_CLERK_UI_URL,
clerkJSVersion: import.meta.env.VITE_CLERK_JS_VERSION,
appearance: {
options: {
Expand Down
1 change: 1 addition & 0 deletions integration/tests/components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('component

const signOut = async ({ app, page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.waitForClerkJsLoaded();
await u.page.evaluate(async () => {
await window.Clerk.signOut();
});
Expand Down
3 changes: 2 additions & 1 deletion packages/astro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,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
4 changes: 2 additions & 2 deletions packages/astro/src/integration/create-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()

// These are not provided when the "bundled" integration is used
const clerkJSUrl = (params as any)?.clerkJSUrl as string | undefined;
const clerkUiUrl = (params as any)?.clerkUiUrl as string | undefined;
const clerkUIUrl = (params as any)?.clerkUIUrl as string | undefined;
const clerkJSVariant = (params as any)?.clerkJSVariant as string | undefined;
const clerkJSVersion = (params as any)?.clerkJSVersion as string | undefined;

Expand Down Expand Up @@ -61,7 +61,7 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
...buildEnvVarFromOption(proxyUrl, 'PUBLIC_CLERK_PROXY_URL'),
...buildEnvVarFromOption(domain, 'PUBLIC_CLERK_DOMAIN'),
...buildEnvVarFromOption(clerkJSUrl, 'PUBLIC_CLERK_JS_URL'),
...buildEnvVarFromOption(clerkUiUrl, 'PUBLIC_CLERK_UI_URL'),
...buildEnvVarFromOption(clerkUIUrl, 'PUBLIC_CLERK_UI_URL'),
...buildEnvVarFromOption(clerkJSVariant, 'PUBLIC_CLERK_JS_VARIANT'),
...buildEnvVarFromOption(clerkJSVersion, 'PUBLIC_CLERK_JS_VERSION'),
},
Expand Down
127 changes: 127 additions & 0 deletions packages/astro/src/internal/__tests__/create-clerk-instance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
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 when options.ui.url is 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;
});

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

// Call createClerkInstance with clerkUIUrl but without ui.url
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('prefers options.ui.url over options.clerkUIUrl when both are 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',
clerkUIUrl: 'https://fallback.example.com/ui.js',
ui: {
version: '1.0.0',
url: 'https://preferred.example.com/ui.js',
} as any,
});

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

it('does not set clerkUIUrl when neither options.ui.url nor options.clerkUIUrl is 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();
});
});
40 changes: 27 additions & 13 deletions packages/astro/src/internal/create-clerk-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
setClerkJsLoadingErrorPackageName,
} from '@clerk/shared/loadClerkJsScript';
import type { ClerkOptions } from '@clerk/shared/types';
import type { ClerkUiConstructor } from '@clerk/shared/ui';
import type { ClerkUIConstructor } from '@clerk/shared/ui';
import type { Ui } from '@clerk/ui/internal';

import { $clerkStore } from '../stores/external';
Expand Down Expand Up @@ -40,7 +40,7 @@ async function createClerkInstanceInternal<TUi extends Ui = Ui>(options?: AstroC
// Both functions return early if the scripts are already loaded
// (e.g., via middleware-injected script tags in the HTML head).
const clerkJsChunk = getClerkJsEntryChunk(options);
const clerkUiCtor = getClerkUiEntryChunk(options);
const ClerkUI = getClerkUiEntryChunk(options);

await clerkJsChunk;

Expand All @@ -58,8 +58,12 @@ async function createClerkInstanceInternal<TUi extends Ui = Ui>(options?: AstroC
routerPush: createNavigationHandler(window.history.pushState.bind(window.history)),
routerReplace: createNavigationHandler(window.history.replaceState.bind(window.history)),
...options,
// Pass the clerk-ui constructor promise to clerk.load()
clerkUiCtor,
// Pass the clerk-ui constructor promise inside ui object
ui: {
version: options?.ui?.version,
url: options?.ui?.url,
ClerkUI,
},
} as unknown as ClerkOptions;

initOptions = clerkOptions;
Expand Down Expand Up @@ -109,23 +113,33 @@ async function getClerkJsEntryChunk<TUi extends Ui = Ui>(options?: AstroClerkCre
}

/**
* Gets the ClerkUI constructor, either from options or by loading the script.
* Returns early if window.__internal_ClerkUiCtor already exists.
* Gets the ClerkUI constructor, either from bundled UI or by loading from CDN.
*/
async function getClerkUiEntryChunk<TUi extends Ui = Ui>(
options?: AstroClerkCreateInstanceParams<TUi>,
): Promise<ClerkUiConstructor> {
if (options?.clerkUiCtor) {
return options.clerkUiCtor;
): Promise<ClerkUIConstructor> {
// Use bundled UI constructor if provided via options.ui.ClerkUI
const bundledClerkUI = (options?.ui as { ClerkUI?: ClerkUIConstructor })?.ClerkUI;
if (bundledClerkUI) {
return bundledClerkUI;
}

await loadClerkUiScript(options);

if (!window.__internal_ClerkUiCtor) {
// Fall back to loading UI from CDN with version pinning from ui.version
await loadClerkUiScript(
options
? {
...options,
clerkUIVersion: options.ui?.version,
clerkUIUrl: options.ui?.url ?? options.clerkUIUrl,
}
: undefined,
);

if (!window.__internal_ClerkUICtor) {
throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.');
}

return window.__internal_ClerkUiCtor;
return window.__internal_ClerkUICtor;
}

export { createClerkInstance, updateClerkOptions };
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ function createInjectionScriptRunner(creator: CreateClerkInstanceInternalFn) {
clientSafeVars = JSON.parse(clientSafeVarsContainer.textContent || '{}');
}

await creator(mergeEnvVarsWithParams({ ...astroClerkOptions, ...clientSafeVars }));
// Pass `ui` separately to avoid TypeScript declaration file issues with the
// branded Ui type that contains an unexported Tags symbol
await creator({
...mergeEnvVarsWithParams({ ...astroClerkOptions, ...clientSafeVars }),
ui: astroClerkOptions?.ui,
});
}

return runner;
Expand Down
7 changes: 5 additions & 2 deletions packages/astro/src/internal/merge-env-vars-with-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish
publishableKey: paramPublishableKey,
telemetry: paramTelemetry,
clerkJSUrl: paramClerkJSUrl,
clerkUiUrl: paramClerkUiUrl,
clerkUIUrl: paramClerkUIUrl,
clerkJSVariant: paramClerkJSVariant,
clerkJSVersion: paramClerkJSVersion,
// Extract `ui` separately to avoid spreading the branded Ui type which contains
// an unexported Tags symbol that breaks TypeScript declaration file generation.
ui: _paramUi,
...rest
} = params || {};

Expand All @@ -28,7 +31,7 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish
proxyUrl: paramProxy || import.meta.env.PUBLIC_CLERK_PROXY_URL,
domain: paramDomain || import.meta.env.PUBLIC_CLERK_DOMAIN,
publishableKey: paramPublishableKey || import.meta.env.PUBLIC_CLERK_PUBLISHABLE_KEY || '',
clerkUiUrl: paramClerkUiUrl || import.meta.env.PUBLIC_CLERK_UI_URL,
clerkUIUrl: paramClerkUIUrl || import.meta.env.PUBLIC_CLERK_UI_URL,
clerkJSUrl: paramClerkJSUrl || import.meta.env.PUBLIC_CLERK_JS_URL,
clerkJSVariant: paramClerkJSVariant || import.meta.env.PUBLIC_CLERK_JS_VARIANT,
clerkJSVersion: paramClerkJSVersion || import.meta.env.PUBLIC_CLERK_JS_VERSION,
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/server/build-clerk-hotload-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function buildClerkHotloadScript(locals: APIContext['locals']) {
publishableKey,
});
const clerkUiScriptSrc = clerkUiScriptUrl({
clerkUiUrl: getSafeEnv(locals).clerkUiUrl,
clerkUIUrl: getSafeEnv(locals).clerkUIUrl,
domain,
proxyUrl,
publishableKey,
Expand Down
Loading
Loading