Skip to content

Topfi/forge

Repository files navigation

Forge

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)

Quickstart

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 it

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

Requirements

  • 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-essential on Linux, Visual Studio on Windows)

Commands

Setup & source management

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 config
Config examples
./forge/forge config firefox.version          # get
./forge/forge config firefox.version 132.0    # set
./forge/forge config custom.key val --force   # unknown key

Building & running

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 brand

Patch workflow

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

Export examples

# 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 --scan
Patch 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.


Wiring & registration

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 example

# 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 MyWidget
Wire options explained
  • Subscript (always): Adds a loadSubScript call wrapped in try/catch to browser-main.js
  • --init <expr>: Adds init expression to gBrowserInit.onLoad() in browser-init.js, wrapped in typeof guard and try/catch
  • --destroy <expr>: Adds destroy expression to onUnload() in browser-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 #include directive for an .inc.xhtml file into browser.xhtml
  • --subscript-dir <dir>: Subscript directory relative to engine/ (default: browser/base/content). Overrides wire.subscriptDir from forge.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}

Design tokens

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

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>/

Typical workflow

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 changes

Deploy is resilient to partial failures — if one registration step fails, the rest still execute. Failures and warnings are reported separately in the summary.

Furnace commands

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 — Skip customElements.js registration
  • --with-tests — Scaffold test files and register in moz.build
  • --compose <tags> — Declare stock components used internally

Override:

  • -t, --type <type>css-only or full
furnace.json schema
{
  "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"]
    }
  }
}

Validation checks

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:

  1. forge download — Download Firefox source
  2. forge furnace deploy --dry-run — Preview changes
  3. forge furnace deploy — Apply + validate
  4. forge build / forge package — Build (both run furnace apply automatically)
  5. forge export — Export changes as patches
  6. forge 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

Configuration reference

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/

Development

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.


Roadmap

  • 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 clone from mozilla-central)

License

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.

About

CLI tool for creating, building, and maintaining custom Firefox-based browsers

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages