Skip to content

Conversation

@jacekradko
Copy link
Member

@jacekradko jacekradko commented Jan 21, 2026

Summary

Fixes: USER-4231

This PR removes the clerkJSVariant: 'headless' option and replaces it with a new prefetchUI prop that controls whether the @clerk/ui script is prefetched.

Changes

  • Removed: clerkJSVariant: 'headless' option from all SDK packages
  • Added: prefetchUI prop (boolean | undefined) to control UI bundle prefetching
    • prefetchUI={false} - Disable prefetching the UI bundle (UI will still load on-demand when needed)
    • prefetchUI omitted or undefined - Prefetch UI normally (default behavior)
  • Added: shouldPrefetchClerkUI helper function
  • Added: Codemod to automatically migrate clerkJSVariant="headless" to prefetchUI={false}
  • Renamed: clerkUiCtorclerkUICtor for casing consistency
  • Deprecated: ClerkJs casing in favor of ClerkJS for consistency
    • loadClerkJsScriptloadClerkJSScript
    • clerkJsScriptUrlclerkJSScriptUrl
    • buildClerkJsScriptAttributesbuildClerkJSScriptAttributes
    • setClerkJsLoadingErrorPackageNamesetClerkJSLoadingErrorPackageName
    • LoadClerkJsScriptOptionsLoadClerkJSScriptOptions
    • Old names are deprecated aliases that will be removed in a future major version

Environment Variables

You can 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

Usage

// Disable UI prefetching (e.g., when using Control Components for custom UI)
<ClerkProvider prefetchUI={false}>
  {children}
</ClerkProvider>

Packages Updated

  • @clerk/clerk-js (major)
  • @clerk/shared (major)
  • @clerk/react
  • @clerk/nextjs
  • @clerk/astro
  • @clerk/nuxt
  • @clerk/vue
  • @clerk/react-router
  • @clerk/tanstack-react-start
  • @clerk/expo
  • @clerk/chrome-extension
  • @clerk/express
  • @clerk/upgrade (codemod added)

Checklist

  • pnpm build passes
  • Updated integration tests
  • Updated integration templates
  • Added codemod for migration
  • Added upgrade documentation

Summary by CodeRabbit

  • Breaking Changes

    • Removed clerkJSVariant/headless bundle; renamed public options: clerkUiUrl → clerkUIUrl, clerkUiCtor → clerkUICtor.
  • New Features

    • Added prefetchUI prop to control UI prefetching (prefetchUI={false} disables).
    • Added environment flags to disable UI prefetching (PUBLIC_CLERK_PREFETCH_UI / CLERK_PREFETCH_UI).
  • Chores

    • Package version bumps across multiple packages.
  • Tests / Docs

    • Updated tests, codemods, and upgrade docs for migration.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Jan 21, 2026

🦋 Changeset detected

Latest commit: 5cff9a0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@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
@clerk/agent-toolkit Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/testing Patch
@clerk/ui Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link

vercel bot commented Jan 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jan 26, 2026 4:32pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

The PR removes the clerkJSVariant option and deletes the headless bundle and related entrypoints/exports. It introduces a boolean prefetchUI flag (and CLERK_PREFETCH_UI handling) to control UI prefetching, renames clerkUiUrlclerkUIUrl, and refactors script-loading APIs and names (e.g., loadClerkJsScriptloadClerkJSScript, clerkUiScriptUrlclerkUIScriptUrl) and adds shouldPrefetchClerkUI. A new native variant (clerk.native) and package export changes replace headless artifacts. Multiple integrations, env helpers, types, tests, codemods, and docs were updated accordingly.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the primary change: removal of the headless variant from clerk-js, which is the main objective of this PR.
Linked Issues check ✅ Passed All code requirements from USER-4231 are met: headless bundle removed, prefetchUI prop added for controlling UI prefetch, shouldPrefetchClerkUI helper implemented, codemod for migration provided, and API standardization completed.
Out of Scope Changes check ✅ Passed Changes are focused on removing the headless variant and adding prefetchUI mechanisms. Minor updates like React DOM version pins and workflow adjustments are auxiliary to the core objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.changeset/light-eagles-stay.md:
- Around line 1-2: The changeset is empty and must document the breaking changes
introduced: explicitly list affected package names and semver bumps (use major
bumps for breaking changes), describe the removals of clerkJSVariant and the old
import path `@clerk/clerk-js/headless`, and note the new ui prop and its migration
steps; update the changeset body to include a short summary, per-package entries
(e.g., package-name: major), and a brief migration note explaining how to
replace/remove clerkJSVariant, how to update import paths to the new
entrypoints, and how to adopt the new ui prop so consumers can upgrade safely.

