🚀 Feature Request: Enhance injectScript with execution order options
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 10.5k
- Forks
- 564
- PR merge metrics
- No merged PRs in 30d
Description
Background
Currently, injectScript always appends the <script> element to <head> (or <html> fallback).
This makes it hard to control execution order (earliest vs latest) when injecting into web pages, especially under different run_at timings (document_start, document_end, document_idle).
Problem
No way to guarantee earliest execution (before page scripts run).
No way to explicitly run latest (after <body> exists).
Only a single "default" injection strategy is supported.
Proposed Solution
Add a new option mode in InjectScriptOptions:
type InjectScriptOptions = {
/**
* 'earliest' - wait until `<head>` exists and prepend script (runs as early as possible)
* 'default' - append to `(head ?? documentElement)`
* 'latest' - append to `<body>` if present, otherwise to `documentElement`
*/
mode?: 'earliest' | 'default' | 'latest';
/** Keep the <script> element in DOM after execution (default: false = remove it) */
keepInDom?: boolean;
};
Implementation Details
Earliest
Use MutationObserver to wait for . Once available, prepend(script) to ensure priority execution.
Default
Current behavior: (document.head ?? document.documentElement).append(script).
Latest
Append to <body> if available, otherwise fallback to <html>.
Example Usage
// Ensure script runs before page scripts
injectScript('injected.js', { mode: 'earliest' });
// Normal injection
injectScript('injected.js', { mode: 'default' });
// Run as late as possible
injectScript('injected.js', { mode: 'latest' });
Benefits
Fine-grained control of execution timing.
Cleaner, predictable behavior across MV2 / MV3.
References
type ScriptPublicPath = string;
type InjectScriptOptions = {
/**
* 'earliest' - wait until <head> exists and prepend script (runs as early as possible)
* 'default' - append to (head ?? documentElement)
* 'latest' - append to <body> if present, otherwise to documentElement
*/
mode?: 'earliest' | 'default' | 'latest';
/** Keep the <script> element in DOM after execution (default: false = remove it) */
keepInDom?: boolean;
};
export async function injectScript(
path: ScriptPublicPath,
options: InjectScriptOptions = {},
): Promise<void> {
// @ts-expect-error: getURL is defined per-project
const url = browser.runtime.getURL(path);
const script = document.createElement('script');
const manifestVersion = browser.runtime.getManifest().manifest_version;
// MV2 requires inline script, MV3 allows external src
if (manifestVersion === 2) {
script.innerHTML = await fetch(url).then((res) => res.text());
} else {
script.src = url;
(script as HTMLScriptElement).async = false; // preserve execution order
}
// Remove script element after execution (unless keepInDom = true)
if (!options.keepInDom) {
if (manifestVersion === 2) {
// Inline script executes synchronously → remove immediately after execution
Promise.resolve().then(() => script.remove());
} else {
// External script executes after load → remove on load
script.onload = () => script.remove();
}
}
const mode = options.mode ?? 'default';
function prependToHead() {
if (document.head) {
document.head.prepend(script);
return true;
}
return false;
}
if (mode === 'earliest') {
if (prependToHead()) return;
// Wait until <head> is available, then prepend
const observer = new MutationObserver(() => {
if (prependToHead()) observer.disconnect();
});
observer.observe(document.documentElement, { childList: true });
return;
}
if (mode === 'latest') {
const target = document.body ?? document.documentElement;
target.append(script);
return;
}
// default
(document.head ?? document.documentElement).append(script);
}
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the injectScript entry point and the InjectScriptOptions type described in the issue. Review the current head-or-documentElement append behavior, then verify the requested earliest, default, and latest modes plus keepInDom behavior across MV2 and MV3. Done means the options are supported and execution order and cleanup match the documented semantics.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- tooling, web-dev
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100