Skip to content

Commit 6d06be8

Browse files
committed
wip
1 parent 939985a commit 6d06be8

File tree

2 files changed

+112
-61
lines changed

2 files changed

+112
-61
lines changed

packages/nextjs/src/utils/clerk-js-script.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,11 @@ function useClerkJSLoadingState() {
150150
* Enhanced ClerkJS Script component with bulletproof load detection.
151151
*
152152
* This component renders TWO script tags:
153-
* 1. A render-blocking inline script that sets up the coordinator BEFORE any ClerkJS scripts
153+
* 1. A render-blocking inline script that sets up the coordinator globally
154154
* 2. The actual ClerkJS script tag (which the coordinator will manage)
155155
*
156-
* The blocking script intercepts script creation and prevents duplicates at the browser level.
156+
* The coordinator uses comprehensive DOM interception to catch scripts regardless
157+
* of where they're placed (head, body, or injected dynamically).
157158
*/
158159
function ClerkJSScript(props: ClerkJSScriptProps) {
159160
const { publishableKey, clerkJSUrl, clerkJSVersion, clerkJSVariant, nonce } = useClerkNextOptions();
@@ -210,6 +211,7 @@ function ClerkJSScript(props: ClerkJSScriptProps) {
210211

211212
if (props.router === 'app') {
212213
// For App Router, use regular script tags
214+
// The coordinator will catch these regardless of placement
213215
return (
214216
<>
215217
{/* Blocking coordinator script - MUST run first */}
@@ -222,15 +224,16 @@ function ClerkJSScript(props: ClerkJSScriptProps) {
222224
<script
223225
ref={scriptRef}
224226
src={scriptUrl}
225-
data-clerk-js-script
227+
data-clerk-js-script='true'
226228
async
227229
crossOrigin='anonymous'
228230
{...scriptAttributes}
229231
/>
230232
</>
231233
);
232234
} else {
233-
// For Pages Router, use Next.js Script components
235+
// For Pages Router, use Next.js Script components with beforeInteractive
236+
// This ensures both scripts are placed in head and execute early
234237
return (
235238
<>
236239
{/* Blocking coordinator script - MUST run first and block */}
@@ -243,7 +246,7 @@ function ClerkJSScript(props: ClerkJSScriptProps) {
243246
{/* Actual ClerkJS script - managed by the coordinator */}
244247
<NextScript
245248
src={scriptUrl}
246-
data-clerk-js-script
249+
data-clerk-js-script='true'
247250
async
248251
defer={false}
249252
crossOrigin='anonymous'

packages/shared/src/clerkJsBlockingCoordinator.ts

Lines changed: 104 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ interface BlockingCoordinator {
3232

3333
/**
3434
* Returns the inline script content that should be injected as a blocking script.
35-
* This script sets up the global coordinator immediately.
35+
* This script sets up the global coordinator immediately and intercepts all script loading.
3636
*/
3737
export function getBlockingCoordinatorScript(): string {
3838
return `
@@ -86,30 +86,36 @@ export function getBlockingCoordinatorScript(): string {
8686
8787
var self = this;
8888
89-
// Set up load monitoring
90-
var originalOnLoad = scriptElement.onload;
91-
var originalOnError = scriptElement.onerror;
92-
93-
scriptElement.onload = function(event) {
89+
// Set up load monitoring with multiple methods for better compatibility
90+
var handleLoad = function() {
9491
scriptElement.setAttribute('data-clerk-loaded', 'true');
9592
self.setState('loaded');
96-
if (originalOnLoad) originalOnLoad.call(this, event);
9793
};
9894
99-
scriptElement.onerror = function(event) {
95+
var handleError = function() {
10096
self.setState('error', new Error('ClerkJS failed to load'));
101-
if (originalOnError) originalOnError.call(this, event);
10297
};
10398
104-
// Also use addEventListener for better compatibility
105-
scriptElement.addEventListener('load', function() {
106-
scriptElement.setAttribute('data-clerk-loaded', 'true');
107-
self.setState('loaded');
108-
});
99+
// Use multiple approaches to catch load events
100+
if (scriptElement.onload !== undefined) {
101+
var originalOnLoad = scriptElement.onload;
102+
scriptElement.onload = function(event) {
103+
handleLoad();
104+
if (originalOnLoad) originalOnLoad.call(this, event);
105+
};
106+
}
109107
110-
scriptElement.addEventListener('error', function() {
111-
self.setState('error', new Error('ClerkJS failed to load'));
112-
});
108+
if (scriptElement.onerror !== undefined) {
109+
var originalOnError = scriptElement.onerror;
110+
scriptElement.onerror = function(event) {
111+
handleError();
112+
if (originalOnError) originalOnError.call(this, event);
113+
};
114+
}
115+
116+
// Also use addEventListener for better compatibility
117+
scriptElement.addEventListener('load', handleLoad);
118+
scriptElement.addEventListener('error', handleError);
113119
},
114120
115121
registerCallback: function(callback) {
@@ -151,54 +157,96 @@ export function getBlockingCoordinatorScript(): string {
151157
}
152158
};
153159
154-
// Intercept script creation to catch ClerkJS scripts before they load
155-
var originalCreateElement = document.createElement;
156-
document.createElement = function(tagName) {
157-
var element = originalCreateElement.apply(this, arguments);
158-
159-
if (tagName.toLowerCase() === 'script') {
160-
// Set up a property setter to intercept src assignment
161-
var originalSrc = element.src;
162-
var srcSet = false;
163-
164-
Object.defineProperty(element, 'src', {
165-
get: function() {
166-
return originalSrc;
167-
},
168-
set: function(value) {
169-
originalSrc = value;
170-
srcSet = true;
171-
172-
// Check if this should be allowed after src is set
173-
// (need to wait for src to be set to make decision)
174-
setTimeout(function() {
175-
if (!coordinator.shouldAllowScript(element)) {
176-
// Prevent the script from loading by clearing its src
177-
element.src = '';
178-
element.remove();
179-
}
180-
}, 0);
181-
},
182-
configurable: true
183-
});
160+
// Function to check and potentially intercept a script
161+
function interceptScript(scriptElement) {
162+
if (scriptElement && scriptElement.tagName === 'SCRIPT') {
163+
if (scriptElement.hasAttribute('data-clerk-js-script')) {
164+
return coordinator.shouldAllowScript(scriptElement);
165+
}
184166
}
185-
186-
return element;
187-
};
167+
return true;
168+
}
188169
189-
// Also intercept appendChild to catch scripts added directly
170+
// Intercept appendChild on all nodes
190171
var originalAppendChild = Node.prototype.appendChild;
191172
Node.prototype.appendChild = function(child) {
192-
if (child.tagName === 'SCRIPT' && child.hasAttribute('data-clerk-js-script')) {
193-
if (!coordinator.shouldAllowScript(child)) {
194-
// Return a dummy element to prevent errors
195-
return child;
196-
}
173+
if (!interceptScript(child)) {
174+
// Create a dummy script element to return
175+
var dummy = document.createElement('script');
176+
dummy.src = child.src;
177+
Object.keys(child.attributes || {}).forEach(function(key) {
178+
var attr = child.attributes[key];
179+
if (attr && attr.name && attr.value) {
180+
dummy.setAttribute(attr.name, attr.value);
181+
}
182+
});
183+
return dummy;
197184
}
198185
199186
return originalAppendChild.call(this, child);
200187
};
201188
189+
// Intercept insertBefore
190+
var originalInsertBefore = Node.prototype.insertBefore;
191+
Node.prototype.insertBefore = function(newNode, referenceNode) {
192+
if (!interceptScript(newNode)) {
193+
var dummy = document.createElement('script');
194+
dummy.src = newNode.src;
195+
return dummy;
196+
}
197+
198+
return originalInsertBefore.call(this, newNode, referenceNode);
199+
};
200+
201+
// Also watch for scripts being set via innerHTML or similar
202+
var observer = new MutationObserver(function(mutations) {
203+
mutations.forEach(function(mutation) {
204+
if (mutation.type === 'childList') {
205+
Array.prototype.slice.call(mutation.addedNodes).forEach(function(node) {
206+
if (node.nodeType === 1) { // Element node
207+
if (node.tagName === 'SCRIPT' && node.hasAttribute('data-clerk-js-script')) {
208+
if (!coordinator.shouldAllowScript(node)) {
209+
node.remove();
210+
}
211+
}
212+
213+
// Also check children in case scripts are added in bulk
214+
var scripts = node.querySelectorAll ? node.querySelectorAll('script[data-clerk-js-script]') : [];
215+
for (var i = 0; i < scripts.length; i++) {
216+
if (!coordinator.shouldAllowScript(scripts[i])) {
217+
scripts[i].remove();
218+
}
219+
}
220+
}
221+
});
222+
}
223+
});
224+
});
225+
226+
// Start observing
227+
if (document.body) {
228+
observer.observe(document.body, {
229+
childList: true,
230+
subtree: true
231+
});
232+
} else {
233+
// If body doesn't exist yet, wait for it
234+
document.addEventListener('DOMContentLoaded', function() {
235+
observer.observe(document.body, {
236+
childList: true,
237+
subtree: true
238+
});
239+
});
240+
}
241+
242+
// Also observe head if it exists
243+
if (document.head) {
244+
observer.observe(document.head, {
245+
childList: true,
246+
subtree: true
247+
});
248+
}
249+
202250
// Expose the coordinator globally
203251
window.__clerkJSBlockingCoordinator = coordinator;
204252
})();

0 commit comments

Comments
 (0)