Skip to content

fix: Make injectScript wait until loaded #1763

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
23 changes: 23 additions & 0 deletions docs/guide/essentials/content-scripts.md
Original file line number Diff line number Diff line change
@@ -591,6 +591,29 @@ 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 manipulated just before it is added to the DOM by using the `manipulateScript` option. This can be used to e.g. pass data to the script as shown in the example:

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

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

## 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.
40 changes: 38 additions & 2 deletions packages/wxt/src/utils/inject-script.ts
Original file line number Diff line number Diff line change
@@ -32,11 +32,39 @@ export async function injectScript(
script.src = url;
}

const loadedPromise = makeLoadedPromise(script);

await options?.manipulateScript?.(script);

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

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

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

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 {
@@ -45,4 +73,12 @@ export interface InjectScriptOptions {
* injected. To disable this behavior, set this flag to true.
*/
keepInDom?: boolean;
/**
* Manipulate the script element just before it is added to the DOM.
*
* It can be useful for e.g. passing data to the script via the `dataset`
* property (which can be accessed by the script via
* `document.currentScript`).
*/
manipulateScript?: (script: HTMLScriptElement) => Promise<void> | void;
}