@clerk/shared
Version:
Internal package utils used by the Clerk SDKs
239 lines (237 loc) • 9.81 kB
JavaScript
import { createDevOrStagingUrlCache, parsePublishableKey } from "./keys.mjs";
import { t as ClerkRuntimeError } from "./_chunks/clerkRuntimeError-DlesLWqO.mjs";
import { y as buildErrorThrower } from "./_chunks/error-uYOdvTDm.mjs";
import { loadScript } from "./loadScript.mjs";
import { isProxyUrlRelative, isValidProxyUrl, proxyUrlToAbsoluteURL } from "./proxy.mjs";
import { addClerkPrefix } from "./url.mjs";
import { versionSelector } from "./versionSelector.mjs";
//#region src/loadClerkJsScript.ts
const { isDevOrStagingUrl } = createDevOrStagingUrlCache();
const errorThrower = buildErrorThrower({ packageName: "@clerk/shared" });
/**
* Validates that window.Clerk exists and is properly initialized.
* This ensures we don't have false positives where the script loads but Clerk is malformed.
*
* @returns `true` if window.Clerk exists and has the expected structure with a load method.
*/
function isClerkGlobalProperlyLoaded(prop) {
if (typeof window === "undefined" || !window[prop]) return false;
return !!window[prop];
}
const isClerkProperlyLoaded = () => isClerkGlobalProperlyLoaded("Clerk");
const isClerkUIProperlyLoaded = () => isClerkGlobalProperlyLoaded("__internal_ClerkUICtor");
/**
* Checks if an existing script has a request error using Performance API.
*
* @param scriptUrl - The URL of the script to check.
* @returns True if the script has failed to load due to a network/HTTP error.
*/
function hasScriptRequestError(scriptUrl) {
if (typeof window === "undefined" || !window.performance) return false;
const entries = performance.getEntriesByName(scriptUrl, "resource");
if (entries.length === 0) return false;
const scriptEntry = entries[entries.length - 1];
if (scriptEntry.transferSize === 0 && scriptEntry.decodedBodySize === 0) {
if (scriptEntry.responseEnd === 0) return true;
if (scriptEntry.responseEnd > 0 && scriptEntry.responseStart > 0) return true;
if ("responseStatus" in scriptEntry) {
if (scriptEntry.responseStatus >= 400) return true;
if (scriptEntry.responseStatus === 0) return true;
}
}
return false;
}
/**
* Hotloads the Clerk JS script with robust failure detection.
*
* Uses a timeout-based approach to ensure absolute certainty about load success/failure.
* If the script fails to load within the timeout period, or loads but doesn't create
* a proper Clerk instance, the promise rejects with an error.
*
* @param opts - The options used to build the Clerk JS script URL and load the script.
* Must include a `publishableKey` if no existing script is found.
* @returns Promise that resolves with null if Clerk loads successfully, or rejects with an error.
*
* @example
* ```typescript
* try {
* await loadClerkJsScript({ publishableKey: 'pk_test_...' });
* console.log('Clerk loaded successfully');
* } catch (error) {
* console.error('Failed to load Clerk:', error.message);
* }
* ```
*/
const loadClerkJSScript = async (opts) => {
const timeout = opts?.scriptLoadTimeout ?? 15e3;
const rejectWith = (error) => new ClerkRuntimeError("Failed to load Clerk JS" + (error?.message ? `, ${error.message}` : ""), {
code: "failed_to_load_clerk_js",
cause: error
});
if (isClerkProperlyLoaded()) return null;
if (!opts?.publishableKey) {
errorThrower.throwMissingPublishableKeyError();
return null;
}
const scriptUrl = clerkJSScriptUrl(opts);
const existingScript = document.querySelector("script[data-clerk-js-script]");
if (existingScript) if (hasScriptRequestError(scriptUrl)) existingScript.remove();
else try {
await waitForPredicateWithTimeout(timeout, isClerkProperlyLoaded, rejectWith(), existingScript);
return null;
} catch {
existingScript.remove();
}
const loadPromise = waitForPredicateWithTimeout(timeout, isClerkProperlyLoaded, rejectWith());
loadScript(scriptUrl, {
async: true,
crossOrigin: "anonymous",
nonce: opts.nonce,
beforeLoad: applyAttributesToScript(buildClerkJSScriptAttributes(opts))
}).catch((error) => {
throw rejectWith(error);
});
return loadPromise;
};
const loadClerkUIScript = async (opts) => {
const timeout = opts?.scriptLoadTimeout ?? 15e3;
const rejectWith = (error) => new ClerkRuntimeError("Failed to load Clerk UI" + (error?.message ? `, ${error.message}` : ""), {
code: "failed_to_load_clerk_ui",
cause: error
});
if (isClerkUIProperlyLoaded()) return null;
if (!opts?.publishableKey) {
errorThrower.throwMissingPublishableKeyError();
return null;
}
const scriptUrl = clerkUIScriptUrl(opts);
const existingScript = document.querySelector("script[data-clerk-ui-script]");
if (existingScript) if (hasScriptRequestError(scriptUrl)) existingScript.remove();
else try {
await waitForPredicateWithTimeout(timeout, isClerkUIProperlyLoaded, rejectWith(), existingScript);
return null;
} catch {
existingScript.remove();
}
const loadPromise = waitForPredicateWithTimeout(timeout, isClerkUIProperlyLoaded, rejectWith());
loadScript(scriptUrl, {
async: true,
crossOrigin: "anonymous",
nonce: opts.nonce,
beforeLoad: applyAttributesToScript(buildClerkUIScriptAttributes(opts))
}).catch((error) => {
throw rejectWith(error);
});
return loadPromise;
};
const clerkJSScriptUrl = (opts) => {
const { __internal_clerkJSUrl, __internal_clerkJSVersion, proxyUrl, domain, publishableKey } = opts;
if (__internal_clerkJSUrl) return __internal_clerkJSUrl;
const version = versionSelector(__internal_clerkJSVersion);
if (proxyUrl && isProxyUrlRelative(proxyUrl)) return buildRelativeProxyScriptUrl(proxyUrl, "clerk-js", version, "clerk.browser.js");
return `https://${buildScriptHost({
publishableKey,
proxyUrl,
domain
})}/npm/@clerk/clerk-js@${version}/dist/clerk.browser.js`;
};
const clerkUIScriptUrl = (opts) => {
const { __internal_clerkUIUrl, __internal_clerkUIVersion, proxyUrl, domain, publishableKey } = opts;
if (__internal_clerkUIUrl) return __internal_clerkUIUrl;
const version = versionSelector(__internal_clerkUIVersion, "1.25.6");
if (proxyUrl && isProxyUrlRelative(proxyUrl)) return buildRelativeProxyScriptUrl(proxyUrl, "ui", version, "ui.browser.js");
return `https://${buildScriptHost({
publishableKey,
proxyUrl,
domain
})}/npm/@clerk/ui@${version}/dist/ui.browser.js`;
};
const buildClerkJSScriptAttributes = (options) => {
const obj = {};
if (options.publishableKey) obj["data-clerk-publishable-key"] = options.publishableKey;
if (options.proxyUrl) obj["data-clerk-proxy-url"] = options.proxyUrl;
if (options.domain) obj["data-clerk-domain"] = options.domain;
if (options.nonce) obj.nonce = options.nonce;
return obj;
};
const buildClerkUIScriptAttributes = (options) => {
return buildClerkJSScriptAttributes(options);
};
const applyAttributesToScript = (attributes) => (script) => {
for (const attribute in attributes) script.setAttribute(attribute, attributes[attribute]);
};
const stripTrailingSlashes = (value) => {
while (value.endsWith("/")) value = value.slice(0, -1);
return value;
};
const buildRelativeProxyScriptUrl = (proxyUrl, packageName, version, fileName) => {
return `${stripTrailingSlashes(proxyUrl)}/npm/@clerk/${packageName}@${version}/dist/${fileName}`;
};
const buildScriptHost = (opts) => {
const { proxyUrl, domain, publishableKey } = opts;
if (!!proxyUrl && isValidProxyUrl(proxyUrl)) {
const resolvedProxyUrl = proxyUrlToAbsoluteURL(proxyUrl);
if (isProxyUrlRelative(resolvedProxyUrl)) return parsePublishableKey(publishableKey)?.frontendApi || "";
return resolvedProxyUrl.replace(/http(s)?:\/\//, "");
} else if (domain && !isDevOrStagingUrl(parsePublishableKey(publishableKey)?.frontendApi || "")) return addClerkPrefix(domain);
else return parsePublishableKey(publishableKey)?.frontendApi || "";
};
function waitForPredicateWithTimeout(timeoutMs, predicate, rejectWith, existingScript) {
return new Promise((resolve, reject) => {
let resolved = false;
const cleanup = (timeoutId, pollInterval) => {
clearTimeout(timeoutId);
clearInterval(pollInterval);
};
existingScript?.addEventListener("error", () => {
cleanup(timeoutId, pollInterval);
reject(rejectWith);
});
const checkAndResolve = () => {
if (resolved) return;
if (predicate()) {
resolved = true;
cleanup(timeoutId, pollInterval);
resolve(null);
}
};
const handleTimeout = () => {
if (resolved) return;
resolved = true;
cleanup(timeoutId, pollInterval);
if (!predicate()) reject(rejectWith);
else resolve(null);
};
const timeoutId = setTimeout(handleTimeout, timeoutMs);
checkAndResolve();
const pollInterval = setInterval(() => {
if (resolved) {
clearInterval(pollInterval);
return;
}
checkAndResolve();
}, 100);
});
}
function setClerkJSLoadingErrorPackageName(packageName) {
errorThrower.setPackageName({ packageName });
}
/**
* @deprecated Use `loadClerkJSScript` instead. This alias will be removed in a future major version.
*/
const loadClerkJsScript = loadClerkJSScript;
/**
* @deprecated Use `clerkJSScriptUrl` instead. This alias will be removed in a future major version.
*/
const clerkJsScriptUrl = clerkJSScriptUrl;
/**
* @deprecated Use `buildClerkJSScriptAttributes` instead. This alias will be removed in a future major version.
*/
const buildClerkJsScriptAttributes = buildClerkJSScriptAttributes;
/**
* @deprecated Use `setClerkJSLoadingErrorPackageName` instead. This alias will be removed in a future major version.
*/
const setClerkJsLoadingErrorPackageName = setClerkJSLoadingErrorPackageName;
//#endregion
export { buildClerkJSScriptAttributes, buildClerkJsScriptAttributes, buildClerkUIScriptAttributes, buildScriptHost, clerkJSScriptUrl, clerkJsScriptUrl, clerkUIScriptUrl, loadClerkJSScript, loadClerkJsScript, loadClerkUIScript, setClerkJSLoadingErrorPackageName, setClerkJsLoadingErrorPackageName };
//# sourceMappingURL=loadClerkJsScript.mjs.map