Skip to content

Commit c67cdb2

Browse files
committed
feat(shared): expose moduleManager via getter across build boundary
1 parent d8fc1d7 commit c67cdb2

5 files changed

Lines changed: 94 additions & 3 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/shared': patch
4+
'@clerk/react': patch
5+
---
6+
7+
Internal plumbing to support `@clerk/ui` composed `UserProfile` / `OrganizationProfile` subcomponents.

packages/clerk-js/src/core/clerk.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,20 @@ export class Clerk implements ClerkInterface {
275275
#pageLifecycle: ReturnType<typeof createPageLifecycle> | null = null;
276276
#touchThrottledUntil = 0;
277277
#publicEventBus = createClerkEventBus();
278+
#moduleManager = new ModuleManager();
279+
280+
/**
281+
* Cross-bundle handle to the ModuleManager. clerk-js is loaded standalone
282+
* from the CDN with its own inlined @clerk/shared, so plain property access
283+
* is the only channel that reliably crosses that boundary. This getter is
284+
* how IsomorphicClerk forwards the manager to consumers that import
285+
* @clerk/shared from node_modules (e.g. @clerk/react, @clerk/ui).
286+
*
287+
* @internal
288+
*/
289+
get __internal_moduleManager(): ModuleManager {
290+
return this.#moduleManager;
291+
}
278292

279293
get __internal_queryClient(): { __tag: 'clerk-rq-client'; client: QueryClient } | undefined {
280294
if (!this.#queryClient) {
@@ -558,7 +572,7 @@ export class Clerk implements ClerkInterface {
558572
() => this,
559573
() => this.environment,
560574
this.#options,
561-
new ModuleManager(),
575+
this.#moduleManager,
562576
),
563577
);
564578
}

packages/react/src/__tests__/isomorphicClerk.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,31 @@ describe('isomorphicClerk', () => {
4848
}).not.toThrow();
4949
});
5050

