Skip to content

Commit 131c4d7

Browse files
committed
fix: load CDN scripts sequentially and preserve script type in widget iframe
External scripts (e.g. Three.js from CDN) were appended via DOM and loaded asynchronously, so inline scripts that depend on them would execute before the library was available. Now scripts are chained sequentially — each external script waits for onload before the next script runs. Also preserves the script `type` attribute (needed for `type="module"`) and adds CDN origins to CSP `connect-src` so dynamic imports and fetch from allowed CDNs work in the sandboxed iframe.
1 parent 98c8c9b commit 131c4d7

1 file changed

Lines changed: 25 additions & 9 deletions

File tree

apps/app/src/components/generative-ui/widget-renderer.tsx

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ window.addEventListener('message', function(e) {
369369
var scriptCloses = (rawHtml.match(/<\\/script>/gi) || []).length;
370370
var allScriptsClosed = scriptOpens <= scriptCloses;
371371
tmp.querySelectorAll('script').forEach(function(s) {
372-
incomingScripts.push({ src: s.src, text: s.textContent });
372+
incomingScripts.push({ src: s.src, text: s.textContent, type: s.type || '' });
373373
s.remove();
374374
});
375375
@@ -414,24 +414,36 @@ window.addEventListener('message', function(e) {
414414
content.innerHTML = tmp.innerHTML;
415415
}
416416
417-
// Execute only new scripts — skip entirely while a <script> tag is still streaming
417+
// Execute only new scripts — skip entirely while a <script> tag is still streaming.
418+
// External scripts (src) are loaded sequentially and we wait for each to finish
419+
// before running subsequent scripts, so inline code can use libraries like Three.js.
418420
if (allScriptsClosed) {
419-
incomingScripts.forEach(function(scriptInfo) {
421+
(function runScripts(scripts, idx) {
422+
if (idx >= scripts.length) return;
423+
var scriptInfo = scripts[idx];
420424
var key = scriptInfo.src || scriptInfo.text;
421-
if (!key || !key.trim()) return;
425+
if (!key || !key.trim()) { runScripts(scripts, idx + 1); return; }
422426
var hash = btoa(unescape(encodeURIComponent(key))).slice(0, 16).replace(/[^a-zA-Z0-9]/g, '');
423-
if (!hash || content.getAttribute('data-exec-' + hash)) return;
427+
if (!hash || content.getAttribute('data-exec-' + hash)) { runScripts(scripts, idx + 1); return; }
424428
content.setAttribute('data-exec-' + hash, '1');
425429
try {
426430
var newScript = document.createElement('script');
431+
if (scriptInfo.type) newScript.type = scriptInfo.type;
427432
if (scriptInfo.src) {
428433
newScript.src = scriptInfo.src;
434+
newScript.onload = function() { runScripts(scripts, idx + 1); };
435+
newScript.onerror = function() { runScripts(scripts, idx + 1); };
436+
content.appendChild(newScript);
429437
} else {
430438
newScript.textContent = scriptInfo.text;
439+
content.appendChild(newScript);
440+
runScripts(scripts, idx + 1);
431441
}
432-
content.appendChild(newScript);
433-
} catch(e) { console.warn('[widget] script exec failed:', e); }
434-
});
442+
} catch(e) {
443+
console.warn('[widget] script exec failed:', e);
444+
runScripts(scripts, idx + 1);
445+
}
446+
})(incomingScripts, 0);
435447
}
436448
reportHeight();
437449
}
@@ -469,7 +481,11 @@ function assembleShell(initialHtml: string = ""): string {
469481
style-src 'unsafe-inline';
470482
img-src 'self' data: blob:;
471483
font-src 'self' data:;
472-
connect-src 'self';
484+
connect-src 'self'
485+
https://cdnjs.cloudflare.com
486+
https://esm.sh
487+
https://cdn.jsdelivr.net
488+
https://unpkg.com;
473489
">
474490
<style>
475491
${THEME_CSS}

0 commit comments

Comments
 (0)