webext-detect
Version:
Detects where the current browser extension code is being run. Compatible with Firefox, Chrome and derivates.
114 lines (113 loc) • 5.78 kB
JavaScript
/* eslint-disable @typescript-eslint/no-unsafe-type-assertion */
let cache = true;
export function disableWebextDetectPageCache() {
cache = false;
}
function isCurrentPathname(path) {
if (!path) {
return false;
}
try {
const { pathname } = new URL(path, location.origin);
return pathname === location.pathname;
}
catch {
return false;
}
}
function getManifest(_version) {
return globalThis.chrome?.runtime?.getManifest?.();
}
function once(function_) {
let result;
return () => {
if (!cache || result === undefined) {
result = function_();
}
return result;
};
}
/** Indicates whether the code is being run on http(s):// pages (it could be in a content script or regular web context) */
export const isWebPage = once(() => ['about:', 'http:', 'https:'].includes(location.protocol));
/** Indicates whether you're in extension contexts that have access to the chrome API */
export const isExtensionContext = once(() => typeof globalThis.chrome?.runtime?.id === 'string');
/** Indicates whether you're in a sandboxed page (-extension:// URL protocol, but no chrome.* API access) */
export const isSandboxedPage = once(() => location.protocol.endsWith('-extension:') && !isExtensionContext());
/** Indicates whether you're in a content script. Note that the MAIN world is not considered a content script. */
export const isContentScript = once(() => isExtensionContext() && isWebPage());
/** Indicates whether you're in a background context */
export const isBackground = () => isBackgroundPage() || isBackgroundWorker();
/** Indicates whether you're in a background page */
export const isBackgroundPage = once(() => {
const manifest = getManifest(2);
if (!manifest) {
return false;
}
if (isCurrentPathname(manifest.background?.page)) {
return true;
}
return Boolean(manifest.background?.scripts
&& isCurrentPathname('/_generated_background_page.html'));
});
/** Indicates whether you're in a background worker */
export const isBackgroundWorker = once(() => isCurrentPathname(getManifest(3)?.background?.service_worker));
/** Indicates whether you're in a persistent background page (as opposed to an Event Page or Background Worker, both of which can be unloaded by the browser) */
export const isPersistentBackgroundPage = once(() => isBackgroundPage()
&& getManifest(2)?.manifest_version === 2 // Firefox can have a background page on MV3, but can't be persistent
&& getManifest(2)?.background?.persistent !== false);
/** Indicates whether you're in an options page. This only works if the current page’s URL matches the one specified in the extension's `manifest.json` */
export const isOptionsPage = once(() => isCurrentPathname(getManifest()?.options_ui?.page ?? getManifest()?.options_page));
/** Indicates whether you're in a side panel. This only works if the current page’s URL matches the one specified in the extension's `manifest.json` */
export const isSidePanel = once(() => isCurrentPathname(getManifest(3)?.side_panel?.default_path));
export const isActionPopup = once(() => {
// Chrome-only; Firefox uses the whole window…
if (globalThis.outerHeight - globalThis.innerHeight === 14) {
return true;
}
return isCurrentPathname(getManifest(3)?.action?.default_popup ?? getManifest(2)?.browser_action?.default_popup);
});
/** Indicates whether you're in the main dev tools page, the one specified in the extension's `manifest.json` `devtools_page` field. */
export const isMainDevToolsPage = once(() => isExtensionContext()
&& Boolean(chrome.devtools)
&& isCurrentPathname(getManifest()?.devtools_page));
/** Indicates whether you're in the dev tools page. Unlike `isMainDevToolsPage`, this works in any page that has the `chrome.devTools` API */
export const isDevTools = () => Boolean(globalThis.chrome?.devtools);
/** Indicates whether you're in a document created via chrome.offscreen */
export const isOffscreenDocument = once(() => isExtensionContext()
&& 'document' in globalThis
&& globalThis.chrome?.extension === undefined);
/** Loosely detect Android via user agent */
export const isAndroid = () => globalThis.navigator?.userAgent.includes('Android');
/** Loosely detect Firefox via user agent */
export const isFirefox = () => globalThis.navigator?.userAgent.includes('Firefox');
/** Loosely detect Chrome via user agent (might also include Chromium and forks like Opera) */
export const isChrome = () => globalThis.navigator?.userAgent.includes('Chrome');
/** Loosely detect Safari via user agent */
export const isSafari = () => !isChrome() && globalThis.navigator?.userAgent.includes('Safari');
/** Loosely detect Mobile Safari via user agent */
export const isMobileSafari = () => isSafari() && globalThis.navigator?.userAgent.includes('Mobile');
/** Loosely detect Mobile Firefox via user agent */
export const isMobileFirefox = () => isFirefox() && isAndroid();
const contextChecks = {
contentScript: isContentScript,
background: isBackground,
options: isOptionsPage,
sidePanel: isSidePanel,
actionPopup: isActionPopup,
devTools: isDevTools,
mainDevToolsPage: isMainDevToolsPage,
offscreenDocument: isOffscreenDocument,
extension: isExtensionContext,
sandbox: isSandboxedPage,
web: isWebPage,
};
export const contextNames = Object.keys(contextChecks);
/** Returns the first matching context among those defined in `ContextName`, depending on the current context. Returns "unknown" if no match is found. */
export function getContextName() {
for (const [name, test] of Object.entries(contextChecks)) {
if (test()) {
return name;
}
}
return 'unknown';
}