From a38de0c03fb241aa33e626c3a74d4866528b0d39 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 4 Aug 2024 09:16:46 -0500 Subject: [PATCH] feat: Add `injectScript` helper (#900) --- .../__snapshots__/auto-imports.test.ts.snap | 4 ++ packages/wxt/e2e/tests/auto-imports.test.ts | 1 + packages/wxt/src/client/index.ts | 1 + packages/wxt/src/client/inject-script.ts | 47 +++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 packages/wxt/src/client/inject-script.ts diff --git a/packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap b/packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap index df226e34..10e9209b 100644 --- a/packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap +++ b/packages/wxt/e2e/tests/__snapshots__/auto-imports.test.ts.snap @@ -19,6 +19,7 @@ exports[`Auto Imports > eslintrc > "enabled: 8" should output a JSON config file "defineUnlistedScript": true, "defineWxtPlugin": true, "fakeBrowser": true, + "injectScript": true, "storage": true, "useAppConfig": true } @@ -44,6 +45,7 @@ const globals = { "defineUnlistedScript": true, "defineWxtPlugin": true, "fakeBrowser": true, + "injectScript": true, "storage": true, "useAppConfig": true } @@ -77,6 +79,7 @@ exports[`Auto Imports > eslintrc > "enabled: true" should output a JSON config f "defineUnlistedScript": true, "defineWxtPlugin": true, "fakeBrowser": true, + "injectScript": true, "storage": true, "useAppConfig": true } @@ -103,6 +106,7 @@ exports[`Auto Imports > eslintrc > should allow customizing the output 1`] = ` "defineUnlistedScript": "readonly", "defineWxtPlugin": "readonly", "fakeBrowser": "readonly", + "injectScript": "readonly", "storage": "readonly", "useAppConfig": "readonly" } diff --git a/packages/wxt/e2e/tests/auto-imports.test.ts b/packages/wxt/e2e/tests/auto-imports.test.ts index 77c43162..c1eecb5f 100644 --- a/packages/wxt/e2e/tests/auto-imports.test.ts +++ b/packages/wxt/e2e/tests/auto-imports.test.ts @@ -31,6 +31,7 @@ describe('Auto Imports', () => { const defineUnlistedScript: typeof import('wxt/sandbox')['defineUnlistedScript'] const defineWxtPlugin: typeof import('wxt/sandbox')['defineWxtPlugin'] const fakeBrowser: typeof import('wxt/testing')['fakeBrowser'] + const injectScript: typeof import('wxt/client')['injectScript'] const storage: typeof import('wxt/storage')['storage'] const useAppConfig: typeof import('wxt/client')['useAppConfig'] } diff --git a/packages/wxt/src/client/index.ts b/packages/wxt/src/client/index.ts index 3733849b..b6818d07 100644 --- a/packages/wxt/src/client/index.ts +++ b/packages/wxt/src/client/index.ts @@ -5,3 +5,4 @@ */ export * from './content-scripts'; export * from './app-config'; +export * from './inject-script'; diff --git a/packages/wxt/src/client/inject-script.ts b/packages/wxt/src/client/inject-script.ts new file mode 100644 index 00000000..989ba248 --- /dev/null +++ b/packages/wxt/src/client/inject-script.ts @@ -0,0 +1,47 @@ +import { browser } from 'wxt/browser'; + +export type ScriptPublicPath = Extract< + // @ts-expect-error: PublicPath is generated per-project + import('wxt/browser').PublicPath, + `${string}.js` +>; + +/** + * This function can only be called inside content scripts. + * + * Inject an unlisted script into the page. Scripts are added to the `` + * element or `document.documentElement` if there is no head. + * + * Make sure to add the injected script to your manifest's + * `web_accessible_resources`. + */ +export async function injectScript( + path: ScriptPublicPath, + options?: InjectScriptOptions, +): Promise { + // @ts-expect-error: getURL is defined per-project, but not inside the package + const url = browser.runtime.getURL(path); + const script = document.createElement('script'); + + if (browser.runtime.getManifest().manifest_version === 2) { + // MV2 requires using an inline script + script.innerHTML = await fetch(url).then((res) => res.text()); + } else { + // MV3 requires using src + script.src = url; + } + + if (!options?.keepInDom) { + script.onload = () => script.remove(); + } + + (document.head ?? document.documentElement).append(script); +} + +export interface InjectScriptOptions { + /** + * By default, the injected script is removed from the DOM after being + * injected. To disable this behavior, set this flag to true. + */ + keepInDom?: boolean; +}