Skip to content

Instantly share code, notes, and snippets.

@westc
Last active February 1, 2024 21:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save westc/5a632ddd50c2b93ad04a803e9269573a to your computer and use it in GitHub Desktop.
Save westc/5a632ddd50c2b93ad04a803e9269573a to your computer and use it in GitHub Desktop.
Loads a <script> tag if necessary and finishes once the `checker` returns a promised value that is not falsy.
/**
* Can load multiple scripts (javascript and css) and finishes once the
* `checker` returns a promised value that is not falsy.
* @param {string[]} urls
* Ordinarily a URL is determined to be for CSS or JS by the extension of the
* pathname but if it doesn't end in .js or .css this will not be possible and
* it will default to JS. If you want to force a URL to be recognized as a JS
* file you should prefix the URL with `"js:"` and if you want it to be
* recognized as a CSS file you should prefix it with `"css:"`.
* @param {null|undefined|{
* checker?: (scripts: (HTMLLinkElement|HTMLScriptElement)[], startTime: number, options: ThisType) => T;
* postCheck?: boolean;
* preCheck?: boolean;
* timeoutMS?: number;
* uniquifyByURL?: boolean;
* }} options
* @return {Promise<T|(HTMLLinkElement|HTMLScriptElement)[]>}
* @template T
*/
async function loadScripts(urls, options) {
return new Promise(async(resolve, reject) => {
const {checker, postCheck, preCheck, timeoutMS, uniquifyByURL} = options ?? {};
// If a pre-check should be done...
if (checker && preCheck) {
const result = await checker(void 0, void 0, options);
if (result) return resolve(result);
}
/**
* Get an array of the results that corresponds to the given urls.
* @type {(HTMLLinkElement|HTMLScriptElement)[]}
*/
const scripts = await Promise.all(urls.map(url => new Promise((resolve, reject) => {
let isCSS;
const realUrl = url.replace(
/^(css|js):\s*|\.(css|js)(?:\?[^]*)?#?/i,
(m, fakeProtocol, ext) => {
isCSS = (ext ?? fakeProtocol).toLowerCase() === 'css';
return fakeProtocol ? '' : m;
}
);
/** @type {HTMLLinkElement|HTMLScriptElement} */
const elem = (uniquifyByURL
&& document.querySelector(`${isCSS ? 'link' : 'script'}[data-url="${CSS.escape(realUrl)}"]`)
)
?? (isCSS
? Object.assign(document.createElement('link'), {rel: 'stylesheet', href: realUrl})
: Object.assign(document.createElement('script'), {src: realUrl})
);
elem.setAttribute('data-url', realUrl);
// If the link or script tag has errored out or has loaded go ahead
if (elem.hasLoaded) {
if (elem.loadError) reject(elem.loadError);
else resolve(elem);
}
else {
elem.addEventListener('load', () => {
elem.hasLoaded = true;
resolve(elem);
});
elem.addEventListener('error', evt => {
elem.hasLoaded = true;
elem.loadError = evt;
reject(evt);
});
if (!elem.parentNode) (document.head ?? document.body).appendChild(elem);
}
})));
// If there not supposed to do the post-check just return the scripts.
if (!(postCheck ?? !!checker)) return resolve(scripts);
// Since the checker needs to pass before returning start things off so that
// the checker will be occurred every 50ms until it passes or times out.
async function callback() {
try {
if (timeoutMS == null || Date.now() - startTime >= timeoutMS) {
throw new Error(`Timed out when using loadScript() on ${JSON.stringify(urls)}.`);
}
const result = await checker(scripts, startTime, options);
if (result) resolve(result);
else setTimeout(callback, 50);
}
catch (err) {
reject(err);
}
}
const startTime = Date.now();
callback();
});
}
// Loads the ace editor code if not already found.
await loadScript(['https://cdnjs.cloudflare.com/ajax/libs/ace/1.13.1/ace.js'], {
preCheck: true,
checker: () => window.ace,
timeoutMS: 3000
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment