diff --git a/packages/clerk-js-pinned/package.json b/packages/clerk-js-pinned/package.json
new file mode 100644
index 00000000000..f0333a54d4b
--- /dev/null
+++ b/packages/clerk-js-pinned/package.json
@@ -0,0 +1,65 @@
+{
+ "name": "@clerk/clerk-js-pinned",
+ "version": "5.114.0",
+ "description": "Lightweight package for pinning @clerk/clerk-js version via dependency management",
+ "keywords": [
+ "clerk",
+ "auth",
+ "authentication",
+ "version",
+ "pinning"
+ ],
+ "homepage": "https://clerk.com/",
+ "bugs": {
+ "url": "https://github.com/clerk/javascript/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/clerk/javascript.git",
+ "directory": "packages/clerk-js-pinned"
+ },
+ "license": "MIT",
+ "author": "Clerk",
+ "sideEffects": false,
+ "type": "module",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsdown && pnpm build:dts",
+ "build:dts": "dts-bundle-generator -o dist/index.d.ts src/index.ts --no-check && cp dist/index.d.ts dist/index.d.cts",
+ "clean": "rimraf ./dist",
+ "format": "node ../../scripts/format-package.mjs",
+ "format:check": "node ../../scripts/format-package.mjs --check",
+ "lint": "eslint src",
+ "lint:attw": "attw --pack . --profile node16",
+ "lint:publint": "publint"
+ },
+ "devDependencies": {
+ "@clerk/shared": "workspace:^",
+ "dts-bundle-generator": "^9.5.1",
+ "tsdown": "catalog:repo"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/clerk-js-pinned/src/index.ts b/packages/clerk-js-pinned/src/index.ts
new file mode 100644
index 00000000000..41fbd6a0a66
--- /dev/null
+++ b/packages/clerk-js-pinned/src/index.ts
@@ -0,0 +1,32 @@
+import type { ClerkJs } from '@clerk/shared/types';
+
+declare const PACKAGE_VERSION: string;
+
+/**
+ * The version of @clerk/clerk-js that this package pins to.
+ * Use this with the `clerkJSVersion` prop for string-based pinning.
+ *
+ * @example
+ * ```tsx
+ * import { version } from '@clerk/clerk-js-pinned';
+ *
+ * ```
+ */
+export const version = PACKAGE_VERSION;
+
+/**
+ * Branded object for pinning @clerk/clerk-js version.
+ * Use this with the `clerkJs` prop in ClerkProvider for dependency-managed pinning.
+ *
+ * @example
+ * ```tsx
+ * import { clerkJs } from '@clerk/clerk-js-pinned';
+ *
+ * ```
+ */
+export const clerkJs = {
+ version: PACKAGE_VERSION,
+} as ClerkJs;
+
+// Re-export the type for consumers who need it
+export type { ClerkJs } from '@clerk/shared/types';
diff --git a/packages/clerk-js-pinned/tsconfig.json b/packages/clerk-js-pinned/tsconfig.json
new file mode 100644
index 00000000000..016a33f3841
--- /dev/null
+++ b/packages/clerk-js-pinned/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "rootDir": "src",
+ "verbatimModuleSyntax": true,
+ "target": "es2022",
+ "strict": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "outDir": "dist",
+ "noUnusedLocals": true,
+ "moduleResolution": "bundler",
+ "moduleDetection": "force",
+ "module": "preserve",
+ "lib": ["ES2023"],
+ "isolatedModules": true,
+ "forceConsistentCasingInFileNames": true,
+ "esModuleInterop": true,
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "exclude": ["node_modules", "dist"],
+ "include": ["src"]
+}
diff --git a/packages/clerk-js-pinned/tsdown.config.mts b/packages/clerk-js-pinned/tsdown.config.mts
new file mode 100644
index 00000000000..1f03d3f61a1
--- /dev/null
+++ b/packages/clerk-js-pinned/tsdown.config.mts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'tsdown';
+
+import packageJson from './package.json' with { type: 'json' };
+
+export default defineConfig({
+ entry: ['./src/index.ts'],
+ outDir: './dist',
+ dts: false, // We use dts-bundle-generator for bundled types
+ sourcemap: true,
+ clean: true,
+ target: 'es2022',
+ platform: 'neutral',
+ format: ['cjs', 'esm'],
+ minify: false,
+ external: [],
+ define: {
+ PACKAGE_NAME: `"${packageJson.name}"`,
+ PACKAGE_VERSION: `"${packageJson.version}"`,
+ },
+});
diff --git a/packages/shared/src/loadClerkJsScript.ts b/packages/shared/src/loadClerkJsScript.ts
index aa48dda86be..9cdc4f67db7 100644
--- a/packages/shared/src/loadClerkJsScript.ts
+++ b/packages/shared/src/loadClerkJsScript.ts
@@ -15,6 +15,11 @@ export type LoadClerkJsScriptOptions = {
clerkJSUrl?: string;
clerkJSVariant?: 'headless' | '';
clerkJSVersion?: string;
+ /**
+ * Branded object for pinning @clerk/clerk-js version.
+ * Takes precedence over clerkJSVersion if both are provided.
+ */
+ clerkJs?: { version: string };
sdkMetadata?: SDKMetadata;
proxyUrl?: string;
domain?: string;
@@ -217,7 +222,7 @@ export const loadClerkUiScript = async (opts?: LoadClerkUiScriptOptions): Promis
};
export const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => {
- const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts;
+ const { clerkJSUrl, clerkJSVariant, clerkJSVersion, clerkJs, proxyUrl, domain, publishableKey } = opts;
if (clerkJSUrl) {
return clerkJSUrl;
@@ -225,7 +230,8 @@ export const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => {
const scriptHost = buildScriptHost({ publishableKey, proxyUrl, domain });
const variant = clerkJSVariant ? `${clerkJSVariant.replace(/\.+$/, '')}.` : '';
- const version = versionSelector(clerkJSVersion);
+ // clerkJs object takes precedence over clerkJSVersion string
+ const version = versionSelector(clerkJs?.version ?? clerkJSVersion);
return `https://${scriptHost}/npm/@clerk/clerk-js@${version}/dist/clerk.${variant}browser.js`;
};
diff --git a/packages/shared/src/types/branded.ts b/packages/shared/src/types/branded.ts
new file mode 100644
index 00000000000..6cb774223b3
--- /dev/null
+++ b/packages/shared/src/types/branded.ts
@@ -0,0 +1,80 @@
+/**
+ * Branded/Tagged type utilities for creating nominal types.
+ * These are used to ensure only official Clerk objects can be passed to certain props.
+ */
+
+declare const Tags: unique symbol;
+
+/**
+ * Creates a branded/tagged type that prevents arbitrary objects from being assigned.
+ * The tag exists only at the type level and has no runtime overhead.
+ *
+ * @example
+ * ```typescript
+ * type UserId = Tagged;
+ * const userId: UserId = 'user_123' as UserId;
+ * ```
+ */
+export type Tagged = BaseType & { [Tags]: { [K in Tag]: void } };
+
+/**
+ * Branded type for Clerk JS version pinning objects.
+ * Used with the `clerkJs` prop in ClerkProvider.
+ *
+ * @example
+ * ```typescript
+ * import { clerkJs } from '@clerk/clerk-js-pinned';
+ *
+ * ```
+ */
+export type ClerkJs = Tagged<
+ {
+ /**
+ * The version string of @clerk/clerk-js to load from CDN.
+ */
+ version: string;
+ },
+ 'ClerkJs'
+>;
+
+/**
+ * Branded type for Clerk UI version pinning objects.
+ * Carries appearance type information via phantom property for type-safe customization.
+ *
+ * @example
+ * ```typescript
+ * import { ui } from '@clerk/ui-pinned';
+ *
+ * ```
+ */
+export type Ui = Tagged<
+ {
+ /**
+ * The version string of @clerk/ui to load from CDN.
+ */
+ version: string;
+ /**
+ * Optional custom URL to load @clerk/ui from.
+ */
+ url?: string;
+ /**
+ * Phantom property for type-level appearance inference.
+ * This property never exists at runtime.
+ * @internal
+ */
+ __appearanceType?: A;
+ },
+ 'ClerkUi'
+>;
+
+/**
+ * Extracts the appearance type from a Ui object. We have 3 cases:
+ * - If the Ui type has __appearanceType with a specific type, extract it
+ * - If __appearanceType is 'any', fallback to base Appearance type
+ * - Otherwise, fallback to the base Appearance type
+ */
+export type ExtractAppearanceType = T extends { __appearanceType?: infer A }
+ ? 0 extends 1 & A // Check if A is 'any' (this trick works because 1 & any = any, and 0 extends any)
+ ? Default
+ : A
+ : Default;
diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts
index 01927cfe4d6..e718e976fed 100644
--- a/packages/shared/src/types/clerk.ts
+++ b/packages/shared/src/types/clerk.ts
@@ -2423,6 +2423,17 @@ export type IsomorphicClerkOptions = Without & {
* The npm version for `@clerk/clerk-js`.
*/
clerkJSVersion?: string;
+ /**
+ * Branded object for pinning @clerk/clerk-js version.
+ * Import from `@clerk/clerk-js-pinned` for dependency-managed version pinning.
+ *
+ * @example
+ * ```tsx
+ * import { clerkJs } from '@clerk/clerk-js-pinned';
+ *
+ * ```
+ */
+ clerkJs?: { version: string };
/**
* The URL that `@clerk/ui` should be hot-loaded from.
*/
@@ -2441,9 +2452,14 @@ export type IsomorphicClerkOptions = Without & {
*/
nonce?: string;
/**
- * @internal
- * This is a structural-only type for the `ui` object that can be passed
- * to Clerk.load() and ClerkProvider
+ * Branded object for pinning @clerk/ui version with full appearance type support.
+ * Import from `@clerk/ui-pinned` for dependency-managed version pinning.
+ *
+ * @example
+ * ```tsx
+ * import { ui } from '@clerk/ui-pinned';
+ *
+ * ```
*/
ui?: { version: string; url?: string };
} & MultiDomainAndOrProxy;
diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts
index 068e5728cae..d424e6e65f9 100644
--- a/packages/shared/src/types/index.ts
+++ b/packages/shared/src/types/index.ts
@@ -1,4 +1,5 @@
export type * from './apiKeys';
+export type * from './branded';
export type * from './apiKeysSettings';
export type * from './attributes';
export type * from './authConfig';
diff --git a/packages/ui-pinned/package.json b/packages/ui-pinned/package.json
new file mode 100644
index 00000000000..afb42c8f43b
--- /dev/null
+++ b/packages/ui-pinned/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "@clerk/ui-pinned",
+ "version": "0.0.1",
+ "description": "Lightweight package for pinning @clerk/ui version via dependency management with full type support",
+ "keywords": [
+ "clerk",
+ "ui",
+ "version",
+ "pinning",
+ "theming"
+ ],
+ "homepage": "https://clerk.com/",
+ "bugs": {
+ "url": "https://github.com/clerk/javascript/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/clerk/javascript.git",
+ "directory": "packages/ui-pinned"
+ },
+ "license": "MIT",
+ "author": "Clerk",
+ "sideEffects": false,
+ "type": "module",
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ },
+ "require": {
+ "types": "./dist/index.d.cts",
+ "default": "./dist/index.cjs"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "main": "./dist/index.cjs",
+ "module": "./dist/index.js",
+ "types": "./dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "tsdown && pnpm build:dts",
+ "build:dts": "dts-bundle-generator -o dist/index.d.ts src/index.ts --no-check && cp dist/index.d.ts dist/index.d.cts",
+ "clean": "rimraf ./dist",
+ "format": "node ../../scripts/format-package.mjs",
+ "format:check": "node ../../scripts/format-package.mjs --check",
+ "lint": "eslint src",
+ "lint:attw": "attw --pack . --profile node16",
+ "lint:publint": "publint"
+ },
+ "devDependencies": {
+ "@clerk/shared": "workspace:^",
+ "@clerk/ui": "workspace:^",
+ "dts-bundle-generator": "^9.5.1",
+ "tsdown": "catalog:repo"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/ui-pinned/src/index.ts b/packages/ui-pinned/src/index.ts
new file mode 100644
index 00000000000..917403bd7ec
--- /dev/null
+++ b/packages/ui-pinned/src/index.ts
@@ -0,0 +1,42 @@
+import type { Ui } from '@clerk/shared/types';
+import type { Appearance } from '@clerk/ui/internal';
+
+declare const PACKAGE_VERSION: string;
+
+/**
+ * The version of @clerk/ui that this package pins to.
+ *
+ * @example
+ * ```tsx
+ * import { version } from '@clerk/ui-pinned';
+ * console.log(`Pinning to @clerk/ui version ${version}`);
+ * ```
+ */
+export const version = PACKAGE_VERSION;
+
+/**
+ * Branded object for pinning @clerk/ui version with full type support.
+ * Use this with the `ui` prop in ClerkProvider for dependency-managed pinning
+ * with type-safe appearance customization.
+ *
+ * @example
+ * ```tsx
+ * import { ui } from '@clerk/ui-pinned';
+ *
+ *
+ * ```
+ */
+export const ui = {
+ version: PACKAGE_VERSION,
+} as Ui;
+
+// Re-export types for consumers who need them
+export type { Ui, ExtractAppearanceType } from '@clerk/shared/types';
+export type { Appearance } from '@clerk/ui/internal';
diff --git a/packages/ui-pinned/tsconfig.json b/packages/ui-pinned/tsconfig.json
new file mode 100644
index 00000000000..016a33f3841
--- /dev/null
+++ b/packages/ui-pinned/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "rootDir": "src",
+ "verbatimModuleSyntax": true,
+ "target": "es2022",
+ "strict": true,
+ "skipLibCheck": true,
+ "resolveJsonModule": true,
+ "outDir": "dist",
+ "noUnusedLocals": true,
+ "moduleResolution": "bundler",
+ "moduleDetection": "force",
+ "module": "preserve",
+ "lib": ["ES2023"],
+ "isolatedModules": true,
+ "forceConsistentCasingInFileNames": true,
+ "esModuleInterop": true,
+ "emitDeclarationOnly": true,
+ "declaration": true,
+ "declarationMap": true
+ },
+ "exclude": ["node_modules", "dist"],
+ "include": ["src"]
+}
diff --git a/packages/ui-pinned/tsdown.config.mts b/packages/ui-pinned/tsdown.config.mts
new file mode 100644
index 00000000000..1f03d3f61a1
--- /dev/null
+++ b/packages/ui-pinned/tsdown.config.mts
@@ -0,0 +1,20 @@
+import { defineConfig } from 'tsdown';
+
+import packageJson from './package.json' with { type: 'json' };
+
+export default defineConfig({
+ entry: ['./src/index.ts'],
+ outDir: './dist',
+ dts: false, // We use dts-bundle-generator for bundled types
+ sourcemap: true,
+ clean: true,
+ target: 'es2022',
+ platform: 'neutral',
+ format: ['cjs', 'esm'],
+ minify: false,
+ external: [],
+ define: {
+ PACKAGE_NAME: `"${packageJson.name}"`,
+ PACKAGE_VERSION: `"${packageJson.version}"`,
+ },
+});
diff --git a/packages/ui/src/internal/index.ts b/packages/ui/src/internal/index.ts
index 2a9e39b207e..1518e18746b 100644
--- a/packages/ui/src/internal/index.ts
+++ b/packages/ui/src/internal/index.ts
@@ -3,37 +3,8 @@ import type { Appearance } from './appearance';
export type { ComponentControls, MountComponentRenderer } from '../Components';
export type { WithInternalRouting } from './routing';
-/**
- * Extracts the appearance type from a Ui object. We got 3 cases:
- * - If the Ui type has __appearanceType with a specific type, extract it
- * - If __appearanceType is 'any', fallback to base Appearance type
- * - Otherwise, fallback to the base Appearance type
- */
-export type ExtractAppearanceType = T extends { __appearanceType?: infer A }
- ? 0 extends 1 & A // Check if A is 'any' (this trick works because 1 & any = any, and 0 extends any)
- ? Default
- : A
- : Default;
-
-declare const Tags: unique symbol;
-type Tagged = BaseType & { [Tags]: { [K in Tag]: void } };
-
-/**
- * Ui type that carries appearance type information via phantom property
- * Tagged to ensure only official ui objects from @clerk/ui can be used
- */
-export type Ui = Tagged<
- {
- version: string;
- url?: string;
- /**
- * Phantom property for type-level appearance inference
- * This property never exists at runtime
- */
- __appearanceType?: A;
- },
- 'ClerkUi'
->;
+// Re-export branded types from @clerk/shared for backwards compatibility
+export type { ExtractAppearanceType, Ui } from '@clerk/shared/types';
export type {
AlphaColorScale,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ed4eb9eb1d1..498ff8d8f66 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -548,6 +548,18 @@ importers:
specifier: ^5.10.0
version: 5.10.0
+ packages/clerk-js-pinned:
+ devDependencies:
+ '@clerk/shared':
+ specifier: workspace:^
+ version: link:../shared
+ dts-bundle-generator:
+ specifier: ^9.5.1
+ version: 9.5.1
+ tsdown:
+ specifier: catalog:repo
+ version: 0.15.7(publint@0.3.15)(typescript@5.8.3)(vue-tsc@3.1.4(typescript@5.8.3))
+
packages/dev-cli:
dependencies:
commander:
@@ -1055,6 +1067,21 @@ importers:
specifier: ^5.10.0
version: 5.10.0
+ packages/ui-pinned:
+ devDependencies:
+ '@clerk/shared':
+ specifier: workspace:^
+ version: link:../shared
+ '@clerk/ui':
+ specifier: workspace:^
+ version: link:../ui
+ dts-bundle-generator:
+ specifier: ^9.5.1
+ version: 9.5.1
+ tsdown:
+ specifier: catalog:repo
+ version: 0.15.7(publint@0.3.15)(typescript@5.8.3)(vue-tsc@3.1.4(typescript@5.8.3))
+
packages/upgrade:
dependencies:
chalk:
@@ -7833,6 +7860,11 @@ packages:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'}
+ dts-bundle-generator@9.5.1:
+ resolution: {integrity: sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
dts-resolver@2.1.2:
resolution: {integrity: sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg==}
engines: {node: '>=20.18.0'}
@@ -23981,6 +24013,11 @@ snapshots:
dset@3.1.4: {}
+ dts-bundle-generator@9.5.1:
+ dependencies:
+ typescript: 5.8.3
+ yargs: 17.7.2
+
dts-resolver@2.1.2: {}
dunder-proto@1.0.1: