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
24 changes: 23 additions & 1 deletion packages/clerk-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@
"types": "./dist/types/index.d.ts",
"default": "./dist/clerk.no-rhc.js"
}
},
"./bundled": {
"react-server": {
"types": "./dist/esm/server.d.mts",
"import": "./dist/esm/server.mjs",
"default": "./dist/esm/server.mjs"
},
"types": "./dist/esm/bundled.d.mts",
"import": "./dist/esm/bundled.mjs",
"default": "./dist/esm/bundled.mjs"
},
"./entry": {
"types": "./dist/esm/entry.d.mts",
"import": "./dist/esm/entry.mjs",
"default": "./dist/esm/entry.mjs"
},
"./internal": {
"types": "./dist/esm/internal/index.d.mts",
"import": "./dist/esm/internal/index.mjs",
"default": "./dist/esm/internal/index.mjs"
}
},
"main": "dist/clerk.js",
Expand All @@ -56,9 +76,10 @@
"no-rhc"
],
"scripts": {
"build": "pnpm build:bundle && pnpm build:declarations",
"build": "pnpm build:bundle && pnpm build:esm && pnpm build:declarations",
"build:analyze": "rspack build --config rspack.config.js --env production --env variant=\"clerk.browser\" --env analysis --analyze",
"build:bundle": "pnpm clean && rspack build --config rspack.config.js --env production",
"build:esm": "tsdown",
"build:declarations": "tsc -p tsconfig.declarations.json",
"build:sandbox": "pnpm --filter @clerk/ui build:umd && rspack build --config rspack.config.js --env production --env sandbox",
"build:stats": "rspack build --config rspack.config.js --env production --json=stats.json --env variant=\"clerk.browser\"",
Expand Down Expand Up @@ -114,6 +135,7 @@
"bundlewatch": "^0.4.1",
"jsdom": "26.1.0",
"minimatch": "^10.0.3",
"tsdown": "catalog:repo",
"webpack-merge": "^5.10.0"
},
"engines": {
Expand Down
27 changes: 27 additions & 0 deletions packages/clerk-js/src/bundled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Js } from './internal';
import { JS_BRAND } from './internal';

import { Clerk } from './core/clerk';

declare const PACKAGE_VERSION: string;

/**
* JS object for bundled Clerk JS.
* Pass this to ClerkProvider to use the bundled clerk-js instead of loading from CDN.
*
* @example
* ```tsx
* import { js } from '@clerk/clerk-js/bundled';
*
* <ClerkProvider js={js}>
* ...
* </ClerkProvider>
* ```
*/
export const js = {
__brand: JS_BRAND,
version: PACKAGE_VERSION,
ClerkJS: Clerk,
} as unknown as Js;

export type { Js } from './internal';
5 changes: 5 additions & 0 deletions packages/clerk-js/src/entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Entry point for dynamic import of ClerkJS constructor.
* Used by the SDK when the js prop is a server-safe marker (without ClerkJS constructor).
*/
export { Clerk as ClerkJs } from './core/clerk';
35 changes: 35 additions & 0 deletions packages/clerk-js/src/internal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { BrowserClerkConstructor } from '@clerk/shared/types';

declare const Tags: unique symbol;
type Tagged<BaseType, Tag extends PropertyKey> = BaseType & { [Tags]: { [K in Tag]: void } };

/**
* Runtime brand value to identify valid JS objects
*/
export const JS_BRAND = '__clerkJS' as const;

/**
* Js type that carries type information via phantom property
* Tagged to ensure only official js objects from @clerk/clerk-js can be used
*
* ClerkJS is optional to support server-safe marker exports (react-server condition).
* When ClerkJS is absent, the SDK will dynamically import it.
*/
export type Js = Tagged<
{
/**
* Runtime brand to identify valid JS objects
*/
__brand: typeof JS_BRAND;
/**
* ClerkJS constructor. Optional to support server-safe marker exports.
* When absent (e.g., in React Server Components), the SDK resolves it via dynamic import.
*/
ClerkJS?: BrowserClerkConstructor;
/**
* Version of the JS package (for potential future use)
*/
version?: string;
},
'ClerkJs'
>;
28 changes: 28 additions & 0 deletions packages/clerk-js/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Js } from './internal';
import { JS_BRAND } from './internal';

