A build tool for creating and maintaining custom Firefox-based browsers.
Forge wraps Mozilla's build system and adds a patch-based workflow on top. You write patches against the Firefox source tree, and Forge handles downloading the right version, applying patches in order, building, and packaging. It also includes Furnace, a component system for creating and overriding Firefox custom elements (MozLitElement).
The patch workflow — export, import, re-export — is designed to survive ESR rebases. Diffs are contextual so upstream security fixes aren't silently dropped.
Inspired by fern.js and Melon.
mybrowser/
├── forge/ ← This repo (cloned as a subdirectory)
├── forge.json ← Created by `forge setup`
├── engine/ ← Firefox source (created by `forge download`)
├── patches/ ← Your patches
│ └── patches.json ← Patch manifest (auto-managed)
├── components/ ← Furnace components
│ ├── overrides/ ← Forked Firefox components
│ └── custom/ ← New components
├── configs/ ← Build configurations (mozconfig templates)
└── .forge/ ← Runtime data (gitignored)
cd /path/to/mybrowser
git clone https://github.com/topfi/forge.git
cd forge && npm install && cd ..
./forge/forge setup # interactive project init
./forge/forge download # fetch Firefox source (~1 GB)
./forge/forge bootstrap # install build deps (may need sudo)
./forge/forge build # build the browser
./forge/forge run # launch itAll commands run from the fork root, not from inside forge/:
| Platform | Invocation |
|---|---|
| macOS/Linux | ./forge/forge ... |
| Windows CMD | .\forge\forge.cmd ... |
| PowerShell | .\forge\forge.ps1 ... |
- Node.js 20+
- Python 3 (version range determined by Firefox
mach; typically 3.8–3.12) - Git
- Platform-specific build tools (Xcode on macOS,
build-essentialon Linux, Visual Studio on Windows)
| Command | Description |
|---|---|
forge setup |
Create forge.json, directory structure, and .gitignore. Interactive by default. |
forge download |
Download and extract Firefox source. Initializes engine/ as a git repo. --force to re-download. |
forge bootstrap |
Install Firefox build dependencies via mach bootstrap. Surfaces diagnostics on failure. |
forge doctor |
Diagnose project issues — dependencies, engine state, patch integrity. Warnings exit 0; failures exit non-zero. |
forge config |
Get or set forge.json values. --force for unknown keys. |
Non-interactive setup (CI/scripting)
./forge/forge setup --name "MyBrowser" --vendor "My Company" \
--app-id "org.example.mybrowser" --binary-name "mybrowser" \
--firefox-version "140.0esr" --product firefox-esr --license EUPL-1.2
./forge/forge setup --force # overwrite existing configConfig examples
./forge/forge config firefox.version # get
./forge/forge config firefox.version 132.0 # set
./forge/forge config custom.key val --force # unknown key| Command | Description |
|---|---|
forge build |
Build the browser. Automatically applies Furnace components first. |
forge build --ui |
Fast UI-only rebuild (chrome JS/CSS only). |
forge run |
Launch the built browser. |
forge package |
Create a distribution package. |
forge watch |
Watch for changes and auto-rebuild. Requires watchman. |
forge test [paths...] |
Run tests via mach test. Supports --headless and --build. Rewrites common failure modes into actionable diagnostics. |
./forge/forge build # full build
./forge/forge build --ui # fast UI-only rebuild
./forge/forge build -j 16 # 16 parallel jobs
./forge/forge build --brand dev # specific brandThe patch system is the core of Forge. Patches live in patches/, are applied in alphabetical order, and tracked in patches.json.
patches/
├── 001-branding-custom-logo.patch
├── 002-privacy-disable-telemetry.patch
├── 003-ui-sidebar-tweaks.patch
└── patches.json
Categories: branding · ui · privacy · security · infra
| Command | Description |
|---|---|
forge import |
Apply all patches. Checks for uncommitted changes first. --force skips guards, --continue keeps going after failures. |
forge export <paths...> |
Export changes as a patch. Accepts multiple files and directories. Contextual diffs for existing files, full-file patches for new files, git binary format for binaries. |
forge export-all |
Export everything as a single patch. Auto-supersedes covered patches (prompts when multiple would be superseded; --supersede to skip). |
forge re-export [patches...] |
Regenerate existing patches from current engine state. --scan picks up new/changed/removed files. |
forge resolve |
After a failed import, fix .rej conflicts manually, then run this to update the patch and continue. |
forge status |
Show modified files, classified into unmanaged, patch-backed, and branding buckets. |
forge reset |
Reset engine to clean state. |
forge discard <file> |
Discard changes to a specific file. |
# Single file
./forge/forge export browser/base/content/browser.js
# Multiple paths with metadata
./forge/forge export browser/modules/mybrowser/*.sys.mjs \
--name "storage-infra" --category ui --description "Core database infrastructure"
# Export all changes
./forge/forge export-all --name "all-changes" --category ui
# Regenerate patches after editing
./forge/forge re-export --all --scanPatch manifest format
patches/patches.json tracks metadata for each patch and is updated automatically by export and re-export:
{
"version": 1,
"patches": [
{
"filename": "001-branding-custom-logo.patch",
"order": 1,
"category": "branding",
"name": "custom-logo",
"description": "Replaces default Firefox branding with custom logo",
"createdAt": "2025-01-15T10:30:00Z",
"sourceEsrVersion": "140.0esr",
"filesAffected": ["browser/branding/official/logo.png"]
}
]
}Engine state commands
forge status classifies modified files into three buckets:
- Unmanaged changes — local drift not explained by any patch or tool
- Patch-backed changes — expected after
forge import, content matches patch expectations - Tool-managed branding — written by Forge's branding pipeline
Additional flags: --raw skips classification, --unmanaged filters to drift only.
forge reset supports --dry-run and --force.
forge discard <file> supports --dry-run.
These commands inject your code into Mozilla's source files so it loads at browser startup.
| Command | Description |
|---|---|
forge register <path> |
Register a file in the correct build manifest (jar.mn, jar.inc.mn, or moz.build). Idempotent. |
forge wire <name> |
Wire a chrome subscript into the browser — up to five coordinated edits in one command. |
# Wire a subscript with init/destroy lifecycle
./forge/forge wire my-widget --init "MyWidget.init()" --destroy "MyWidget.destroy()"
# Wire with a DOM fragment
./forge/forge wire my-widget --dom engine/browser/components/mybrowser/my-widget.inc.xhtml
# Control ordering between dependent subscripts
./forge/forge wire my-widget-pan --init "MyWidgetPan.init()" --after MyWidgetWire options explained
- Subscript (always): Adds a
loadSubScriptcall wrapped in try/catch tobrowser-main.js --init <expr>: Adds init expression togBrowserInit.onLoad()inbrowser-init.js, wrapped intypeofguard and try/catch--destroy <expr>: Adds destroy expression toonUnload()inbrowser-init.js. Uses LIFO ordering so teardown runs in reverse of init order--after <n>: Insert init block after the named object's block. Controls ordering between dependent subscripts--dom <file>: Inserts#includedirective for an.inc.xhtmlfile intobrowser.xhtml--subscript-dir <dir>: Subscript directory relative toengine/(default:browser/base/content). Overrideswire.subscriptDirfromforge.json
Supported register patterns
| File pattern | Manifest | Entry format |
|---|---|---|
browser/themes/shared/*.css |
browser/themes/shared/jar.inc.mn |
skin/classic/browser/{name}.css |
browser/base/content/*.js |
browser/base/jar.mn |
content/browser/{name}.js |
browser/base/content/test/*/browser.toml |
browser/base/moz.build |
"content/test/{dir}/browser.toml" |
browser/modules/mybrowser/*.sys.mjs |
browser/modules/mybrowser/moz.build |
"{name}.sys.mjs" |
toolkit/content/widgets/*/*.{mjs,css} |
toolkit/content/jar.mn |
content/global/elements/{file} |
| Command | Description |
|---|---|
forge token add <name> <value> |
Add a design token to the CSS variables file and documentation in sync. |
forge token coverage |
Measure token usage across modified CSS files. |
./forge/forge token add mybrowser-widget-dot-size 1px \
--category "Colors — Canvas" --mode static --description "Dot grid dot diameter"Supports static, auto, and override modes. Override mode requires --dark-value.
Token add details
Parameters: <token-name> (with or without -- prefix), <value>, --category <cat>, --mode <auto|static|override>, --description <desc>, --dark-value <val> (required for override mode).
The command validates against tokenPrefix in furnace.json, inserts into the correct category section of {binaryName}-tokens.css, handles dark mode blocks for override tokens, and updates the documentation table in SRC_TOKENS.md.
Coverage formula: tokens / (tokens + unknown + rawColors) * 100. Allowlisted Firefox vars (from furnace.json tokenAllowlist) are excluded from the denominator.
Furnace manages Firefox custom elements (MozLitElement). Three component types:
| Type | Description | Local files |
|---|---|---|
| Stock | Engine components tracked for Storybook preview. | None |
| Override | Forked copies — css-only (restyle) or full (behavior + style). |
components/overrides/<name>/ |
| Custom | New elements that don't exist in Firefox. | components/custom/<name>/ |
forge furnace scan # find available components
forge furnace override moz-button -t css-only -d "Restyle" # fork one
forge furnace create moz-my-widget -d "A new widget" # or make a new one
forge furnace deploy --dry-run # preview changes
forge furnace deploy # apply + validate
forge build # build with changesDeploy is resilient to partial failures — if one registration step fails, the rest still execute. Failures and warnings are reported separately in the summary.
| Command | Description |
|---|---|
forge furnace |
Show status (component counts, last apply, pending changes) |
forge furnace status [name] |
Overview, or detailed registration checks for a component |
forge furnace scan |
Scan engine for available MozLitElement components |
forge furnace list |
List all registered components |
forge furnace create [name] |
Scaffold a new custom component |
forge furnace override [name] |
Fork an existing component |
forge furnace apply |
Apply all components to engine (supports --dry-run) |
forge furnace deploy [name] |
Apply + validate (supports --dry-run) |
forge furnace preview |
Launch Storybook |
forge furnace validate [name] |
Run structure, accessibility, and registration checks |
forge furnace diff <name> |
Show changes vs Firefox original (overrides only) |
forge furnace remove <name> |
Remove a component |
Create and override options
Create:
--localized— Include Fluent (.ftl) localization--no-register— SkipcustomElements.jsregistration--with-tests— Scaffold test files and register inmoz.build--compose <tags>— Declare stock components used internally
Override:
-t, --type <type>—css-onlyorfull
furnace.json schema
Furnace validates components on deploy. Issues are classified as errors (block apply) or warnings (advisory).
| Check | Severity | Description |
|---|---|---|
missing-mjs |
error | Custom component missing .mjs file |
missing-css |
warning | No .css file |
filename-mismatch |
error | File name doesn't match tag name |
missing-override-json |
error | Override missing override.json |
no-aria-role |
warning | No ARIA role found |
no-keyboard-handler |
warning | Has @click but no @keydown/@keypress/@keyup |
hardcoded-text |
warning | Possible hardcoded string (use data-l10n-id) |
no-delegates-focus |
warning | Interactive component without delegatesFocus |
relative-import |
error | Imports must use chrome:// URIs |
no-custom-element-define |
error | Missing customElements.define() call |
not-moz-lit-element |
error | Must extend MozLitElement |
raw-color-value |
error | Raw hex/rgb/hsl (use CSS custom properties) |
token-prefix-violation |
error | CSS variable doesn't match tokenPrefix |
missing-token-link |
warning | Component uses tokens but browser.xhtml doesn't link token CSS |
wrong-registration-pattern |
error | .mjs in wrong registration block |
missing-jar-mn-mjs |
error | Missing .mjs entry in jar.mn |
missing-jar-mn-css |
warning | Missing .css entry in jar.mn |
Integration with the patch workflow
Furnace operates on the engine source tree before patches are generated:
forge download— Download Firefox sourceforge furnace deploy --dry-run— Preview changesforge furnace deploy— Apply + validateforge build/forge package— Build (both runfurnace applyautomatically)forge export— Export changes as patchesforge re-export— Regenerate patches after further edits
Component changes are applied as file copies and registration edits, then captured by the patch system alongside other source modifications.
Architecture: source injection
Forge injects custom code into Mozilla's source files (customElements.js, browser-main.js, jar.mn, moz.build). To survive formatting changes across ESR versions, injection uses AST-based parsing rather than regex:
- JavaScript files are parsed with acorn, walked with estree-walker, and modified with magic-string
- Proprietary files (
jar.mn,moz.build,browser.xhtml) use lightweight custom tokenizers
All parsers follow a Strangler Fig fallback: if acorn.parse throws on non-standard Mozilla syntax, a warning is logged and the regex implementation runs instead.
| File | Purpose |
|---|---|
src/core/ast-utils.ts |
Shared AST parsing utilities |
src/core/browser-wire.ts |
AST-based injection into browser-main.js, browser-init.js; tokenizer for browser.xhtml |
src/core/furnace-apply.ts |
AST-based customElements.js registration |
src/core/manifest-register.ts |
Tokenizers for jar.mn/jar.inc.mn and moz.build |
forge.json defines your browser project:
{
"name": "MyBrowser",
"vendor": "My Company",
"appId": "org.example.mybrowser",
"binaryName": "mybrowser",
"license": "EUPL-1.2",
"firefox": {
"version": "140.0esr",
"product": "firefox-esr"
},
"build": { "jobs": 8 },
"wire": { "subscriptDir": "browser/components/mybrowser" }
}| Field | Description |
|---|---|
name |
Display name of your browser |
vendor |
Company/organization name |
appId |
Application ID (reverse-domain) |
binaryName |
Executable name |
license |
SPDX identifier |
firefox.version |
Firefox version to base on |
firefox.product |
firefox, firefox-esr, or firefox-beta |
build.jobs |
Parallel build jobs |
wire.subscriptDir |
Subscript directory relative to engine/ |
npm run forge -- setup # run forge directly during dev
npm run typecheck # type check
npm run lint # lint (lint:fix to auto-fix)
npm run format # format (format:check to verify)Strict TypeScript, ESLint (no any, explicit return types), Prettier, and pre-commit hooks.
- Docker builds — Reproducible builds using Docker containers
- CI mode — Automated setup for continuous integration pipelines
- Update manifests — Generate update server manifests for auto-updates
- Nightly — Nightly support (requires
hg clonefrom mozilla-central)
EUPL-1.2. Firefox source in engine/ is under MPL-2.0 and is not distributed by this repository.
During forge setup, you choose a license for your project files (patches, configs, scripts). Options: EUPL-1.2 (default, copyleft, MPL-compatible), MPL-2.0, 0BSD, GPL-2.0+. This only applies to your files — Firefox-derived files generated by Furnace always carry MPL-2.0 headers.
{ "version": 1, "componentPrefix": "moz-", "stock": ["moz-button", "moz-toggle"], "overrides": { "moz-button": { "type": "css-only", "description": "Custom button styles", "basePath": "toolkit/content/widgets/moz-button", "baseVersion": "134.0" } }, "custom": { "moz-my-widget": { "description": "A new widget", "targetPath": "toolkit/content/widgets/moz-my-widget", "register": true, "localized": false, "composes": ["moz-button"] } } }