From 9d7ecc1fcc141a1be0e666505e335dae53b82676 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Wed, 19 Nov 2025 12:46:53 +0200 Subject: [PATCH] feat(react,astro,vue): Do not hotload clerkUi if clerkUiCtor was passed --- integration/presets/astro.ts | 6 +- .../templates/express-vite/src/client/main.ts | 2 +- .../src/internal/create-clerk-instance.ts | 16 +++--- packages/astro/src/types.ts | 1 - packages/clerk-js/src/core/clerk.ts | 2 +- packages/react/src/isomorphicClerk.ts | 56 ++++++++++--------- packages/shared/src/types/clerk.ts | 2 +- packages/vue/src/plugin.ts | 16 +++--- 8 files changed, 55 insertions(+), 46 deletions(-) diff --git a/integration/presets/astro.ts b/integration/presets/astro.ts index e286bf76586..65cbebff911 100644 --- a/integration/presets/astro.ts +++ b/integration/presets/astro.ts @@ -10,9 +10,9 @@ const astroNode = applicationConfig() .addScript('dev', 'pnpm dev') .addScript('build', 'pnpm build') .addScript('serve', 'pnpm preview') - .addDependency('@clerk/astro', linkPackage('astro')) - .addDependency('@clerk/shared', linkPackage('types')) - .addDependency('@clerk/localizations', linkPackage('localizations')); + .addDependency('@clerk/astro', linkPackage('astro', 'integration')) + .addDependency('@clerk/shared', linkPackage('types', 'integration')) + .addDependency('@clerk/localizations', linkPackage('localizations', 'integration')); const astroStatic = astroNode.clone().setName('astro-hybrid').useTemplate(templates['astro-hybrid']); diff --git a/integration/templates/express-vite/src/client/main.ts b/integration/templates/express-vite/src/client/main.ts index d07733d4105..bf19f46d7b7 100644 --- a/integration/templates/express-vite/src/client/main.ts +++ b/integration/templates/express-vite/src/client/main.ts @@ -7,7 +7,7 @@ document.addEventListener('DOMContentLoaded', async function () { const clerk = new Clerk(publishableKey); await clerk.load({ - clerkUiCtor: Promise.resolve(ClerkUi), + clerkUiCtor: ClerkUi, }); if (clerk.isSignedIn) { diff --git a/packages/astro/src/internal/create-clerk-instance.ts b/packages/astro/src/internal/create-clerk-instance.ts index b1e6ef64197..88407459660 100644 --- a/packages/astro/src/internal/create-clerk-instance.ts +++ b/packages/astro/src/internal/create-clerk-instance.ts @@ -41,13 +41,15 @@ async function createClerkInstanceInternal(options?: AstroClerkCreateInstancePar if (!clerkJSInstance) { // Load both clerk-js and clerk-ui in parallel const clerkPromise = loadClerkJsScript(options); - clerkUiCtor = loadClerkUiScript(options).then(() => { - if (!window.__unstable_ClerkUiCtor) { - throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.'); - } - // After the check, TypeScript knows it's defined - return window.__unstable_ClerkUiCtor; - }); + clerkUiCtor = options?.clerkUiCtor + ? Promise.resolve(options.clerkUiCtor) + : loadClerkUiScript(options).then(() => { + if (!window.__unstable_ClerkUiCtor) { + throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.'); + } + // After the check, TypeScript knows it's defined + return window.__unstable_ClerkUiCtor; + }); await clerkPromise; diff --git a/packages/astro/src/types.ts b/packages/astro/src/types.ts index d127c432a31..707006e66dc 100644 --- a/packages/astro/src/types.ts +++ b/packages/astro/src/types.ts @@ -21,7 +21,6 @@ type AstroClerkIntegrationParams = Without< | 'routerPush' | 'polling' | 'touchSession' - | 'clerkUiCtor' > & MultiDomainAndOrProxyPrimitives & { clerkJSUrl?: string; diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index da308837b35..63c7e8690e6 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -461,7 +461,7 @@ export class Clerk implements ClerkInterface { // Initialize ClerkUi if it was provided if (this.#options.clerkUiCtor) { - this.#clerkUi = this.#options.clerkUiCtor.then( + this.#clerkUi = Promise.resolve(this.#options.clerkUiCtor).then( ClerkUI => new ClerkUI( () => this, diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 0d004990e2c..e868f13de9d 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -250,12 +250,11 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } constructor(options: IsomorphicClerkOptions) { - const { Clerk = null, publishableKey } = options || {}; - this.#publishableKey = publishableKey; + this.#publishableKey = options?.publishableKey; this.#proxyUrl = options?.proxyUrl; this.#domain = options?.domain; this.options = options; - this.Clerk = Clerk; + this.Clerk = options?.Clerk || null; this.mode = inBrowser() ? 'browser' : 'server'; this.#stateProxy = new StateProxy(this); @@ -266,7 +265,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { this.#eventBus.prioritizedOn(clerkEvents.Status, status => (this.#status = status)); if (this.#publishableKey) { - void this.loadClerkEntryChunks(); + void this.getEntryChunks(); } } @@ -436,7 +435,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { }); } - async loadClerkEntryChunks(): Promise { + async getEntryChunks(): Promise { if (this.mode !== 'browser' || this.loaded) { return; } @@ -456,21 +455,15 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } try { - const clerkPromise = this.loadClerkJsEntryChunk(); - const clerkUiCtor = this.loadClerkUiEntryChunk(); - await clerkPromise; + const clerkUiCtor = this.getClerkUiEntryChunk(); + const clerk = await this.getClerkJsEntryChunk(); - 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.'); + if (!clerk.loaded) { + this.beforeLoad(clerk); + await clerk.load({ ...this.options, clerkUiCtor }); } - - if (!global.Clerk.loaded) { - this.beforeLoad(global.Clerk); - await global.Clerk.load({ ...this.options, clerkUiCtor }); - } - if (global.Clerk.loaded) { - this.replayInterceptedInvocations(global.Clerk); + if (clerk.loaded) { + this.replayInterceptedInvocations(clerk); } } catch (err) { const error = err as Error; @@ -480,10 +473,11 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } } - private async loadClerkJsEntryChunk() { + private async getClerkJsEntryChunk(): Promise { // Hotload bundle - if (!this.Clerk && !__BUILD_DISABLE_RHC__) { + if (!this.options.Clerk && !__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, @@ -494,15 +488,25 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } // Otherwise, set global.Clerk to the bundled ctor or instance - if (this.Clerk) { - global.Clerk = isConstructor(this.Clerk) - ? new this.Clerk(this.#publishableKey, { proxyUrl: this.proxyUrl, domain: this.domain }) - : this.Clerk; + if (this.options.Clerk) { + global.Clerk = isConstructor(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.'); + } + return global.Clerk; } - private async loadClerkUiEntryChunk() { + private async getClerkUiEntryChunk(): Promise { + if (this.options.clerkUiCtor) { + return this.options.clerkUiCtor; + } + await loadClerkUiScript({ ...this.options, publishableKey: this.#publishableKey, @@ -510,9 +514,11 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { domain: this.domain, nonce: this.options.nonce, }); + if (!global.__unstable_ClerkUiCtor) { throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.'); } + return global.__unstable_ClerkUiCtor; } diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index 67801b36fe8..c2f385cef37 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -1056,7 +1056,7 @@ export type ClerkOptions = ClerkOptionsNavigation & /** * Clerk UI entrypoint. */ - clerkUiCtor?: Promise; + clerkUiCtor?: ClerkUiConstructor | Promise; /** * Optional object to style your components. Will only affect [Clerk Components](https://clerk.com/docs/reference/components/overview) and not [Account Portal](https://clerk.com/docs/guides/customizing-clerk/account-portal) pages. */ diff --git a/packages/vue/src/plugin.ts b/packages/vue/src/plugin.ts index 6478c2aa8dc..21d4a6c8855 100644 --- a/packages/vue/src/plugin.ts +++ b/packages/vue/src/plugin.ts @@ -76,13 +76,15 @@ export const clerkPlugin: Plugin<[PluginOptions]> = { void (async () => { try { const clerkPromise = loadClerkJsScript(options); - const clerkUiCtorPromise = (async () => { - await loadClerkUiScript(options); - if (!window.__unstable_ClerkUiCtor) { - throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.'); - } - return window.__unstable_ClerkUiCtor; - })(); + const clerkUiCtorPromise = pluginOptions.clerkUiCtor + ? Promise.resolve(pluginOptions.clerkUiCtor) + : (async () => { + await loadClerkUiScript(options); + if (!window.__unstable_ClerkUiCtor) { + throw new Error('Failed to download latest Clerk UI. Contact support@clerk.com.'); + } + return window.__unstable_ClerkUiCtor; + })(); await clerkPromise;