diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx index 42341d04adc..2db280398a6 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 ( { + test.describe.configure({ mode: 'serial' }); + let app: Application; + + test.beforeAll(async () => { + app = await appConfigs.next.appRouter.clone().commit(); + await app.setup(); + // Use withEmailCodes but add the headless variant + const env = appConfigs.envs.withEmailCodes.clone().setEnvVariable('public', 'CLERK_JS_VARIANT', 'headless'); + await app.withEnv(env); + await app.dev(); + }); + + test.afterAll(async () => { + await app.teardown(); + }); + + test('does not inject clerk-ui script when headless variant is used', 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/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index e1cbd520144..655d5c2695e 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -111,10 +111,16 @@ 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 for headless variant (no UI needed). */ async function getClerkUiEntryChunk( options?: AstroClerkCreateInstanceParams, -): Promise { +): Promise { + // Skip UI loading for headless variant + if (options?.clerkJSVariant === 'headless') { + return undefined; + } + if (options?.clerkUiCtor) { return options.clerkUiCtor; } diff --git a/packages/astro/src/server/build-clerk-hotload-script.ts b/packages/astro/src/server/build-clerk-hotload-script.ts index b3cf7d37089..44a5a5c6b2c 100644 --- a/packages/astro/src/server/build-clerk-hotload-script.ts +++ b/packages/astro/src/server/build-clerk-hotload-script.ts @@ -10,21 +10,17 @@ function buildClerkHotloadScript(locals: APIContext['locals']) { const proxyUrl = getSafeEnv(locals).proxyUrl!; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const domain = getSafeEnv(locals).domain!; + const clerkJsVariant = getSafeEnv(locals).clerkJsVariant; const clerkJsScriptSrc = clerkJsScriptUrl({ clerkJSUrl: getSafeEnv(locals).clerkJsUrl, - clerkJSVariant: getSafeEnv(locals).clerkJsVariant, + clerkJSVariant: clerkJsVariant, clerkJSVersion: getSafeEnv(locals).clerkJsVersion, domain, proxyUrl, publishableKey, }); - const clerkUiScriptSrc = clerkUiScriptUrl({ - clerkUiUrl: getSafeEnv(locals).clerkUiUrl, - domain, - proxyUrl, - publishableKey, - }); - return ` + + const clerkJsScript = ` + >`; + + // Skip clerk-ui script for headless variant + if (clerkJsVariant === 'headless') { + return clerkJsScript + '\n'; + } + + const clerkUiScriptSrc = clerkUiScriptUrl({ + clerkUiUrl: getSafeEnv(locals).clerkUiUrl, + domain, + proxyUrl, + publishableKey, + }); + + const clerkUiScript = ` \n`; + >`; + + return clerkJsScript + clerkUiScript + '\n'; } export { buildClerkHotloadScript }; diff --git a/packages/nextjs/src/utils/clerk-script.tsx b/packages/nextjs/src/utils/clerk-script.tsx index aceb76c4d9d..22d4a088277 100644 --- a/packages/nextjs/src/utils/clerk-script.tsx +++ b/packages/nextjs/src/utils/clerk-script.tsx @@ -70,12 +70,14 @@ export function ClerkScripts({ router }: { router: ClerkScriptProps['router'] }) dataAttribute='data-clerk-js-script' router={router} /> - + {clerkJSVariant !== 'headless' && ( + + )} ); } diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index b39bf352b8c..1de5a4ee295 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -508,7 +508,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { return global.Clerk; } - private async getClerkUiEntryChunk(): Promise { + private async getClerkUiEntryChunk(): Promise { + // Skip UI loading for headless variant + if (this.options.clerkJSVariant === 'headless') { + return undefined; + } + if (this.options.clerkUiCtor) { return this.options.clerkUiCtor; } diff --git a/packages/vue/src/plugin.ts b/packages/vue/src/plugin.ts index 91a89ffe91d..f6ed9933884 100644 --- a/packages/vue/src/plugin.ts +++ b/packages/vue/src/plugin.ts @@ -78,15 +78,19 @@ 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 for headless variant + const clerkUiCtorPromise = + pluginOptions.clerkJSVariant === 'headless' + ? 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;