Skip to content

Commit c6e376b

Browse files
committed
Add assets helpers
1 parent a337ce1 commit c6e376b

File tree

6 files changed

+379
-3
lines changed

6 files changed

+379
-3
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
v1.0.8
5+
------
6+
7+
* Add assets helpers.
8+
49
v1.0.7
510
------
611

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fivelab/web-utils",
3-
"version": "1.0.7",
3+
"version": "1.0.8",
44
"private": false,
55
"type": "module",
66
"description": "The helpers for easy manipulate with dom.",

src/browser/assets.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
export type AppendScriptOptions = {
2+
type?: string;
3+
async?: boolean;
4+
defer?: boolean;
5+
attributes?: Record<string, string>;
6+
};
7+
8+
export type AppendStyleOptions = {
9+
media?: string;
10+
attributes?: Record<string, string>;
11+
};
12+
13+
export type AppendAssetOptions = | AppendScriptOptions | AppendStyleOptions;
14+
15+
let scriptsMap = new Map<string, Promise<HTMLScriptElement>>();
16+
let stylesMap = new Map<string, Promise<HTMLLinkElement>>();
17+
18+
function findScriptByKey(key: string): HTMLScriptElement | undefined {
19+
const scripts = document.getElementsByTagName('script');
20+
21+
return Array.from(scripts).find((e) => e.src === key);
22+
}
23+
24+
function findStyleByKey(key: string): HTMLLinkElement | undefined {
25+
const links = document.getElementsByTagName('link');
26+
27+
return Array.from(links).find((e) => e.rel === 'stylesheet' && e.href === key);
28+
}
29+
30+
function getExtension(url: string): string | null {
31+
const clean = url.split('#')[0]!.split('?')[0]!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
32+
const idx = clean.lastIndexOf('.');
33+
34+
return idx >= 0 ? clean.slice(idx + 1).toLowerCase() : null;
35+
}
36+
37+
export function appendScript(src: string, options: AppendScriptOptions = {}): Promise<HTMLScriptElement> {
38+
const key = new URL(src, document.baseURI).href;
39+
40+
const cached = scriptsMap.get(key);
41+
42+
if (cached) {
43+
return cached;
44+
}
45+
46+
const existing = findScriptByKey(key);
47+
48+
if (existing) {
49+
const ready = Promise.resolve(existing);
50+
scriptsMap.set(key, ready);
51+
52+
return ready;
53+
}
54+
55+
const promise: Promise<HTMLScriptElement> = new Promise((resolve, reject) => {
56+
const script = document.createElement('script');
57+
58+
script.src = src;
59+
script.type = options.type ?? 'text/javascript';
60+
61+
if (options.async !== undefined) {
62+
script.async = options.async;
63+
}
64+
65+
if (options.defer !== undefined) {
66+
script.defer = options.defer;
67+
}
68+
69+
if (options.attributes) {
70+
Object.entries(options.attributes).forEach(([k, v]) => script.setAttribute(k, v));
71+
}
72+
73+
const cleanup = () => {
74+
script.removeEventListener('load', onLoad);
75+
script.removeEventListener('error', onError);
76+
};
77+
78+
const onLoad = () => {
79+
resolve(script);
80+
cleanup();
81+
};
82+
83+
const onError = () => {
84+
script.remove();
85+
scriptsMap.delete(key);
86+
87+
reject(new Error(`Failed to load script: ${src}`));
88+
89+
cleanup();
90+
};
91+
92+
script.addEventListener('load', onLoad);
93+
script.addEventListener('error', onError);
94+
95+
document.head.appendChild(script);
96+
});
97+
98+
scriptsMap.set(key, promise);
99+
100+
return promise;
101+
}
102+
103+
export function appendStyle(href: string, options: AppendStyleOptions = {}): Promise<HTMLLinkElement> {
104+
const key = new URL(href, document.baseURI).href;
105+
106+
const cached = stylesMap.get(key);
107+
108+
if (cached) {
109+
return cached;
110+
}
111+
112+
const existing = findStyleByKey(key);
113+
114+
if (existing) {
115+
const ready = Promise.resolve(existing);
116+
stylesMap.set(key, ready);
117+
118+
return ready;
119+
}
120+
121+
const promise = new Promise<HTMLLinkElement>((resolve, reject) => {
122+
const link = document.createElement('link');
123+
124+
link.rel = 'stylesheet';
125+
link.href = href;
126+
127+
if (options.media) {
128+
link.media = options.media;
129+
}
130+
131+
if (options.attributes) {
132+
Object.entries(options.attributes).forEach(([k, v]) => link.setAttribute(k, v));
133+
}
134+
135+
const onLoad = () => {
136+
resolve(link);
137+
};
138+
139+
const onError = () => {
140+
link.remove();
141+
stylesMap.delete(key);
142+
143+
reject(new Error(`Failed to load stylesheet: ${href}`));
144+
};
145+
146+
link.addEventListener('load', onLoad, { once: true });
147+
link.addEventListener('error', onError, { once: true });
148+
149+
document.head.appendChild(link);
150+
});
151+
152+
stylesMap.set(key, promise);
153+
154+
return promise;
155+
}
156+
157+
export function appendAsset(url: string, options: AppendAssetOptions = {}): Promise<HTMLScriptElement | HTMLLinkElement> {
158+
const ext = getExtension(url);
159+
160+
if (ext === 'css') {
161+
return appendStyle(url, options as AppendStyleOptions);
162+
}
163+
164+
if (ext === 'js' || ext === 'mjs') {
165+
return appendScript(url, options as AppendScriptOptions);
166+
}
167+
168+
throw new Error(`Unsupported asset type for "${url}". Expected ".css", ".js" or ".mjs".`);
169+
}
170+
171+
export const __test__ = import.meta.env?.MODE === 'test'
172+
? {
173+
reset: () => {
174+
scriptsMap = new Map();
175+
stylesMap = new Map();
176+
},
177+
}
178+
: undefined;
179+

src/browser/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './assets';
12
export * from './clipboard';
23
export * from './file';
34
export * from './network';

0 commit comments

Comments
 (0)