Skip to content

feat(runner): Create new @wxt-dev/runner package #1566

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

Merged
merged 33 commits into from
Jun 2, 2025
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f2c0c89
feat(runner): Create new package
aklinker1 Apr 8, 2025
ef7e5f1
Update .gitignore
aklinker1 Apr 8, 2025
251a73a
Support more targets
aklinker1 Apr 8, 2025
26520f4
Cleanup
aklinker1 Apr 8, 2025
c5947de
Implement dataPersistence
aklinker1 Apr 8, 2025
4ea55c5
Cleanup
aklinker1 Apr 8, 2025
ca3b4e5
Update browser-paths.ts with arch bins
aklinker1 Apr 10, 2025
01ac8bc
Update browser-paths.ts
aklinker1 Apr 11, 2025
d6d67d8
Update browser-paths.ts
aklinker1 Apr 11, 2025
b6b48c5
Add firefox warning
aklinker1 Apr 13, 2025
add416b
Add errors for already open instances
aklinker1 Apr 13, 2025
5349a44
Update readme
aklinker1 Apr 13, 2025
9b68683
Fix lockfile
aklinker1 Apr 13, 2025
852004c
Add to release workflows
aklinker1 Apr 13, 2025
bd6a967
Merge remote-tracking branch 'origin/main' into runner
aklinker1 Apr 13, 2025
22af1b3
Cleanup
aklinker1 Apr 13, 2025
e36478c
Denormalize binary path, add dataDir to resolved options
aklinker1 Apr 13, 2025
21f08bf
Fix checks
aklinker1 Apr 13, 2025
9ef0969
Fix type error
aklinker1 Apr 13, 2025
466bddf
Add unit tests
aklinker1 Apr 13, 2025
c001f3c
Update chrome flags
aklinker1 Apr 13, 2025
55f03f1
Fix checks
aklinker1 Apr 13, 2025
fda8901
Cleanup README
aklinker1 Apr 13, 2025
7875c9e
Update docs
aklinker1 Apr 14, 2025
6572c83
Update docs
aklinker1 Apr 14, 2025
8dba8f5
add fallback targets if the main target is not found
aklinker1 Jun 2, 2025
55cbe88
fix windows tests
aklinker1 Jun 2, 2025
41fb76b
Cleanup
aklinker1 Jun 2, 2025
a95c339
Merge remote-tracking branch 'origin/main' into runner
aklinker1 Jun 2, 2025
37ed6a6
Cleanup
aklinker1 Jun 2, 2025
d485e9b
Cleanup
aklinker1 Jun 2, 2025
e593291
Cleanup
aklinker1 Jun 2, 2025
6608ed6
Update gitignore
aklinker1 Jun 2, 2025
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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
- module-solid
- module-svelte
- module-vue
- runner
- storage
- unocss
- webextension-polyfill
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/sync-releases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ on:
- module-solid
- module-svelte
- module-vue
- runner
- storage
- webextension-polyfill
- wxt
Expand Down
46 changes: 46 additions & 0 deletions packages/runner/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# `@wxt-dev/runner`

> [!NOTE]
> This package is still in development and is not ready for production use.

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

## 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

## Features

- Supports all Chromium and Firefox based browsers
- Zero dependencies
- Persist data between launches on Chrome

## Requirements

| Runtime | Version |
| ------- | --------- |
| NodeJS | >= 22.4.0 |
| Bun | >= 1.2.0 |

| Browser | Version |
| ------- | ------- |
| Chrome | Unknown |
| Firefox | >= 139 |

## Usage

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

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

## Implementation Details

On Chrome, this uses the [CDP](https://chromedevtools.github.io/devtools-protocol/) to install the extension. On Firefox, it uses the new [WebDriver BiDi protocol](https://www.w3.org/TR/webdriver-bidi).

BiDi was not used on Chrome because it requires use of the Chrome driver, which would require a post-install step to download it, plus additional startup time to spin it up.
1 change: 1 addition & 0 deletions packages/runner/demo-extension/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('Hello background!');
9 changes: 9 additions & 0 deletions packages/runner/demo-extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "Test",
"version": "1.0.0",
"manifest_version": 3,
"background": {
"service_worker": "background.js",
"scripts": ["background.js"]
}
}
16 changes: 16 additions & 0 deletions packages/runner/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// USAGE:
// pnpm dev
// pnpm dev firefox-nightly
// pnpm dev <target>
//