In `@packages/expo/src/provider/singleton/createClerkInstance.ts`:
- Around line 1-2: The import path for FapiRequestInit/FapiResponse is invalid;
instead derive request/response types from the public Clerk API: create a
Handler type from Parameters<Clerk["__internal_onBeforeRequest"]>[0], then
extract Req = Parameters<Handler>[0] and use Req as the type for the requestInit
parameter, and similarly extract the response type from
Clerk["__internal_onAfterResponse"] to replace FapiResponse usage; update
function signatures in createClerkInstance.ts to use these derived types
(referencing Clerk, __internal_onBeforeRequest, __internal_onAfterResponse,
Handler, and Req).
🧹 Nitpick comments (1)
packages/react-router/src/client/ReactRouterClerkProvider.tsx (1)

102-109: Type safety concern with as any casts.

The as any casts on lines 102 and 108 bypass TypeScript's type checking. While this may be a workaround for complex generic constraints with TUi, it could mask type mismatches at runtime.

Consider whether these casts can be replaced with more precise type assertions or if the underlying types can be aligned to avoid the need for any.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 21, 2026

Open in StackBlitz

@clerk/agent-toolkit

npm i https://pkg.pr.new/@clerk/agent-toolkit@7629

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@7629

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@7629

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@7629

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@7629

@clerk/dev-cli

npm i https://pkg.pr.new/@clerk/dev-cli@7629

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@7629

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@7629

@clerk/express

npm i https://pkg.pr.new/@clerk/express@7629

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@7629

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@7629

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@7629

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@7629

@clerk/react

npm i https://pkg.pr.new/@clerk/react@7629

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@7629

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@7629

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@7629

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@7629

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@7629

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@7629

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@7629

commit: 5cff9a0

@jacekradko jacekradko changed the title feat(clerk-js): remove headless variant, add ui prop feat(clerk-js): remove headless variant, add prefetchUI prop Jan 21, 2026
Copy link
Member

@nikosdouvlis nikosdouvlis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good! Few minor things:

clerkUIVersion in isomorphicClerk.ts

The old code pulled this.options.ui?.version into clerkUiVersion when calling loadClerkUiScript. Now it just spreads this.options, but there's no clerkUIVersion on IsomorphicClerkOptions, so the version never actually makes it to the script URL builder. Was this dead code before, or do we need to add the field?

clerkUiCtor casing

We renamed clerkUiUrlclerkUIUrl to match clerkJSUrl, but clerkUiCtor still has the old casing. Should probably be clerkUICtor while we're at it

Env var naming

CLERK_PREFETCH_UI_DISABLED=true mapping to prefetchUI: false is a double negation. Can we just use CLERK_PREFETCH_UI=false instead? Same for the framework-prefixed variants (NEXT_PUBLIC_CLERK_PREFETCH_UI, PUBLIC_CLERK_PREFETCH_UI, etc.)

@jacekradko
Copy link
Member Author

