Skip to content
Draft
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
7 changes: 7 additions & 0 deletions .changeset/shared-moduleManager-registry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/react': patch
---

Internal plumbing to support `@clerk/ui` composed `UserProfile` / `OrganizationProfile` subcomponents.
16 changes: 15 additions & 1 deletion packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,20 @@ export class Clerk implements ClerkInterface {
#pageLifecycle: ReturnType<typeof createPageLifecycle> | null = null;
#touchThrottledUntil = 0;
#publicEventBus = createClerkEventBus();
#moduleManager = new ModuleManager();

/**
* Cross-bundle handle to the ModuleManager. clerk-js is loaded standalone
* from the CDN with its own inlined @clerk/shared, so plain property access
* is the only channel that reliably crosses that boundary. This getter is
* how IsomorphicClerk forwards the manager to consumers that import
* @clerk/shared from node_modules (e.g. @clerk/react, @clerk/ui).
*
* @internal
*/
get __internal_moduleManager(): ModuleManager {
return this.#moduleManager;
}

get __internal_queryClient(): { __tag: 'clerk-rq-client'; client: QueryClient } | undefined {
if (!this.#queryClient) {
Expand Down Expand Up @@ -557,7 +571,7 @@ export class Clerk implements ClerkInterface {
() => this,
() => this.environment,
this.#options,
new ModuleManager(),
this.#moduleManager,
),
);
}
Expand Down
25 changes: 25 additions & 0 deletions packages/react/src/__tests__/isomorphicClerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,31 @@ describe('isomorphicClerk', () => {
}).not.toThrow();
});

// Regression: composed/subcomponent UserProfile reads moduleManager via
// `useClerk().__internal_moduleManager`. `useClerk()` returns the
// IsomorphicClerk wrapper, so its getter must chain through to the loaded
// clerk-js's own `__internal_moduleManager`. This plain property access is
// the cross-bundle channel: clerk-js ships standalone from the CDN with its
// own inlined @clerk/shared, so module-scoped state cannot bridge the two.
//
// Without this chain, every dynamic-imported feature (Coinbase Wallet, Base,
// Stripe, zxcvbn) falls back to a rejecting manager.
it('exposes the inner clerk-js moduleManager through the __internal_moduleManager getter', () => {
const isomorphicClerk = new IsomorphicClerk({ publishableKey: 'pk_test_XXX' });
const mm = { import: vi.fn(() => Promise.resolve(undefined)) };

// Before clerk-js loads, the getter is undefined so readers fall back.
expect(isomorphicClerk.__internal_moduleManager).toBeUndefined();

const innerClerk: any = {
addListener: vi.fn(),
__internal_moduleManager: mm,
};
(isomorphicClerk as any).replayInterceptedInvocations(innerClerk);

expect(isomorphicClerk.__internal_moduleManager).toBe(mm);
});

