-
-
Notifications
You must be signed in to change notification settings - Fork 337
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
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
f2c0c89
feat(runner): Create new package
aklinker1 ef7e5f1
Update .gitignore
aklinker1 251a73a
Support more targets
aklinker1 26520f4
Cleanup
aklinker1 c5947de
Implement dataPersistence
aklinker1 4ea55c5
Cleanup
aklinker1 ca3b4e5
Update browser-paths.ts with arch bins
aklinker1 01ac8bc
Update browser-paths.ts
aklinker1 d6d67d8
Update browser-paths.ts
aklinker1 b6b48c5
Add firefox warning
aklinker1 add416b
Add errors for already open instances
aklinker1 5349a44
Update readme
aklinker1 9b68683
Fix lockfile
aklinker1 852004c
Add to release workflows
aklinker1 bd6a967
Merge remote-tracking branch 'origin/main' into runner
aklinker1 22af1b3
Cleanup
aklinker1 e36478c
Denormalize binary path, add dataDir to resolved options
aklinker1 21f08bf
Fix checks
aklinker1 9ef0969
Fix type error
aklinker1 466bddf
Add unit tests
aklinker1 c001f3c
Update chrome flags
aklinker1 55f03f1
Fix checks
aklinker1 fda8901
Cleanup README
aklinker1 7875c9e
Update docs
aklinker1 6572c83
Update docs
aklinker1 8dba8f5
add fallback targets if the main target is not found
aklinker1 55cbe88
fix windows tests
aklinker1 41fb76b
Cleanup
aklinker1 a95c339
Merge remote-tracking branch 'origin/main' into runner
aklinker1 37ed6a6
Cleanup
aklinker1 d485e9b
Cleanup
aklinker1 e593291
Cleanup
aklinker1 6608ed6
Update gitignore
aklinker1 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,3 +22,4 @@ stats.html | |
.tool-versions | ||
.cache | ||
*-stats.txt | ||
data | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
use-node-version=22.14.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
# `@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] Launch Chrome with extension installed | ||
- [x] Launch Firefox with extension installed | ||
- [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 | ||
- [ ] Implement the `chromiumDataPersistence` option | ||
- [ ] Implement some form of data persistence for Firefox | ||
- [ ] Close browsers when process exits | ||
- [ ] Don't error out if the browser is already open | ||
- [ ] Validate list of known browser paths on Mac | ||
- [ ] Validate list of known browser paths on Windows | ||
- [ ] Validate list of known browser paths on Linux | ||
- [ ] Support more than just "chromium" or "firefox" for targets | ||
|
||
## 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
console.log('Hello background!'); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { run } from './src'; | ||
|
||
// Uncomment to enable debug logs | ||
process.env.DEBUG = '@wxt-dev/runner'; | ||
|
||
await run({ | ||
extensionDir: 'demo-extension', | ||
target: process.argv[2] as 'chromium' | 'firefox' | undefined, | ||
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
{ | ||
"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 dev.ts", | ||
"dev:firefox": "tsx dev.ts firefox", | ||
"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" | ||
] | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { openWebSocket } from './web-socket'; | ||
import { debug } from './debug'; | ||
|
||
const debugBidi = debug.scoped('bidi'); | ||
|
||
export interface BidiConnection extends Disposable { | ||
send<T>(method: string, params: any, timeout?: number): Promise<T>; | ||
close(): void; | ||
} | ||
|
||
export async function createBidiConnection( | ||
baseUrl: string, | ||
): Promise<BidiConnection> { | ||
const url = new URL('/session', baseUrl); | ||
debugBidi('Connecting to BiDi server @', url.href); | ||
|
||
const webSocket = await openWebSocket(url.href); | ||
debugBidi('Connected'); | ||
|
||
let requestId = 0; | ||
|
||
return { | ||
send(method, params, timeout = 10e3) { | ||
const id = ++requestId; | ||
const command = { id, method, params }; | ||
debugBidi('Sending command:', command); | ||
|
||
return new Promise((resolve, reject) => { | ||
const cleanup = () => { | ||
webSocket.removeEventListener('message', onMessage); | ||
webSocket.removeEventListener('error', onError); | ||
}; | ||
|
||
setTimeout(() => { | ||
cleanup(); | ||
reject( | ||
new Error( | ||
`Timed out after ${timeout}ms waiting for ${method} response`, | ||
), | ||
); | ||
}, timeout); | ||
|
||
const onMessage = (event: MessageEvent) => { | ||
const data = JSON.parse(event.data); | ||
if (data.id === id) { | ||
debugBidi('Received response:', data); | ||
cleanup(); | ||
if (data.type === 'success') resolve(data.result); | ||
else reject(Error(data.message, { cause: data })); | ||
} | ||
}; | ||
const onError = (error: any) => { | ||
cleanup(); | ||
reject(new Error('Error sending request', { cause: error })); | ||
}; | ||
|
||
webSocket.addEventListener('message', onMessage); | ||
webSocket.addEventListener('error', onError); | ||
|
||
webSocket.send(JSON.stringify(command)); | ||
}); | ||
}, | ||
|
||
close() { | ||
debugBidi('Closing connection...'); | ||
webSocket.close(); | ||
debugBidi('Closed connection'); | ||
}, | ||
[Symbol.dispose]() { | ||
debugBidi('Disposing connection...'); | ||
webSocket.close(); | ||
debugBidi('Disposed connection'); | ||
}, | ||
}; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
export type BrowserPlatform = 'win32' | 'darwin' | 'linux'; | ||
|
||
export const CHROMIUM_PATHS: Record<BrowserPlatform, string[]> = { | ||
darwin: ['/Applications/Chrome.app/Contents/MacOS/Google Chrome'], | ||
linux: [ | ||
'/usr/bin/google-chrome', | ||
'/usr/bin/chromium', | ||
'/usr/bin/microsoft-edge', | ||
'/usr/bin/microsoft-edge-dev', | ||
], | ||
win32: [ | ||
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', | ||
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', | ||
'C:\\Program Files\\Chromium\\Application\\chrome.exe', | ||
'C:\\Program Files (x86)\\Chromium\\Application\\chrome.exe', | ||
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', | ||
'C:\\Program Files\\Microsoft\\Edge Dev\\Application\\msedge.exe', | ||
], | ||
}; | ||
|
||
export const FIREFOX_PATHS: Record<BrowserPlatform, string[]> = { | ||
darwin: [ | ||
'/Applications/Firefox Nightly.app/Contents/MacOS/firefox', | ||
'/Applications/Firefox Developer Edition.app/Contents/MacOS/firefox', | ||
'/Applications/Firefox.app/Contents/MacOS/firefox', | ||
], | ||
linux: [ | ||
'/usr/bin/firefox-nightly', | ||
'/usr/bin/firefox-developer-edition', | ||
'/usr/bin/firefox', | ||
], | ||
win32: [ | ||
'C:\\Program Files\\Firefox Nightly\\firefox.exe', | ||
'C:\\Program Files\\Firefox Developer Edition\\firefox.exe', | ||
'C:\\Program Files\\Mozilla Firefox\\firefox.exe', | ||
], | ||
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import type { ChildProcess } from 'node:child_process'; | ||
import type { Readable, Writable } from 'node:stream'; | ||
import { debug } from './debug'; | ||
|
||
const debugCdp = debug.scoped('cdp'); | ||
|
||
export interface CDPConnection extends Disposable { | ||
send<T>(method: string, params: any, timeout?: number): Promise<T>; | ||
close(): void; | ||
} | ||
|
||
export function createCdpConnection( | ||
browserProcess: ChildProcess, | ||
): CDPConnection { | ||
const inputStream = browserProcess.stdio[3] as Writable; | ||
const outputStream = browserProcess.stdio[4] as Readable; | ||
|
||
let requestId = 0; | ||
|
||
return { | ||
send(method, params, timeout = 10e3) { | ||
const id = ++requestId; | ||
const command = { id, method, params }; | ||
debugCdp('Sending command:', command); | ||
|
||
return new Promise((resolve, reject) => { | ||
const timer = setTimeout(() => { | ||
reject(new Error(`CDP command timed out: ${method}`)); | ||
}, timeout); | ||
|
||
const onData = (data: Buffer) => { | ||
// Trim the trailing null character | ||
const text = data.toString().slice(0, -1); | ||
const res = JSON.parse(text); | ||
|
||
if (res.id !== id) return; | ||
|
||
debugCdp('Received response:', res); | ||
clearTimeout(timer); | ||
outputStream.removeListener('data', onData); | ||
|
||
if ('error' in res) { | ||
reject(new Error(res.error.message, { cause: res.error })); | ||
} else { | ||
resolve(res.result); | ||
} | ||
}; | ||
|
||
outputStream.addListener('data', onData); | ||
|
||
inputStream.write(JSON.stringify(command) + '\0'); | ||
}); | ||
}, | ||
close() {}, | ||
[Symbol.dispose]() {}, | ||
}; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
export interface Debug { | ||
(...args: any[]): void; | ||
scoped: (scope: string) => Debug; | ||
} | ||
|
||
function createDebug(scopes: string[]): Debug { | ||
const debug = (...args: any[]) => { | ||
const scope = scopes.join(':'); | ||
if ( | ||
process.env.DEBUG === '1' || | ||
process.env.DEBUG === 'true' || | ||
scope.startsWith(process.env.DEBUG ?? '@NOT') | ||
) { | ||
const params = scope ? [`\x1b[31m${scope}\x1b[0m`, ...args] : args; | ||
console.log(...params); | ||
} | ||
}; | ||
|
||
debug.scoped = (scope: string) => createDebug([...scopes, scope]); | ||
|
||
return debug; | ||
} | ||
|
||
export const debug = createDebug(['@wxt-dev/runner']); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export * from './run'; | ||
export * from './options'; | ||
export * from './install'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import type { ChildProcess } from 'node:child_process'; | ||
import { createBidiConnection } from './bidi'; | ||
import { createCdpConnection } from './cdp'; | ||
|
||
/** | ||
* Install an extension to an already running instance of Firefox. | ||
* @param debuggerUrl The URL of the Firefox BiDi server (ex: `ws://127.0.0.1:45912`). | ||
* @param extensionDir Absolute path to the directory containing the extension to be installed. | ||
*/ | ||
export async function installFirefox( | ||
debuggerUrl: string, | ||
extensionDir: string, | ||
): Promise<BidiWebExtensionInstallResponse> { | ||
using bidi = await createBidiConnection(debuggerUrl); | ||
|
||
// Start a session | ||
await bidi.send<unknown>('session.new', { capabilities: {} }); | ||
|
||
// Install the extension | ||
return await bidi.send<BidiWebExtensionInstallResponse>( | ||
'webExtension.install', | ||
{ | ||
extensionData: { | ||
type: 'path', | ||
path: extensionDir, | ||
}, | ||
}, | ||
); | ||
} | ||
|
||
export type BidiWebExtensionInstallResponse = { | ||
extension: string; | ||
}; | ||
|
||
/** | ||
* Given a child process of Chrome, install an extension. The process must be started with the following flags: | ||
* | ||
* - `--remote-debugging-pipe` | ||
* - `--user-data-dir=...` | ||
* - `--enable-unsafe-extension-debugging` | ||
* | ||
* Otherwise it the CDP doesn't have permission to install extensions. | ||
*/ | ||
export async function installChrome( | ||
browserProcess: ChildProcess, | ||
extensionDir: string, | ||
): Promise<CdpExtensionsLoadUnpackedResponse> { | ||
using cdp = createCdpConnection(browserProcess); | ||
return await cdp.send<CdpExtensionsLoadUnpackedResponse>( | ||
'Extensions.loadUnpacked', | ||
{ | ||
path: extensionDir, | ||
}, | ||
); | ||
} | ||
|
||
export type CdpExtensionsLoadUnpackedResponse = { | ||
id: string; | ||
}; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.