# `@wxt-dev/runner`

Programmatically open a browser and install a web extension from a local directory.

## Usage

### With WXT

> [!WARNING]
> This package is intended to replace [`web-ext`](https://github.com/mozilla/web-ext) in the future, but it is not ready at the moment. Once it's ready for testing in WXT, more details will be added here.

```ts
// ~/wxt.runner.config.ts OR <project>/wxt.runner.config.ts
import { defineRunnerConfig } from 'wxt';

export default defineRunnerConfig({
  // Options go here
});
```

### JS API

```ts
import { run } from '@wxt-dev/runner';

await run({
  extensionDir: '/path/to/extension',
  // Other options...
});
```

## Features

- Supports all Chromium and Firefox based browsers
- Zero dependencies
- One-line config for persisting data between launches

## Requirements

`@wxt-dev/runner` requires a JS runtime that implements the `WebSocket` standard:

| JS Runtime | Version     |
| ---------- | ----------- |
| NodeJS     | &ge; 22.4.0 |
| Bun        | &ge; 1.2.0  |

You also need to have a specific version of the browser installed that supports the latest features so extensions can be loaded:

| Browser  | Version  |
| -------- | -------- |
| Chromium | Unknown  |
| Firefox  | &ge; 139 |

## TODO

- [x] Provide install functions to allow hooking into already running instances of Chrome/Firefox
  - [ ] Try to setup E2E tests on Firefox with Puppeteer using this approach
  - [ ] Try to setup E2E tests on Chrome with Puppeteer using this approach

## Options

### Target

To open a specific browser, use the `target` option:

```ts
import { run } from '@wxt-dev/runner';

await run({
  extensionDir: 'path/to/extension',
  target: 'firefox',
});
```

Defaults to opening `chrome`. You may see type-hints for a list of popular browsers, but you can enter any string you want here.

### Data Persistence

Browsers block you from using your normal browser profiles when using the [BiDi and CDP protocols](#implementation-details) for security reasons.

To change how the new profile's data is saved between sessions, use the `dataPersistence` option:

```ts
import { run } from '@wxt-dev/runner';

await run({
  dataPersistence: 'user',
});
```

- `"none"` (default): Use a brand new browser profile every time the browser is opened (stored in the system's tmp directory)
- `"project"`: Create a new profile that is re-used for your current directory (by default stored in `.wxt-runner` or `.wxt/runner` for WXT projects)
- `"user"`: Create a new profile that is re-used for all projects using `@wxt-dev/runner` (by default stored in `$HOME/.wxt-runner`)

These presets configure different flags for different operating systems when spawning the browser process.

If you want to customize your data persistence beyond what these presets define, [you can override the browser flags yourself](#arguments) to configure persistence.

### Browser Binaries

`@wxt-dev/runner` will look for browser binaries/executables in [a hard-coded list of paths](https://github.com/wxt-dev/wxt/blob/main/packages/runner/src/browser-paths.ts). It does not and will not explore your filesystem/`$PATH` to find where the browser is installed. That means there are times you will need to specify the path to a browser's binary on your system:

- Your browser's path is non-standard or missing from the hard-coded list.
- You want to use a specific version/release of the browser.
- You're using a less popular browser and `@wxt-dev/runner` doesn't have hard-coded paths for it.

To do this, use the `browserBinaries` option and set the path to the browser's binary:

```ts
import { run } from '@wxt-dev/runner';

await run({
  extensionDir: 'path/to/extension',
  browserBinaries: {
    chrome: '/path/to/chrome',
    firefox: '/path/to/firefox',
  },
});
```

### Arguments

To pass custom arguments to the browser on startup, use the `chromiumArgs` or `firefoxArgs` options:

```ts
import { run } from '@wxt-dev/runner';

await run({
  extensionDir: 'path/to/extension',
  chromiumArgs: ['--window-size=1920,1080'],
  firefoxArgs: ['--window-size', '1920,1080'],
});
```

### Start URLs

To open specific URLs in tabs by default, you also use the `chromiumArgs` or `firefoxArgs` options.

Any URLs passed as a CLI argument will be opened in the browser when it starts.

```ts
import { run } from '@wxt-dev/runner';

await run({
  extensionDir: 'path/to/extension',
  chromiumArgs: ['https://example.com'],
  firefoxArgs: ['https://example.com'],
});
```

### Debugging

To see debug logs, set the `DEBUG` env var to `"@wxt-dev/runner"`. This will print the resolved config, commands used to spawn the browser, any messages sent on the browser's communication protocol, and more for you to debug.

<details>
<summary>Example debug output</summary>

```plaintext
@wxt-dev/runner:options User options: { extensionDir: 'demo-extension', target: undefined }
@wxt-dev/runner:options Resolved options: {
  browserBinary: '/usr/bin/chromium',
  chromiumArgs: [
    '--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculateNativeWinOcclusion,InterestFeedContentSuggestions,CertificateTransparencyComponentUpdater,AutofillServerCommunication,PrivacySandboxSettings4',
    '--disable-component-extensions-with-background-pages',
    '--disable-background-networking',
    '--disable-component-update',
    '--disable-client-side-phishing-detection',
    '--disable-sync',
    '--metrics-recording-only',
    '--disable-default-apps',
    '--no-default-browser-check',
    '--no-first-run',
    '--disable-background-timer-throttling',
    '--disable-ipc-flooding-protection',
    '--password-store=basic',
    '--use-mock-keychain',
    '--force-fieldtrials=*BackgroundTracing/default/',
    '--disable-hang-monitor',
    '--disable-prompt-on-repost',
    '--disable-domain-reliability',
    '--propagate-iph-for-testing',
    '--remote-debugging-port=0',
    '--remote-debugging-pipe',
    '--user-data-dir=/tmp/wxt-runner-pWXLO1',
    '--enable-unsafe-extension-debugging'
  ],
  dataDir: '/tmp/wxt-runner-pWXLO1',
  dataPersistence: 'none',
  chromiumRemoteDebuggingPort: 0,
  extensionDir: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension',
  firefoxArgs: [
    '--new-instance',
    '--no-remote',
    '--profile',
    '/tmp/wxt-runner-pWXLO1',
    '--remote-debugging-port=0',
    'about:debugging#/runtime/this-firefox'
  ],
  firefoxRemoteDebuggingPort: 0,
  target: 'chrome'
}
@wxt-dev/runner:chrome:stderr DevTools listening on ws://127.0.0.1:38397/devtools/browser/93dc4de5-64cb-4e0b-a9d3-7549527015f0
@wxt-dev/runner:cdp Sending command: {
  id: 1,
  method: 'Extensions.loadUnpacked',
  params: {
    path: '/home/aklinker1/Development/github.com/wxt-dev/wxt/packages/runner/demo-extension'
  }
}
@wxt-dev/runner:cdp Received response: { id: 1, result: { id: 'hckhakegfgenefhikdcfkaaonnclljmf' } }
```

</details>

## Implementation Details

All this package does is spawn a child process to open the browser with some default flags before using remote protocols to install the extension.

### Firefox

We use the new [WebDriver BiDi protocol](https://www.w3.org/TR/webdriver-bidi) to install the extension. This just involves connecting to a web socket and sending a few messages.

### Chrome

We use the [CDP](https://chromedevtools.github.io/devtools-protocol/) with `--remote-debugging-pipe` and `--enable-unsafe-extension-debugging` to install the extension by sending a message via IO pipes 3 and 4.

We don't use Webdriver Bidi because it's not built into Chrome yet. It requires us instantiating a separate child process for `chromedriver`. This is slower and more difficult than just using the CDP built into Chrome.
