Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/guide/essentials/content-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,76 @@ For MV3, `injectScript` is synchronous and the injected script will be evaluated
However for MV2, `injectScript` has to `fetch` the script's text content and create an inline `<script>` block. This means for MV2, your script is injected asynchronously and it will not be evaluated at the same time as your content script's `run_at`.
:::

The `script` element can be modified just before it is added to the DOM by using the `modifyScript` option. This can be used to e.g. modify `script.async`/`script.defer`, add event listeners to the element, or pass data to the script via `script.dataset`. An example:

```ts
// entrypoints/example.content.ts
export default defineContentScript({
matches: ['*://*/*'],
async main() {
await injectScript('/example-main-world.js', {
modifyScript(script) {
script.dataset['greeting'] = 'Hello there';
},
});
},
});
```

```ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
console.log(document.currentScript?.dataset['greeting']);
});
```

`injectScript` returns the created script element. It can be used to e.g. send messages to the script in the form of custom events. The script can add an event listener for them via `document.currentScript`. An example of bidirectional communication:

```ts
// entrypoints/example.content.ts
export default defineContentScript({
matches: ['*://*/*'],
async main() {
const { script } = await injectScript('/example-main-world.js', {
modifyScript(script) {
// Add a listener before the injected script is loaded.
script.addEventListener('from-injected-script', (event) => {
if (event instanceof CustomEvent) {
console.log(`${event.type}:`, event.detail);
}
});
},
});

// Send an event after the injected script is loaded.
script.dispatchEvent(
new CustomEvent('from-content-script', {
detail: 'General Kenobi',
}),
);
},
});
```

```ts
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
const script = document.currentScript;

script?.addEventListener('from-content-script', (event) => {
if (event instanceof CustomEvent) {
console.log(`${event.type}:`, event.detail);
}
});

script?.dispatchEvent(
new CustomEvent('from-injected-script', {
detail: 'Hello there',
}),
);
});
```

## Mounting UI to dynamic element

In many cases, you may need to mount a UI to a DOM element that does not exist at the time the web page is initially loaded. To handle this, use the `autoMount` API to automatically mount the UI when the target element appears dynamically and unmount it when the element disappears. In WXT, the `anchor` option is used to target the element, enabling automatic mounting and unmounting based on its appearance and removal.
Expand Down
57 changes: 54 additions & 3 deletions packages/wxt/src/utils/inject-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ export type ScriptPublicPath = Extract<
*
* Make sure to add the injected script to your manifest's
* `web_accessible_resources`.
*
* @returns A result object containing the created script element.
*/
export async function injectScript(
path: ScriptPublicPath,
options?: InjectScriptOptions,
): Promise<void> {
): Promise<InjectScriptResult> {
// @ts-expect-error: getURL is defined per-project, but not inside the package
const url = browser.runtime.getURL(path);
const script = document.createElement('script');
Expand All @@ -32,11 +34,43 @@ export async function injectScript(
script.src = url;
}

const loadedPromise = makeLoadedPromise(script);

await options?.modifyScript?.(script);

(document.head ?? document.documentElement).append(script);

if (!options?.keepInDom) {
script.onload = () => script.remove();
script.remove();
}

(document.head ?? document.documentElement).append(script);
await loadedPromise;

return {
script,
};
}

function makeLoadedPromise(script: HTMLScriptElement): Promise<void> {
return new Promise((resolve, reject) => {
const onload = () => {
resolve();
cleanup();
};

const onerror = () => {
reject(new Error(`Failed to load script: ${script.src}`));
cleanup();
};

const cleanup = () => {
script.removeEventListener('load', onload);
script.removeEventListener('error', onerror);
};

script.addEventListener('load', onload);
script.addEventListener('error', onerror);
});
}

export interface InjectScriptOptions {
Expand All @@ -45,4 +79,21 @@ export interface InjectScriptOptions {
* injected. To disable this behavior, set this flag to true.
*/
keepInDom?: boolean;
/**
* Modify the script element just before it is added to the DOM.
*
* It can be used to e.g. modify `script.async`/`script.defer`, add event
* listeners to the element, or pass data to the script via `script.dataset`
* (which can be accessed by the script via `document.currentScript`).
*/
modifyScript?: (script: HTMLScriptElement) => Promise<void> | void;
}

export interface InjectScriptResult {
/**
* The created script element. It can be used to e.g. send messages to the
* script in the form of custom events. The script can add an event listener
* for them via `document.currentScript`.
*/
script: HTMLScriptElement;
}