From db222d18451e7906fbb2cdc5cd95cb4826bf31c6 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 27 Jan 2026 21:19:24 -0600 Subject: [PATCH] feat(*): add lightweight version pinning packages Introduces @clerk/clerk-js-pinned and @clerk/ui-pinned packages for dependency-managed version pinning without heavy transitive dependencies. - Add @clerk/clerk-js-pinned: exports clerkJs branded object and version - Add @clerk/ui-pinned: exports ui branded object with full Appearance types - Add ClerkJs and Ui branded types to @clerk/shared - Add clerkJs prop to IsomorphicClerkOptions - Update loadClerkJsScript to handle clerkJs object - Use dts-bundle-generator to bundle all types inline (zero type deps) Both packages are <1KB gzipped with zero runtime dependencies. --- packages/clerk-js-pinned/package.json | 65 ++++++++++++++++++ packages/clerk-js-pinned/src/index.ts | 32 +++++++++ packages/clerk-js-pinned/tsconfig.json | 24 +++++++ packages/clerk-js-pinned/tsdown.config.mts | 20 ++++++ packages/shared/src/loadClerkJsScript.ts | 10 ++- packages/shared/src/types/branded.ts | 80 ++++++++++++++++++++++ packages/shared/src/types/clerk.ts | 22 +++++- packages/shared/src/types/index.ts | 1 + packages/ui-pinned/package.json | 66 ++++++++++++++++++ packages/ui-pinned/src/index.ts | 42 ++++++++++++ packages/ui-pinned/tsconfig.json | 24 +++++++ packages/ui-pinned/tsdown.config.mts | 20 ++++++ packages/ui/src/internal/index.ts | 33 +-------- pnpm-lock.yaml | 37 ++++++++++ 14 files changed, 440 insertions(+), 36 deletions(-) create mode 100644 packages/clerk-js-pinned/package.json create mode 100644 packages/clerk-js-pinned/src/index.ts create mode 100644 packages/clerk-js-pinned/tsconfig.json create mode 100644 packages/clerk-js-pinned/tsdown.config.mts create mode 100644 packages/shared/src/types/branded.ts create mode 100644 packages/ui-pinned/package.json create mode 100644 packages/ui-pinned/src/index.ts create mode 100644 packages/ui-pinned/tsconfig.json create mode 100644 packages/ui-pinned/tsdown.config.mts 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: