Skip to content

loadScript

Edit

Loads browser scripts with URL deduplication and safe retry behavior.

loadScript.ts
const pending = new Map<string, Promise<HTMLScriptElement>>();
const states = new WeakMap<HTMLScriptElement, "loading" | "loaded" | "failed">();
export type LoadScriptAttributes = Readonly<Record<string, string>>;
/** Loads an external browser script with URL deduplication and retryable failures. */
export const loadScript = (
source: string,
attributes: LoadScriptAttributes = {}
): Promise<HTMLScriptElement> => {
const normalizedSource = source.trim();
if (normalizedSource.length === 0) {
return Promise.reject(new TypeError("source must not be empty"));
}
let url: string;
try {
const base =
document.baseURI === "about:blank"
? "https://localhost/"
: document.baseURI;
url = new URL(normalizedSource, base).href;
} catch (error) {
return Promise.reject(error);
}
const cached = pending.get(url);
if (cached) return cached;
const matching = Array.from(document.scripts).filter((script) => script.src === url);
const ready = matching.find((script) => states.get(script) === "loaded")
?? matching.find((script) => !states.has(script));
if (ready) return Promise.resolve(ready);
for (const failed of matching.filter((script) => states.get(script) === "failed")) {
failed.remove();
}
const existing = matching.find((script) => states.get(script) === "loading");
const script = existing ?? document.createElement("script");
const created = existing === undefined;
if (created) {
try {
for (const [name, value] of Object.entries(attributes)) {
const normalizedName = name.toLowerCase();
if (normalizedName !== "src") script.setAttribute(name, value);
}
states.set(script, "loading");
script.src = url;
} catch (error) {
return Promise.reject(error);
}
}
let resolvePromise: (script: HTMLScriptElement) => void = () => {};
let rejectPromise: (error: unknown) => void = () => {};
const promise = new Promise<HTMLScriptElement>((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});
pending.set(url, promise);
let settled = false;
const cleanup = () => {
script.removeEventListener("load", loaded);
script.removeEventListener("error", failed);
if (pending.get(url) === promise) pending.delete(url);
};
const loaded = () => {
if (settled) return;
settled = true;
states.set(script, "loaded");
cleanup();
resolvePromise(script);
};
const failed = () => {
if (settled) return;
settled = true;
states.set(script, "failed");
cleanup();
script.remove();
rejectPromise(new Error(`Failed to load script: ${url}`));
};
script.addEventListener("load", loaded, { once: true });
script.addEventListener("error", failed, { once: true });
if (created) {
try {
document.head.append(script);
} catch {
failed();
}
}
return promise;
};
Terminal
wget -O src/lib/loadScript.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/functions/loadScript.ts
loadScript.example.ts
import { loadScript, type LoadScriptAttributes } from "./loadScript";
const attributes = {
integrity: "sha384-...",
crossorigin: "anonymous",
referrerpolicy: "no-referrer"
} satisfies LoadScriptAttributes;
const script: HTMLScriptElement = await loadScript(
"https://cdn.example.com/widget.js",
attributes
);
  • source (string) — Non-empty absolute or document-relative script URL; surrounding whitespace is ignored.
  • attributes (Record<string, string>) — Optional script attributes.
  • Calls for the same resolved URL share one promise while this helper is loading it. Attributes from that first call win, and source always overrides a src attribute.
  • A matching unmarked script is assumed to have been loaded and is returned without mutation, so its existing integrity, CORS, and other attributes win. Browsers do not expose a reliable load state for arbitrary existing scripts.
  • Helper-owned state is kept privately: concurrent loads are awaited and failed elements are removed, so the next call creates a fresh script and retries.
  • Event listeners and pending state are installed before insertion, so synchronous load, error, and insertion failures settle correctly.
  • Invalid sources, attributes, and other setup failures reject the returned promise instead of escaping as synchronous exceptions.