@@ -16,8 +16,11 @@ const errorThrower = buildErrorThrower({ packageName: '@clerk/shared' });
16
16
/**
17
17
* Sets the package name for error messages during ClerkJS script loading.
18
18
*
19
+ * @param packageName - The name of the package to use in error messages (e.g., '@clerk/clerk-react').
19
20
* @example
21
+ * ```typescript
20
22
* setClerkJsLoadingErrorPackageName('@clerk/clerk-react');
23
+ * ```
21
24
*/
22
25
export function setClerkJsLoadingErrorPackageName ( packageName : string ) {
23
26
errorThrower . setPackageName ( { packageName } ) ;
@@ -32,58 +35,147 @@ type LoadClerkJsScriptOptions = Without<ClerkOptions, 'isSatellite'> & {
32
35
proxyUrl ?: string ;
33
36
domain ?: string ;
34
37
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 ;
35
44
} ;
36
45
37
46
/**
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.
39
49
*
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.
43
121
*
44
122
* @param opts - The options used to build the Clerk JS script URL and load the script.
45
123
* 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.
46
125
*
47
126
* @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
+ * ```
49
135
*/
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
+
51
143
const existingScript = document . querySelector < HTMLScriptElement > ( 'script[data-clerk-js-script]' ) ;
52
144
53
145
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 ) ;
63
147
}
64
148
65
149
if ( ! opts ?. publishableKey ) {
66
150
errorThrower . throwMissingPublishableKeyError ( ) ;
67
- return ;
151
+ return null ;
68
152
}
69
153
70
- return loadScript ( clerkJsScriptUrl ( opts ) , {
154
+ const loadPromise = waitForClerkWithTimeout ( timeout ) ;
155
+
156
+ loadScript ( clerkJsScriptUrl ( opts ) , {
71
157
async : true ,
72
158
crossOrigin : 'anonymous' ,
73
159
nonce : opts . nonce ,
74
160
beforeLoad : applyClerkJsScriptAttributes ( opts ) ,
75
161
} ) . catch ( ( ) => {
76
162
throw new Error ( FAILED_TO_LOAD_ERROR ) ;
77
163
} ) ;
164
+
165
+ return loadPromise ;
78
166
} ;
79
167
80
168
/**
81
- * Generates a Clerk JS script URL.
169
+ * Generates a Clerk JS script URL based on the provided options .
82
170
*
83
171
* @param opts - The options to use when building the Clerk JS script URL.
172
+ * @returns The complete URL to the Clerk JS script.
84
173
*
85
174
* @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
+ * ```
87
179
*/
88
180
const clerkJsScriptUrl = ( opts : LoadClerkJsScriptOptions ) => {
89
181
const { clerkJSUrl, clerkJSVariant, clerkJSVersion, proxyUrl, domain, publishableKey } = opts ;
@@ -107,7 +199,10 @@ const clerkJsScriptUrl = (opts: LoadClerkJsScriptOptions) => {
107
199
} ;
108
200
109
201
/**
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.
111
206
*/
112
207
const buildClerkJsScriptAttributes = ( options : LoadClerkJsScriptOptions ) => {
113
208
const obj : Record < string , string > = { } ;
@@ -131,6 +226,12 @@ const buildClerkJsScriptAttributes = (options: LoadClerkJsScriptOptions) => {
131
226
return obj ;
132
227
} ;
133
228
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
+ */
134
235
const applyClerkJsScriptAttributes = ( options : LoadClerkJsScriptOptions ) => ( script : HTMLScriptElement ) => {
135
236
const attributes = buildClerkJsScriptAttributes ( options ) ;
136
237
for ( const attribute in attributes ) {
0 commit comments