@nikosdouvlis Addressed your feedback. Thanks for going through it!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@integration/templates/next-app-router/src/app/layout.tsx`:
- Line 15: The prefetch toggle currently checks the wrong env var
(`NEXT_PUBLIC_CLERK_PREFETCH_UI_DISABLED`) so documented users won't disable
prefetching; update the prop check in app/layout.tsx for the prefetchUI prop to
read `process.env.NEXT_PUBLIC_CLERK_PREFETCH_UI === 'false' ? false : undefined`
(keeping undefined as the default), referencing the existing prefetchUI prop to
locate and replace the condition.

In `@packages/astro/src/server/build-clerk-hotload-script.ts`:
- Around line 37-42: The Astro hotload script builder is not passing the
extracted PUBLIC_CLERK_UI_VERSION through, so pinned UI versions are ignored;
update getSafeEnv to include clerkUIVersion in its returned object (extracting
PUBLIC_CLERK_UI_VERSION into clerkUIVersion) and then forward that value into
the clerkUIScriptUrl call (the clerkUiScriptSrc creation) by adding
clerkUIVersion to the argument list so clerkUIScriptUrl can select the correct
UI package version.

Comment on lines 37 to 43
const clerkUiScriptSrc = clerkUIScriptUrl({
clerkUIUrl: env.clerkUIUrl,
domain,
proxyUrl,
publishableKey,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find and read the specific file and lines mentioned
fd "build-clerk-hotload-script.ts" packages/astro/src --exec wc -l {} \;

Repository: clerk/javascript

Length of output: 118


🏁 Script executed:

#!/bin/bash
# Read the file to see the actual code at lines 37-42
cat -n packages/astro/src/server/build-clerk-hotload-script.ts | sed -n '30,50p'

Repository: clerk/javascript

Length of output: 743


🏁 Script executed:

#!/bin/bash
# Search for clerkUIVersion usage in astro package
rg "clerkUIVersion" packages/astro/src -n -B 2 -A 2

Repository: clerk/javascript

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Find clerkUIScriptUrl definition to check its parameters
rg "clerkUIScriptUrl" packages/astro/src -n -A 5

Repository: clerk/javascript

Length of output: 1156


🏁 Script executed:

#!/bin/bash
# Check getSafeEnv to see what environment variables are included
rg "getSafeEnv\|clerkUIVersion" packages/astro/src -n -B 2 -A 10

Repository: clerk/javascript

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Read the getSafeEnv function to see what env variables are provided
cat -n packages/astro/src/server/get-safe-env.ts

Repository: clerk/javascript

Length of output: 3182


🏁 Script executed:

#!/bin/bash
# Find clerkUIScriptUrl definition in shared package to check parameters
fd "loadClerkJsScript" --type f | head -5

Repository: clerk/javascript

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Search broadly for clerkUIVersion in the entire repo
rg "clerkUIVersion" --type ts --type tsx --max-count 20 | head -30

Repository: clerk/javascript

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Read the clerkUIScriptUrl definition to check its signature
cat -n packages/shared/src/loadClerkJsScript.ts

Repository: clerk/javascript

Length of output: 14498


🏁 Script executed:

#!/bin/bash
# Search for clerkUIVersion more broadly - might be in comments or different spelling
rg "UI.*[Vv]ersion|ui.*version" packages/astro/src -i -n

Repository: clerk/javascript

Length of output: 360


Forward clerkUIVersion into the UI script URL builder after extracting it in getSafeEnv.

clerkUIScriptUrl accepts clerkUIVersion and uses it to determine the UI package version (see packages/shared/src/loadClerkJsScript.ts:244-251), but the Astro integration doesn't extract PUBLIC_CLERK_UI_VERSION from the environment in getSafeEnv. This means pinned UI versions in PUBLIC_CLERK_UI_VERSION are currently ignored in Astro hotload mode.

The fix requires two changes:

  1. Add clerkUIVersion to the getSafeEnv return object in packages/astro/src/server/get-safe-env.ts
  2. Forward it to clerkUIScriptUrl in packages/astro/src/server/build-clerk-hotload-script.ts:37-42
🛠️ Proposed changes

In packages/astro/src/server/get-safe-env.ts:

  clerkJsVersion: getContextEnvVar('PUBLIC_CLERK_JS_VERSION', context),
  clerkUIUrl: getContextEnvVar('PUBLIC_CLERK_UI_URL', context),
+ clerkUIVersion: getContextEnvVar('PUBLIC_CLERK_UI_VERSION', context),
  prefetchUI: getContextEnvVar('PUBLIC_CLERK_PREFETCH_UI', context) === 'false' ? false : undefined,

In packages/astro/src/server/build-clerk-hotload-script.ts (lines 37-42):

  const clerkUiScriptSrc = clerkUIScriptUrl({
    clerkUIUrl: env.clerkUIUrl,
+   clerkUIVersion: env.clerkUIVersion,
    domain,
    proxyUrl,
    publishableKey,
  });
🤖 Prompt for AI Agents
In `@packages/astro/src/server/build-clerk-hotload-script.ts` around lines 37 -
42, The Astro hotload script builder is not passing the extracted
PUBLIC_CLERK_UI_VERSION through, so pinned UI versions are ignored; update
getSafeEnv to include clerkUIVersion in its returned object (extracting
PUBLIC_CLERK_UI_VERSION into clerkUIVersion) and then forward that value into
the clerkUIScriptUrl call (the clerkUiScriptSrc creation) by adding
clerkUIVersion to the argument list so clerkUIScriptUrl can select the correct
UI package version.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/clerk-js/rspack.config.js (1)

283-297: Add LimitChunkCountPlugin to force the native build into a single chunk.

Dynamic imports exist in production code (e.g., moduleManager.ts, clerk.ts) which would create separate chunks despite splitChunks: false. React Native cannot load those additional chunks at runtime. Match the pattern used for other single-bundle variants:

Proposed fix
     {
       output: {
         publicPath: '',
       },
+      plugins: [
+        new rspack.optimize.LimitChunkCountPlugin({
+          maxChunks: 1,
+        }),
+      ],
       optimization: {
         splitChunks: false,
       },
     },
🤖 Fix all issues with AI agents
In `@packages/astro/src/env.d.ts`:
- Around line 8-9: Add the missing PUBLIC_CLERK_UI_VERSION declaration to the
InternalEnv type: update the InternalEnv interface/type (where
PUBLIC_CLERK_UI_URL and PUBLIC_CLERK_PREFETCH_UI are declared) to include a
readonly PUBLIC_CLERK_UI_VERSION?: string; so TypeScript knows this env var
exists and the use in get-safe-env.ts compiles cleanly.

In `@packages/astro/src/server/get-safe-env.ts`:
- Around line 35-37: Add the missing PUBLIC_CLERK_UI_VERSION declaration to the
InternalEnv type in env.d.ts so the usage in get-safe-env.ts (specifically the
clerkUIVersion key and getContextEnvVar('PUBLIC_CLERK_UI_VERSION', context)) has
a matching type; update the InternalEnv interface/type to include
PUBLIC_CLERK_UI_VERSION with the appropriate string (or optional string) type to
resolve the TS2345 build error.
🧹 Nitpick comments (1)
packages/shared/src/loadClerkJsScript.ts (1)

277-280: TODO comment should be addressed.

Line 278 contains a TODO comment questioning whether this delegation is needed. This should be resolved before merging or tracked in an issue.

Do you want me to open an issue to track this TODO, or should it be addressed in this PR?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/react/src/isomorphicClerk.ts`:
- Around line 511-518: The function getClerkUiEntryChunk currently returns
undefined early when shouldPrefetchClerkUI(this.options.prefetchUI) is false, so
any provided this.options.clerkUICtor is ignored; reorder the checks in
getClerkUiEntryChunk so you first return this.options.clerkUICtor if present,
and only consult shouldPrefetchClerkUI afterwards. Apply the same reorder in
create-clerk-instance (the equivalent initialization function there) so a
supplied clerkUICtor is honored even when prefetchUI is false.
♻️ Duplicate comments (2)
packages/astro/src/server/get-safe-env.ts (1)