declare const PACKAGE_VERSION: string;

/**
* Server-safe JS marker for React Server Components.
*
* This export does not include the ClerkJS constructor, making it safe to import
* in server components. The constructor is resolved via dynamic import when needed.
*
* @example
* ```tsx
* // app/layout.tsx (server component)
* import { ClerkProvider } from '@clerk/nextjs';
* import { js } from '@clerk/clerk-js/bundled';
*
* export default function Layout({ children }) {
* return <ClerkProvider js={js}>{children}</ClerkProvider>;
* }
* ```
*/
export const js = {
__brand: JS_BRAND,
version: PACKAGE_VERSION,
} as unknown as Js;

export type { Js } from './internal';
35 changes: 35 additions & 0 deletions packages/clerk-js/tsdown.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Options } from 'tsdown';
import { defineConfig } from 'tsdown';

import clerkJsPackage from './package.json' with { type: 'json' };

export default defineConfig(({ watch }) => {
const common = {
dts: true,
sourcemap: true,
clean: false,
target: 'es2022',
platform: 'browser',
external: ['@clerk/shared'],
format: ['esm'],
minify: false,
define: {
PACKAGE_NAME: `"${clerkJsPackage.name}"`,
PACKAGE_VERSION: `"${clerkJsPackage.version}"`,
__PKG_VERSION__: `"${clerkJsPackage.version}"`,
__DEV__: `${watch}`,
__BUILD_DISABLE_RHC__: JSON.stringify(false),
},
} satisfies Options;

return [
// Build internal types, server (marker), entry point, and bundled wrapper
// These are unbundled - the user's bundler handles dependency resolution
{
...common,
entry: ['./src/internal/index.ts', './src/server.ts', './src/entry.ts', './src/bundled.ts'],
outDir: './dist/esm',
unbundle: true,
},
];
});
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
"tslib": "catalog:repo"
},
"devDependencies": {
"@clerk/clerk-js": "workspace:*",
"@clerk/localizations": "workspace:*",
"@clerk/ui": "workspace:*",
"@types/semver": "^7.7.1",
Expand Down
37 changes: 27 additions & 10 deletions packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import type {
HeadlessBrowserClerkConstructor,
IsomorphicClerkOptions,
} from './types';
import { JS_BRAND } from './types';
import { isConstructor } from './utils';

if (typeof globalThis.__BUILD_DISABLE_RHC__ === 'undefined') {
Expand All @@ -91,6 +92,7 @@ const SDK_METADATA = {
export interface Global {
Clerk?: HeadlessBrowserClerk | BrowserClerk;
__internal_ClerkUICtor?: ClerkUiConstructor;
__internal_ClerkJSCtor?: BrowserClerkConstructor;
}

declare const global: Global;
Expand Down Expand Up @@ -487,10 +489,32 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
}

private async getClerkJsEntryChunk(): Promise<HeadlessBrowserClerk | BrowserClerk> {
// Hotload bundle
if (!this.options.Clerk && !__BUILD_DISABLE_RHC__) {
// Support bundled JS via js.ClerkJS prop
const jsProp = (this.options as { js?: { __brand?: string; ClerkJS?: BrowserClerkConstructor } }).js;
if (jsProp?.ClerkJS) {
global.Clerk = new jsProp.ClerkJS(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain });
return global.Clerk;
}

// Support server-safe JS marker (react-server condition)
// When js prop is present but ClerkJS is absent, dynamically import
if (jsProp?.__brand === JS_BRAND) {
const { ClerkJs } = await import('@clerk/clerk-js/entry');
global.Clerk = new ClerkJs(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain });
return global.Clerk;
}

// Legacy: Support bundled Clerk via Clerk prop
if (this.options.Clerk) {
global.Clerk = isConstructor<BrowserClerkConstructor | HeadlessBrowserClerkConstructor>(this.options.Clerk)
? new this.options.Clerk(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain })
: this.options.Clerk;
return global.Clerk;
}

// Hotload bundle from CDN
if (!__BUILD_DISABLE_RHC__) {
// the UMD script sets the global.Clerk instance
// we do not want to await here as we
await loadClerkJSScript({
...this.options,
publishableKey: this.#publishableKey,
Expand All @@ -500,13 +524,6 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
});
}

// Otherwise, set global.Clerk to the bundled ctor or instance
if (this.options.Clerk) {
global.Clerk = isConstructor<BrowserClerkConstructor | HeadlessBrowserClerkConstructor>(this.options.Clerk)
? new this.options.Clerk(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain })
: this.options.Clerk;
}

