Skip to content

Commit 230b487

Browse files
committed
feat: adds isBrowser guard to ensure function only runs in browser environments
1 parent 051652d commit 230b487

File tree

2 files changed

+49
-10
lines changed

2 files changed

+49
-10
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,33 @@ describe("loadScript", () => {
1010
__resetScriptCache();
1111
});
1212

13+
test("rejects when not in a browser environment", async () => {
14+
const originalWindow = globalThis.window;
15+
const originalDocument = globalThis.document;
16+
17+
Object.defineProperty(globalThis, "window", {
18+
value: undefined,
19+
writable: false,
20+
});
21+
Object.defineProperty(globalThis, "document", {
22+
value: undefined,
23+
writable: false,
24+
});
25+
26+
const promise = loadScript("https://cdn.example.com/test.js");
27+
28+
await expect(promise).rejects.toThrow("loadScript can only be used in the browser");
29+
30+
Object.defineProperty(globalThis, "window", {
31+
value: originalWindow,
32+
writable: false,
33+
});
34+
Object.defineProperty(globalThis, "document", {
35+
value: originalDocument,
36+
writable: false,
37+
});
38+
});
39+
1340
test("rejects if src is not provided", async () => {
1441
await expect(loadScript("")).rejects.toThrow('No "src" provided to loadScript');
1542
});

src/loadScript.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ export type LoadScriptOptions = {
6262
// Promise cache for script sources
6363
const SCRIPT_CACHE = new Map<string, Promise<HTMLScriptElement>>();
6464

65+
const isBrowser = () => typeof window !== "undefined" && typeof window.document !== "undefined";
66+
6567
export const __resetScriptCache = () => {
6668
SCRIPT_CACHE.clear();
6769
};
@@ -87,7 +89,7 @@ export const __resetScriptCache = () => {
8789
export const loadScript = async (
8890
src: string,
8991
options: LoadScriptOptions = {},
90-
container: HTMLElement | null = document.head,
92+
container?: HTMLElement | null,
9193
): Promise<HTMLScriptElement> => {
9294
const {
9395
id,
@@ -107,8 +109,19 @@ export const loadScript = async (
107109
...attributes
108110
} = options;
109111

110-
// 1. Reject if no src provided
111-
if (!src) return Promise.reject(new Error('No "src" provided to loadScript'));
112+
// 1. Reject if not in browser
113+
if (!isBrowser()) {
114+
const error = new Error("loadScript can only be used in the browser");
115+
error.name = "NotBrowserEnvironmentError";
116+
return Promise.reject(error);
117+
}
118+
119+
// 2. Reject if no src provided
120+
if (!src) {
121+
const error = new Error('No "src" provided to loadScript');
122+
error.name = "NoSrcProvidedError";
123+
return Promise.reject(error);
124+
}
112125

113126
// We strictly consider scripts with innerHTML as non-cacheable, since they are not guaranteed to be idempotent.
114127
// Note: We intentionally ignore Solid reactivity here, since this logic is static and not part of a reactive execution context.
@@ -121,25 +134,24 @@ export const loadScript = async (
121134
// the script from the DOM but we still have the promise for it cached.
122135
const existingTag = document.querySelector(`script[src="${src}"]`) as HTMLScriptElement | null;
123136

124-
// 2. Check if script already exists in cache
137+
// 3. Check if script already exists in cache
125138
if (SCRIPT_CACHE.has(src)) {
126-
// 2a. If script exists in cache and is still in the DOM, return the cached promise
139+
// 3a. If script exists in cache and is still in the DOM, return the cached promise
127140
if (document.contains(existingTag)) return SCRIPT_CACHE.get(src)!;
128141

129-
// 2b. Script element was removed from DOM — evict from cache
142+
// 3b. Script element was removed from DOM — evict from cache
130143
SCRIPT_CACHE.delete(src);
131144
}
132145

133-
// 3. Check if script already exists (may have been added externally not via this hook)
146+
// 4. Check if script already exists (may have been added externally not via this hook)
134147
if (existingTag) return Promise.resolve(existingTag);
135148
}
136149

137-
// 4. Create new script element
150+
// 5. Create new script element
138151
const promise = new Promise<HTMLScriptElement>((resolve, reject) => {
139152
const script = document.createElement("script");
140153

141-
const _container = container ?? document.head;
142-
_container.appendChild(script);
154+
(container ?? document.head).appendChild(script);
143155

144156
script.src = src;
145157
script.type = type;

0 commit comments

Comments
 (0)