@@ -16,8 +16,11 @@ const errorThrower = buildErrorThrower({ packageName: '@clerk/shared' });
1616/**
1717 * Sets the package name for error messages during ClerkJS script loading.
1818 *
19+ * @param packageName - The name of the package to use in error messages (e.g., '@clerk/clerk-react').
1920 * @example
21+ * ```typescript
2022 * setClerkJsLoadingErrorPackageName('@clerk/clerk-react');
23+ * ```
2124 */
2225export function setClerkJsLoadingErrorPackageName ( packageName : string ) {
2326 errorThrower . setPackageName ( { packageName } ) ;
@@ -32,58 +35,147 @@ type LoadClerkJsScriptOptions = Without<ClerkOptions, 'isSatellite'> & {
3235 proxyUrl ?: string ;
3336 domain ?: string ;
3437 nonce ?: string ;
38+ /**
39+ * Timeout in milliseconds to wait for clerk-js to load before considering it failed.
40+ *
41+ * @default 15000 (15 seconds)
42+ */
43+ scriptLoadTimeout ?: number ;
3544} ;
3645
3746/**
38- * Hotloads the Clerk JS script.
47+ * Validates that window.Clerk exists and is properly initialized.
48+ * This ensures we don't have false positives where the script loads but Clerk is malformed.
3949 *
40- * Checks for an existing Clerk JS script. If found, it returns a promise
41- * that resolves when the script loads. If not found, it uses the provided options to
42- * build the Clerk JS script URL and load the script.
50+ * @returns `true` if window.Clerk exists and has the expected structure with a load method.
51+ */
52+ function isClerkProperlyLoaded ( ) : boolean {
53+ if ( typeof window === 'undefined' || ! ( window as any ) . Clerk ) {
54+ return false ;
55+ }
56+
57+ // Basic validation that window.Clerk has the expected structure
58+ const clerk = ( window as any ) . Clerk ;
59+ return typeof clerk === 'object' && typeof clerk . load === 'function' ;
60+ }
61+
62+ /**
63+ * Waits for Clerk to be properly loaded with a timeout mechanism.
64+ * Uses polling to check if Clerk becomes available within the specified timeout.
65+ *
66+ * @param timeoutMs - Maximum time to wait in milliseconds.
67+ * @returns Promise that resolves with null if Clerk loads successfully, or rejects with an error if timeout is reached.
68+ */
69+ function waitForClerkWithTimeout ( timeoutMs : number ) : Promise < HTMLScriptElement | null > {
70+ return new Promise ( ( resolve , reject ) => {
71+ let resolved = false ;
72+
73+ const cleanup = ( timeoutId : ReturnType < typeof setTimeout > , pollInterval : ReturnType < typeof setInterval > ) => {
74+ clearTimeout ( timeoutId ) ;
75+ clearInterval ( pollInterval ) ;
76+ } ;
77+
78+ const checkAndResolve = ( ) => {
79+ if ( resolved ) return ;
80+
81+ if ( isClerkProperlyLoaded ( ) ) {
82+ resolved = true ;
83+ cleanup ( timeoutId , pollInterval ) ;
84+ resolve ( null ) ;
85+ }
86+ } ;
87+
88+ const handleTimeout = ( ) => {
89+ if ( resolved ) return ;
90+
91+ resolved = true ;
92+ cleanup ( timeoutId , pollInterval ) ;
93+
94+ if ( ! isClerkProperlyLoaded ( ) ) {
95+ reject ( new Error ( FAILED_TO_LOAD_ERROR ) ) ;
96+ } else {
97+ resolve ( null ) ;
98+ }
99+ } ;
100+
101+ const timeoutId = setTimeout ( handleTimeout , timeoutMs ) ;
102+
103+ checkAndResolve ( ) ;
104+
105+ const pollInterval = setInterval ( ( ) => {
106+ if ( resolved ) {
107+ clearInterval ( pollInterval ) ;
108+ return ;
109+ }
110+ checkAndResolve ( ) ;
111+ } , 100 ) ;
112+ } ) ;
113+ }
114+
115+ /**
116+ * Hotloads the Clerk JS script with robust failure detection.
117+ *
118+ * Uses a timeout-based approach to ensure absolute certainty about load success/failure.
119+ * If the script fails to load within the timeout period, or loads but doesn't create
120+ * a proper Clerk instance, the promise rejects with an error.
43121 *
44122 * @param opts - The options used to build the Clerk JS script URL and load the script.
45123 * Must include a `publishableKey` if no existing script is found.
124+ * @returns Promise that resolves with null if Clerk loads successfully, or rejects with an error.
46125 *
47126 * @example
48- * loadClerkJsScript({ publishableKey: 'pk_' });
127+ * ```typescript
128+ * try {
129+ * await loadClerkJsScript({ publishableKey: 'pk_test_...' });
130+ * console.log('Clerk loaded successfully');
131+ * } catch (error) {
132+ * console.error('Failed to load Clerk:', error.message);
133+ * }
134+ * ```
49135 */
50- const loadClerkJsScript = async ( opts ?: LoadClerkJsScriptOptions ) => {
136+ const loadClerkJsScript = async ( opts ?: LoadClerkJsScriptOptions ) : Promise < HTMLScriptElement | null > => {
137+ const timeout = opts ?. scriptLoadTimeout ?? 15000 ;
138+
139+ if ( isClerkProperlyLoaded ( ) ) {
140+ return null ;
141+ }
142+
51143 const existingScript = document . querySelector < HTMLScriptElement > ( 'script[data-clerk-js-script]' ) ;
52144
53145 if ( existingScript ) {
54- return new Promise ( ( resolve , reject ) => {
55- existingScript . addEventListener ( 'load' , ( ) => {
56- resolve ( existingScript ) ;
57- } ) ;
58-
59- existingScript . addEventListener ( 'error' , ( ) => {
60- reject ( FAILED_TO_LOAD_ERROR ) ;
61- } ) ;
62- } ) ;
146+ return waitForClerkWithTimeout ( timeout ) ;
63147 }
64148
65149 if ( ! opts ?. publishableKey ) {
66150 errorThrower . throwMissingPublishableKeyError ( ) ;
67- return ;
151+ return null ;
68152 }
69153
70- return loadScript ( clerkJsScriptUrl ( opts ) , {
154+ const loadPromise = waitForClerkWithTimeout ( timeout ) ;
155+
156+ loadScript ( clerkJsScriptUrl ( opts ) , {
71157 async : true ,
72158 crossOrigin : 'anonymous' ,
73159 nonce : opts . nonce ,
74160 beforeLoad : applyClerkJsScriptAttributes ( opts ) ,
75161 } ) . catch ( ( ) => {
76162 throw new Error ( FAILED_TO_LOAD_ERROR ) ;
77163 } ) ;
164+
165+ return loadPromise ;
78166} ;
79167
80168/**
81- * Generates a Clerk JS script URL.
169+ * Generates a Clerk JS script URL based on the provided options .
82170 *
83171 * @param opts - The options to use when building the Clerk JS script URL.
172+ * @returns The complete URL to the Clerk JS script.
84173 *
85174 * @example
86- * clerkJsScriptUrl({ publishableKey: 'pk_' });
175+ * ```typescript
176+ * const url = clerkJsScriptUrl({ publishableKey: 'pk_test_...' });
177+ * // Returns: "https://example.clerk.accounts.dev/npm/@clerk/clerk-js@5/dist/clerk.browser.js"
178+ * ```
87179 */
88180const clerkJsScriptUrl = ( opts : LoadClerkJsScriptOptions ) => {
89181 const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts ;
@@ -107,7 +199,10 @@ const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => {
107199} ;
108200
109201/**
110- * Builds an object of Clerk JS script attributes.
202+ * Builds an object of Clerk JS script attributes based on the provided options.
203+ *
204+ * @param options - The options containing the values for script attributes.
205+ * @returns An object containing data attributes to be applied to the script element.
111206 */
112207const buildClerkJsScriptAttributes = ( options : LoadClerkJsScriptOptions ) => {
113208 const obj : Record < string , string > = { } ;
@@ -131,6 +226,12 @@ const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => {
131226 return obj ;
132227} ;
133228
229+ /**
230+ * Returns a function that applies Clerk JS script attributes to a script element.
231+ *
232+ * @param options - The options containing the values for script attributes.
233+ * @returns A function that accepts a script element and applies the attributes to it.
234+ */
134235const applyClerkJsScriptAttributes = ( options : LoadClerkJsScriptOptions ) => ( script : HTMLScriptElement ) => {
135236 const attributes = buildClerkJsScriptAttributes ( options ) ;
136237 for ( const attribute in attributes ) {
0 commit comments