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/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..b9e0064dc1c 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -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,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; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57d54b6b39e..3424adde0e6 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==} @@ -14908,10 +14908,12 @@ packages: whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}