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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 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
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