import { run } from './src';

// Uncomment to enable debug logs
process.env.DEBUG = '@wxt-dev/runner';

await run({
extensionDir: 'demo-extension',
target: process.argv[2],
});
51 changes: 51 additions & 0 deletions packages/runner/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@wxt-dev/runner",
"description": "Launch Chrome and Firefox with a web extension installed",
"version": "0.1.0",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/wxt-dev/wxt.git",
"directory": "packages/runner"
},
"homepage": "https://github.com/wxt-dev/wxt/tree/main/packages/runner#readme",
"keywords": [
"web-extension",
"chrome-extension",
"wxt"
],
"author": {
"name": "Aaron Klinker",
"email": "[email protected]"
},
"license": "MIT",
"funding": "https://github.com/sponsors/wxt-dev",
"scripts": {
"check": "pnpm build && check",
"test": "buildc --deps-only -- vitest",
"dev": "tsx --trace-warnings dev.ts",
"build": "buildc -- unbuild",
"prepublishOnly": "pnpm build"
},
"dependencies": {},
"devDependencies": {
"@aklinker1/check": "catalog:",
"oxlint": "catalog:",
"publint": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",
"unbuild": "catalog:",
"vitest": "catalog:"
},
"main": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"files": [
"dist"
]
}
80 changes: 80 additions & 0 deletions packages/runner/src/__tests__/install.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { describe, it, expect, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { createCdpConnection, type CDPConnection } from '../cdp';
import { createBidiConnection, type BidiConnection } from '../bidi';
import type { ChildProcess } from 'node:child_process';
import { installChromium, installFirefox } from '../install';

vi.mock('../cdp');
const createCdpConnectionMock = vi.mocked(createCdpConnection);

vi.mock('../bidi');
const createBidiConnectionMock = vi.mocked(createBidiConnection);

describe('Install', () => {
describe('Chromium', () => {
it('Should send the install command to the process', async () => {
const browserProcess = mock<ChildProcess>();
const connection = mock<CDPConnection>({
[Symbol.dispose]: vi.fn(),
});
const extensionDir = '/path/to/extension';
const expectedExtensionId = 'chromium-extension-id';

createCdpConnectionMock.mockReturnValue(connection);
connection.send.mockImplementation(async (method) => {
if (method === 'Extensions.loadUnpacked')
return { id: expectedExtensionId };
throw Error('Unknown method');
});

const res = await installChromium(browserProcess, extensionDir);

expect(createCdpConnectionMock).toBeCalledTimes(1);
expect(createCdpConnectionMock).toBeCalledWith(browserProcess);

expect(connection.send).toBeCalledTimes(1);
expect(connection.send).toBeCalledWith('Extensions.loadUnpacked', {
path: extensionDir,
});

expect(res).toEqual({ id: expectedExtensionId });
});
});

describe('Firefox', () => {
it('Should connect to the server, start a session, then install the extension', async () => {
const debuggerUrl = 'http://127.0.0.1:9222';
const extensionDir = '/path/to/extension';
const expectedExtensionId = 'firefox-extension-id';
const connection = mock<BidiConnection>({
[Symbol.dispose]: vi.fn(),
});

createBidiConnectionMock.mockResolvedValue(connection);
connection.send.mockImplementation(async (method) => {
if (method === 'session.new') return { sessionId: 'session-id' };
if (method === 'webExtension.install')
return { extension: expectedExtensionId };
});

const res = await installFirefox(debuggerUrl, extensionDir);

expect(createBidiConnectionMock).toBeCalledTimes(1);
expect(createBidiConnectionMock).toBeCalledWith(debuggerUrl);

expect(connection.send).toBeCalledTimes(2);
expect(connection.send).toBeCalledWith('session.new', {
capabilities: {},
});
expect(connection.send).toBeCalledWith('webExtension.install', {
extensionData: {
type: 'path',
path: extensionDir,
},
});

expect(res).toEqual({ extension: expectedExtensionId });
});
});
});
Loading
Loading