Repository navigation
Development
This page covers setting up a local checkout, the package scripts, building for Chrome and Firefox, the upload services' build variables, and the checks CI runs on each pull request. Read it before you change the extension's code. To ship a change, see Releasing.
The extension needs Node 26 or later and pnpm:
-
.nvmrcpins Node 26, andengines.nodeinpackage.jsonrequires>=26. With nvm, runnvm usein the repo root. -
packageManagerinpackage.jsonpins the pnpm version. CI'spnpm/action-setupstep reads it, and Corepack uses it locally when you've enabled Corepack. -
pnpm-workspace.yamlexempts a few pinned packages from pnpm's minimum release age check.
To run the Firefox build you also need Firefox 140 or later.
pnpm install runs the postinstall script, wxt prepare, which generates .wxt/. The root tsconfig.json extends .wxt/tsconfig.json, so the typecheck fails until that script has run once.
| Script | Runs | Does |
|---|---|---|
pnpm dev |
wxt |
Opens Chrome with the extension loaded and hot reload |
pnpm dev:firefox |
wxt -b firefox |
Opens Firefox with the extension loaded and hot reload |
pnpm compile |
tsc --noEmit |
Typechecks the project |
pnpm build |
wxt build |
Writes a production build to .output/chrome-mv3
|
pnpm build:firefox |
wxt build -b firefox |
Writes a production build to .output/firefox-mv3
|
pnpm zip |
wxt zip |
Builds and zips the Chrome extension into .output/
|
pnpm zip:firefox |
wxt zip -b firefox |
Builds and zips the Firefox add-on, plus a sources zip, into .output/
|
pnpm screenshots |
node scripts/capture-screenshots.ts |
Captures the listing screenshots; see Store Listing Images |
pnpm showcase |
node scripts/build-showcase.ts |
Composes the listing images; see Store Listing Images |
The two listing scripts run TypeScript directly with Node, which Node 26 supports without a loader.
Run pnpm dev, or pnpm dev:firefox for Firefox. WXT builds the extension, starts a browser profile with it loaded, and reloads the extension when you save a file. wxt.config.ts sets src/ as the source directory and Manifest V3 for both browsers, adds the React module and the Tailwind CSS v4 Vite plugin, and builds the manifest fields that aren't derived from package.json for each browser: the name, description, minimum version (Chrome 116, Firefox 140), permissions, optional upload permissions, the options page (app.html#/settings) and, for Firefox, the add-on ID. The Architecture page sets the two manifests side by side.
Code that differs between the browsers goes in src/platform/, not in checks scattered through the app. Call the browser object from wxt/browser rather than chrome, so the code runs in both. Try a change in both browsers when it touches capture, the toolbar or permissions.
The entry points live in src/entrypoints/: background.ts, popup/ and app/. WXT turns each one into a manifest entry, so a new page or script goes in that directory.
Uploads to Google Drive and Dropbox need an OAuth app for each service, and the build reads the apps' public IDs from the environment. Copy .env.example to .env.local, which git ignores, and fill it in:
| Variable | Holds |
|---|---|
WXT_GOOGLE_CLIENT_ID |
Google OAuth client ID |
WXT_DROPBOX_APP_KEY |
Dropbox app key |
WXT_EXTENSION_KEY |
The Chrome Web Store public key, so a local build gets the store's extension ID |
All three are public identifiers, not secrets. They're read at build time, so any build that should offer uploads, release builds included, needs them set. A build without a service's ID still works: Settings shows that service as "Not available in this build" with no Connect button. Upload servers need no build variables.
WXT passes .env files to the app code but not to wxt.config.ts, so the config loads .env.local itself to read WXT_EXTENSION_KEY. A variable already set in the shell wins. The key matters because the OAuth redirect URL, https://<extension-id>.chromiumapp.org/, depends on the extension ID, and an unpacked build without a key gets an ID from its folder path.
The cloud connections guide covers setting up the OAuth apps, the Firefox redirect URL and adding a service. The Upload Pipeline page covers how uploads work in the code.
Run pnpm compile before you push. The tsconfig.json turns on strict, noUncheckedIndexedAccess and noFallthroughCasesInSwitch, and maps @/* and ~/* to src/*. The repo has no lint or test script, so the typecheck and the production build are the only automated checks.
Run pnpm build, then load .output/chrome-mv3 on chrome://extensions with Load unpacked to try it the way users get it. pnpm zip writes .output/screen-tools-<version>-chrome.zip, the file the release workflow attaches to each GitHub release. WXT takes the version from package.json, so don't edit the version by hand.
For Firefox, run pnpm build:firefox, open about:debugging#/runtime/this-firefox, click Load Temporary Add-on… and pick .output/firefox-mv3/manifest.json. Firefox removes temporary add-ons when it closes. pnpm zip:firefox writes .output/screen-tools-<version>-firefox.zip, plus the sources zip that addons.mozilla.org asks for.
scripts/make-icons.mjs draws the icon, a red dot in a white ring on a dark rounded square, and writes public/icon/16.png, 32.png, 48.png and 128.png. WXT picks those files up for the manifest. The script has no dependencies: it paints the shapes in a 128 × 128 space, anti-aliases with 8 × 8 samples per pixel, and encodes the PNGs with node:zlib. Run it from the repo root after changing a shape or colour in the script:
node scripts/make-icons.mjsThe icon PNGs are committed, so you only run the script when the design changes.
The UI uses shadcn/ui on Base UI (@base-ui/react). components.json configures the shadcn CLI:
- Style
base-novawith theneutralbase colour and CSS variables. - No React Server Components (
rsc: false), TSX on, and Lucide for icons. - Tailwind v4 with no config file: the theme lives in
src/assets/globals.css. - Aliases: components in
@/components, primitives in@/components/ui, and thecnhelper in@/lib/utils.
Add a primitive with the shadcn CLI (pnpm dlx shadcn@latest add <component>) so it lands in src/components/ui/ with the project's style.
.github/workflows/ci.yml runs on every pull request and every push to main. One job on ubuntu-latest sets up pnpm and the Node version from .nvmrc, then runs:
pnpm install --frozen-lockfilepnpm compilepnpm buildpnpm build:firefox
CI builds without the upload services' variables, so its builds have Connect turned off.
The frozen lockfile makes the install fail when package.json and pnpm-lock.yaml disagree, so commit the lockfile with any dependency change. CI doesn't run the listing scripts, because they need Playwright's Chromium.
Using Screen Tools
- Installation and Updating
- Firefox
- Recording
- Screenshots
- Uploading
- Settings
- Troubleshooting and FAQ
- Privacy
How it works
Contributing