35-37: Ensure PUBLIC_CLERK_UI_VERSION is declared in InternalEnv.

This concern was raised in a previous review. Line 36 references 'PUBLIC_CLERK_UI_VERSION' which must be added to the InternalEnv type in packages/astro/src/env.d.ts to avoid TypeScript errors (TS2345).

packages/astro/src/server/build-clerk-hotload-script.ts (1)

37-43: Past review concern addressed.

The clerkUIVersion is now properly forwarded to clerkUIScriptUrl on line 39, resolving the previously flagged issue about pinned UI versions being ignored.

🧹 Nitpick comments (1)
packages/astro/src/server/build-clerk-hotload-script.ts (1)

9-13: Non-null assertions on potentially undefined values.

The non-null assertions (!) on env.pk, env.proxyUrl, and env.domain could cause runtime issues if these environment variables are not configured. While publishableKey is typically required, proxyUrl and domain are optional and commonly undefined.

Consider removing the non-null assertions for optional values:

Suggested fix
-  // eslint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-  const publishableKey = env.pk!;
-  // eslint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-  const proxyUrl = env.proxyUrl!;
-  // eslint-disable-next-line `@typescript-eslint/no-non-null-assertion`
-  const domain = env.domain!;
+  const publishableKey = env.pk ?? '';
+  const proxyUrl = env.proxyUrl;
+  const domain = env.domain;

Comment on lines +511 to 518
private async getClerkUiEntryChunk(): Promise<ClerkUiConstructor | undefined> {
if (!shouldPrefetchClerkUI(this.options.prefetchUI)) {
return undefined;
}

if (this.options.clerkUICtor) {
return this.options.clerkUICtor;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Honor provided clerkUICtor even when prefetchUI is false

Line 512–518 returns undefined before checking this.options.clerkUICtor, so consumers who supply a bundled UI ctor and set prefetchUI={false} lose the ctor and may fail to render UI without an extra script load. This is a functional regression. The same ordering issue exists in packages/astro/src/internal/create-clerk-instance.ts (Line 120–126).

Proposed fix (reorder checks)
-  if (!shouldPrefetchClerkUI(this.options.prefetchUI)) {
-    return undefined;
-  }
-
-  if (this.options.clerkUICtor) {
-    return this.options.clerkUICtor;
-  }
+  if (this.options.clerkUICtor) {
+    return this.options.clerkUICtor;
+  }
+
+  if (!shouldPrefetchClerkUI(this.options.prefetchUI)) {
+    return undefined;
+  }
🤖 Prompt for AI Agents
In `@packages/react/src/isomorphicClerk.ts` around lines 511 - 518, The function
getClerkUiEntryChunk currently returns undefined early when
shouldPrefetchClerkUI(this.options.prefetchUI) is false, so any provided
this.options.clerkUICtor is ignored; reorder the checks in getClerkUiEntryChunk
so you first return this.options.clerkUICtor if present, and only consult
shouldPrefetchClerkUI afterwards. Apply the same reorder in
create-clerk-instance (the equivalent initialization function there) so a
supplied clerkUICtor is honored even when prefetchUI is false.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants