fix(js): fire load/error for dynamically inserted stylesheet links (#412)

A <link rel=stylesheet href> appended via JS (createElement +
appendChild) was never fetched and fired neither load nor error, so any
Promise.all awaiting the link's onload (lazy CSS loaders, antd/bootstrap
fetchers) hung forever and the page stalled at its pre-load stage.

appendChild already drives this for <script src>; mirror it for links:
resolve the href against <base>/document URL, fetch it, and dispatch
load on success or error on failure. CSS is not applied (no layout
engine yet) but the load-event contract matches Chrome.

Read the stylesheet intent from both the rel attribute and the rel
property, since obscura does not yet reflect the rel IDL attribute back
to the content attribute. Closes #409.
This commit is contained in:
SGavrl authored and GitHub committed 2026-07-14 12:13:25 +00:00
1 parent a92166897f
commit c8c78ba229
2 files changed
+167

No files matched your search

+48
View File
@@ -198,6 +198,51 @@ async function __processDynScriptQueue() {
__dynScriptBusy = false;
}
}
// Resolve a resource URL (script src / link href) against <base href> or the
// document URL, the way the inline dynamic-script path does. Guarded so a bad
// base or href never throws into appendChild.
function _resolveResourceUrl(src) {
let baseHref = null;
try {
const baseEl = globalThis.document?.querySelector('base[href]');
baseHref = baseEl ? baseEl.getAttribute('href') : null;
} catch(e) { baseHref = null; }
const docUrl = globalThis.location?.href || 'http://localhost/';
let baseUrl;
try { baseUrl = baseHref ? new URL(baseHref, docUrl).href : docUrl; }
catch(e) { baseUrl = docUrl; }
try {
return src.startsWith('http') || src.startsWith('data:')
? src
: new URL(src, baseUrl).href;
} catch(e) { return src; }
}
// A dynamically-inserted <link rel="stylesheet" href> must fetch and fire
// load/error so frameworks awaiting the link's onload (Promise.all of lazy
// CSS + JS, antd/bootstrap loaders, etc.) resolve instead of hanging forever.
// There is no layout engine to apply the CSS, but the load-event contract
// matches Chrome. Issue #409.
async function _loadLinkedStylesheet(c) {
// obscura does not yet reflect the `rel` IDL attribute back to the content
// attribute, so `link.rel = "stylesheet"` leaves getAttribute('rel') null.
// Read both so the property-assignment form (the common framework pattern)
// and the parsed-from-HTML form are both recognized.
const rel = (c.getAttribute('rel') || c.rel || '').toString().toLowerCase();
if (!rel.split(/\s+/).includes('stylesheet')) return;
const href = c.getAttribute('href');
if (!href) return;
const fullUrl = _resolveResourceUrl(href);
let pageOrigin = "";
try { pageOrigin = new URL(fullUrl).origin; } catch(e) {}
try {
await Deno.core.ops.op_fetch_url(fullUrl, "GET", "{}", "", pageOrigin, "no-cors");
try { c.dispatchEvent(new Event('load', { bubbles: true })); } catch(e) {}
} catch(e) {
try { c.dispatchEvent(new Event('error', { bubbles: true })); } catch(e) {}
}
}
function _fpRand(salt) {
let h = (_fpSeed ^ (salt || 0)) | 0;
h = Math.imul(h ^ (h >>> 16), 0x45d9f3b);
@@ -687,6 +732,9 @@ class Node {
}
}
}
if (c instanceof Element && c.tagName === 'LINK') {
_loadLinkedStylesheet(c);
}
return c;
}
removeChild(c) {