Skip to content

Development

Tom Bouquet edited this page Nov 2, 2026 · 2 revisions

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.

Prerequisites

The extension needs Node 26 or later and pnpm:

  • .nvmrc pins Node 26, and engines.node in package.json requires >=26. With nvm, run nvm use in the repo root.
  • packageManager in package.json pins the pnpm version. CI's pnpm/action-setup step reads it, and Corepack uses it locally when you've enabled Corepack.
  • pnpm-workspace.yaml exempts 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.

Package scripts

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.

Dev loop

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.

Upload services

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.

Typecheck

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.

Production build

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.

Icons

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.mjs

The icon PNGs are committed, so you only run the script when the design changes.

UI components

The UI uses shadcn/ui on Base UI (@base-ui/react). components.json configures the shadcn CLI:

  • Style base-nova with the neutral base 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 the cn helper 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.

Continuous integration

.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:

  1. pnpm install --frozen-lockfile
  2. pnpm compile
  3. pnpm build
  4. pnpm 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.

Clone this wiki locally