diff --git a/.changeset/light-eagles-stay.md b/.changeset/light-eagles-stay.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/light-eagles-stay.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/integration/templates/custom-flows-react-vite/src/main.tsx b/integration/templates/custom-flows-react-vite/src/main.tsx index 7f8b6058c2c..3e718971953 100644 --- a/integration/templates/custom-flows-react-vite/src/main.tsx +++ b/integration/templates/custom-flows-react-vite/src/main.tsx @@ -23,7 +23,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/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 ( { // @ts-ignore publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY as string} 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={{ 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 }) { { + 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..68ebfcf200a 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_DISABLED?: 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..02c4156942a 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, 'PUBLIC_CLERK_PREFETCH_UI_DISABLED'), }, 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_DISABLED: envField.boolean({ 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..fdb73d1fa6f 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -2,6 +2,7 @@ import { loadClerkJsScript, loadClerkUiScript, setClerkJsLoadingErrorPackageName, + shouldPrefetchClerkUi, } from '@clerk/shared/loadClerkJsScript'; import type { ClerkOptions } from '@clerk/shared/types'; import type { ClerkUiConstructor } from '@clerk/shared/ui'; @@ -111,15 +112,20 @@ async function getClerkJsEntryChunk(options?: AstroClerkCre /** * 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( options?: AstroClerkCreateInstanceParams, -): Promise { +): Promise { + if (!shouldPrefetchClerkUi(options?.prefetchUI)) { + return undefined; + } + if (options?.clerkUiCtor) { return options.clerkUiCtor; } - await loadClerkUiScript(options); + 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..a18f0cbc804 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_DISABLED` is "true", 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 for disabled + if (import.meta.env.PUBLIC_CLERK_PREFETCH_UI_DISABLED === 'true') { + 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..0217bbf11f7 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 domain = env.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, + 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..e45d48848b9 100644 --- a/packages/astro/src/server/get-safe-env.ts +++ b/packages/astro/src/server/get-safe-env.ts @@ -21,6 +21,8 @@ function getContextEnvVar(envVarName: keyof InternalEnv, contextOrLocals: Contex * @internal */ function getSafeEnv(context: ContextOrLocals) { + const prefetchUIDisabled = getContextEnvVar('PUBLIC_CLERK_PREFETCH_UI_DISABLED', context) === 'true'; + return { domain: getContextEnvVar('PUBLIC_CLERK_DOMAIN', context), isSatellite: getContextEnvVar('PUBLIC_CLERK_IS_SATELLITE', context) === 'true', @@ -31,9 +33,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: prefetchUIDisabled ? 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/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/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/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..577a685cbf7 100644 --- a/packages/express/src/utils.ts +++ b/packages/express/src/utils.ts @@ -8,11 +8,14 @@ export const requestHasAuthObject = (req: ExpressRequest): req is ExpressRequest }; export const loadClientEnv = () => { + // Build prefetchUI config from env vars + const prefetchUIDisabled = process.env.CLERK_PREFETCH_UI_DISABLED === 'true'; + 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 || '', + prefetchUI: prefetchUIDisabled ? false : undefined, }; }; 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..9ff8b483a93 100644 --- a/packages/nextjs/src/utils/clerk-script.tsx +++ b/packages/nextjs/src/utils/clerk-script.tsx @@ -4,6 +4,7 @@ import { 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,12 +55,10 @@ export function ClerkScripts({ router }: { router: ClerkScriptProps['router'] }) publishableKey, clerkJSUrl, clerkJSVersion, - clerkJSVariant, + clerkUIUrl, nonce, domain, proxyUrl, - clerkUiVersion: ui?.version, - clerkUiUrl: ui?.url || clerkUiUrl, }; return ( @@ -70,12 +69,14 @@ export function ClerkScripts({ router }: { router: ClerkScriptProps['router'] }) dataAttribute='data-clerk-js-script' router={router} /> - + {shouldPrefetchClerkUi(prefetchUI) && ( + + )} ); } diff --git a/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts b/packages/nextjs/src/utils/mergeNextClerkPropsWithEnv.ts index dab4279d06a..f9cf73e45e7 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 for disabled + if (process.env.NEXT_PUBLIC_CLERK_PREFETCH_UI_DISABLED === 'true') { + 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..1e8fd4e89d7 100644 --- a/packages/nuxt/src/runtime/plugin.ts +++ b/packages/nuxt/src/runtime/plugin.ts @@ -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..990d73f866d 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, @@ -99,13 +101,13 @@ function ClerkProviderBase({ children, ...rest }: ClerkProv }; return ( - + awaitableNavigateRef.current?.(to)} routerReplace={(to: string) => awaitableNavigateRef.current?.(to, { replace: true })} initialState={__clerk_ssr_state} sdkMetadata={SDK_METADATA} - {...mergedProps} + {...(mergedProps as any)} {...restProps} > {children} 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..1184161e775 100644 --- a/packages/react-router/src/utils/env.ts +++ b/packages/react-router/src/utils/env.ts @@ -7,6 +7,9 @@ export const getPublicEnvVariables = (context: AppLoadContext | undefined) => { return getEnvVariable(`VITE_${name}`, context) || getEnvVariable(name, context); }; + // Build prefetchUI config from env vars + const prefetchUIDisabled = getValue('CLERK_PREFETCH_UI_DISABLED') === 'true'; + return { publishableKey: getValue('CLERK_PUBLISHABLE_KEY'), domain: getValue('CLERK_DOMAIN'), @@ -15,9 +18,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: prefetchUIDisabled ? 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..a0efe53f51b 100644 --- a/packages/react/src/__tests__/isomorphicClerk.test.ts +++ b/packages/react/src/__tests__/isomorphicClerk.test.ts @@ -7,6 +7,7 @@ import { IsomorphicClerk } from '../isomorphicClerk'; vi.mock('@clerk/shared/loadClerkJsScript', () => ({ 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/internal.ts b/packages/react/src/internal.ts index 26e71d2e998..2862d6f5ed1 100644 --- a/packages/react/src/internal.ts +++ b/packages/react/src/internal.ts @@ -9,6 +9,7 @@ export { clerkUiScriptUrl, buildClerkUiScriptAttributes, setClerkJsLoadingErrorPackageName, + shouldPrefetchClerkUi, } from '@clerk/shared/loadClerkJsScript'; export type { Ui } from '@clerk/ui/internal'; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index b39bf352b8c..2129b5bfc1b 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,7 +461,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } try { - const clerkUiCtor = this.getClerkUiEntryChunk(); + const clerkUiCtor = await this.getClerkUiEntryChunk(); const clerk = await this.getClerkJsEntryChunk(); if (!clerk.loaded) { @@ -508,15 +508,17 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { return global.Clerk; } - private async getClerkUiEntryChunk(): Promise { + private async getClerkUiEntryChunk(): Promise { + if (!shouldPrefetchClerkUi(this.options.prefetchUI)) { + return undefined; + } + if (this.options.clerkUiCtor) { return this.options.clerkUiCtor; } 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..16c2819f217 100644 --- a/packages/shared/src/__tests__/loadClerkJsScript.spec.ts +++ b/packages/shared/src/__tests__/loadClerkJsScript.spec.ts @@ -11,6 +11,7 @@ import { loadClerkJsScript, 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/'); @@ -393,9 +387,9 @@ 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); }); @@ -470,3 +464,17 @@ describe('buildClerkUiScriptAttributes()', () => { 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..4f8a464753d 100644 --- a/packages/shared/src/loadClerkJsScript.ts +++ b/packages/shared/src/loadClerkJsScript.ts @@ -13,7 +13,6 @@ const errorThrower = buildErrorThrower({ packageName: '@clerk/shared' }); export type LoadClerkJsScriptOptions = { publishableKey: string; clerkJSUrl?: string; - clerkJSVariant?: 'headless' | ''; clerkJSVersion?: string; sdkMetadata?: SDKMetadata; proxyUrl?: string; @@ -27,9 +26,18 @@ export type LoadClerkJsScriptOptions = { scriptLoadTimeout?: number; }; +/** + * 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; + clerkUIUrl?: string; clerkUiVersion?: string; proxyUrl?: string; domain?: string; @@ -216,23 +224,22 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis }; export const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => { - const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts; + 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; + const { clerkUIUrl, clerkUiVersion, proxyUrl, domain, publishableKey } = opts; - if (clerkUiUrl) { - return clerkUiUrl; + if (clerkUIUrl) { + return clerkUIUrl; } const scriptHost = buildScriptHost({ publishableKey, proxyUrl, domain }); diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 9d3f7945f38..a8ba952e3ab 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -2397,10 +2397,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`. */ @@ -2408,7 +2404,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. */ @@ -2418,11 +2414,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..b1cf507830e 100644 --- a/packages/tanstack-react-start/src/server/utils/index.ts +++ b/packages/tanstack-react-start/src/server/utils/index.ts @@ -16,10 +16,18 @@ 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 { + const prefetchUIDisabled = getEnvVariable('CLERK_PREFETCH_UI_DISABLED') === 'true'; + if (prefetchUIDisabled) { + return false; + } + return undefined; +} + export function getResponseClerkState(requestState: RequestState, additionalStateOptions: AdditionalStateOptions = {}) { const { reason, message, isSignedIn, ...rest } = requestState; @@ -35,8 +43,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..7af2041b74e 100644 --- a/packages/tanstack-react-start/src/utils/env.ts +++ b/packages/tanstack-react-start/src/utils/env.ts @@ -6,6 +6,9 @@ export const getPublicEnvVariables = () => { return getEnvVariable(`VITE_${name}`) || getEnvVariable(name); }; + // Build prefetchUI config from env vars + const prefetchUIDisabled = getValue('CLERK_PREFETCH_UI_DISABLED') === 'true'; + return { publishableKey: getValue('CLERK_PUBLISHABLE_KEY'), domain: getValue('CLERK_DOMAIN'), @@ -14,9 +17,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: prefetchUIDisabled ? 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/vue/src/plugin.ts b/packages/vue/src/plugin.ts index 91a89ffe91d..78c83882daa 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, @@ -78,15 +83,18 @@ export const clerkPlugin: Plugin<[PluginOptions]> = { 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; - })(); + // 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; 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==}