Local-first Chrome and Firefox extension that matches YouTube playback speed to a target words-per-minute.
License: MIT. Privacy: PRIVACY.md.
These steps reproduce the Firefox add-on from this source tree. Reviewers should use this section.
package.json engines.node is >=22.npm ci needs access to the npm registry. The build itself is offline after that.Install Node from https://nodejs.org (22.x LTS) or a version manager (nvm install 22). Confirm:
node -v # v22.x.x
npm -v # 10.x.x
From the repository root (the directory that contains package.json):
npm ci
npm run build:firefox
npm ci installs the exact versions in package-lock.json, then runs wxt prepare (postinstall).
npm run build:firefox runs wxt build -b firefox --mv3. That compiles TypeScript/React, bundles with Vite, and writes an unpacked Manifest V3 extension.
Unpacked Firefox add-on:
dist/firefox-mv3/
That directory is the add-on. Compare it to the uploaded .xpi / .zip. manifest.json is at dist/firefox-mv3/manifest.json.
To also produce the store zip and this sources zip:
npx wxt zip -b firefox --mv3
Source in src/ is TypeScript and React. It is not pre-minified. At build time:
scripts/patch-react-innerhtml.ts rewrites React DOM innerHTML assignments so addons-linter does not flag them. The extension does not use dangerouslySetInnerHTML.No remote code is fetched during the build except npm packages via npm ci.
| Command | Result |
|---|---|
npm run build:firefox |
Firefox unpacked build (dist/firefox-mv3) |
npm run build:chrome |
Chrome unpacked build (dist/chrome-mv3) |
npm run build |
Both browsers, then checks the manifests |
npm install
npm run dev # Chrome MV3
npm run dev:firefox # Firefox MV3
Load unpacked in Chrome from dist/chrome-mv3. In Firefox (about:debugging) load dist/firefox-mv3/manifest.json.
npm test # unit + DOM (Vitest)
npm run test:e2e # headed Chromium with the unpacked Chrome build
The e2e suite loads dist/chrome-mv3 in headed Playwright Chromium, opens the options and popup pages, then a live YouTube watch page. It fails on console warnings/errors and uncaught exceptions that come from the extension (service worker, isolated content script, or MAIN-world player script). YouTube’s own page noise is ignored.
YouTube often refuses playback in a fresh automated Chromium profile. To verify caption acquire against your real Chrome session:
& "$env:ProgramFiles\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9222
$env:E2E_CDP = 'http://127.0.0.1:9222'
npx playwright test