51+
// Regression: composed/subcomponent UserProfile reads moduleManager via
52+
// `useClerk().__internal_moduleManager`. `useClerk()` returns the
53+
// IsomorphicClerk wrapper, so its getter must chain through to the loaded
54+
// clerk-js's own `__internal_moduleManager`. This plain property access is
55+
// the cross-bundle channel: clerk-js ships standalone from the CDN with its
56+
// own inlined @clerk/shared, so module-scoped state cannot bridge the two.
57+
//
58+
// Without this chain, every dynamic-imported feature (Coinbase Wallet, Base,
59+
// Stripe, zxcvbn) falls back to a rejecting manager.
60+
it('exposes the inner clerk-js moduleManager through the __internal_moduleManager getter', () => {
61+
const isomorphicClerk = new IsomorphicClerk({ publishableKey: 'pk_test_XXX' });
62+
const mm = { import: vi.fn(() => Promise.resolve(undefined)) };
63+
64+
// Before clerk-js loads, the getter is undefined so readers fall back.
65+
expect(isomorphicClerk.__internal_moduleManager).toBeUndefined();
66+
67+
const innerClerk: any = {
68+
addListener: vi.fn(),
69+
__internal_moduleManager: mm,
70+
};
71+
(isomorphicClerk as any).replayInterceptedInvocations(innerClerk);
72+
73+
expect(isomorphicClerk.__internal_moduleManager).toBe(mm);
74+
});
75+
5176
it('updates props asynchronously after clerkjs has loaded', async () => {
5277
const propsHistory: any[] = [];
5378
const dummyClerkJS = {

packages/react/src/isomorphicClerk.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { inBrowser } from '@clerk/shared/browser';
22
import { clerkEvents, createClerkEventBus } from '@clerk/shared/clerkEventBus';
33
import { ALLOWED_PROTOCOLS, windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate';
44
import { loadClerkJSScript, loadClerkUIScript } from '@clerk/shared/loadClerkJsScript';
5+
import type { ModuleManager } from '@clerk/shared/moduleManager';
56
import type {
67
__internal_AttemptToEnableEnvironmentSettingParams,
78
__internal_AttemptToEnableEnvironmentSettingResult,
@@ -285,6 +286,18 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
285286
windowNavigate(to, { allowedProtocols });
286287
};
287288

289+
/**
290+
* Proxies to the inner Clerk instance's ModuleManager. Returns `undefined`
291+
* before clerk-js has loaded; composed UI components read this getter
292+
* (via `useClerk()`) to resolve dynamic-imported modules and fall back to a
293+
* rejecting manager while it is `undefined`.
294+
*
295+
* @internal
296+
*/
297+
public get __internal_moduleManager(): ModuleManager | undefined {
298+
return this.clerkjs?.__internal_moduleManager;
299+
}
300+
288301
constructor(options: IsomorphicClerkOptions) {
289302
this.#publishableKey = options?.publishableKey;
290303
this.#proxyUrl = options?.proxyUrl;

packages/shared/src/types/clerk.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { ClerkGlobalHookError } from '../errors/globalHookError';
1+
import type { ClerkGlobalHookError } from '@/errors/globalHookError';
2+
3+
import type { ModuleManager } from '../moduleManager';
24
import type { ClerkUIConstructor } from '../ui/types';
35
import type { APIKeysNamespace } from './apiKeys';
46
import type {
@@ -139,11 +141,13 @@ export type SDKMetadata = {
139141

140142
/**
141143
* A callback function that is called when Clerk resources change.
144+
*
142145
* @inline
143146
*/
144147
export type ListenerCallback = (emission: Resources) => void;
145148
/**
146149
* Optional configuration for the `addListener()` method.
150+
*
147151
* @param skipInitialEmit - If `true`, the callback will not be called immediately after registration. Defaults to `false`.
148152
* @inline
149153
*/
@@ -188,7 +192,8 @@ export type SetActiveNavigate = (params: {
188192

189193
/**
190194
* A callback that runs after sign out completes.
191-
* @inline */
195+
*
196+
@inline */
192197
export type SignOutCallback = () => void | Promise<any>;
193198

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

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

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

318337
/**
319338
* 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.
339+
*
320340
* @inline
321341
*/
322342
isStandardBrowser: boolean | undefined;
@@ -350,6 +370,7 @@ export interface Clerk {
350370
* `effect()` that can be used to subscribe to changes from Signals.
351371
*
352372
* @hidden
373+
*
353374
* @experimental This experimental API is subject to change.
354375
*/
355376
__internal_state: State;
@@ -398,6 +419,7 @@ export interface Clerk {
398419

399420
/**
400421
* Closes the Clerk Checkout drawer.
422+
*
401423
* @hidden
402424
*/
403425
__internal_closeCheckout: () => void;
@@ -412,6 +434,7 @@ export interface Clerk {
412434

413435
/**
414436
* Closes the Clerk PlanDetails drawer.
437+
*
415438
* @hidden
416439
*/
417440
__internal_closePlanDetails: () => void;
@@ -426,6 +449,7 @@ export interface Clerk {
426449

427450
/**
428451
* Closes the Clerk SubscriptionDetails drawer.
452+
*
429453
* @hidden
430454
*/
431455
__internal_closeSubscriptionDetails: () => void;
@@ -440,12 +464,14 @@ export interface Clerk {
440464

441465
/**
442466
* Closes the Clerk user verification modal.
467+
*
443468
* @hidden
444469
*/
445470
__internal_closeReverification: () => void;
446471

447472
/**
448473
* Attempts to enable a environment setting from a development instance, prompting if disabled.
474+
*
449475
* @hidden
450476
*/
451477
__internal_attemptToEnableEnvironmentSetting: (
@@ -454,12 +480,14 @@ export interface Clerk {
454480

455481
/**
456482
* Opens the Clerk Enable Organizations prompt for development instance
483+
*
457484
* @hidden
458485
*/
459486
__internal_openEnableOrganizationsPrompt: (props: __internal_EnableOrganizationsPromptProps) => void;
460487

461488
/**
462489
* Closes the Clerk Enable Organizations modal.
490+
*
463491
* @hidden
464492
*/
465493
__internal_closeEnableOrganizationsPrompt: () => void;
@@ -939,12 +967,14 @@ export interface Clerk {
939967

940968
/**
941969
* Returns the configured `afterSignInUrl` of the instance.
970+
*
942971
* @param params - Optional query parameters to append to the URL.
943972
*/
944973
buildAfterSignInUrl({ params }?: { params?: URLSearchParams }): string;
945974

946975
/**
947976
* Returns the configured `afterSignUpUrl` of the instance.
977+
*
948978
* @param params - Optional query parameters to append to the URL.
949979
*/
950980
buildAfterSignUpUrl({ params }?: { params?: URLSearchParams }): string;
@@ -1100,6 +1130,7 @@ export interface Clerk {
11001130

11011131
/**
11021132
* 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.
1133+
*
11031134
* @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.
11041135
* @param customNavigate - A function that overrides Clerk's default navigation behavior, allowing custom handling of navigation during sign-up and sign-in flows.
11051136
*/
@@ -1385,6 +1416,7 @@ export type ClerkOptions = ClerkOptionsNavigation &
13851416
localization?: LocalizationResource;
13861417
/**
13871418
* Indicates whether Clerk should poll against Clerk's backend every 5 minutes.
1419+
*
13881420
* @default true
13891421
*/
13901422
polling?: boolean;

0 commit comments

Comments
 (0)