wxt-dev / wxt-dev/wxt

🚀 Feature Request: Enhance injectScript with execution order options

Open
#1,904 0 comments 3 reactions 0 assignees View on GitHub

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

Chrome Extensions run_at docs

DOM MutationObserver

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.