diff --git a/.changeset/light-eagles-stay.md b/.changeset/light-eagles-stay.md
new file mode 100644
index 00000000000..69d5328fff0
--- /dev/null
+++ b/.changeset/light-eagles-stay.md
@@ -0,0 +1,15 @@
+---
+'@clerk/clerk-js': major
+'@clerk/shared': major
+'@clerk/react': minor
+'@clerk/nextjs': minor
+'@clerk/astro': minor
+'@clerk/nuxt': minor
+'@clerk/vue': minor
+'@clerk/react-router': minor
+'@clerk/tanstack-react-start': minor
+'@clerk/expo': minor
+'@clerk/chrome-extension': minor
+---
+
+Remove `clerkJSVariant` option and headless bundle. Use `prefetchUI={false}` instead.
diff --git a/integration/templates/custom-flows-react-vite/src/main.tsx b/integration/templates/custom-flows-react-vite/src/main.tsx
index 08bb652e4c2..225a3ddd20f 100644
--- a/integration/templates/custom-flows-react-vite/src/main.tsx
+++ b/integration/templates/custom-flows-react-vite/src/main.tsx
@@ -15,7 +15,7 @@ createRoot(document.getElementById('root')!).render(
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,
diff --git a/integration/templates/express-vite/src/client/main.ts b/integration/templates/express-vite/src/client/main.ts
index bf19f46d7b7..7c46c42a815 100644
--- a/integration/templates/express-vite/src/client/main.ts
+++ b/integration/templates/express-vite/src/client/main.ts
@@ -7,7 +7,7 @@ document.addEventListener('DOMContentLoaded', async function () {
const clerk = new Clerk(publishableKey);
await clerk.load({
- clerkUiCtor: ClerkUi,
+ clerkUICtor: ClerkUi,
});
if (clerk.isSignedIn) {
diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx
index 42341d04adc..f931fe2c271 100644
--- a/integration/templates/next-app-router/src/app/layout.tsx
+++ b/integration/templates/next-app-router/src/app/layout.tsx
@@ -12,6 +12,7 @@ export const metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{
return (
navigate(to)}
routerReplace={(to: string) => navigate(to, { replace: true })}
appearance={{
diff --git a/integration/templates/tanstack-react-start/src/routes/__root.tsx b/integration/templates/tanstack-react-start/src/routes/__root.tsx
index 4dd7cf9d763..b9adc012c75 100644
--- a/integration/templates/tanstack-react-start/src/routes/__root.tsx
+++ b/integration/templates/tanstack-react-start/src/routes/__root.tsx
@@ -29,7 +29,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
{
const u = createTestUtils({ app, page, context });
+ await u.page.waitForClerkJsLoaded();
await u.page.evaluate(async () => {
await window.Clerk.signOut();
});
diff --git a/integration/tests/prefetch-ui.test.ts b/integration/tests/prefetch-ui.test.ts
new file mode 100644
index 00000000000..069a47adfca
--- /dev/null
+++ b/integration/tests/prefetch-ui.test.ts
@@ -0,0 +1,32 @@
+import { expect, test } from '@playwright/test';
+
+import type { Application } from '../models/application';
+import { appConfigs } from '../presets';
+
+test.describe('prefetchUI disabled @nextjs', () => {
+ test.describe.configure({ mode: 'serial' });
+ let app: Application;
+
+ test.beforeAll(async () => {
+ app = await appConfigs.next.appRouter.clone().commit();
+ await app.setup();
+ // Use withEmailCodes but disable the UI prefetching
+ const env = appConfigs.envs.withEmailCodes.clone().setEnvVariable('public', 'CLERK_PREFETCH_UI_DISABLED', 'true');
+ await app.withEnv(env);
+ await app.dev();
+ });
+
+ test.afterAll(async () => {
+ await app.teardown();
+ });
+
+ test('does not inject clerk-ui script when prefetchUI is disabled', async ({ page }) => {
+ await page.goto(app.serverUrl);
+
+ // Wait for clerk-js script to be present (ensures page has loaded)
+ await expect(page.locator('script[data-clerk-js-script]')).toBeAttached();
+
+ // clerk-ui script should NOT be present
+ await expect(page.locator('script[data-clerk-ui-script]')).not.toBeAttached();
+ });
+});
diff --git a/packages/astro/src/env.d.ts b/packages/astro/src/env.d.ts
index 8ead567ea80..1240941e1e2 100644
--- a/packages/astro/src/env.d.ts
+++ b/packages/astro/src/env.d.ts
@@ -4,9 +4,9 @@ interface InternalEnv {
readonly PUBLIC_CLERK_FRONTEND_API?: string;
readonly PUBLIC_CLERK_PUBLISHABLE_KEY?: string;
readonly PUBLIC_CLERK_JS_URL?: string;
- readonly PUBLIC_CLERK_UI_URL?: string;
- readonly PUBLIC_CLERK_JS_VARIANT?: 'headless' | '';
readonly PUBLIC_CLERK_JS_VERSION?: string;
+ readonly PUBLIC_CLERK_UI_URL?: string;
+ readonly PUBLIC_CLERK_PREFETCH_UI?: string;
readonly CLERK_API_KEY?: string;
readonly CLERK_API_URL?: string;
readonly CLERK_API_VERSION?: string;
diff --git a/packages/astro/src/integration/create-integration.ts b/packages/astro/src/integration/create-integration.ts
index e8902271a96..7ac03cd4796 100644
--- a/packages/astro/src/integration/create-integration.ts
+++ b/packages/astro/src/integration/create-integration.ts
@@ -20,9 +20,8 @@ function createIntegration()
// 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 clerkJSVariant = (params as any)?.clerkJSVariant as string | undefined;
const clerkJSVersion = (params as any)?.clerkJSVersion as string | undefined;
+ const prefetchUI = (params as any)?.prefetchUI as boolean | undefined;
return {
name: '@clerk/astro/integration',
@@ -32,10 +31,6 @@ function createIntegration()
logger.error('Missing adapter, please update your Astro config to use one.');
}
- if (typeof clerkJSVariant !== 'undefined' && clerkJSVariant !== 'headless' && clerkJSVariant !== '') {
- logger.error('Invalid value for clerkJSVariant. Acceptable values are `"headless"`, `""`, and `undefined`');
- }
-
const internalParams: ClerkOptions = {
...params,
sdkMetadata: {
@@ -61,9 +56,8 @@ function createIntegration()
...buildEnvVarFromOption(proxyUrl, 'PUBLIC_CLERK_PROXY_URL'),
...buildEnvVarFromOption(domain, 'PUBLIC_CLERK_DOMAIN'),
...buildEnvVarFromOption(clerkJSUrl, 'PUBLIC_CLERK_JS_URL'),
- ...buildEnvVarFromOption(clerkUiUrl, 'PUBLIC_CLERK_UI_URL'),
- ...buildEnvVarFromOption(clerkJSVariant, 'PUBLIC_CLERK_JS_VARIANT'),
...buildEnvVarFromOption(clerkJSVersion, 'PUBLIC_CLERK_JS_VERSION'),
+ ...buildEnvVarFromOption(prefetchUI === false ? 'false' : undefined, 'PUBLIC_CLERK_PREFETCH_UI'),
},
ssr: {
@@ -170,14 +164,10 @@ function createClerkEnvSchema() {
PUBLIC_CLERK_PROXY_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }),
PUBLIC_CLERK_DOMAIN: envField.string({ context: 'client', access: 'public', optional: true, url: true }),
PUBLIC_CLERK_JS_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }),
- PUBLIC_CLERK_UI_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }),
- PUBLIC_CLERK_JS_VARIANT: envField.enum({
- context: 'client',
- access: 'public',
- optional: true,
- values: ['headless'],
- }),
PUBLIC_CLERK_JS_VERSION: envField.string({ context: 'client', access: 'public', optional: true }),
+ PUBLIC_CLERK_PREFETCH_UI: envField.string({ context: 'client', access: 'public', optional: true }),
+ PUBLIC_CLERK_UI_URL: envField.string({ context: 'client', access: 'public', optional: true, url: true }),
+ PUBLIC_CLERK_UI_VERSION: envField.string({ context: 'client', access: 'public', optional: true }),
PUBLIC_CLERK_TELEMETRY_DISABLED: envField.boolean({ context: 'client', access: 'public', optional: true }),
PUBLIC_CLERK_TELEMETRY_DEBUG: envField.boolean({ context: 'client', access: 'public', optional: true }),
CLERK_SECRET_KEY: envField.string({ context: 'server', access: 'secret' }),
diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts
index e1cbd520144..88b5ec25a90 100644
--- a/packages/astro/src/internal/create-clerk-instance.ts
+++ b/packages/astro/src/internal/create-clerk-instance.ts
@@ -1,7 +1,8 @@
import {
- loadClerkJsScript,
- loadClerkUiScript,
- setClerkJsLoadingErrorPackageName,
+ loadClerkJSScript,
+ loadClerkUIScript,
+ setClerkJSLoadingErrorPackageName,
+ shouldPrefetchClerkUI,
} from '@clerk/shared/loadClerkJsScript';
import type { ClerkOptions } from '@clerk/shared/types';
import type { ClerkUiConstructor } from '@clerk/shared/ui';
@@ -16,7 +17,7 @@ import { runOnce } from './run-once';
let initOptions: ClerkOptions | undefined;
-setClerkJsLoadingErrorPackageName(PACKAGE_NAME);
+setClerkJSLoadingErrorPackageName(PACKAGE_NAME);
function createNavigationHandler(
windowNav: typeof window.history.pushState | typeof window.history.replaceState,
@@ -40,7 +41,7 @@ async function createClerkInstanceInternal(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 clerkUICtor = getClerkUIEntryChunk(options);
await clerkJsChunk;
@@ -59,7 +60,7 @@ async function createClerkInstanceInternal(options?: AstroC
routerReplace: createNavigationHandler(window.history.replaceState.bind(window.history)),
...options,
// Pass the clerk-ui constructor promise to clerk.load()
- clerkUiCtor,
+ clerkUICtor,
} as unknown as ClerkOptions;
initOptions = clerkOptions;
@@ -105,21 +106,26 @@ function updateClerkOptions(options: AstroClerkUpdateOption
* Returns early if window.Clerk already exists.
*/
async function getClerkJsEntryChunk(options?: AstroClerkCreateInstanceParams): Promise {
- await loadClerkJsScript(options);
+ await loadClerkJSScript(options);
}
/**
* Gets the ClerkUI constructor, either from options or by loading the script.
* Returns early if window.__internal_ClerkUiCtor already exists.
+ * Returns undefined when prefetchUI={false} (no UI needed).
*/
-async function getClerkUiEntryChunk(
+async function getClerkUIEntryChunk(
options?: AstroClerkCreateInstanceParams,
-): Promise {
- if (options?.clerkUiCtor) {
- return options.clerkUiCtor;
+): Promise {
+ if (!shouldPrefetchClerkUI(options?.prefetchUI)) {
+ return undefined;
}
- await loadClerkUiScript(options);
+ if (options?.clerkUICtor) {
+ return options.clerkUICtor;
+ }
+
+ await loadClerkUIScript(options as any);
if (!window.__internal_ClerkUiCtor) {
throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.');
diff --git a/packages/astro/src/internal/merge-env-vars-with-params.ts b/packages/astro/src/internal/merge-env-vars-with-params.ts
index dea24762b16..1cac532cc6f 100644
--- a/packages/astro/src/internal/merge-env-vars-with-params.ts
+++ b/packages/astro/src/internal/merge-env-vars-with-params.ts
@@ -2,6 +2,26 @@ import { isTruthy } from '@clerk/shared/underscore';
import type { AstroClerkIntegrationParams } from '../types';
+/**
+ * Merges `prefetchUI` param with env vars.
+ * - If param `prefetchUI` is explicitly `false`, return `false`
+ * - If env `PUBLIC_CLERK_PREFETCH_UI` is "false", return `false`
+ * - Otherwise return `undefined` (default behavior: prefetch UI)
+ */
+function mergePrefetchUIConfig(paramPrefetchUI: AstroClerkIntegrationParams['prefetchUI']): boolean | undefined {
+ // Explicit false from param takes precedence
+ if (paramPrefetchUI === false) {
+ return false;
+ }
+
+ // Check env var
+ if (import.meta.env.PUBLIC_CLERK_PREFETCH_UI === 'false') {
+ return false;
+ }
+
+ return undefined;
+}
+
/**
* @internal
*/
@@ -15,9 +35,9 @@ const mergeEnvVarsWithParams = (params?: AstroClerkIntegrationParams & { publish
publishableKey: paramPublishableKey,
telemetry: paramTelemetry,
clerkJSUrl: paramClerkJSUrl,
- clerkUiUrl: paramClerkUiUrl,
- clerkJSVariant: paramClerkJSVariant,
clerkJSVersion: paramClerkJSVersion,
+ clerkUIUrl: paramClerkUiUrl,
+ prefetchUI: paramPrefetchUI,
...rest
} = params || {};
@@ -28,10 +48,10 @@ 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,
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,
+ clerkUIUrl: paramClerkUiUrl || import.meta.env.PUBLIC_CLERK_UI_URL,
+ prefetchUI: mergePrefetchUIConfig(paramPrefetchUI),
telemetry: paramTelemetry || {
disabled: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DISABLED),
debug: isTruthy(import.meta.env.PUBLIC_CLERK_TELEMETRY_DEBUG),
diff --git a/packages/astro/src/server/build-clerk-hotload-script.ts b/packages/astro/src/server/build-clerk-hotload-script.ts
index b3cf7d37089..ad96e4cb1b8 100644
--- a/packages/astro/src/server/build-clerk-hotload-script.ts
+++ b/packages/astro/src/server/build-clerk-hotload-script.ts
@@ -1,30 +1,26 @@
-import { clerkJsScriptUrl, clerkUiScriptUrl } from '@clerk/shared/loadClerkJsScript';
+import { clerkJSScriptUrl, clerkUIScriptUrl, shouldPrefetchClerkUI } from '@clerk/shared/loadClerkJsScript';
import type { APIContext } from 'astro';
import { getSafeEnv } from './get-safe-env';
function buildClerkHotloadScript(locals: APIContext['locals']) {
+ const env = getSafeEnv(locals);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const publishableKey = getSafeEnv(locals).pk!;
+ const publishableKey = env.pk!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const proxyUrl = getSafeEnv(locals).proxyUrl!;
+ const proxyUrl = env.proxyUrl!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const domain = getSafeEnv(locals).domain!;
- const clerkJsScriptSrc = clerkJsScriptUrl({
- clerkJSUrl: getSafeEnv(locals).clerkJsUrl,
- clerkJSVariant: getSafeEnv(locals).clerkJsVariant,
- clerkJSVersion: getSafeEnv(locals).clerkJsVersion,
- domain,
- proxyUrl,
- publishableKey,
- });
- const clerkUiScriptSrc = clerkUiScriptUrl({
- clerkUiUrl: getSafeEnv(locals).clerkUiUrl,
+ const domain = env.domain!;
+
+ const clerkJsScriptSrc = clerkJSScriptUrl({
+ clerkJSUrl: env.clerkJsUrl,
+ clerkJSVersion: env.clerkJsVersion,
domain,
proxyUrl,
publishableKey,
});
- return `
+
+ const clerkJsScript = `
+ >`;
+
+ if (!shouldPrefetchClerkUI(env.prefetchUI)) {
+ return clerkJsScript + '\n';
+ }
+
+ const clerkUiScriptSrc = clerkUIScriptUrl({
+ clerkUIUrl: env.clerkUIUrl,
+ domain,
+ proxyUrl,
+ publishableKey,
+ });
+
+ const clerkUiScript = `
\n`;
+ >`;
+
+ return clerkJsScript + clerkUiScript + '\n';
}
export { buildClerkHotloadScript };
diff --git a/packages/astro/src/server/get-safe-env.ts b/packages/astro/src/server/get-safe-env.ts
index 7ec1824029b..9d17c86321d 100644
--- a/packages/astro/src/server/get-safe-env.ts
+++ b/packages/astro/src/server/get-safe-env.ts
@@ -31,9 +31,9 @@ function getSafeEnv(context: ContextOrLocals) {
signInUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_IN_URL', context),
signUpUrl: getContextEnvVar('PUBLIC_CLERK_SIGN_UP_URL', context),
clerkJsUrl: getContextEnvVar('PUBLIC_CLERK_JS_URL', context),
- clerkUiUrl: getContextEnvVar('PUBLIC_CLERK_UI_URL', context),
- clerkJsVariant: getContextEnvVar('PUBLIC_CLERK_JS_VARIANT', context) as 'headless' | '' | undefined,
clerkJsVersion: getContextEnvVar('PUBLIC_CLERK_JS_VERSION', context),
+ clerkUIUrl: getContextEnvVar('PUBLIC_CLERK_UI_URL', context),
+ prefetchUI: getContextEnvVar('PUBLIC_CLERK_PREFETCH_UI', context) === 'false' ? false : undefined,
apiVersion: getContextEnvVar('CLERK_API_VERSION', context),
apiUrl: getContextEnvVar('CLERK_API_URL', context),
telemetryDisabled: isTruthy(getContextEnvVar('PUBLIC_CLERK_TELEMETRY_DISABLED', context)),
diff --git a/packages/astro/src/types.ts b/packages/astro/src/types.ts
index 7f0613e5968..523e3583d5d 100644
--- a/packages/astro/src/types.ts
+++ b/packages/astro/src/types.ts
@@ -30,12 +30,17 @@ type AstroClerkIntegrationParams = Without<
MultiDomainAndOrProxyPrimitives & {
appearance?: Appearance;
clerkJSUrl?: string;
- clerkJSVariant?: 'headless' | '';
clerkJSVersion?: string;
/**
* The URL that `@clerk/ui` should be hot-loaded from.
*/
- clerkUiUrl?: string;
+ clerkUIUrl?: string;
+ /**
+ * Controls prefetching of the `@clerk/ui` script.
+ * - `false` - Skip prefetching the UI (for custom UIs using Control Components)
+ * - `undefined` (default) - Prefetch UI normally
+ */
+ prefetchUI?: boolean;
};
type AstroClerkCreateInstanceParams = AstroClerkIntegrationParams & {
diff --git a/packages/chrome-extension/src/internal/clerk.ts b/packages/chrome-extension/src/internal/clerk.ts
index bcd6362a529..3cd2eb4044d 100644
--- a/packages/chrome-extension/src/internal/clerk.ts
+++ b/packages/chrome-extension/src/internal/clerk.ts
@@ -35,12 +35,6 @@ export function createClerkClient({
storageCache = BrowserStorageCache,
syncHost,
}: CreateClerkClientOptions) {
- if (scope === SCOPE.BACKGROUND) {
- // TODO @nikos
- // @ts-expect-error will be replaced by clerk ui
- Clerk.mountComponentRenderer = undefined;
- }
-
// Don't cache background scripts as it can result in out-of-sync client information.
if (clerk && scope !== SCOPE.BACKGROUND) {
return clerk;
diff --git a/packages/chrome-extension/src/internal/utils/request-handler.ts b/packages/chrome-extension/src/internal/utils/request-handler.ts
index 448755df3bf..60cde36adc3 100644
--- a/packages/chrome-extension/src/internal/utils/request-handler.ts
+++ b/packages/chrome-extension/src/internal/utils/request-handler.ts
@@ -7,7 +7,7 @@ type Handler = Parameters[0];
type Req = Parameters[0];
/** Append the JWT to the FAPI request */
-export function requestHandler(jwtHandler: JWTHandler, { isProd }: { isProd: boolean }) {
+export function requestHandler(jwtHandler: JWTHandler, { isProd }: { isProd: boolean }): Handler {
const handler: Handler = async requestInit => {
requestInit.credentials = 'omit';
diff --git a/packages/chrome-extension/src/internal/utils/response-handler.ts b/packages/chrome-extension/src/internal/utils/response-handler.ts
index 9a5c161e952..7bcf3a16e4b 100644
--- a/packages/chrome-extension/src/internal/utils/response-handler.ts
+++ b/packages/chrome-extension/src/internal/utils/response-handler.ts
@@ -7,7 +7,7 @@ type Handler = Parameters[0];
type Res = Parameters[1];
/** Retrieve the JWT to the FAPI response */
-export function responseHandler(jwtHandler: JWTHandler, { isProd }: { isProd: boolean }) {
+export function responseHandler(jwtHandler: JWTHandler, { isProd }: { isProd: boolean }): Handler {
const handler: Handler = async (_, response) => {
if (isProd) {
await prodHandler(response, jwtHandler);
diff --git a/packages/chrome-extension/src/react/ClerkProvider.tsx b/packages/chrome-extension/src/react/ClerkProvider.tsx
index 45237484496..c293d1382d7 100644
--- a/packages/chrome-extension/src/react/ClerkProvider.tsx
+++ b/packages/chrome-extension/src/react/ClerkProvider.tsx
@@ -36,7 +36,7 @@ export function ClerkProvider(props: ChromeExtensionClerkPr
{children}
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 834dd41db10..4f78f5c0554 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -5,7 +5,7 @@
{ "path": "./dist/clerk.chips.browser.js", "maxSize": "66KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "105KB" },
{ "path": "./dist/clerk.no-rhc.js", "maxSize": "305KB" },
- { "path": "./dist/clerk.headless*.js", "maxSize": "65KB" },
+ { "path": "./dist/clerk.native.js", "maxSize": "65KB" },
{ "path": "./dist/vendors*.js", "maxSize": "7KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "36KB" },
{ "path": "./dist/base-account-sdk*.js", "maxSize": "203KB" },
diff --git a/packages/clerk-js/headless/index.d.ts b/packages/clerk-js/headless/index.d.ts
deleted file mode 100644
index b29913ac3f0..00000000000
--- a/packages/clerk-js/headless/index.d.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { Clerk } from '../dist/types/index.headless';
-
-export * from '../dist/types/index.headless';
diff --git a/packages/clerk-js/headless/index.js b/packages/clerk-js/headless/index.js
deleted file mode 100644
index eb34c85affa..00000000000
--- a/packages/clerk-js/headless/index.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = require('../dist/clerk.headless');
diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json
index b747bd2ec60..a41a1197d97 100644
--- a/packages/clerk-js/package.json
+++ b/packages/clerk-js/package.json
@@ -21,13 +21,38 @@
},
"license": "MIT",
"author": "Clerk",
+ "exports": {
+ ".": {
+ "react-native": {
+ "types": "./dist/types/index.d.ts",
+ "default": "./dist/clerk.native.js"
+ },
+ "import": {
+ "types": "./dist/types/index.d.ts",
+ "default": "./dist/clerk.mjs"
+ },
+ "require": {
+ "types": "./dist/types/index.d.ts",
+ "default": "./dist/clerk.js"
+ }
+ },
+ "./no-rhc": {
+ "import": {
+ "types": "./dist/types/index.d.ts",
+ "default": "./dist/clerk.no-rhc.mjs"
+ },
+ "require": {
+ "types": "./dist/types/index.d.ts",
+ "default": "./dist/clerk.no-rhc.js"
+ }
+ }
+ },
"main": "dist/clerk.js",
"jsdelivr": "dist/clerk.browser.js",
"module": "dist/clerk.mjs",
"types": "dist/types/index.d.ts",
"files": [
"dist",
- "headless",
"no-rhc"
],
"scripts": {
@@ -42,13 +67,12 @@
"clean": "rimraf ./dist",
"dev": "rspack serve --config rspack.config.js",
"dev:chips": "rspack serve --config rspack.config.js --env variant=\"clerk.chips.browser\"",
- "dev:headless": "rspack serve --config rspack.config.js --env variant=\"clerk.headless.browser\"",
"dev:origin": "rspack serve --config rspack.config.js --env devOrigin=http://localhost:${PORT:-4000}",
"dev:sandbox": "rspack serve --config rspack.config.js --env devOrigin=http://localhost:${PORT:-4000} --env sandbox=1",
"format": "node ../../scripts/format-package.mjs",
"format:check": "node ../../scripts/format-package.mjs --check",
"lint": "eslint src",
- "lint:attw": "attw --pack . --profile node16 --ignore-rules named-exports",
+ "lint:attw": "attw --pack . --profile node16 --ignore-rules named-exports --ignore-rules false-cjs",
"lint:publint": "publint || true",
"postbuild:disabled": "node ../../scripts/search-for-rhc.mjs file dist/clerk.no-rhc.mjs",
"test": "vitest --watch=false",
diff --git a/packages/clerk-js/rspack.config.js b/packages/clerk-js/rspack.config.js
index 55133e59808..f010c4ebe46 100644
--- a/packages/clerk-js/rspack.config.js
+++ b/packages/clerk-js/rspack.config.js
@@ -14,8 +14,7 @@ const variants = {
clerk: 'clerk',
clerkNoRHC: 'clerk.no-rhc', // Omit Remotely Hosted Code
clerkBrowser: 'clerk.browser',
- clerkHeadless: 'clerk.headless',
- clerkHeadlessBrowser: 'clerk.headless.browser',
+ clerkNative: 'clerk.native', // For React Native (no chunk splitting)
clerkLegacyBrowser: 'clerk.legacy.browser',
clerkCHIPS: 'clerk.chips.browser',
};
@@ -24,8 +23,7 @@ const variantToSourceFile = {
[variants.clerk]: './src/index.ts',
[variants.clerkNoRHC]: './src/index.ts',
[variants.clerkBrowser]: './src/index.browser.ts',
- [variants.clerkHeadless]: './src/index.headless.ts',
- [variants.clerkHeadlessBrowser]: './src/index.headless.browser.ts',
+ [variants.clerkNative]: './src/index.ts',
[variants.clerkLegacyBrowser]: './src/index.legacy.browser.ts',
[variants.clerkCHIPS]: './src/index.browser.ts',
};
@@ -229,16 +227,6 @@ const commonForProd = () => {
};
};
-// /** @type { () => (import('webpack').Configuration) } */
-// const externalsForHeadless = () => {
-// return {
-// externals: {
-// react: 'react',
-// 'react-dom': 'react-dom',
-// },
-// };
-// };
-
/**
*
* @param {string} variant
@@ -292,14 +280,13 @@ const prodConfig = ({ mode, env, analysis }) => {
commonForProdChunked({ targets: packageJSON.browserslistLegacy, useCoreJs: true }),
);
- const clerkHeadless = merge(
- entryForVariant(variants.clerkHeadless),
- common({ mode, variant: variants.clerkHeadless }),
+ const clerkNative = merge(
+ entryForVariant(variants.clerkNative),
+ common({ mode, variant: variants.clerkNative }),
commonForProd(),
commonForProdChunked(),
- // Disable chunking for the headless variant, since it's meant to be used in a non-browser environment and
- // attempting to load chunks causes issues due to usage of a dynamic publicPath. We generally are only concerned with
- // chunking in our browser bundles.
+ // Disable chunking for the native variant, since it's meant to be used in React Native
+ // where dynamic chunk loading is not supported.
{
output: {
publicPath: '',
@@ -308,15 +295,6 @@ const prodConfig = ({ mode, env, analysis }) => {
splitChunks: false,
},
},
- // externalsForHeadless(),
- );
-
- const clerkHeadlessBrowser = merge(
- entryForVariant(variants.clerkHeadlessBrowser),
- common({ mode, variant: variants.clerkHeadlessBrowser }),
- commonForProd(),
- commonForProdChunked(),
- // externalsForHeadless(),
);
const clerkCHIPS = merge(
@@ -434,17 +412,7 @@ const prodConfig = ({ mode, env, analysis }) => {
return [clerkBrowser];
}
- return [
- clerkBrowser,
- clerkLegacyBrowser,
- clerkHeadless,
- clerkHeadlessBrowser,
- clerkCHIPS,
- clerkEsm,
- clerkEsmNoRHC,
- clerkCjs,
- clerkCjsNoRHC,
- ];
+ return [clerkBrowser, clerkLegacyBrowser, clerkNative, clerkCHIPS, clerkEsm, clerkEsmNoRHC, clerkCjs, clerkCjsNoRHC];
};
/**
@@ -534,17 +502,10 @@ const devConfig = ({ mode, env }) => {
common({ mode, disableRHC: true, variant: variants.clerkBrowserNoRHC }),
commonForDev(),
),
- [variants.clerkHeadless]: merge(
- entryForVariant(variants.clerkHeadless),
- common({ mode, variant: variants.clerkHeadless }),
- commonForDev(),
- // externalsForHeadless(),
- ),
- [variants.clerkHeadlessBrowser]: merge(
- entryForVariant(variants.clerkHeadlessBrowser),
- common({ mode, variant: variants.clerkHeadlessBrowser }),
+ [variants.clerkNative]: merge(
+ entryForVariant(variants.clerkNative),
+ common({ mode, variant: variants.clerkNative }),
commonForDev(),
- // externalsForHeadless(),
),
[variants.clerkCHIPS]: merge(
entryForVariant(variants.clerkCHIPS),
diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts
index 315e0822f31..f349f032ad4 100644
--- a/packages/clerk-js/sandbox/app.ts
+++ b/packages/clerk-js/sandbox/app.ts
@@ -446,7 +446,7 @@ void (async () => {
...(componentControls.clerk.getProps() ?? {}),
signInUrl: '/sign-in',
signUpUrl: '/sign-up',
- clerkUiCtor: window.__internal_ClerkUiCtor,
+ clerkUICtor: window.__internal_ClerkUiCtor,
});
renderCurrentRoute();
updateVariables();
diff --git a/packages/clerk-js/src/__tests__/headless.test.ts b/packages/clerk-js/src/__tests__/headless.test.ts
deleted file mode 100644
index 8ea5196a2a9..00000000000
--- a/packages/clerk-js/src/__tests__/headless.test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-/**
- * @vitest-environment node
- */
-
-import { describe, expect, it } from 'vitest';
-
-describe('clerk/headless', () => {
- it('JS-689: should not error when loading headless', () => {
- expect(() => {
- // eslint-disable-next-line @typescript-eslint/no-require-imports
- require('../../headless/index.js');
- }).not.toThrow();
- });
-});
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 38a8e411478..72dd816983d 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -484,9 +484,9 @@ export class Clerk implements ClerkInterface {
this.#options = this.#initOptions(options);
- // Initialize ClerkUi if it was provided
- if (this.#options.clerkUiCtor) {
- this.#clerkUi = Promise.resolve(this.#options.clerkUiCtor).then(
+ // Initialize ClerkUI if it was provided
+ if (this.#options.clerkUICtor) {
+ this.#clerkUi = Promise.resolve(this.#options.clerkUICtor).then(
ClerkUI =>
new ClerkUI(
() => this,
diff --git a/packages/clerk-js/src/index.headless.browser.ts b/packages/clerk-js/src/index.headless.browser.ts
deleted file mode 100644
index 45fcfdbc7d8..00000000000
--- a/packages/clerk-js/src/index.headless.browser.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-// It's crucial this is the first import,
-// otherwise chunk loading will not work
-
-import './utils/setWebpackChunkPublicPath';
-
-import { Clerk } from './core/clerk';
-
-const publishableKey =
- document.querySelector('script[data-clerk-publishable-key]')?.getAttribute('data-clerk-publishable-key') ||
- window.__clerk_publishable_key ||
- '';
-
-const proxyUrl =
- document.querySelector('script[data-clerk-proxy-url]')?.getAttribute('data-clerk-proxy-url') ||
- window.__clerk_proxy_url ||
- '';
-
-const domain =
- document.querySelector('script[data-clerk-domain]')?.getAttribute('data-clerk-domain') || window.__clerk_domain || '';
-
-if (!window.Clerk) {
- window.Clerk = new Clerk(publishableKey, {
- proxyUrl,
- domain,
- });
-}
-
-if (module.hot) {
- module.hot.accept();
-}
diff --git a/packages/clerk-js/src/index.headless.ts b/packages/clerk-js/src/index.headless.ts
deleted file mode 100644
index 82812769c40..00000000000
--- a/packages/clerk-js/src/index.headless.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Clerk } from './core/clerk';
-
-export {
- ClerkAPIResponseError,
- ClerkRuntimeError,
- EmailLinkError,
- EmailLinkErrorCode,
- EmailLinkErrorCodeStatus,
- isClerkAPIResponseError,
- isClerkRuntimeError,
- isEmailLinkError,
- isKnownError,
- isMetamaskError,
- isUserLockedError,
- type MetamaskError,
-} from '@clerk/shared/error';
-
-export { Clerk };
-
-if (module.hot) {
- module.hot.accept();
-}
diff --git a/packages/clerk-js/tsconfig.declarations.json b/packages/clerk-js/tsconfig.declarations.json
index 29c5eced402..1ed4e456652 100644
--- a/packages/clerk-js/tsconfig.declarations.json
+++ b/packages/clerk-js/tsconfig.declarations.json
@@ -11,11 +11,5 @@
"declarationDir": "./dist/types",
"noImplicitReturns": false
},
- "include": [
- "src/index.ts",
- "src/index.browser.ts",
- "src/index.headless.ts",
- "src/index.headless.browser.ts",
- "src/**/*.d.ts"
- ]
+ "include": ["src/index.ts", "src/index.browser.ts", "src/**/*.d.ts"]
}
diff --git a/packages/clerk-js/turbo.json b/packages/clerk-js/turbo.json
index f9aa3069acd..d0557a2bc8c 100644
--- a/packages/clerk-js/turbo.json
+++ b/packages/clerk-js/turbo.json
@@ -4,7 +4,6 @@
"build": {
"inputs": [
"*.d.ts",
- "headless/**",
"src/**",
"tsconfig.json",
"tsconfig.declarations.json",
diff --git a/packages/clerk-js/vitest.config.mts b/packages/clerk-js/vitest.config.mts
index c74923a9bfd..e343397351c 100644
--- a/packages/clerk-js/vitest.config.mts
+++ b/packages/clerk-js/vitest.config.mts
@@ -41,8 +41,6 @@ export default defineConfig({
'src/**/index.ts',
'src/**/index.browser.ts',
'src/**/index.chips.browser.ts',
- 'src/**/index.headless.ts',
- 'src/**/index.headless.browser.ts',
'src/**/index.legacy.browser.ts',
'src/**/coverage/**',
'src/**/dist/**',
diff --git a/packages/expo/src/provider/singleton/createClerkInstance.ts b/packages/expo/src/provider/singleton/createClerkInstance.ts
index f80234590d6..f41208d9388 100644
--- a/packages/expo/src/provider/singleton/createClerkInstance.ts
+++ b/packages/expo/src/provider/singleton/createClerkInstance.ts
@@ -1,5 +1,4 @@
-import type { FapiRequestInit, FapiResponse } from '@clerk/clerk-js/dist/types/core/fapiClient';
-import { type Clerk, isClerkRuntimeError } from '@clerk/clerk-js/headless';
+import { type Clerk, isClerkRuntimeError } from '@clerk/clerk-js';
import type { BrowserClerk, HeadlessBrowserClerk } from '@clerk/react';
import { is4xxError } from '@clerk/shared/error';
import type {
@@ -23,6 +22,20 @@ import { errorThrower } from '../../errorThrower';
import { isNative } from '../../utils';
import type { BuildClerkOptions } from './types';
+/**
+ * Internal types for FAPI client callbacks.
+ * These are simplified versions of the internal clerk-js types,
+ * used only for the __internal_onBeforeRequest and __internal_onAfterResponse hooks.
+ */
+type FapiRequestInit = RequestInit & {
+ url?: URL;
+ headers?: Headers;
+};
+
+type FapiResponse = Response & {
+ payload: { errors?: Array<{ code: string }> } | null;
+};
+
const KEY = '__clerk_client_jwt';
let __internal_clerk: HeadlessBrowserClerk | BrowserClerk | undefined;
@@ -168,7 +181,7 @@ export function createClerkInstance(ClerkClass: typeof Clerk) {
let nativeApiErrorShown = false;
// @ts-expect-error - This is an internal API
- __internal_clerk.__internal_onAfterResponse(async (_: FapiRequestInit, response: FapiResponse) => {
+ __internal_clerk.__internal_onAfterResponse(async (_: FapiRequestInit, response: FapiResponse) => {
const authHeader = response.headers.get('authorization');
if (authHeader) {
await saveToken(KEY, authHeader);
diff --git a/packages/expo/src/provider/singleton/singleton.ts b/packages/expo/src/provider/singleton/singleton.ts
index 22e6fc4ca16..eaf34ba0f3f 100644
--- a/packages/expo/src/provider/singleton/singleton.ts
+++ b/packages/expo/src/provider/singleton/singleton.ts
@@ -1,4 +1,4 @@
-import { Clerk } from '@clerk/clerk-js/headless';
+import { Clerk } from '@clerk/clerk-js';
import { createClerkInstance } from './createClerkInstance';
diff --git a/packages/express/src/utils.ts b/packages/express/src/utils.ts
index 92664fa8280..a2c1c78677c 100644
--- a/packages/express/src/utils.ts
+++ b/packages/express/src/utils.ts
@@ -11,8 +11,9 @@ export const loadClientEnv = () => {
return {
publishableKey: process.env.CLERK_PUBLISHABLE_KEY || '',
clerkJSUrl: process.env.CLERK_JS || process.env.CLERK_JS_URL || '',
- clerkUiUrl: process.env.CLERK_UI_URL || '',
clerkJSVersion: process.env.CLERK_JS_VERSION || '',
+ clerkUIUrl: process.env.CLERK_UI_URL || '',
+ prefetchUI: process.env.CLERK_PREFETCH_UI === 'false' ? false : undefined,
};
};
diff --git a/packages/nextjs/src/pages/ClerkProvider.tsx b/packages/nextjs/src/pages/ClerkProvider.tsx
index 786da7c7654..36c5e468bf5 100644
--- a/packages/nextjs/src/pages/ClerkProvider.tsx
+++ b/packages/nextjs/src/pages/ClerkProvider.tsx
@@ -1,7 +1,7 @@
import { ClerkProvider as ReactClerkProvider } from '@clerk/react';
import type { Ui } from '@clerk/react/internal';
// Override Clerk React error thrower to show that errors come from @clerk/nextjs
-import { setClerkJsLoadingErrorPackageName, setErrorThrowerOptions } from '@clerk/react/internal';
+import { setClerkJSLoadingErrorPackageName, setErrorThrowerOptions } from '@clerk/react/internal';
import { useRouter } from 'next/router';
import React from 'react';
@@ -15,7 +15,7 @@ import { removeBasePath } from '../utils/removeBasePath';
import { RouterTelemetry } from '../utils/router-telemetry';
setErrorThrowerOptions({ packageName: PACKAGE_NAME });
-setClerkJsLoadingErrorPackageName(PACKAGE_NAME);
+setClerkJSLoadingErrorPackageName(PACKAGE_NAME);
export function ClerkProvider({ children, ...props }: NextClerkProviderProps): JSX.Element {
const { __internal_invokeMiddlewareOnAuthStateChange = true } = props;
diff --git a/packages/nextjs/src/pages/__tests__/index.test.tsx b/packages/nextjs/src/pages/__tests__/index.test.tsx
index 071e36fcb57..b8bf97ccf76 100644
--- a/packages/nextjs/src/pages/__tests__/index.test.tsx
+++ b/packages/nextjs/src/pages/__tests__/index.test.tsx
@@ -17,14 +17,27 @@ describe('ClerkProvider', () => {
});
});
- describe('clerkJSVariant', () => {
+ describe('prefetchUI', () => {
const defaultProps = { children: '' };
- it('is either headless or empty', () => {
- expectTypeOf({ ...defaultProps, clerkJSVariant: 'headless' as const }).toMatchTypeOf();
- expectTypeOf({ ...defaultProps, clerkJSVariant: '' as const }).toMatchTypeOf();
- expectTypeOf({ ...defaultProps, clerkJSVariant: undefined }).toMatchTypeOf();
- expectTypeOf({ ...defaultProps, clerkJSVariant: 'test' }).not.toMatchTypeOf();
+ it('accepts false to disable UI prefetching', () => {
+ expectTypeOf({ ...defaultProps, prefetchUI: false as const }).toMatchTypeOf();
+ });
+
+ it('accepts undefined for default UI prefetching', () => {
+ expectTypeOf({ ...defaultProps, prefetchUI: undefined }).toMatchTypeOf();
+ });
+ });
+
+ describe('clerkUIUrl', () => {
+ const defaultProps = { children: '' };
+
+ it('accepts string URL for custom UI location', () => {
+ expectTypeOf({ ...defaultProps, clerkUIUrl: 'https://custom.com/ui.js' }).toMatchTypeOf();
+ });
+
+ it('accepts undefined', () => {
+ expectTypeOf({ ...defaultProps, clerkUIUrl: undefined }).toMatchTypeOf();
});
});
diff --git a/packages/nextjs/src/utils/clerk-script.tsx b/packages/nextjs/src/utils/clerk-script.tsx
index aceb76c4d9d..8071470476e 100644
--- a/packages/nextjs/src/utils/clerk-script.tsx
+++ b/packages/nextjs/src/utils/clerk-script.tsx
@@ -1,9 +1,10 @@
import { useClerk } from '@clerk/react';
import {
- buildClerkJsScriptAttributes,
- buildClerkUiScriptAttributes,
- clerkJsScriptUrl,
- clerkUiScriptUrl,
+ buildClerkJSScriptAttributes,
+ buildClerkUIScriptAttributes,
+ clerkJSScriptUrl,
+ clerkUIScriptUrl,
+ shouldPrefetchClerkUI,
} from '@clerk/react/internal';
import NextScript from 'next/script';
import React from 'react';
@@ -43,7 +44,7 @@ function ClerkScript(props: ClerkScriptProps) {
}
export function ClerkScripts({ router }: { router: ClerkScriptProps['router'] }) {
- const { publishableKey, clerkJSUrl, clerkJSVersion, clerkJSVariant, nonce, clerkUiUrl, ui } = useClerkNextOptions();
+ const { publishableKey, clerkJSUrl, clerkJSVersion, clerkUIUrl, nonce, prefetchUI } = useClerkNextOptions();
const { domain, proxyUrl } = useClerk();
if (!publishableKey) {
@@ -54,28 +55,28 @@ export function ClerkScripts({ router }: { router: ClerkScriptProps['router'] })
publishableKey,
clerkJSUrl,
clerkJSVersion,
- clerkJSVariant,
+ clerkUIUrl,
nonce,
domain,
proxyUrl,
- clerkUiVersion: ui?.version,
- clerkUiUrl: ui?.url || clerkUiUrl,
};
return (
<>
-
+ {shouldPrefetchClerkUI(prefetchUI) && (
+
+ )}
>
);
}
diff --git a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts
index dab4279d06a..0050cb522f4 100644
--- a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts
+++ b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts
@@ -3,14 +3,29 @@ import { isTruthy } from '@clerk/shared/underscore';
import { SDK_METADATA } from '../server/constants';
import type { NextClerkProviderProps } from '../types';
+function getPrefetchUIFromEnvAndProps(propsPrefetchUI: NextClerkProviderProps['prefetchUI']): boolean | undefined {
+ // Props take precedence
+ if (propsPrefetchUI === false) {
+ return false;
+ }
+
+ // Check env var
+ if (process.env.NEXT_PUBLIC_CLERK_PREFETCH_UI === 'false') {
+ return false;
+ }
+
+ return undefined;
+}
+
// @ts-ignore - https://github.com/microsoft/TypeScript/issues/47663
export const mergeNextClerkPropsWithEnv = (props: Omit): any => {
return {
...props,
publishableKey: props.publishableKey || process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY || '',
clerkJSUrl: props.clerkJSUrl || process.env.NEXT_PUBLIC_CLERK_JS_URL,
- clerkUiUrl: (props as any).clerkUiUrl || process.env.NEXT_PUBLIC_CLERK_UI_URL,
clerkJSVersion: props.clerkJSVersion || process.env.NEXT_PUBLIC_CLERK_JS_VERSION,
+ clerkUIUrl: props.clerkUIUrl || process.env.NEXT_PUBLIC_CLERK_UI_URL,
+ prefetchUI: getPrefetchUIFromEnvAndProps(props.prefetchUI),
proxyUrl: props.proxyUrl || process.env.NEXT_PUBLIC_CLERK_PROXY_URL || '',
domain: props.domain || process.env.NEXT_PUBLIC_CLERK_DOMAIN || '',
isSatellite: props.isSatellite || isTruthy(process.env.NEXT_PUBLIC_CLERK_IS_SATELLITE),
diff --git a/packages/nuxt/src/global.d.ts b/packages/nuxt/src/global.d.ts
index 7220a9070ab..84eec72d191 100644
--- a/packages/nuxt/src/global.d.ts
+++ b/packages/nuxt/src/global.d.ts
@@ -16,7 +16,7 @@ declare module 'nuxt/schema' {
};
}
interface PublicRuntimeConfig {
- clerk: Omit & {
+ clerk: Omit & {
/**
* The URL that `@clerk/clerk-js` should be hot-loaded from.
* Supports NUXT_PUBLIC_CLERK_JS_URL env var.
diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts
index 0f0fb72e6f0..e38f8d864f8 100644
--- a/packages/nuxt/src/module.ts
+++ b/packages/nuxt/src/module.ts
@@ -64,12 +64,13 @@ export default defineNuxtModule({
signUpForceRedirectUrl: options.signUpForceRedirectUrl,
signUpUrl: options.signUpUrl,
domain: options.domain,
- // Using jsUrl/uiUrl instead of clerkJSUrl/clerkUiUrl to support
+ // Using jsUrl/uiUrl instead of clerkJSUrl/clerkUIUrl to support
// NUXT_PUBLIC_CLERK_JS_URL and NUXT_PUBLIC_CLERK_UI_URL env vars.
jsUrl: options.clerkJSUrl,
- uiUrl: options.clerkUiUrl,
- clerkJSVariant: options.clerkJSVariant,
+ uiUrl: options.clerkUIUrl,
clerkJSVersion: options.clerkJSVersion,
+ // prefetchUI config: can be false or undefined
+ prefetchUI: options.prefetchUI,
isSatellite: options.isSatellite,
// Backend specific variables that are safe to share.
// We want them to be overridable like the other public keys (e.g NUXT_PUBLIC_CLERK_PROXY_URL)
diff --git a/packages/nuxt/src/runtime/plugin.ts b/packages/nuxt/src/runtime/plugin.ts
index 8879860afb6..03fdeb75d25 100644
--- a/packages/nuxt/src/runtime/plugin.ts
+++ b/packages/nuxt/src/runtime/plugin.ts
@@ -1,11 +1,11 @@
-import { setClerkJsLoadingErrorPackageName } from '@clerk/shared/loadClerkJsScript';
+import { setClerkJSLoadingErrorPackageName } from '@clerk/shared/loadClerkJsScript';
import type { InitialState } from '@clerk/shared/types';
import { clerkPlugin } from '@clerk/vue';
import { setErrorThrowerOptions } from '@clerk/vue/internal';
import { defineNuxtPlugin, navigateTo, useRuntimeConfig, useState } from 'nuxt/app';
setErrorThrowerOptions({ packageName: PACKAGE_NAME });
-setClerkJsLoadingErrorPackageName(PACKAGE_NAME);
+setClerkJSLoadingErrorPackageName(PACKAGE_NAME);
export default defineNuxtPlugin(nuxtApp => {
// SSR-friendly shared state
@@ -21,9 +21,9 @@ export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(clerkPlugin as any, {
...clerkConfig,
- // Map jsUrl/uiUrl to clerkJSUrl/clerkUiUrl as expected by the Vue plugin
+ // Map jsUrl/uiUrl to clerkJSUrl/clerkUIUrl as expected by the Vue plugin
clerkJSUrl: clerkConfig.jsUrl,
- clerkUiUrl: clerkConfig.uiUrl,
+ clerkUIUrl: clerkConfig.uiUrl,
sdkMetadata: {
name: PACKAGE_NAME,
version: PACKAGE_VERSION,
diff --git a/packages/react-router/src/client/ReactRouterClerkProvider.tsx b/packages/react-router/src/client/ReactRouterClerkProvider.tsx
index 33ea4406868..5da08272f20 100644
--- a/packages/react-router/src/client/ReactRouterClerkProvider.tsx
+++ b/packages/react-router/src/client/ReactRouterClerkProvider.tsx
@@ -62,8 +62,9 @@ function ClerkProviderBase({ children, ...rest }: ClerkProv
__signInFallbackRedirectUrl,
__signUpFallbackRedirectUrl,
__clerkJSUrl,
- __clerkUiUrl,
__clerkJSVersion,
+ __clerkUIUrl,
+ __prefetchUI,
__telemetryDisabled,
__telemetryDebug,
} = clerkState?.__internal_clerk_state || {};
@@ -90,8 +91,9 @@ function ClerkProviderBase({ children, ...rest }: ClerkProv
signInFallbackRedirectUrl: __signInFallbackRedirectUrl,
signUpFallbackRedirectUrl: __signUpFallbackRedirectUrl,
clerkJSUrl: __clerkJSUrl,
- clerkUiUrl: __clerkUiUrl,
clerkJSVersion: __clerkJSVersion,
+ clerkUIUrl: __clerkUIUrl,
+ prefetchUI: __prefetchUI,
telemetry: {
disabled: __telemetryDisabled,
debug: __telemetryDebug,
diff --git a/packages/react-router/src/client/types.ts b/packages/react-router/src/client/types.ts
index 63f07aea3c4..1c7c15fcbb3 100644
--- a/packages/react-router/src/client/types.ts
+++ b/packages/react-router/src/client/types.ts
@@ -19,8 +19,9 @@ export type ClerkState = {
__signUpFallbackRedirectUrl: string | undefined;
__clerk_debug: any;
__clerkJSUrl: string | undefined;
- __clerkUiUrl: string | undefined;
__clerkJSVersion: string | undefined;
+ __clerkUIUrl: string | undefined;
+ __prefetchUI: boolean | undefined;
__telemetryDisabled: boolean | undefined;
__telemetryDebug: boolean | undefined;
};
diff --git a/packages/react-router/src/server/utils.ts b/packages/react-router/src/server/utils.ts
index 1b0bd9a9fe7..2070bdeef5e 100644
--- a/packages/react-router/src/server/utils.ts
+++ b/packages/react-router/src/server/utils.ts
@@ -79,6 +79,7 @@ export const injectRequestStateIntoResponse = async (
*/
export function getResponseClerkState(requestState: RequestStateWithRedirectUrls, context: AppLoadContext) {
const { reason, message, isSignedIn, ...rest } = requestState;
+ const envVars = getPublicEnvVariables(context);
const clerkState = wrapWithClerkState({
__clerk_ssr_state: rest.toAuth(),
__publishableKey: requestState.publishableKey,
@@ -92,11 +93,12 @@ export function getResponseClerkState(requestState: RequestStateWithRedirectUrls
__signInFallbackRedirectUrl: requestState.signInFallbackRedirectUrl,
__signUpFallbackRedirectUrl: requestState.signUpFallbackRedirectUrl,
__clerk_debug: debugRequestState(requestState),
- __clerkJSUrl: getPublicEnvVariables(context).clerkJsUrl,
- __clerkUiUrl: getPublicEnvVariables(context).clerkUiUrl,
- __clerkJSVersion: getPublicEnvVariables(context).clerkJsVersion,
- __telemetryDisabled: getPublicEnvVariables(context).telemetryDisabled,
- __telemetryDebug: getPublicEnvVariables(context).telemetryDebug,
+ __clerkJSUrl: envVars.clerkJsUrl,
+ __clerkJSVersion: envVars.clerkJsVersion,
+ __clerkUIUrl: envVars.clerkUIUrl,
+ __prefetchUI: envVars.prefetchUI,
+ __telemetryDisabled: envVars.telemetryDisabled,
+ __telemetryDebug: envVars.telemetryDebug,
});
return {
diff --git a/packages/react-router/src/utils/env.ts b/packages/react-router/src/utils/env.ts
index 6c03570a7b2..0935e5653eb 100644
--- a/packages/react-router/src/utils/env.ts
+++ b/packages/react-router/src/utils/env.ts
@@ -15,9 +15,9 @@ export const getPublicEnvVariables = (context: AppLoadContext | undefined) => {
signInUrl: getValue('CLERK_SIGN_IN_URL'),
signUpUrl: getValue('CLERK_SIGN_UP_URL'),
clerkJsUrl: getValue('CLERK_JS_URL'),
- clerkUiUrl: getValue('CLERK_UI_URL'),
- clerkJsVariant: getValue('CLERK_JS_VARIANT') as '' | 'headless' | undefined,
clerkJsVersion: getValue('CLERK_JS_VERSION'),
+ clerkUIUrl: getValue('CLERK_UI_URL'),
+ prefetchUI: getValue('CLERK_PREFETCH_UI') === 'false' ? false : undefined,
telemetryDisabled: isTruthy(getValue('CLERK_TELEMETRY_DISABLED')),
telemetryDebug: isTruthy(getValue('CLERK_TELEMETRY_DEBUG')),
signInForceRedirectUrl: getValue('CLERK_SIGN_IN_FORCE_REDIRECT_URL'),
diff --git a/packages/react/src/__tests__/isomorphicClerk.test.ts b/packages/react/src/__tests__/isomorphicClerk.test.ts
index 3e96f127ef2..f2ea649e0f1 100644
--- a/packages/react/src/__tests__/isomorphicClerk.test.ts
+++ b/packages/react/src/__tests__/isomorphicClerk.test.ts
@@ -5,8 +5,9 @@ import { IsomorphicClerk } from '../isomorphicClerk';
// Mock the script loading functions to prevent unhandled promise rejections in tests
vi.mock('@clerk/shared/loadClerkJsScript', () => ({
- loadClerkJsScript: vi.fn().mockResolvedValue(null),
- loadClerkUiScript: vi.fn().mockResolvedValue(null),
+ loadClerkJSScript: vi.fn().mockResolvedValue(null),
+ loadClerkUIScript: vi.fn().mockResolvedValue(null),
+ shouldPrefetchClerkUI: vi.fn().mockReturnValue(true),
}));
describe('isomorphicClerk', () => {
diff --git a/packages/react/src/contexts/__tests__/ClerkProvider.test.tsx b/packages/react/src/contexts/__tests__/ClerkProvider.test.tsx
index 014117e332c..1d98d20da67 100644
--- a/packages/react/src/contexts/__tests__/ClerkProvider.test.tsx
+++ b/packages/react/src/contexts/__tests__/ClerkProvider.test.tsx
@@ -35,14 +35,23 @@ describe('ClerkProvider', () => {
});
});
- describe('clerkJSVariant', () => {
+ describe('prefetchUI', () => {
const defaultProps = { publishableKey: 'test', children: '' };
- it('is either headless or empty', () => {
- expectTypeOf({ ...defaultProps, clerkJSVariant: 'headless' as const }).toMatchTypeOf();
- expectTypeOf({ ...defaultProps, clerkJSVariant: '' as const }).toMatchTypeOf();
- expectTypeOf({ ...defaultProps, clerkJSVariant: undefined }).toMatchTypeOf();
- expectTypeOf({ ...defaultProps, clerkJSVariant: 'test' }).not.toMatchTypeOf();
+ it('accepts false to disable UI prefetching', () => {
+ expectTypeOf({ ...defaultProps, prefetchUI: false as const }).toMatchTypeOf();
+ });
+
+ it('accepts undefined for default behavior', () => {
+ expectTypeOf({ ...defaultProps, prefetchUI: undefined }).toMatchTypeOf();
+ });
+
+ it('accepts true to explicitly enable UI prefetching', () => {
+ expectTypeOf({ ...defaultProps, prefetchUI: true as const }).toMatchTypeOf();
+ });
+
+ it('rejects non-boolean values', () => {
+ expectTypeOf({ ...defaultProps, prefetchUI: 'test' }).not.toMatchTypeOf();
});
});
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 3ffd47e9e7d..454615fc367 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -1,7 +1,7 @@
import './polyfills';
import './types/appearance';
-import { setClerkJsLoadingErrorPackageName } from '@clerk/shared/loadClerkJsScript';
+import { setClerkJSLoadingErrorPackageName } from '@clerk/shared/loadClerkJsScript';
import { setErrorThrowerOptions } from './errors/errorThrower';
@@ -21,4 +21,4 @@ export type {
export type { ClerkProviderProps } from './types';
setErrorThrowerOptions({ packageName: PACKAGE_NAME });
-setClerkJsLoadingErrorPackageName(PACKAGE_NAME);
+setClerkJSLoadingErrorPackageName(PACKAGE_NAME);
diff --git a/packages/react/src/internal.ts b/packages/react/src/internal.ts
index 26e71d2e998..b7aadd67705 100644
--- a/packages/react/src/internal.ts
+++ b/packages/react/src/internal.ts
@@ -4,10 +4,15 @@ export { useRoutingProps } from './hooks/useRoutingProps';
export { useDerivedAuth } from './hooks/useAuth';
export {
+ clerkJSScriptUrl,
+ buildClerkJSScriptAttributes,
+ clerkUIScriptUrl,
+ buildClerkUIScriptAttributes,
+ setClerkJSLoadingErrorPackageName,
+ shouldPrefetchClerkUI,
+ // Deprecated aliases - will be removed in a future major version
clerkJsScriptUrl,
buildClerkJsScriptAttributes,
- clerkUiScriptUrl,
- buildClerkUiScriptAttributes,
setClerkJsLoadingErrorPackageName,
} from '@clerk/shared/loadClerkJsScript';
diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts
index b39bf352b8c..bab7e008ec8 100644
--- a/packages/react/src/isomorphicClerk.ts
+++ b/packages/react/src/isomorphicClerk.ts
@@ -1,6 +1,6 @@
import { inBrowser } from '@clerk/shared/browser';
import { clerkEvents, createClerkEventBus } from '@clerk/shared/clerkEventBus';
-import { loadClerkJsScript, loadClerkUiScript } from '@clerk/shared/loadClerkJsScript';
+import { loadClerkJSScript, loadClerkUIScript, shouldPrefetchClerkUI } from '@clerk/shared/loadClerkJsScript';
import type {
__internal_AttemptToEnableEnvironmentSettingParams,
__internal_AttemptToEnableEnvironmentSettingResult,
@@ -461,12 +461,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
}
try {
- const clerkUiCtor = this.getClerkUiEntryChunk();
+ const clerkUICtor = await this.getClerkUiEntryChunk();
const clerk = await this.getClerkJsEntryChunk();
if (!clerk.loaded) {
this.beforeLoad(clerk);
- await clerk.load({ ...this.options, clerkUiCtor });
+ await clerk.load({ ...this.options, clerkUICtor });
}
if (clerk.loaded) {
this.replayInterceptedInvocations(clerk);
@@ -484,7 +484,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
if (!this.options.Clerk && !__BUILD_DISABLE_RHC__) {
// the UMD script sets the global.Clerk instance
// we do not want to await here as we
- await loadClerkJsScript({
+ await loadClerkJSScript({
...this.options,
publishableKey: this.#publishableKey,
proxyUrl: this.proxyUrl,
@@ -508,15 +508,17 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
return global.Clerk;
}
- private async getClerkUiEntryChunk(): Promise {
- if (this.options.clerkUiCtor) {
- return this.options.clerkUiCtor;
+ private async getClerkUiEntryChunk(): Promise {
+ if (!shouldPrefetchClerkUI(this.options.prefetchUI)) {
+ return undefined;
+ }
+
+ if (this.options.clerkUICtor) {
+ return this.options.clerkUICtor;
}
- await loadClerkUiScript({
+ await loadClerkUIScript({
...this.options,
- clerkUiVersion: this.options.ui?.version,
- clerkUiUrl: this.options.ui?.url || this.options.clerkUiUrl,
publishableKey: this.#publishableKey,
proxyUrl: this.proxyUrl,
domain: this.domain,
diff --git a/packages/shared/src/__tests__/loadClerkJsScript.spec.ts b/packages/shared/src/__tests__/loadClerkJsScript.spec.ts
index f25cec679ff..ebf414a7256 100644
--- a/packages/shared/src/__tests__/loadClerkJsScript.spec.ts
+++ b/packages/shared/src/__tests__/loadClerkJsScript.spec.ts
@@ -4,13 +4,14 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { ClerkRuntimeError } from '../error';
import {
buildClerkJsScriptAttributes,
- buildClerkUiScriptAttributes,
+ buildClerkUIScriptAttributes,
buildScriptHost,
clerkJsScriptUrl,
- clerkUiScriptUrl,
+ clerkUIScriptUrl,
loadClerkJsScript,
- loadClerkUiScript,
+ loadClerkUIScript,
setClerkJsLoadingErrorPackageName,
+ shouldPrefetchClerkUI,
} from '../loadClerkJsScript';
import { loadScript } from '../loadScript';
import { getMajorVersion } from '../versionSelector';
@@ -165,13 +166,6 @@ describe('clerkJsScriptUrl()', () => {
expect(result).toBe(`https://example.clerk.com/npm/@clerk/clerk-js@${jsPackageMajorVersion}/dist/clerk.browser.js`);
});
- test('includes clerkJSVariant in URL when provided', () => {
- const result = clerkJsScriptUrl({ publishableKey: mockProdPublishableKey, clerkJSVariant: 'headless' });
- expect(result).toBe(
- `https://example.clerk.com/npm/@clerk/clerk-js@${jsPackageMajorVersion}/dist/clerk.headless.browser.js`,
- );
- });
-
test('uses provided clerkJSVersion', () => {
const result = clerkJsScriptUrl({ publishableKey: mockDevPublishableKey, clerkJSVersion: '6' });
expect(result).toContain('/npm/@clerk/clerk-js@6/');
@@ -269,7 +263,7 @@ describe('buildClerkJsScriptAttributes()', () => {
});
});
-describe('loadClerkUiScript(options)', () => {
+describe('loadClerkUIScript(options)', () => {
const mockPublishableKey = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk';
const mockClerkUi = {
@@ -291,7 +285,7 @@ describe('loadClerkUiScript(options)', () => {
});
test('throws error when publishableKey is missing', async () => {
- await expect(loadClerkUiScript({} as any)).rejects.toThrow(
+ await expect(loadClerkUIScript({} as any)).rejects.toThrow(
'@clerk/react: Missing publishableKey. You can get your key at https://dashboard.clerk.com/last-active?path=api-keys.',
);
});
@@ -299,13 +293,13 @@ describe('loadClerkUiScript(options)', () => {
test('returns null immediately when ClerkUI is already loaded', async () => {
(window as any).__internal_ClerkUiCtor = mockClerkUi;
- const result = await loadClerkUiScript({ publishableKey: mockPublishableKey });
+ const result = await loadClerkUIScript({ publishableKey: mockPublishableKey });
expect(result).toBeNull();
expect(loadScript).not.toHaveBeenCalled();
});
test('loads script and waits for ClerkUI to be available', async () => {
- const loadPromise = loadClerkUiScript({ publishableKey: mockPublishableKey });
+ const loadPromise = loadClerkUIScript({ publishableKey: mockPublishableKey });
// Simulate ClerkUI becoming available after 250ms
setTimeout(() => {
@@ -332,7 +326,7 @@ describe('loadClerkUiScript(options)', () => {
test('times out and rejects when ClerkUI does not load', async () => {
let rejectedWith: any;
- const loadPromise = loadClerkUiScript({ publishableKey: mockPublishableKey, scriptLoadTimeout: 1000 });
+ const loadPromise = loadClerkUIScript({ publishableKey: mockPublishableKey, scriptLoadTimeout: 1000 });
try {
vi.advanceTimersByTime(1000);
@@ -350,7 +344,7 @@ describe('loadClerkUiScript(options)', () => {
const mockExistingScript = document.createElement('script');
document.querySelector = vi.fn().mockReturnValue(mockExistingScript);
- const loadPromise = loadClerkUiScript({ publishableKey: mockPublishableKey });
+ const loadPromise = loadClerkUIScript({ publishableKey: mockPublishableKey });
// Simulate ClerkUI becoming available after 250ms
setTimeout(() => {
@@ -366,7 +360,7 @@ describe('loadClerkUiScript(options)', () => {
});
test('handles race condition when ClerkUI loads just as timeout fires', async () => {
- const loadPromise = loadClerkUiScript({ publishableKey: mockPublishableKey, scriptLoadTimeout: 1000 });
+ const loadPromise = loadClerkUIScript({ publishableKey: mockPublishableKey, scriptLoadTimeout: 1000 });
setTimeout(() => {
(window as any).__internal_ClerkUiCtor = mockClerkUi;
@@ -382,49 +376,49 @@ describe('loadClerkUiScript(options)', () => {
test('validates ClerkUI is properly loaded', async () => {
(window as any).__internal_ClerkUiCtor = mockClerkUi;
- const result = await loadClerkUiScript({ publishableKey: mockPublishableKey });
+ const result = await loadClerkUIScript({ publishableKey: mockPublishableKey });
expect(result).toBeNull();
expect((window as any).__internal_ClerkUiCtor).toBe(mockClerkUi);
});
});
-describe('clerkUiScriptUrl()', () => {
+describe('clerkUIScriptUrl()', () => {
const mockDevPublishableKey = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk';
const mockProdPublishableKey = 'pk_live_ZXhhbXBsZS5jbGVyay5jb20k'; // example.clerk.com
- test('returns clerkUiUrl when provided', () => {
+ test('returns clerkUIUrl when provided', () => {
const customUrl = 'https://custom.clerk.com/ui.js';
- const result = clerkUiScriptUrl({ clerkUiUrl: customUrl, publishableKey: mockDevPublishableKey });
+ const result = clerkUIScriptUrl({ clerkUIUrl: customUrl, publishableKey: mockDevPublishableKey });
expect(result).toBe(customUrl);
});
test('constructs URL correctly for development key', () => {
- const result = clerkUiScriptUrl({ publishableKey: mockDevPublishableKey });
+ const result = clerkUIScriptUrl({ publishableKey: mockDevPublishableKey });
expect(result).toBe(
`https://foo-bar-13.clerk.accounts.dev/npm/@clerk/ui@${uiPackageMajorVersion}/dist/ui.browser.js`,
);
});
test('constructs URL correctly for production key', () => {
- const result = clerkUiScriptUrl({ publishableKey: mockProdPublishableKey });
+ const result = clerkUIScriptUrl({ publishableKey: mockProdPublishableKey });
expect(result).toBe(`https://example.clerk.com/npm/@clerk/ui@${uiPackageMajorVersion}/dist/ui.browser.js`);
});
- test('uses provided clerkUiVersion', () => {
- const result = clerkUiScriptUrl({ publishableKey: mockDevPublishableKey, clerkUiVersion: '1' });
+ test('uses provided clerkUIVersion', () => {
+ const result = clerkUIScriptUrl({ publishableKey: mockDevPublishableKey, clerkUIVersion: '1' });
expect(result).toContain('/npm/@clerk/ui@1/');
});
test('uses latest as default version when not specified', () => {
- const result = clerkUiScriptUrl({ publishableKey: mockDevPublishableKey });
+ const result = clerkUIScriptUrl({ publishableKey: mockDevPublishableKey });
// When no version is specified, versionSelector should return the major version
expect(result).toContain(`/npm/@clerk/ui@${uiPackageMajorVersion}/`);
});
test('uses UI_PACKAGE_VERSION independently from JS_PACKAGE_VERSION', () => {
- // Verify that clerkUiScriptUrl uses UI_PACKAGE_VERSION, not JS_PACKAGE_VERSION
- const uiResult = clerkUiScriptUrl({ publishableKey: mockDevPublishableKey });
+ // Verify that clerkUIScriptUrl uses UI_PACKAGE_VERSION, not JS_PACKAGE_VERSION
+ const uiResult = clerkUIScriptUrl({ publishableKey: mockDevPublishableKey });
const jsResult = clerkJsScriptUrl({ publishableKey: mockDevPublishableKey });
// UI script should use UI package version
@@ -439,7 +433,7 @@ describe('clerkUiScriptUrl()', () => {
});
});
-describe('buildClerkUiScriptAttributes()', () => {
+describe('buildClerkUIScriptAttributes()', () => {
const mockPublishableKey = 'pk_test_Zm9vLWJhci0xMy5jbGVyay5hY2NvdW50cy5kZXYk';
const mockProxyUrl = 'https://proxy.clerk.com';
const mockDomain = 'custom.com';
@@ -467,6 +461,20 @@ describe('buildClerkUiScriptAttributes()', () => {
['no options', {}, {}],
])('returns correct attributes with %s', (_, input, expected) => {
// @ts-ignore input loses correct type because of empty object
- expect(buildClerkUiScriptAttributes(input)).toEqual(expected);
+ expect(buildClerkUIScriptAttributes(input)).toEqual(expected);
+ });
+});
+
+describe('shouldPrefetchClerkUI()', () => {
+ test('returns true when prefetchUI is undefined', () => {
+ expect(shouldPrefetchClerkUI(undefined)).toBe(true);
+ });
+
+ test('returns false when prefetchUI is false', () => {
+ expect(shouldPrefetchClerkUI(false)).toBe(false);
+ });
+
+ test('returns true when prefetchUI is true', () => {
+ expect(shouldPrefetchClerkUI(true)).toBe(true);
});
});
diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts
index 2e3a6012dbe..4471694f782 100644
--- a/packages/shared/src/loadClerkJsScript.ts
+++ b/packages/shared/src/loadClerkJsScript.ts
@@ -10,10 +10,9 @@ const { isDevOrStagingUrl } = createDevOrStagingUrlCache();
const errorThrower = buildErrorThrower({ packageName: '@clerk/shared' });
-export type LoadClerkJsScriptOptions = {
+export type LoadClerkJSScriptOptions = {
publishableKey: string;
clerkJSUrl?: string;
- clerkJSVariant?: 'headless' | '';
clerkJSVersion?: string;
sdkMetadata?: SDKMetadata;
proxyUrl?: string;
@@ -27,10 +26,24 @@ export type LoadClerkJsScriptOptions = {
scriptLoadTimeout?: number;
};
-export type LoadClerkUiScriptOptions = {
+/**
+ * @deprecated Use `LoadClerkJSScriptOptions` instead. This alias will be removed in a future major version.
+ */
+export type LoadClerkJsScriptOptions = LoadClerkJSScriptOptions;
+
+/**
+ * Determines whether the Clerk UI should be prefetched based on the `prefetchUI` option.
+ * @param prefetchUI - The prefetchUI option from ClerkProvider/options
+ * @returns `true` if UI should be prefetched, `false` if it should be skipped
+ */
+export const shouldPrefetchClerkUI = (prefetchUI: boolean | undefined): boolean => {
+ return prefetchUI !== false;
+};
+
+export type LoadClerkUIScriptOptions = {
publishableKey: string;
- clerkUiUrl?: string;
- clerkUiVersion?: string;
+ clerkUIUrl?: string;
+ clerkUIVersion?: string;
proxyUrl?: string;
domain?: string;
nonce?: string;
@@ -53,7 +66,7 @@ function isClerkGlobalProperlyLoaded(prop: 'Clerk' | '__internal_ClerkUiCtor'):
return !!val;
}
const isClerkProperlyLoaded = () => isClerkGlobalProperlyLoaded('Clerk');
-const isClerkUiProperlyLoaded = () => isClerkGlobalProperlyLoaded('__internal_ClerkUiCtor');
+const isClerkUIProperlyLoaded = () => isClerkGlobalProperlyLoaded('__internal_ClerkUiCtor');
/**
* Checks if an existing script has a request error using Performance API.
@@ -121,7 +134,7 @@ function hasScriptRequestError(scriptUrl: string): boolean {
* }
* ```
*/
-export const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promise => {
+export const loadClerkJSScript = async (opts?: LoadClerkJSScriptOptions): Promise => {
const timeout = opts?.scriptLoadTimeout ?? 15000;
const rejectWith = (error?: Error) =>
new ClerkRuntimeError('Failed to load Clerk JS' + (error?.message ? `, ${error.message}` : ''), {
@@ -138,7 +151,7 @@ export const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promis
return null;
}
- const scriptUrl = clerkJsScriptUrl(opts);
+ const scriptUrl = clerkJSScriptUrl(opts);
const existingScript = document.querySelector('script[data-clerk-js-script]');
if (existingScript) {
@@ -160,7 +173,7 @@ export const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promis
async: true,
crossOrigin: 'anonymous',
nonce: opts.nonce,
- beforeLoad: applyAttributesToScript(buildClerkJsScriptAttributes(opts)),
+ beforeLoad: applyAttributesToScript(buildClerkJSScriptAttributes(opts)),
}).catch(error => {
throw rejectWith(error);
});
@@ -168,7 +181,7 @@ export const loadClerkJsScript = async (opts?: LoadClerkJsScriptOptions): Promis
return loadPromise;
};
-export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promise => {
+export const loadClerkUIScript = async (opts?: LoadClerkUIScriptOptions): Promise => {
const timeout = opts?.scriptLoadTimeout ?? 15000;
const rejectWith = (error?: Error) =>
new ClerkRuntimeError('Failed to load Clerk UI' + (error?.message ? `, ${error.message}` : ''), {
@@ -176,7 +189,7 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis
cause: error,
});
- if (isClerkUiProperlyLoaded()) {
+ if (isClerkUIProperlyLoaded()) {
return null;
}
@@ -185,7 +198,7 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis
return null;
}
- const scriptUrl = clerkUiScriptUrl(opts);
+ const scriptUrl = clerkUIScriptUrl(opts);
const existingScript = document.querySelector('script[data-clerk-ui-script]');
if (existingScript) {
@@ -193,7 +206,7 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis
existingScript.remove();
} else {
try {
- await waitForPredicateWithTimeout(timeout, isClerkUiProperlyLoaded, rejectWith(), existingScript);
+ await waitForPredicateWithTimeout(timeout, isClerkUIProperlyLoaded, rejectWith(), existingScript);
return null;
} catch {
existingScript.remove();
@@ -201,13 +214,13 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis
}
}
- const loadPromise = waitForPredicateWithTimeout(timeout, isClerkUiProperlyLoaded, rejectWith());
+ const loadPromise = waitForPredicateWithTimeout(timeout, isClerkUIProperlyLoaded, rejectWith());
loadScript(scriptUrl, {
async: true,
crossOrigin: 'anonymous',
nonce: opts.nonce,
- beforeLoad: applyAttributesToScript(buildClerkUiScriptAttributes(opts)),
+ beforeLoad: applyAttributesToScript(buildClerkUIScriptAttributes(opts)),
}).catch(error => {
throw rejectWith(error);
});
@@ -215,32 +228,31 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis
return loadPromise;
};
-export const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => {
- const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts;
+export const clerkJSScriptUrl = (opts: LoadClerkJSScriptOptions) => {
+ const { clerkJSUrl, clerkJSVersion, proxyUrl, domain, publishableKey } = opts;
if (clerkJSUrl) {
return clerkJSUrl;
}
const scriptHost = buildScriptHost({ publishableKey, proxyUrl, domain });
- const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : '';
const version = versionSelector(clerkJSVersion);
- return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`;
+ return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.browser.js`;
};
-export const clerkUiScriptUrl = (opts: LoadClerkUiScriptOptions) => {
- const { clerkUiUrl, clerkUiVersion, proxyUrl, domain, publishableKey } = opts;
+export const clerkUIScriptUrl = (opts: LoadClerkUIScriptOptions) => {
+ const { clerkUIUrl, clerkUIVersion, proxyUrl, domain, publishableKey } = opts;
- if (clerkUiUrl) {
- return clerkUiUrl;
+ if (clerkUIUrl) {
+ return clerkUIUrl;
}
const scriptHost = buildScriptHost({ publishableKey, proxyUrl, domain });
- const version = versionSelector(clerkUiVersion, UI_PACKAGE_VERSION);
+ const version = versionSelector(clerkUIVersion, UI_PACKAGE_VERSION);
return `https://${scriptHost}/npm/@clerk/ui@${version}/dist/ui.browser.js`;
};
-export const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => {
+export const buildClerkJSScriptAttributes = (options: LoadClerkJSScriptOptions) => {
const obj: Record = {};
if (options.publishableKey) {
@@ -262,9 +274,9 @@ export const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions)
return obj;
};
-export const buildClerkUiScriptAttributes = (options: LoadClerkUiScriptOptions) => {
+export const buildClerkUIScriptAttributes = (options: LoadClerkUIScriptOptions) => {
// TODO @nikos do we need this?
- return buildClerkJsScriptAttributes(options);
+ return buildClerkJSScriptAttributes(options);
};
const applyAttributesToScript = (attributes: Record) => (script: HTMLScriptElement) => {
@@ -346,6 +358,26 @@ function waitForPredicateWithTimeout(
});
}
-export function setClerkJsLoadingErrorPackageName(packageName: string) {
+export function setClerkJSLoadingErrorPackageName(packageName: string) {
errorThrower.setPackageName({ packageName });
}
+
+/**
+ * @deprecated Use `loadClerkJSScript` instead. This alias will be removed in a future major version.
+ */
+export const loadClerkJsScript = loadClerkJSScript;
+
+/**
+ * @deprecated Use `clerkJSScriptUrl` instead. This alias will be removed in a future major version.
+ */
+export const clerkJsScriptUrl = clerkJSScriptUrl;
+
+/**
+ * @deprecated Use `buildClerkJSScriptAttributes` instead. This alias will be removed in a future major version.
+ */
+export const buildClerkJsScriptAttributes = buildClerkJSScriptAttributes;
+
+/**
+ * @deprecated Use `setClerkJSLoadingErrorPackageName` instead. This alias will be removed in a future major version.
+ */
+export const setClerkJsLoadingErrorPackageName = setClerkJSLoadingErrorPackageName;
diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts
index 314d186793b..bc00f1daa7b 100644
--- a/packages/shared/src/types/clerk.ts
+++ b/packages/shared/src/types/clerk.ts
@@ -1085,7 +1085,7 @@ export type ClerkOptions = ClerkOptionsNavigation &
/**
* Clerk UI entrypoint.
*/
- clerkUiCtor?: ClerkUiConstructor | Promise;
+ clerkUICtor?: ClerkUiConstructor | Promise;
/**
* Optional object to style your components. Will only affect [Clerk Components](https://clerk.com/docs/reference/components/overview) and not [Account Portal](https://clerk.com/docs/guides/account-portal/overview) pages.
*/
@@ -2411,10 +2411,6 @@ export type IsomorphicClerkOptions = Without & {
* The URL that `@clerk/clerk-js` should be hot-loaded from.
*/
clerkJSUrl?: string;
- /**
- * If your web application only uses [Control Components](https://clerk.com/docs/reference/components/overview#control-components), you can set this value to `'headless'` and load a minimal ClerkJS bundle for optimal page performance.
- */
- clerkJSVariant?: 'headless' | '';
/**
* The npm version for `@clerk/clerk-js`.
*/
@@ -2422,7 +2418,7 @@ export type IsomorphicClerkOptions = Without & {
/**
* The URL that `@clerk/ui` should be hot-loaded from.
*/
- clerkUiUrl?: string;
+ clerkUIUrl?: string;
/**
* The Clerk Publishable Key for your instance. This can be found on the [API keys](https://dashboard.clerk.com/last-active?path=api-keys) page in the Clerk Dashboard.
*/
@@ -2432,11 +2428,11 @@ export type IsomorphicClerkOptions = Without & {
*/
nonce?: string;
/**
- * @internal
- * This is a structural-only type for the `ui` object that can be passed
- * to Clerk.load() and ClerkProvider
+ * Controls prefetching of the `@clerk/ui` script.
+ * - `false` - Skip prefetching the UI (for custom UIs using Control Components)
+ * - `undefined` (default) - Prefetch UI normally
*/
- ui?: { version: string; url?: string };
+ prefetchUI?: boolean;
} & MultiDomainAndOrProxy;
export interface LoadedClerk extends Clerk {
diff --git a/packages/tanstack-react-start/src/client/types.ts b/packages/tanstack-react-start/src/client/types.ts
index 49e3a71c1b2..beccafca94f 100644
--- a/packages/tanstack-react-start/src/client/types.ts
+++ b/packages/tanstack-react-start/src/client/types.ts
@@ -17,8 +17,8 @@ export type ClerkState = {
__afterSignUpUrl: string | undefined;
__clerk_debug: any;
__clerkJSUrl: string | undefined;
- __clerkUiUrl: string | undefined;
__clerkJSVersion: string | undefined;
+ __prefetchUI: boolean | undefined;
__telemetryDisabled: boolean | undefined;
__telemetryDebug: boolean | undefined;
};
diff --git a/packages/tanstack-react-start/src/client/utils.ts b/packages/tanstack-react-start/src/client/utils.ts
index 3798f1b212f..c281a0437b2 100644
--- a/packages/tanstack-react-start/src/client/utils.ts
+++ b/packages/tanstack-react-start/src/client/utils.ts
@@ -19,7 +19,6 @@ export const pickFromClerkInitState = (
__signInUrl,
__signUpUrl,
__clerkJSUrl,
- __clerkUiUrl,
__clerkJSVersion,
__telemetryDisabled,
__telemetryDebug,
@@ -29,6 +28,7 @@ export const pickFromClerkInitState = (
__signUpFallbackRedirectUrl,
__keylessClaimUrl,
__keylessApiKeysUrl,
+ __prefetchUI,
} = clerkInitState || {};
return {
@@ -40,8 +40,8 @@ export const pickFromClerkInitState = (
signInUrl: __signInUrl,
signUpUrl: __signUpUrl,
clerkJSUrl: __clerkJSUrl,
- clerkUiUrl: __clerkUiUrl,
clerkJSVersion: __clerkJSVersion,
+ prefetchUI: __prefetchUI,
telemetry: {
disabled: __telemetryDisabled,
debug: __telemetryDebug,
@@ -56,17 +56,17 @@ export const pickFromClerkInitState = (
};
export const mergeWithPublicEnvs = (restInitState: any) => {
+ const envVars = getPublicEnvVariables();
return {
...restInitState,
- publishableKey: restInitState.publishableKey || getPublicEnvVariables().publishableKey,
- domain: restInitState.domain || getPublicEnvVariables().domain,
- isSatellite: restInitState.isSatellite || getPublicEnvVariables().isSatellite,
- signInUrl: restInitState.signInUrl || getPublicEnvVariables().signInUrl,
- signUpUrl: restInitState.signUpUrl || getPublicEnvVariables().signUpUrl,
- clerkJSUrl: restInitState.clerkJSUrl || getPublicEnvVariables().clerkJsUrl,
- clerkUiUrl: restInitState.clerkUiUrl || getPublicEnvVariables().clerkUiUrl,
- clerkJSVersion: restInitState.clerkJSVersion || getPublicEnvVariables().clerkJsVersion,
+ publishableKey: restInitState.publishableKey || envVars.publishableKey,
+ domain: restInitState.domain || envVars.domain,
+ isSatellite: restInitState.isSatellite || envVars.isSatellite,
+ signInUrl: restInitState.signInUrl || envVars.signInUrl,
+ signUpUrl: restInitState.signUpUrl || envVars.signUpUrl,
+ clerkJSUrl: restInitState.clerkJSUrl || envVars.clerkJsUrl,
+ clerkJSVersion: restInitState.clerkJSVersion || envVars.clerkJsVersion,
signInForceRedirectUrl: restInitState.signInForceRedirectUrl,
- clerkJSVariant: restInitState.clerkJSVariant || getPublicEnvVariables().clerkJsVariant,
+ prefetchUI: restInitState.prefetchUI ?? envVars.prefetchUI,
};
};
diff --git a/packages/tanstack-react-start/src/server/constants.ts b/packages/tanstack-react-start/src/server/constants.ts
index 7ef5ec26b8c..a757a2b9497 100644
--- a/packages/tanstack-react-start/src/server/constants.ts
+++ b/packages/tanstack-react-start/src/server/constants.ts
@@ -10,7 +10,7 @@ export const commonEnvs = () => {
// Public environment variables
CLERK_JS_VERSION: publicEnvs.clerkJsVersion,
CLERK_JS_URL: publicEnvs.clerkJsUrl,
- CLERK_UI_URL: publicEnvs.clerkUiUrl,
+ PREFETCH_UI: publicEnvs.prefetchUI,
PUBLISHABLE_KEY: publicEnvs.publishableKey,
DOMAIN: publicEnvs.domain,
PROXY_URL: publicEnvs.proxyUrl,
diff --git a/packages/tanstack-react-start/src/server/utils/index.ts b/packages/tanstack-react-start/src/server/utils/index.ts
index 780ea1d8e79..78774a88f98 100644
--- a/packages/tanstack-react-start/src/server/utils/index.ts
+++ b/packages/tanstack-react-start/src/server/utils/index.ts
@@ -16,10 +16,17 @@ export const wrapWithClerkState = (data: any) => {
};
/**
- * Returns the clerk state object and observability headers to be injected into a context.
+ * Returns the prefetchUI config from environment variables.
*
* @internal
*/
+function getPrefetchUIFromEnv(): boolean | undefined {
+ if (getEnvVariable('CLERK_PREFETCH_UI') === 'false') {
+ return false;
+ }
+ return undefined;
+}
+
export function getResponseClerkState(requestState: RequestState, additionalStateOptions: AdditionalStateOptions = {}) {
const { reason, message, isSignedIn, ...rest } = requestState;
@@ -35,8 +42,8 @@ export function getResponseClerkState(requestState: RequestState, additionalStat
__afterSignUpUrl: requestState.afterSignUpUrl,
__clerk_debug: debugRequestState(requestState),
__clerkJSUrl: getEnvVariable('CLERK_JS') || getEnvVariable('CLERK_JS_URL'),
- __clerkUiUrl: getEnvVariable('CLERK_UI_URL'),
__clerkJSVersion: getEnvVariable('CLERK_JS_VERSION'),
+ __prefetchUI: getPrefetchUIFromEnv(),
__telemetryDisabled: isTruthy(getEnvVariable('CLERK_TELEMETRY_DISABLED')),
__telemetryDebug: isTruthy(getEnvVariable('CLERK_TELEMETRY_DEBUG')),
__signInForceRedirectUrl:
diff --git a/packages/tanstack-react-start/src/utils/env.ts b/packages/tanstack-react-start/src/utils/env.ts
index bb8b008a0ee..c22da072878 100644
--- a/packages/tanstack-react-start/src/utils/env.ts
+++ b/packages/tanstack-react-start/src/utils/env.ts
@@ -14,9 +14,9 @@ export const getPublicEnvVariables = () => {
signInUrl: getValue('CLERK_SIGN_IN_URL'),
signUpUrl: getValue('CLERK_SIGN_UP_URL'),
clerkJsUrl: getValue('CLERK_JS_URL') || getValue('CLERK_JS'),
- clerkUiUrl: getValue('CLERK_UI_URL'),
- clerkJsVariant: getValue('CLERK_JS_VARIANT') as '' | 'headless' | undefined,
clerkJsVersion: getValue('CLERK_JS_VERSION'),
+ clerkUIUrl: getValue('CLERK_UI_URL'),
+ prefetchUI: getValue('CLERK_PREFETCH_UI') === 'false' ? false : undefined,
telemetryDisabled: isTruthy(getValue('CLERK_TELEMETRY_DISABLED')),
telemetryDebug: isTruthy(getValue('CLERK_TELEMETRY_DEBUG')),
afterSignInUrl: getValue('CLERK_AFTER_SIGN_IN_URL'),
diff --git a/packages/ui/src/utils/__tests__/warnAboutCustomizationWithoutPinning.test.ts b/packages/ui/src/utils/__tests__/warnAboutCustomizationWithoutPinning.test.ts
index 63f01603f13..5cfa02f7716 100644
--- a/packages/ui/src/utils/__tests__/warnAboutCustomizationWithoutPinning.test.ts
+++ b/packages/ui/src/utils/__tests__/warnAboutCustomizationWithoutPinning.test.ts
@@ -52,11 +52,11 @@ describe('warnAboutCustomizationWithoutPinning', () => {
expect(message).toContain('elements.card "& > div"');
});
- test('still warns when clerkUiCtor is provided without ui (CDN scenario)', () => {
- // clerkUiCtor is always set when loading from CDN, but ui is only set
+ test('still warns when clerkUICtor is provided without ui (CDN scenario)', () => {
+ // clerkUICtor is always set when loading from CDN, but ui is only set
// when the user explicitly imports @clerk/ui
warnAboutCustomizationWithoutPinning({
- clerkUiCtor: class MockClerkUi {} as any,
+ clerkUICtor: class MockClerkUi {} as any,
appearance: {
elements: { card: { '& > div': { color: 'red' } } },
},
diff --git a/packages/ui/src/utils/warnAboutCustomizationWithoutPinning.ts b/packages/ui/src/utils/warnAboutCustomizationWithoutPinning.ts
index 8ed7c9cf9b7..d08dfc6c931 100644
--- a/packages/ui/src/utils/warnAboutCustomizationWithoutPinning.ts
+++ b/packages/ui/src/utils/warnAboutCustomizationWithoutPinning.ts
@@ -129,7 +129,7 @@ function collectElementPatterns(elements: Record): string[] {
* If the user has explicitly imported @clerk/ui and passed it via the `ui` option,
* they have "pinned" their version and no warning is shown.
*
- * Note: We check `options.ui` (not `options.clerkUiCtor`) because clerkUiCtor is
+ * Note: We check `options.ui` (not `options.clerkUICtor`) because clerkUICtor is
* always set when loading from CDN via window.__internal_ClerkUiCtor.
*/
export function warnAboutCustomizationWithoutPinning(options?: ClerkOptions): void {
diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js
index 3eaab4af5af..f78a1ef9b63 100644
--- a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js
+++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js
@@ -205,6 +205,54 @@ import * as Clerk from '@clerk/nextjs';
export const Provider = ({ children }) => (
+);
+ `,
+ },
+ {
+ name: 'clerkJSVariant headless to prefetchUI false',
+ source: `
+import { ClerkProvider } from '@clerk/nextjs';
+
+export function App({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+ `,
+ output: `
+import { ClerkProvider } from '@clerk/nextjs';
+
+export function App({ children }) {
+ return (
+
+ {children}
+
+ );
+}
+ `,
+ },
+ {
+ name: 'clerkJSVariant empty string removal',
+ source: `
+import { ClerkProvider } from '@clerk/react';
+
+export const Provider = ({ children }) => (
+ {children}
+);
+ `,
+ output: `
+import { ClerkProvider } from '@clerk/react';
+
+export const Provider = ({ children }) => (
+ {children}
);
`,
},
diff --git a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs
index 30909cdcf9f..33b5f7783cb 100644
--- a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs
+++ b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs
@@ -1,10 +1,17 @@
const CLERK_PACKAGE_PREFIX = '@clerk/';
const COMPONENTS_WITH_HIDE_SLUG = new Set(['CreateOrganization', 'OrganizationSwitcher', 'OrganizationList']);
const COMPONENT_RENAMES = new Map([
- ['ClerkProvider', { afterSignInUrl: 'signInFallbackRedirectUrl', afterSignUpUrl: 'signUpFallbackRedirectUrl' }],
+ [
+ 'ClerkProvider',
+ {
+ afterSignInUrl: 'signInFallbackRedirectUrl',
+ afterSignUpUrl: 'signUpFallbackRedirectUrl',
+ },
+ ],
['SignIn', { afterSignInUrl: 'fallbackRedirectUrl', afterSignUpUrl: 'signUpFallbackRedirectUrl' }],
['SignUp', { afterSignInUrl: 'signInFallbackRedirectUrl', afterSignUpUrl: 'fallbackRedirectUrl' }],
]);
+const COMPONENTS_WITH_CLERK_JS_VARIANT = new Set(['ClerkProvider']);
const COMPONENT_REDIRECT_ATTR = new Map([
['ClerkProvider', { targetAttrs: ['signInFallbackRedirectUrl', 'signUpFallbackRedirectUrl'] }],
['SignIn', { targetAttrs: ['fallbackRedirectUrl'] }],
@@ -36,6 +43,12 @@ module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j,
}
}
+ if (COMPONENTS_WITH_CLERK_JS_VARIANT.has(canonicalName)) {
+ if (transformClerkJsVariantToPrefetchUI(j, jsxNode, stats)) {
+ dirty = true;
+ }
+ }
+
if (COMPONENTS_WITH_USER_BUTTON_REMOVALS.has(canonicalName)) {
const propsToRemove = COMPONENTS_WITH_USER_BUTTON_REMOVALS.get(canonicalName);
for (const attrName of propsToRemove) {
@@ -501,3 +514,46 @@ function renameEntityName(entity, oldName, newName) {
return false;
}
+
+function transformClerkJsVariantToPrefetchUI(j, jsxNode, stats) {
+ if (!jsxNode.attributes) {
+ return false;
+ }
+
+ const attrIndex = jsxNode.attributes.findIndex(attr => isJsxAttrNamed(attr, 'clerkJSVariant'));
+ if (attrIndex === -1) {
+ return false;
+ }
+
+ const attr = jsxNode.attributes[attrIndex];
+ const value = attr.value;
+
+ // Check if the value is "headless" (either as string literal or JSX expression)
+ let isHeadless = false;
+ if (value && value.type === 'StringLiteral') {
+ isHeadless = value.value === 'headless';
+ } else if (value && value.type === 'Literal') {
+ isHeadless = value.value === 'headless';
+ } else if (value && value.type === 'JSXExpressionContainer') {
+ const expr = value.expression;
+ if (expr.type === 'StringLiteral' || expr.type === 'Literal') {
+ isHeadless = expr.value === 'headless';
+ }
+ }
+
+ // Check if prefetchUI already exists
+ const prefetchUIExists = jsxNode.attributes.some(attr => isJsxAttrNamed(attr, 'prefetchUI'));
+
+ if (isHeadless && !prefetchUIExists) {
+ // Replace clerkJSVariant="headless" with prefetchUI={false}
+ const newAttr = j.jsxAttribute(j.jsxIdentifier('prefetchUI'), j.jsxExpressionContainer(j.booleanLiteral(false)));
+ jsxNode.attributes.splice(attrIndex, 1, newAttr);
+ stats('clerkJSVariantTransformed');
+ } else {
+ // Just remove the attribute
+ jsxNode.attributes.splice(attrIndex, 1);
+ stats('clerkJSVariantRemoved');
+ }
+
+ return true;
+}
diff --git a/packages/upgrade/src/versions/core-3/changes/clerk-js-variant-removed.md b/packages/upgrade/src/versions/core-3/changes/clerk-js-variant-removed.md
new file mode 100644
index 00000000000..545b7a2bd4b
--- /dev/null
+++ b/packages/upgrade/src/versions/core-3/changes/clerk-js-variant-removed.md
@@ -0,0 +1,25 @@
+---
+title: '`clerkJSVariant` prop removed'
+matcher:
+ - 'clerkJSVariant'
+ - 'headless'
+category: 'deprecation-removal'
+---
+
+The `clerkJSVariant` prop has been removed. Use `prefetchUI={false}` instead to disable prefetching the UI bundle. The UI will still be loaded on-demand when needed, but won't be prefetched during initial page load.
+
+A codemod is available to automatically apply this change:
+
+```diff
+
+```
+
+You can also disable UI prefetching via environment variable:
+
+- Next.js: `NEXT_PUBLIC_CLERK_PREFETCH_UI=false`
+- Astro: `PUBLIC_CLERK_PREFETCH_UI=false`
+- React Router / TanStack Start: `CLERK_PREFETCH_UI=false`
diff --git a/packages/vue/src/index.ts b/packages/vue/src/index.ts
index 6b04b6b5ce4..1e4e672d4e3 100644
--- a/packages/vue/src/index.ts
+++ b/packages/vue/src/index.ts
@@ -1,4 +1,4 @@
-import { setClerkJsLoadingErrorPackageName } from '@clerk/shared/loadClerkJsScript';
+import { setClerkJSLoadingErrorPackageName } from '@clerk/shared/loadClerkJsScript';
import { setErrorThrowerOptions } from './errors/errorThrower';
@@ -10,4 +10,4 @@ export { updateClerkOptions } from './utils';
export { getToken } from '@clerk/shared/getToken';
setErrorThrowerOptions({ packageName: PACKAGE_NAME });
-setClerkJsLoadingErrorPackageName(PACKAGE_NAME);
+setClerkJSLoadingErrorPackageName(PACKAGE_NAME);
diff --git a/packages/vue/src/plugin.ts b/packages/vue/src/plugin.ts
index 91a89ffe91d..dc1c83735b6 100644
--- a/packages/vue/src/plugin.ts
+++ b/packages/vue/src/plugin.ts
@@ -1,6 +1,11 @@
import { inBrowser } from '@clerk/shared/browser';
import { deriveState } from '@clerk/shared/deriveState';
-import { loadClerkJsScript, type LoadClerkJsScriptOptions, loadClerkUiScript } from '@clerk/shared/loadClerkJsScript';
+import {
+ loadClerkJSScript,
+ type LoadClerkJSScriptOptions,
+ loadClerkUIScript,
+ shouldPrefetchClerkUI,
+} from '@clerk/shared/loadClerkJsScript';
import type {
Clerk,
ClerkOptions,
@@ -70,23 +75,26 @@ export const clerkPlugin: Plugin<[PluginOptions]> = {
const options = {
...pluginOptions,
sdkMetadata: pluginOptions.sdkMetadata || SDK_METADATA,
- } as LoadClerkJsScriptOptions;
+ } as LoadClerkJSScriptOptions;
// We need this check for SSR apps like Nuxt as it will try to run this code on the server
- // and loadClerkJsScript contains browser-specific code
+ // and loadClerkJSScript contains browser-specific code
if (inBrowser()) {
void (async () => {
try {
- const clerkPromise = loadClerkJsScript(options);
- const clerkUiCtorPromise = pluginOptions.clerkUiCtor
- ? Promise.resolve(pluginOptions.clerkUiCtor)
- : (async () => {
- await loadClerkUiScript(options);
- if (!window.__internal_ClerkUiCtor) {
- throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.');
- }
- return window.__internal_ClerkUiCtor;
- })();
+ const clerkPromise = loadClerkJSScript(options);
+ // Skip UI loading when prefetchUI={false}
+ const clerkUICtorPromise = !shouldPrefetchClerkUI(pluginOptions.prefetchUI)
+ ? Promise.resolve(undefined)
+ : pluginOptions.clerkUICtor
+ ? Promise.resolve(pluginOptions.clerkUICtor)
+ : (async () => {
+ await loadClerkUIScript(options);
+ if (!window.__internal_ClerkUiCtor) {
+ throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.');
+ }
+ return window.__internal_ClerkUiCtor;
+ })();
await clerkPromise;
@@ -95,7 +103,7 @@ export const clerkPlugin: Plugin<[PluginOptions]> = {
}
clerk.value = window.Clerk;
- const loadOptions = { ...options, clerkUiCtor: clerkUiCtorPromise } as unknown as ClerkOptions;
+ const loadOptions = { ...options, clerkUICtor: clerkUICtorPromise } as unknown as ClerkOptions;
await window.Clerk.load(loadOptions);
loaded.value = true;
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 10b45161c90..f4176285e8f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2460,7 +2460,7 @@ packages:
'@expo/bunyan@4.0.1':
resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==}
- engines: {'0': node >=0.10.0}
+ engines: {node: '>=0.10.0'}
'@expo/cli@0.22.26':
resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==}