it('updates props asynchronously after clerkjs has loaded', async () => {
const propsHistory: any[] = [];
const dummyClerkJS = {
Expand Down
13 changes: 13 additions & 0 deletions packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { inBrowser } from '@clerk/shared/browser';
import { clerkEvents, createClerkEventBus } from '@clerk/shared/clerkEventBus';
import { ALLOWED_PROTOCOLS, windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate';
import { loadClerkJSScript, loadClerkUIScript } from '@clerk/shared/loadClerkJsScript';
import type { ModuleManager } from '@clerk/shared/moduleManager';
import type {
__internal_AttemptToEnableEnvironmentSettingParams,
__internal_AttemptToEnableEnvironmentSettingResult,
Expand Down Expand Up @@ -285,6 +286,18 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
windowNavigate(to, { allowedProtocols });
};

/**
* Proxies to the inner Clerk instance's ModuleManager. Returns `undefined`
* before clerk-js has loaded; composed UI components read this getter
* (via `useClerk()`) to resolve dynamic-imported modules and fall back to a
* rejecting manager while it is `undefined`.
*
* @internal
*/
public get __internal_moduleManager(): ModuleManager | undefined {
return this.clerkjs?.__internal_moduleManager;
}

constructor(options: IsomorphicClerkOptions) {
this.#publishableKey = options?.publishableKey;
this.#proxyUrl = options?.proxyUrl;
Expand Down
36 changes: 34 additions & 2 deletions packages/shared/src/types/clerk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { ClerkGlobalHookError } from '../errors/globalHookError';
import type { ClerkGlobalHookError } from '@/errors/globalHookError';

import type { ModuleManager } from '../moduleManager';
import type { ClerkUIConstructor } from '../ui/types';
import type { APIKeysNamespace } from './apiKeys';
import type {
Expand Down Expand Up @@ -139,11 +141,13 @@ export type SDKMetadata = {

/**
* A callback function that is called when Clerk resources change.
*
* @inline
*/
export type ListenerCallback = (emission: Resources) => void;
/**
* Optional configuration for the `addListener()` method.
*
* @param skipInitialEmit - If `true`, the callback will not be called immediately after registration. Defaults to `false`.
* @inline
*/
Expand Down Expand Up @@ -188,7 +192,8 @@ export type SetActiveNavigate = (params: {

/**
* A callback that runs after sign out completes.
* @inline */
*
@inline */
export type SignOutCallback = () => void | Promise<any>;

/**
Expand Down Expand Up @@ -298,6 +303,20 @@ export interface Clerk {
*/
__internal_windowNavigate: (to: URL | string, opts?: { useStaticAllowlistOnly?: boolean }) => void;

/**
* Internal handle to the bundled ModuleManager. Exposed so framework SDK
* wrappers (e.g. IsomorphicClerk) can forward it to composed UI components
* that need dynamic-imported modules (Coinbase Wallet, Base, Stripe, zxcvbn).
* Plain property access crosses the bundle boundary that other channels
* cannot — clerk-js inlines its own @clerk/shared, so module-scoped state is
* invisible to consumers loading @clerk/shared from node_modules. It is
* `undefined` on a wrapper whose inner clerk-js has not loaded yet, so
* readers must handle the absent case.
*
* @internal
*/
__internal_moduleManager: ModuleManager | undefined;

frontendApi: string;

/** Your Clerk [Publishable Key](!publishable-key). */
Expand All @@ -317,6 +336,7 @@ export interface Clerk {

/**
* Indicates whether the instance is being loaded in a standard browser environment. Set to `false` on native platforms where cookies cannot be set. When `undefined`, Clerk assumes a standard browser.
*
* @inline
*/
isStandardBrowser: boolean | undefined;
Expand Down Expand Up @@ -350,6 +370,7 @@ export interface Clerk {
* `effect()` that can be used to subscribe to changes from Signals.
*
* @hidden
*
* @experimental This experimental API is subject to change.
*/
__internal_state: State;
Expand Down Expand Up @@ -398,6 +419,7 @@ export interface Clerk {

/**
* Closes the Clerk Checkout drawer.
*
* @hidden
*/
__internal_closeCheckout: () => void;
Expand All @@ -412,6 +434,7 @@ export interface Clerk {

/**
* Closes the Clerk PlanDetails drawer.
*
* @hidden
*/
__internal_closePlanDetails: () => void;
Expand All @@ -426,6 +449,7 @@ export interface Clerk {

/**
* Closes the Clerk SubscriptionDetails drawer.
*
* @hidden
*/
__internal_closeSubscriptionDetails: () => void;
Expand All @@ -440,12 +464,14 @@ export interface Clerk {

/**
* Closes the Clerk user verification modal.
*
* @hidden
*/
__internal_closeReverification: () => void;

/**
* Attempts to enable a environment setting from a development instance, prompting if disabled.
*
* @hidden
*/
__internal_attemptToEnableEnvironmentSetting: (
Expand All @@ -454,12 +480,14 @@ export interface Clerk {

/**
* Opens the Clerk Enable Organizations prompt for development instance
*
* @hidden
*/
__internal_openEnableOrganizationsPrompt: (props: __internal_EnableOrganizationsPromptProps) => void;

/**
* Closes the Clerk Enable Organizations modal.
*
* @hidden
*/
__internal_closeEnableOrganizationsPrompt: () => void;
Expand Down Expand Up @@ -939,12 +967,14 @@ export interface Clerk {

/**
* Returns the configured `afterSignInUrl` of the instance.
*
* @param params - Optional query parameters to append to the URL.
*/
buildAfterSignInUrl({ params }?: { params?: URLSearchParams }): string;

/**
* Returns the configured `afterSignUpUrl` of the instance.
*
* @param params - Optional query parameters to append to the URL.
*/
buildAfterSignUpUrl({ params }?: { params?: URLSearchParams }): string;
Expand Down Expand Up @@ -1100,6 +1130,7 @@ export interface Clerk {

/**
* Completes an email link verification flow started by `Clerk.client.signIn.createEmailLinkFlow` or `Clerk.client.signUp.createEmailLinkFlow`, by processing the verification results from the redirect URL query parameters. This method should be called after the user is redirected back from visiting the verification link in their email.
*
* @param params - Allows you to define the URLs where the user should be redirected to on successful verification or pending/completed sign-up or sign-in attempts. If the email link is successfully verified on another device, there's a callback function parameter that allows custom code execution.
* @param customNavigate - A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows.
*/
Expand Down Expand Up @@ -1385,6 +1416,7 @@ export type ClerkOptions = ClerkOptionsNavigation &
localization?: LocalizationResource;
/**
* Indicates whether Clerk should poll against Clerk's backend every 5 minutes.
*
* @default true
*/
polling?: boolean;
Expand Down
Loading