if (!global.Clerk) {
// TODO @nikos: somehow throw if clerk ui failed to load but it was not headless
throw new Error('Failed to download latest ClerkJS. Contact support@clerk.com.');
Expand Down
44 changes: 43 additions & 1 deletion packages/react/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
BrowserClerkConstructor,
Clerk,
InitialState,
IsomorphicClerkOptions,
Expand All @@ -12,6 +13,40 @@ import type { ClerkUiConstructor } from '@clerk/shared/ui';
import type { Appearance, ExtractAppearanceType, Ui } from '@clerk/ui/internal';
import type React from 'react';

/**
* Runtime brand value to identify valid JS objects
*/
export const JS_BRAND = '__clerkJS' as const;

declare const Tags: unique symbol;
type Tagged<BaseType, Tag extends PropertyKey> = BaseType & { [Tags]: { [K in Tag]: void } };

/**
* Js type that carries type information via phantom property
* Tagged to ensure only official js objects from @clerk/clerk-js can be used
*
* ClerkJS is optional to support server-safe marker exports (react-server condition).
* When ClerkJS is absent, the SDK will dynamically import it.
*/
export type Js = Tagged<
{
/**
* Runtime brand to identify valid JS objects
*/
__brand: typeof JS_BRAND;
/**
* ClerkJS constructor. Optional to support server-safe marker exports.
* When absent (e.g., in React Server Components), the SDK resolves it via dynamic import.
*/
ClerkJS?: BrowserClerkConstructor;
/**
* Version of the JS package (for potential future use)
*/
version?: string;
},
'ClerkJs'
>;

// Re-export types from @clerk/shared that are used by other modules
export type {
IsomorphicClerkOptions,
Expand All @@ -30,13 +65,14 @@ declare global {
__clerk_proxy_url?: Clerk['proxyUrl'];
__clerk_domain?: Clerk['domain'];
__internal_ClerkUICtor?: ClerkUiConstructor;
__internal_ClerkJSCtor?: BrowserClerkConstructor;
}
}

/**
* @interface
*/
export type ClerkProviderProps<TUi extends Ui = Ui> = Omit<IsomorphicClerkOptions, 'appearance'> & {
export type ClerkProviderProps<TUi extends Ui = Ui, TJs extends Js = Js> = Omit<IsomorphicClerkOptions, 'appearance'> & {
children: React.ReactNode;
/**
* Provide an initial state of the Clerk client during server-side rendering. You don't need to set this value yourself unless you're [developing an SDK](https://clerk.com/docs/guides/development/sdk-development/overview).
Expand All @@ -59,6 +95,12 @@ export type ClerkProviderProps<TUi extends Ui = Ui> = Omit<IsomorphicClerkOption
* Note: When `ui` is used, appearance is automatically typed based on the specific UI version.
*/
ui?: TUi;
/**
* Optional object to use the bundled Clerk JS instead of loading from CDN.
* Import `js` from `@clerk/clerk-js/bundled` and pass it here to bundle clerk-js with your application.
* When omitted, clerk-js is loaded from Clerk's CDN.
*/
js?: TJs;
};

export type WithClerkProp<T = unknown> = T & {
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.