Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions integration/templates/next-app-router/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<ClerkProvider
clerkJSVariant={process.env.NEXT_PUBLIC_CLERK_JS_VARIANT === 'headless' ? 'headless' : undefined}
appearance={{
options: {
showOptionalFields: true,
Expand Down
32 changes: 32 additions & 0 deletions integration/tests/headless-variant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { expect, test } from '@playwright/test';

import type { Application } from '../models/application';
import { appConfigs } from '../presets';

test.describe('headless variant @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 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();
});
});
8 changes: 7 additions & 1 deletion packages/astro/src/internal/create-clerk-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,16 @@ async function getClerkJsEntryChunk<TUi extends Ui = Ui>(options?: AstroClerkCre
/**
* Gets the ClerkUI constructor, either from options or by loading the script.
* Returns early if window.__internal_ClerkUiCtor already exists.
* Returns undefined for headless variant (no UI needed).
*/
async function getClerkUiEntryChunk<TUi extends Ui = Ui>(
options?: AstroClerkCreateInstanceParams<TUi>,
): Promise<ClerkUiConstructor> {
): Promise<ClerkUiConstructor | undefined> {
// Skip UI loading for headless variant
if (options?.clerkJSVariant === 'headless') {
return undefined;
}

if (options?.clerkUiCtor) {
return options.clerkUiCtor;
}
Expand Down
32 changes: 22 additions & 10 deletions packages/astro/src/server/build-clerk-hotload-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,49 @@ 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 = `
<script src="${clerkJsScriptSrc}"
data-clerk-js-script
async
crossOrigin='anonymous'
${publishableKey ? `data-clerk-publishable-key="${publishableKey}"` : ``}
${proxyUrl ? `data-clerk-proxy-url="${proxyUrl}"` : ``}
${domain ? `data-clerk-domain="${domain}"` : ``}
></script>
></script>`;

// 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 = `
<script src="${clerkUiScriptSrc}"
data-clerk-ui-script
async
crossOrigin='anonymous'
${publishableKey ? `data-clerk-publishable-key="${publishableKey}"` : ``}
${proxyUrl ? `data-clerk-proxy-url="${proxyUrl}"` : ``}
${domain ? `data-clerk-domain="${domain}"` : ``}
></script>\n`;
></script>`;

return clerkJsScript + clerkUiScript + '\n';
}

export { buildClerkHotloadScript };
14 changes: 8 additions & 6 deletions packages/nextjs/src/utils/clerk-script.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,14 @@ export function ClerkScripts({ router }: { router: ClerkScriptProps['router'] })
dataAttribute='data-clerk-js-script'
router={router}
/>
<ClerkScript
scriptUrl={clerkUiScriptUrl(opts)}
attributes={buildClerkUiScriptAttributes(opts)}
dataAttribute='data-clerk-ui-script'
router={router}
/>
{clerkJSVariant !== 'headless' && (
<ClerkScript
scriptUrl={clerkUiScriptUrl(opts)}
attributes={buildClerkUiScriptAttributes(opts)}
dataAttribute='data-clerk-ui-script'
router={router}
/>
)}
</>
);
}
7 changes: 6 additions & 1 deletion packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
return global.Clerk;
}

private async getClerkUiEntryChunk(): Promise<ClerkUiConstructor> {
private async getClerkUiEntryChunk(): Promise<ClerkUiConstructor | undefined> {
// Skip UI loading for headless variant
if (this.options.clerkJSVariant === 'headless') {
return undefined;
}

if (this.options.clerkUiCtor) {
return this.options.clerkUiCtor;
}
Expand Down
22 changes: 13 additions & 9 deletions packages/vue/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [email protected].');
}
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 [email protected].');
}
return window.__internal_ClerkUiCtor;
})();

await clerkPromise;

Expand Down
Loading