UNPKG

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
/* 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'; }