@hominis/fireforge 0.21.4 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -751
- package/README.md +52 -775
- package/dist/src/commands/doctor-furnace-manifest-sync.js +42 -4
- package/dist/src/commands/doctor.js +17 -3
- package/dist/src/commands/patch/compact.js +18 -1
- package/dist/src/commands/verify.d.ts +19 -0
- package/dist/src/commands/verify.js +90 -61
- package/dist/src/types/commands/options.d.ts +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,826 +3,103 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@hominis/fireforge)
|
|
4
4
|
[](LICENSE.md)
|
|
5
5
|
[](package.json)
|
|
6
|
-
[](https://www.npmjs.com/package/@hominis/fireforge)
|
|
7
|
-
[](https://www.npmjs.com/package/@hominis/fireforge)
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
FireForge gives you a toolkit for forking Firefox: download a specific ESR release, manage your customisations as a series of patches, survive version upgrades with semi-automated rebase, wire custom code into Mozilla's startup paths and build the result. It also ships **Furnace**, a component system for creating and overriding Firefox custom elements under `toolkit/content/widgets`.
|
|
7
|
+
FireForge is a CLI tool for maintaining Firefox-based browser forks. It downloads Firefoxs source code, lets you export changes as patches, helps apply them and has some neat/mad code quality enforcement, built by taking a lot of learnings from the existing Firefox source code and trying to make the process as robust as can be. Ideally, FireForge makes everything from managing custom UI components to enforcing some rudimentary typechecking much easier, but I'll be honest, there is only so much one can do, so understanding upstream is advised, especially once future updates come into the mix. I cannot guarantee how reliably these will apply, but I did try to assist in that process as much as I can by learning from prior Firefox versions which had major changes made to them, though this is of course still no guarantee for future success or that the methods will remain reliable. Beware of that.
|
|
12
8
|
|
|
13
9
|
Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon](https://github.com/dothq/melon).
|
|
14
10
|
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
- **Patch-based fork management** Your customisations live as portable, ordered `.patch` files. Export single files, multiple paths, or everything at once. Contextual diffs mean upstream security fixes are not silently dropped when you rebase.
|
|
18
|
-
|
|
19
|
-
- **Semi-automated ESR rebase** `fireforge rebase` replays your patch stack onto new Firefox source with escalating fuzz matching. When a patch fails, you fix it manually and `--continue`. The full stack gets re-exported with updated version stamps.
|
|
20
|
-
|
|
21
|
-
- **Wiring and registration** `fireforge wire` and `fireforge register` inject your code into Mozilla's startup paths, build manifests and JAR files with a single command. The injection is AST-based (via Acorn), so it survives formatting changes applied between versions.
|
|
22
|
-
|
|
23
|
-
- **Furnace component system** Override existing Firefox custom elements or create new ones under `toolkit/content/widgets` (CSS-only restyles, full behavioural forks, or entirely new widgets).
|
|
11
|
+
## What It Does
|
|
24
12
|
|
|
25
|
-
- **
|
|
13
|
+
- **Patch based** Edit Firefox inside the `engine/` directory, then export changes into `.patch` files with manifest metadata.
|
|
14
|
+
- **ESR rebasing** Reapply your patches onto a newer Firefox source tree, resolve rejects and re-export the queue against the new baseline, hopefully...
|
|
15
|
+
- **Firefox source and build helpers** Download, bootstrap, build, run, test, package, smoke-check, etc.
|
|
16
|
+
- **Wiring and registration** Add chrome scripts, DOM fragments, modules, styles, tests and manifest entries through commands built by learning from existing Firefox conventions.
|
|
17
|
+
- **Furnace components** Create or override `MozLitElement` widgets easily to add new or adapt existing UI components to your needs.
|
|
18
|
+
- **Quality** `lint`, `typecheck`, `verify` and `doctor` catch common issues early.
|
|
19
|
+
- **Tests** Fireforge was build by taking apart and applying patches of all sorts to original Firefox ESR source code across different versions, learning what works vs doesn't and creating some quite extensive tests based on that covering all manner of scenarios. Yes, we mock quite a bit, but when building a tool that modifies a separate code base, I think it's a solid compromise for the time being. Full end-to-end runs are currently run locally on my MacBook, as they require about 30 GB of disk and significant compute for multiple full builds. Full end-to-end via Actions will be added soonishlyTM but might need a different runner...
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
## Requirements
|
|
28
22
|
|
|
29
|
-
-
|
|
23
|
+
- Node.js 20+
|
|
24
|
+
- Python 3
|
|
25
|
+
- Git
|
|
26
|
+
- The normal Firefox platform build tools: Xcode command line tools on macOS, `build-essential`-style packages on Linux, Visual Studio Build Tools on Windows (never tested on Windows tbh)
|
|
27
|
+
- Watchman, if you want `fireforge watch` (optional)
|
|
30
28
|
|
|
31
|
-
##
|
|
29
|
+
## Getting Started
|
|
32
30
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- **Node.js 20+**
|
|
36
|
-
- **Python 3** (required by Firefox's `mach` build system).
|
|
37
|
-
- **Git**
|
|
38
|
-
- Platform build tools: Xcode on macOS, `build-essential` on Linux, Visual Studio Build Tools on Windows.
|
|
39
|
-
- **Watchman** (optional, only required by `fireforge watch`). Install via `brew install watchman` (macOS), `dnf install watchman` (Fedora), or follow the upstream [Meta docs](https://facebook.github.io/watchman/). `fireforge doctor` surfaces a warning row when it is not on `PATH`; build, run, package, and test workflows still work without Watchman. `fireforge watch` resolves watchman's absolute path via `which` / `where` and prepends its directory to the subprocess `PATH` it hands mach, so a homebrew-installed watchman at `/opt/homebrew/bin/watchman` (absent from the Node subprocess's default `PATH` on macOS) is still visible to `mach watch` without the operator having to re-export `PATH` manually. If `watchman` is installed but `fireforge watch` still refuses, make sure the same shell or automation environment that launches FireForge has Watchman on `PATH`. If `mach watch` reports `Operation not permitted` / `EPERM` on macOS, FireForge points at the usual privacy fix: grant Full Disk Access or Files and Folders access to the terminal/Codex app and watchman, then restart watchman with `watchman shutdown-server`.
|
|
40
|
-
|
|
41
|
-
### Setup
|
|
31
|
+
Create a project and install FireForge locally:
|
|
42
32
|
|
|
43
33
|
```bash
|
|
44
34
|
mkdir mybrowser && cd mybrowser
|
|
45
35
|
npm init -y
|
|
46
36
|
npm install --save-dev @hominis/fireforge
|
|
47
|
-
|
|
48
|
-
npx fireforge setup # interactive project init
|
|
49
|
-
npx fireforge download # fetch Firefox source (~1 GB)
|
|
50
|
-
npx fireforge bootstrap # install build deps (may need sudo)
|
|
51
|
-
npx fireforge import # apply your patches (if any exist)
|
|
52
|
-
npx fireforge build # build the browser
|
|
53
|
-
npx fireforge run # launch it
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
If your shell does not provide `npm` / `npx`, run the installed binary directly instead: `fireforge setup`, `fireforge download`, and so on when `fireforge` is on `PATH`, or `./node_modules/.bin/fireforge setup` from a project-local install.
|
|
57
|
-
|
|
58
|
-
Your project now has `fireforge.json`, an `engine/` directory with Firefox source and a `patches/` directory with an empty `patches.json` manifest ready for your first customisation.
|
|
59
|
-
|
|
60
|
-
#### Known upstream build issues
|
|
61
|
-
|
|
62
|
-
- **macOS 15 (Darwin 25+) — `gecko-profiler` bindgen error `cannot find type _CharT in this scope`.** An Apple toolchain update changed `std::__CharT_pointer` to `_CharT_pointer` in the libc++ headers Firefox's bindgen walks, so `toolkit/library/rust/target-objects` fails during `mach build` even on a clean `fireforge bootstrap`. This is an upstream Firefox issue, not a FireForge bug. Two workarounds: pin Xcode's command line tools to a pre-September-2025 release via `xcode-select --install` / [Apple developer downloads](https://developer.apple.com/download/all/), or apply a one-line bindgen-basic-string-workaround patch (a downstream consumer may ship one in its patch queue). If you interrupt the resulting `fireforge build` and re-run `fireforge doctor`, the download/engine state is unaffected — the failure is isolated to the Rust compile phase.
|
|
63
|
-
|
|
64
|
-
- **macOS (including Apple Silicon) — toolkit / MochiKit widget tests idle until ~370s (`TEST_END: TIMEOUT`, no subtests).** The **mochitest-chrome** flavor used for `toolkit/content/tests/widgets/test_*.html` runs **single-process** (`e10s: false`), which interacts badly with headed or headless compositing (for example SWGL) in some setups. Prefer **`furnace create --with-tests`** (defaults to **`browser-chrome`**, multi-process) for interactive chrome coverage, or place tests alongside your fork's browser modules (for example `engine/browser/modules/<fork>/test/`). If you must use **`--test-style=mochikit`**, expect possible hangs on macOS. Details: [Picking a test harness for `furnace create`](#picking-a-test-harness-for-furnace-create), [Test harness options](#test-harness-options).
|
|
65
|
-
|
|
66
|
-
### Workflow Overview
|
|
67
|
-
|
|
68
|
-
1. Make changes inside the `engine/` directory.
|
|
69
|
-
2. Export your changes as a patch:
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npx fireforge export browser/base/content/browser.js --name "custom-toolbar" --category ui
|
|
73
|
-
# Direct binary equivalent:
|
|
74
|
-
fireforge export browser/base/content/browser.js --name "custom-toolbar" --category ui
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
3. Your patch is now in `patches/`.
|
|
78
|
-
4. Reset and import to verify everything applies cleanly:
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
npx fireforge reset --yes # or: fireforge reset --yes
|
|
82
|
-
npx fireforge import # or: fireforge import --dry-run
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
5. When Mozilla releases a new version, update fireforge.json, re-download and rebase:
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
npx fireforge download --force # or: fireforge download --force
|
|
89
|
-
npx fireforge rebase # or: fireforge rebase
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
`fireforge download` indexes the extracted Firefox source into a fresh git repository — a one-time 1–3 minute pass on a cold SSD, longer on slow or loaded disks. The monolithic `git add -A` is capped at 10 minutes by default and falls back to a per-directory chunked pass (30 minutes per chunk) when the cap hits. If indexing still times out, the command now raises `GitIndexingTimeoutError` with recovery guidance: extend the cap via `FIREFORGE_GIT_ADD_TIMEOUT_MS` (monolithic) and/or `FIREFORGE_GIT_ADD_CHUNK_TIMEOUT_MS` (chunked) in milliseconds, e.g. `FIREFORGE_GIT_ADD_TIMEOUT_MS=1800000 fireforge download --force` for a 30-minute monolithic budget, then re-run `fireforge download --force` — the resume path picks up from the partial git state so the repeat is not wasted work.
|
|
93
|
-
|
|
94
|
-
`fireforge download` is serialised by a project lock under `.fireforge/`, covering the engine-exists check, forced replacement, extraction, git initialisation/resume, patch cleanup and state update. Archive cache entries are also guarded by per-archive locks, so parallel downloads queue instead of racing over the same `engine/` directory or deleting each other's cache results. For reproducible workflows, set `firefox.sha256` to the expected 64-character archive SHA-256; FireForge verifies both cached and freshly downloaded archives before extraction and refuses a mismatch before touching the engine.
|
|
95
|
-
|
|
96
|
-
`fireforge rebase --dry-run` refuses when the engine has no baseline commit yet (e.g. the aftermath of an aborted `download --force`), so dry-run and real-run preconditions stay in sync.
|
|
97
|
-
|
|
98
|
-
## Patch Workflow
|
|
99
|
-
|
|
100
|
-
Patches live in `patches/`, applied by numeric filename prefix and tracked in `patches/patches.json`:
|
|
101
|
-
|
|
102
|
-
```
|
|
103
|
-
patches/
|
|
104
|
-
001-branding-custom-logo.patch
|
|
105
|
-
002-privacy-disable-telemetry.patch
|
|
106
|
-
003-ui-sidebar-tweaks.patch
|
|
107
|
-
patches.json
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**Categories:** `branding` | `ui` | `privacy` | `security` | `infra`
|
|
111
|
-
|
|
112
|
-
The category system is intentionally broad. The numeric ordering provides sequencing.
|
|
113
|
-
|
|
114
|
-
Projects that need stricter queue semantics can add an optional `patchPolicy` block to
|
|
115
|
-
`fireforge.json`. When present, FireForge checks the policy before mutating the patch queue and
|
|
116
|
-
reports the same policy findings from `fireforge verify` and `fireforge lint --per-patch`.
|
|
117
|
-
|
|
118
|
-
```json
|
|
119
|
-
{
|
|
120
|
-
"patchPolicy": {
|
|
121
|
-
"filenamePattern": "^(?<order>\\d{3})-(?<category>branding|infra|ui)-(?<slug>[a-z0-9-]+)\\.patch$",
|
|
122
|
-
"requireDescription": true,
|
|
123
|
-
"allowGaps": true,
|
|
124
|
-
"mutationMode": "error",
|
|
125
|
-
"ranges": [
|
|
126
|
-
{ "from": 1, "to": 99, "category": "branding" },
|
|
127
|
-
{ "from": 100, "to": 199, "category": "infra" },
|
|
128
|
-
{ "from": 200, "to": 299, "category": "ui" }
|
|
129
|
-
],
|
|
130
|
-
"reservedRanges": [
|
|
131
|
-
{
|
|
132
|
-
"from": 900,
|
|
133
|
-
"to": 999,
|
|
134
|
-
"allowed": [
|
|
135
|
-
{
|
|
136
|
-
"filename": "900-infra-bootstrap-workaround.patch",
|
|
137
|
-
"files": ["tools/profiler/rust-api/build.rs"],
|
|
138
|
-
"adr": "docs/architecture/adr/0001-bootstrap-workaround.md"
|
|
139
|
-
}
|
|
140
|
-
]
|
|
141
|
-
}
|
|
142
|
-
]
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Policy ranges are category-owned: a `ui` patch in the example must use `200-299`, while
|
|
148
|
-
`900-999` is reserved for exact allowlisted exceptions. Reserved exceptions must include either
|
|
149
|
-
`adr` or `documentation`; when `files` is present, the patch may not touch paths outside that
|
|
150
|
-
allowlist. `filenamePattern` must expose named captures `order`, `category`, and `slug`.
|
|
151
|
-
`mutationMode` controls mutating commands: `"error"` refuses, `"warn"` prints warnings and
|
|
152
|
-
continues, and `"force"` refuses unless the command supports and receives `--force-unsafe`.
|
|
153
|
-
When a policy-owned queue has sparse category ranges before reserved exceptions, use
|
|
154
|
-
`fireforge export <paths> --order 241 --category ui --name new-ui-feature` to create the exact
|
|
155
|
-
unused order without renumbering later patches. Positional insertion with `--before` / `--after`
|
|
156
|
-
still renumbers following patches, and policy enforcement refuses the operation if that would move a
|
|
157
|
-
reserved exact exception such as `900-infra-bootstrap-workaround.patch`.
|
|
158
|
-
Without `patchPolicy`, existing repositories keep the broad category and numeric ordering behavior.
|
|
159
|
-
|
|
160
|
-
### Importing patches
|
|
161
|
-
|
|
162
|
-
```bash
|
|
163
|
-
# Apply all patches from patches/ to the engine
|
|
164
|
-
fireforge import
|
|
165
|
-
|
|
166
|
-
# Preview what would be applied without modifying the engine
|
|
167
|
-
fireforge import --dry-run
|
|
168
|
-
|
|
169
|
-
# Apply patches up to (and including) a specific one
|
|
170
|
-
fireforge import --until 003-ui-sidebar-tweaks.patch
|
|
171
|
-
|
|
172
|
-
# Keep going if a patch fails instead of stopping
|
|
173
|
-
fireforge import --continue
|
|
174
|
-
|
|
175
|
-
# Force-apply even when the engine has drifted or has unmanaged changes
|
|
176
|
-
fireforge import --force
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Exporting changes
|
|
180
|
-
|
|
181
|
-
`export`, `export-all`, `lint`, `register`, and `test` all accept either engine-relative paths (`browser/base/content/foo.js`) or repo-root-relative paths with a leading `engine/` segment (`engine/browser/base/content/foo.js`). The prefix is case-insensitive and tolerates leading whitespace; operators commonly paste the repo-rooted form from `git status` output or shell tab-completion.
|
|
182
|
-
|
|
183
|
-
```bash
|
|
184
|
-
# Single file
|
|
185
|
-
fireforge export browser/base/content/browser.js
|
|
186
|
-
|
|
187
|
-
# Multiple paths with metadata
|
|
188
|
-
fireforge export browser/modules/mybrowser/*.sys.mjs \
|
|
189
|
-
--name "storage-infra" --category infra
|
|
190
|
-
|
|
191
|
-
# Everything at once
|
|
192
|
-
fireforge export-all --name "all-changes" --category ui
|
|
193
|
-
|
|
194
|
-
# Regenerate patches after further edits
|
|
195
|
-
fireforge re-export --all --scan
|
|
196
|
-
|
|
197
|
-
# Preview what an export would do without writing
|
|
198
|
-
fireforge export browser/base/content/browser.js --dry-run
|
|
199
|
-
|
|
200
|
-
# Same preview surface for the aggregate path
|
|
201
|
-
fireforge export-all --name "all-changes" --category ui --dry-run
|
|
202
|
-
|
|
203
|
-
# Create a sparse patch at an exact unused order without renumbering later patches
|
|
204
|
-
fireforge export browser/base/content/browser.js --order 241 --name "new-ui-feature" --category ui
|
|
205
|
-
|
|
206
|
-
# Insert a new patch at a positional anchor, renumbering later patches
|
|
207
|
-
fireforge export browser/base/content/browser.js --before 005-ui-sidebar.patch --name "prelim"
|
|
208
|
-
|
|
209
|
-
# Restrict a re-export to a specific file subset
|
|
210
|
-
fireforge re-export --files browser/base/content/browser.js 002-ui-toolbar
|
|
211
|
-
|
|
212
|
-
# Refresh every patch AND stamp sourceEsrVersion from fireforge.json onto each
|
|
213
|
-
# one. Only stamps when every selected patch refreshes cleanly — partial
|
|
214
|
-
# runs refuse to stamp. Use when you re-exported after a manual Firefox
|
|
215
|
-
# bump that did not go through `rebase`. By default `re-export` refreshes
|
|
216
|
-
# patch bodies and filesAffected but does NOT change sourceEsrVersion.
|
|
217
|
-
fireforge re-export --all --scan --stamp
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
`export` refuses when the new patch's `filesAffected` would overlap with files already claimed by another non-superseded patch. Repartitioning ownership is a deliberate operation: the message points at `fireforge re-export --files <paths> <patch>` as the safe primitive. Pass `--allow-overlap` to acknowledge the conflict and proceed anyway — the resulting queue will fail `fireforge verify` immediately, so this is an intentional escape hatch, not a default. The flag covers cross-patch _modification_ overlap, where two patches both edit the same file. It does NOT bypass the new-file creation guard: two patches creating the same path on `/dev/null` cannot coexist in any apply order, so that case stays a hard refusal regardless of `--allow-overlap`.
|
|
221
|
-
|
|
222
|
-
`re-export --scan` also prompts before broadening a patch with more than a handful of newly discovered files or with files spanning multiple directories. The gate keeps the common refresh case frictionless (small, same-directory additions) while catching the failure mode where `--scan` silently pulls an adjacent feature into the wrong patch. Non-interactive mode requires `--yes` to acknowledge a broad expansion; dry-run previews never require confirmation.
|
|
223
|
-
|
|
224
|
-
### Rebasing on top of a new Firefox version
|
|
225
|
-
|
|
226
|
-
1. Update `firefox.version` in `fireforge.json`
|
|
227
|
-
2. `fireforge download --force`
|
|
228
|
-
3. `fireforge rebase`
|
|
229
|
-
4. Fix any rejects, then `fireforge rebase --continue`
|
|
230
|
-
5. If stuck, `fireforge rebase --abort` to restore the pre-rebase state
|
|
231
|
-
|
|
232
|
-
### Resolving conflicts
|
|
233
|
-
|
|
234
|
-
When `fireforge import` fails on a patch, fix the `.rej` files in `engine/`, then:
|
|
235
|
-
|
|
236
|
-
```bash
|
|
237
|
-
fireforge resolve
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
This re-exports the fixed patch and clears the conflict state. The command is deliberately a single-patch refresh — to continue applying the remainder of the queue, run `fireforge import` afterwards. For scripted or CI-driven recovery, pass `--yes` (or `-y`) to skip the interactive "are you done?" prompt; the flag is the explicit opt-in for non-interactive use once the manual merge is complete.
|
|
241
|
-
|
|
242
|
-
<details>
|
|
243
|
-
<summary>Patch manifest format</summary>
|
|
244
|
-
|
|
245
|
-
`patches/patches.json` is updated automatically by `export` and `re-export`:
|
|
246
|
-
|
|
247
|
-
```json
|
|
248
|
-
{
|
|
249
|
-
"version": 1,
|
|
250
|
-
"patches": [
|
|
251
|
-
{
|
|
252
|
-
"filename": "001-branding-custom-logo.patch",
|
|
253
|
-
"order": 1,
|
|
254
|
-
"category": "branding",
|
|
255
|
-
"name": "custom-logo",
|
|
256
|
-
"description": "Replaces default Firefox branding with custom logo",
|
|
257
|
-
"createdAt": "2025-01-15T10:30:00Z",
|
|
258
|
-
"sourceEsrVersion": "140.9.0esr",
|
|
259
|
-
"filesAffected": [
|
|
260
|
-
"browser/branding/official/logo.png",
|
|
261
|
-
"browser/themes/custom-shared/tokens.css"
|
|
262
|
-
],
|
|
263
|
-
"lintIgnore": ["large-patch-lines", "large-patch-files"],
|
|
264
|
-
"tier": "branding"
|
|
265
|
-
}
|
|
266
|
-
]
|
|
267
|
-
}
|
|
268
|
-
```
|
|
269
|
-
|
|
270
|
-
The optional `lintIgnore` field lists lint check IDs to suppress for that patch specifically. Useful for the class of patch that is advisory-noisy by nature — a cohesive branding bundle, a localised-resource pack, an auto-generated manifest — where `--skip-lint` is too blunt and a per-line marker cannot exist (the `.patch` body is regenerated on every export). Threaded through `export`, `re-export`, `re-export --files`, and `lint --per-patch`. Unknown check IDs are a no-op.
|
|
271
|
-
|
|
272
|
-
Settable through the CLI in two places. `fireforge export --lint-ignore <check-id>` (repeatable) writes the field on creation; `fireforge re-export <name> --lint-ignore <check-id>` (repeatable, append/union semantics, de-duplicated) adds entries to an existing patch on the next refresh. For metadata-only edits that should **not** regenerate the `.patch` body — including the inverse `--remove` and `--clear` modes that re-export's append-only flag cannot express — use `fireforge patch lint-ignore <name> --add <id> | --remove <id> | --clear`.
|
|
273
|
-
|
|
274
|
-
The optional `tier` field (only `"branding"` recognised) forces the branding threshold tier for the `large-patch-files` and `large-patch-lines` rules regardless of what `filesAffected` looks like. The automatic branding-tier detection already fires when every file is under `browser/branding/` plus a narrow allowlist of branding-registration siblings (`browser/moz.configure`, `browser/confvars.sh`) — covering the canonical Firefox fork shape. Declare `tier: "branding"` only when the patch legitimately also touches a non-allowlisted sibling the auto-detector cannot reach (a fork-specific theme override under `browser/themes/<name>/`, a vendor-specific icon resource, etc.). Precedence is `test > branding > general`: a patch of all-tests always gets the more permissive test-tier thresholds even if it declares `tier: "branding"`. Unknown tier values are rejected at load time rather than silently stripped, so a typo surfaces as a loader error. Prefer `tier` over `lintIgnore: ["large-patch-lines"]` when the patch is legitimately branding-shaped — `tier` keeps the rule running at the correct thresholds (so the warning still surfaces if the patch crosses them); `lintIgnore` drops the rule entirely.
|
|
275
|
-
|
|
276
|
-
Settable via `fireforge export --tier branding` on creation, `fireforge re-export <name> --tier branding` on refresh, and `fireforge patch tier <name> --tier branding | --clear` for a metadata-only edit that does not rewrite the `.patch` body. The CLI rejects values other than `branding` up-front (matching the validator's strictness), and `re-export --tier` / `--lint-ignore` refuse `--all` because mass tier/ignore edits across a heterogeneous queue are virtually always footguns.
|
|
277
|
-
|
|
278
|
-
If the manifest drifts after an interrupted export or manual edits, `fireforge import` will stop rather then silently applying a stale stack. Use `fireforge doctor --repair-patches-manifest` to rebuild it from disk. Because the rebuild is deterministic, the result will always be consistent with what is actually on the filesystem.
|
|
279
|
-
|
|
280
|
-
</details>
|
|
281
|
-
|
|
282
|
-
<details>
|
|
283
|
-
<summary>Patch lint checks</summary>
|
|
284
|
-
|
|
285
|
-
`fireforge lint` runs automatically during export, export-all and re-export. Use `--skip-lint` to downgrade errors to warnings. Errors block the export; warnings are printed but do not block.
|
|
286
|
-
|
|
287
|
-
By default, a standalone `fireforge lint` (no arguments) lints the **aggregate** `git diff HEAD` — i.e. every applied patch summed — with tool-managed branding paths (`browser/branding/<binaryName>/`) excluded. A fresh-setup workspace carries a large generated branding diff that operators did not author directly, and letting it through tripped the patch-size and license-header rules on content that matches the `branding` bucket in `fireforge status`. When the exclusion fires the command prints a one-line note naming the excluded count so the filter is visible. On a repo where `fireforge import` or `fireforge rebase` has just applied the full queue, the patch-size rules (`large-patch-lines`, `large-patch-files`) fire against the sum, which reads as "my queue is broken" when it is really an artefact of aggregation. Use `fireforge lint --per-patch` to rescope the diff to each patch's own `filesAffected`, honouring the patch's own `lintIgnore`. Cross-patch rules (`duplicate-new-file-creation`, `forward-import`) still run once over the whole queue either way. Pass explicit file paths to narrow the scope further — explicit-path mode does lint branding files (the operator's explicit request wins over the branding exclusion); the three modes (aggregate, file-scoped, per-patch) are mutually exclusive. Warnings stay advisory by default; release gates that require a warning-clean queue should use `fireforge lint --per-patch --max-warnings 0`.
|
|
288
|
-
|
|
289
|
-
| Check | Scope | Severity |
|
|
290
|
-
| ------------------------------------ | ------------------------------------------------------------------------------------------------------- | ------------------------ |
|
|
291
|
-
| `missing-license-header` | New files (JS/CSS/FTL) | error |
|
|
292
|
-
| `relative-import` | JS/MJS files | error |
|
|
293
|
-
| `token-prefix-violation` | CSS files (with furnace) | error |
|
|
294
|
-
| `raw-color-value` | Introduced CSS color values (allowlist via `patchLint.rawColorAllowlist`) | error |
|
|
295
|
-
| `duplicate-new-file-creation` | Same path created by multiple patches | error |
|
|
296
|
-
| `forward-import` | Patch imports from a later-patch file | error |
|
|
297
|
-
| `missing-jsdoc` | Exports in patch-owned `.sys.mjs` | error |
|
|
298
|
-
| `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs` | error |
|
|
299
|
-
| `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs` | error |
|
|
300
|
-
| `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in `patchLint.checkJs`) | error |
|
|
301
|
-
| `missing-jsdoc-class-method` | Class-method exports in patch-owned `.sys.mjs` (opt-in) | configurable |
|
|
302
|
-
| `jsdoc-class-method-param-mismatch` | Class-method exports in patch-owned `.sys.mjs` (opt-in) | configurable |
|
|
303
|
-
| `jsdoc-class-method-missing-returns` | Class-method exports in patch-owned `.sys.mjs` (opt-in) | configurable |
|
|
304
|
-
| `test-needs-assertion` | Patch-introduced `browser_*.js` test files (opt-in) | configurable |
|
|
305
|
-
| `missing-modification-comment` | Modified upstream JS/MJS | warning |
|
|
306
|
-
| `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL); Mozilla MPL-2.0 `/* */` headers with wrapped lines are recognized | warning |
|
|
307
|
-
| `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test) | notice / warning / error |
|
|
308
|
-
| `observer-topic-naming` | Observer topics with binaryName | warning |
|
|
309
|
-
| `large-patch-files` | Patches affecting many files (tiered: >5 general, >5 test, >60 branding) | warning |
|
|
310
|
-
| `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test, 8000/18000/30000 branding) | notice / warning / error |
|
|
311
|
-
|
|
312
|
-
**JSDoc validation** uses AST-based analysis (Acorn) to validate exported APIs in patch-owned `.sys.mjs` files. A file is "patch-owned" if it was newly created by the current diff or by an existing patch in the queue. Functions must document every `@param` (names must match) and include `@returns` when the function returns a value. Exported constants and classes require a JSDoc block.
|
|
313
|
-
|
|
314
|
-
**Optional `checkJs` pass.** Enable a TypeScript-esque bastardization of type checking for patch-owned `.sys.mjs` files by adding `"patchLint": { "checkJs": true }` to `fireforge.json`. This uses the TypeScript compiler API with `allowJs + checkJs + noEmit`, scoped only to patch-owned files. Firefox globals (`Services`, `ChromeUtils`, `lazy`, etc.) are shimmed automatically. Module-resolution errors from Firefox's `resource://` and `chrome://` URL schemes are suppressed since TypeScript cannot follow these; the built-in shim also declares ambient `resource:*` and `chrome:*` modules so lazy `import("resource:-…")` / `import("chrome:-…")` stay loosely typed under `noResolve` instead of degrading into spurious `checkjs-type-error` cascades — refine further with `"patchLint": { "checkJsExtraShim": "..." }`. By default the pass uses a **loose** compiler preset (`strict: false`, `noImplicitAny: false`) so implicit `any` from untyped parameters does not flood the output. Set `"patchLint": { "checkJsStrict": true }` (requires `checkJs: true`) to enable `strict` and `noImplicitAny` for parity with strict whole-project checkJs — implicit-any parameters then surface as `checkjs-type-error`. Optional `"patchLint": { "checkJsCompilerOptions": { "strictNullChecks": false } }` (requires `checkJsStrict: true`) merges **allowlisted boolean** strict flags after that preset so forks can tighten `noImplicitAny` while relaxing e.g. null checks. The same built-in shim and the same eight suppressed diagnostic codes as `fireforge typecheck` apply; only explicit strictness differs. Projects that need to extend the built-in shim (e.g. for `MozLitElement`, `MozXULElement`, or fork-specific component bases) can point at an additional `.d.ts` via `"patchLint": { "checkJsExtraShim": "tools/types/<fork>-globals.d.ts" }`; the file is concatenated to the built-in shim — augment, don't redeclare.
|
|
315
|
-
|
|
316
|
-
**Whole-project type checking — `fireforge typecheck`.** `patchLint.checkJs` is patch-hygiene: scoped to patch-owned `.sys.mjs`, suppresses module-resolution noise, and runs every time `fireforge lint` runs. With `checkJsStrict`, that pass can match a strict jsconfig's `noImplicitAny` behaviour without replacing full-project resolution. `fireforge typecheck` remains the CI-grade complement: it runs whole projects you point at via `typecheck.projects` in `fireforge.json`, honours each jsconfig's strictness/include/exclude/`paths`, and is intended as a CI gate. The two are complementary; the recommended setup is `fireforge lint` on every patch export and `fireforge typecheck` on CI for the project-level baseline.
|
|
317
|
-
|
|
318
|
-
```jsonc
|
|
319
|
-
{
|
|
320
|
-
"typecheck": {
|
|
321
|
-
"projects": ["components/custom/jsconfig.json", "engine/browser/base/content/jsconfig.json"],
|
|
322
|
-
"extraShim": "tools/types/<fork>-globals.d.ts",
|
|
323
|
-
},
|
|
324
|
-
}
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
The command resolves each `projects` entry through TypeScript's own config parser (`readConfigFile` + `parseJsonConfigFileContent`), so `paths` mappings, `include`/`exclude` globs, and `lib` settings all behave the same as `tsc --noEmit -p <path>`. FireForge forces `noEmit: true` and defaults `allowJs`/`checkJs` to `true` only when the user has not set them — explicit `"checkJs": false` in a jsconfig is honoured (one notice, no diagnostics) so the IDE-noise opt-out remains an opt-out. The same `FIREFOX_GLOBALS_SHIM` and the same eight suppressed diagnostic codes (2304 / 2305 / 2306 / 2307 / 2552 / 2580 / 2792 / 7016 — module-resolution + global-name noise) apply, so a file that lints clean cannot fail typecheck for an inferable-only-from-source reason. Pass `--project <path>` for a one-off run against a single jsconfig (replaces the configured list, preserves `extraShim`). Exits non-zero on any error-severity diagnostic; warnings print but do not fail. TypeScript stays a dev-dependency — install it (`npm i -D typescript`) before running. The command does not honour `--since` or any patch-diff filter: it is whole-project by design.
|
|
328
|
-
|
|
329
|
-
**Optional `jsdocClassMethods` enforcement.** Set `"patchLint": { "jsdocClassMethods": "warning" | "error" }` in `fireforge.json` to extend JSDoc validation to class-method exports inside patch-owned `.sys.mjs` files. Every public method (instance and static), parameter-bearing constructor, getter, and setter must carry a leading JSDoc block; `@param` names must match the parameter list, and `@returns` is required when a method returns a value (getters and setters are exempt from `@returns`). Methods whose name starts with `_` or `#`, methods carrying `@private` or `@internal` in their JSDoc, and zero-parameter constructors are exempt. Defaults to `"off"`, so upgrading is a no-op until the knob is set.
|
|
330
|
-
|
|
331
|
-
**Optional `testAssertionFloor` enforcement.** Set `"patchLint": { "testAssertionFloor": "warning" | "error" }` to require that every `browser_*.js` test file introduced by the current patch contains at least one assertion (`Assert.*`, `ok()`, `is()`, `isnot()`, or `isDeeply()`). Smoke-only tests that load the script and exit without asserting any user-visible behavior are flagged as `test-needs-assertion`. Comment-only assertions do not count — comments are stripped before scanning. `head.js` and `head_*.js` test helpers are exempt; modified upstream tests are out of scope (V1 only flags newly-introduced files). Defaults to `"off"`.
|
|
332
|
-
|
|
333
|
-
The two cross-patch rules (`duplicate-new-file-creation` and `forward-import`) run over the whole patch queue rather than a single diff, catching ordering issues that only surface during `import`. Forward-import detection compares leaf filenames, so a false positive is theoretically possible when two patches create files with the same basename in different directories. Suppress with an inline `// fireforge-ignore: forward-import` comment on or above the import line. Both `forward-import` and `raw-color-value` support inline suppression comments (`// fireforge-ignore: forward-import` and `/* fireforge-ignore: raw-color-value */` respectively).
|
|
334
|
-
|
|
335
|
-
</details>
|
|
336
|
-
|
|
337
|
-
### Repairing a broken patch queue
|
|
338
|
-
|
|
339
|
-
When a patch queue drifts, e.g. due to overlapping new-file creations, forward imports, manifest desync, etc. start with diagnosing the root cause:
|
|
340
|
-
|
|
341
|
-
```bash
|
|
342
|
-
fireforge verify # fsck: manifest + cross-patch lint
|
|
343
|
-
fireforge lint # includes the same cross-patch rules
|
|
344
|
-
fireforge status --ownership # flat path → owning patch table
|
|
345
|
-
fireforge status --json # machine-readable classified object
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
`status --json` emits a versioned object: `{ "schemaVersion": 1, "summary": { "total": <n>, "byClassification": { ... } }, "files": [...] }`. Error paths also emit a JSON object with `schemaVersion`, `code`, and `error` before exiting non-zero, so scripts can parse both clean and failing runs.
|
|
349
|
-
|
|
350
|
-
Then fix with the appropriate primitive:
|
|
351
|
-
|
|
352
|
-
| Problem | Fix |
|
|
353
|
-
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
354
|
-
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files` |
|
|
355
|
-
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>` |
|
|
356
|
-
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>` |
|
|
357
|
-
| Ordinal gaps after deletes/splits | `fireforge patch compact` |
|
|
358
|
-
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>` |
|
|
359
|
-
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
|
|
360
|
-
| Dangling widget / locale registration in patch | Re-run `fireforge export` without `--exclude-furnace` to capture the source files, or revert furnace changes |
|
|
361
|
-
| Unmanaged changes you want to discard | `fireforge discard <file>` (also accepts a directory path to discard everything beneath it) or `fireforge reset` |
|
|
362
|
-
|
|
363
|
-
Every destructive command defaults to an interactive confirmation with a change summary. `--dry-run` previews without writing; `--yes` skips the prompt for CI; `--force-unsafe` bypasses structural refusals when you have context the linter cannot see. Do not hand-edit `patches.json` as the file is owned by FireForge — `doctor --repair-patches-manifest` reconstructs missing metadata, and `fireforge re-export <filename> --description "<text>"` overwrites recovered entries with operator-supplied metadata through the tool. `fireforge verify` cross-checks every registration hunk in each patch body against the files the queue and engine supply, so a patch that registers a widget / locale without carrying its source surfaces as a `dangling-registration` error rather than slipping through as "Verify clean"; `fireforge export-all --exclude-furnace` refuses up-front when it would produce that shape.
|
|
364
|
-
|
|
365
|
-
## Wiring Custom Code
|
|
366
|
-
|
|
367
|
-
```bash
|
|
368
|
-
# Wire a subscript with init/destroy lifecycle
|
|
369
|
-
fireforge wire my-widget --init "MyWidget.init()" --destroy "MyWidget.destroy()"
|
|
370
|
-
|
|
371
|
-
# Register a file in the correct build manifest (engine-relative or
|
|
372
|
-
# repo-root-relative — a leading `engine/` segment is stripped)
|
|
373
|
-
fireforge register browser/modules/mybrowser/MyStore.sys.mjs
|
|
374
|
-
fireforge register engine/browser/modules/mybrowser/MyStore.sys.mjs
|
|
375
|
-
|
|
376
|
-
# Both support --dry-run to preview changes
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
For the first module in a new `browser/modules/<binaryName>/` namespace, create `engine/browser/modules/<binaryName>/moz.build` before running `fireforge register`. The minimal manifest is:
|
|
380
|
-
|
|
381
|
-
```python
|
|
382
|
-
# SPDX-License-Identifier: MPL-2.0
|
|
383
|
-
|
|
384
|
-
EXTRA_JS_MODULES += []
|
|
385
|
-
```
|
|
386
|
-
|
|
387
|
-
Also make sure `browser/modules/moz.build` references the namespace directory with `DIRS += ["<binaryName>"]`; `register` adds the module entry inside the namespace manifest, not the parent `DIRS` entry.
|
|
388
|
-
|
|
389
|
-
<details>
|
|
390
|
-
<summary>Wire options</summary>
|
|
391
|
-
|
|
392
|
-
- **Subscript** (always): Adds `loadSubScript` call to `browser-main.js`
|
|
393
|
-
- **`--init <expr>`**: Adds init expression to `gBrowserInit.onLoad()` in `browser-init.js`
|
|
394
|
-
- **`--destroy <expr>`**: Adds destroy expression to `onUnload()` (LIFO ordering, which matters because destroy handlers that run in the wrong order can leave dangling references)
|
|
395
|
-
- **`--after <name>`**: Controls ordering between dependent subscripts
|
|
396
|
-
- **`--dom <file>`**: Inserts `#include` directive for `.inc.xhtml` into `browser.xhtml`
|
|
397
|
-
- **`--subscript-dir <dir>`**: Override the subscript directory (default: `browser/base/content`)
|
|
398
|
-
|
|
399
|
-
</details>
|
|
400
|
-
|
|
401
|
-
<details>
|
|
402
|
-
<summary>Supported register patterns</summary>
|
|
403
|
-
|
|
404
|
-
| File pattern | Manifest | Entry format |
|
|
405
|
-
| ------------------------------------------- | ------------------------------------- | ----------------------------------- |
|
|
406
|
-
| `browser/themes/shared/*.css` | `browser/themes/shared/jar.inc.mn` | `skin/classic/browser/{name}.css` |
|
|
407
|
-
| `browser/base/content/*.{js,mjs,xhtml,css}` | `browser/base/jar.mn` | `content/browser/{file}` |
|
|
408
|
-
| `browser/base/content/test/*/browser.toml` | `browser/base/moz.build` | `"content/test/{dir}/browser.toml"` |
|
|
409
|
-
| `browser/modules/mybrowser/*.sys.mjs` | `browser/modules/mybrowser/moz.build` | `"{name}.sys.mjs"` |
|
|
410
|
-
| `toolkit/content/widgets/*/*.{mjs,css}` | `toolkit/content/jar.mn` | `content/global/elements/{file}` |
|
|
411
|
-
|
|
412
|
-
</details>
|
|
413
|
-
|
|
414
|
-
## Furnace (UI Component System)
|
|
415
|
-
|
|
416
|
-
Furnace manages Firefox custom elements (`MozLitElement`) under `toolkit/content/widgets`. You can override existing components or create new ones. Changes feed into the same patch workflow as everything else, Furnace is not a separate persistence layer.
|
|
417
|
-
|
|
418
|
-
There are three component types:
|
|
419
|
-
|
|
420
|
-
| Type | What it is | Local files |
|
|
421
|
-
| ------------ | ------------------------------------------------------ | ------------------------------ |
|
|
422
|
-
| **Stock** | Engine components tracked for Storybook preview | None |
|
|
423
|
-
| **Override** | Forked copy: `css-only` (restyle) or `full` (JS + CSS) | `components/overrides/<name>/` |
|
|
424
|
-
| **Custom** | New element that does not exist in Firefox | `components/custom/<name>/` |
|
|
425
|
-
|
|
426
|
-
```bash
|
|
427
|
-
fireforge furnace scan # discover components in the engine
|
|
428
|
-
fireforge furnace override moz-button -t css-only # fork with CSS-only restyle
|
|
429
|
-
fireforge furnace create moz-my-widget # scaffold a new component
|
|
430
|
-
fireforge furnace chrome-doc create mybrowser # scaffold a top-level chrome document
|
|
431
|
-
fireforge furnace deploy # apply to engine/ + validate
|
|
432
|
-
fireforge furnace status # workspace vs engine drift
|
|
433
|
-
fireforge furnace diff moz-button # unified diff against baseline
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
`furnace scan` is read-only in non-interactive shells and reports which discovered widgets are already tracked. In interactive shells it can add selected discoveries to `furnace.json#stock`. `furnace create --compose <tag>` also consults the scan inventory: if `<tag>` is a discovered engine widget that is not yet in `stock`, create auto-adds it to `stock` in the same `furnace.json` write as the new custom component. Unknown compose targets still fail.
|
|
437
|
-
|
|
438
|
-
`furnace deploy` validates components before applying. As always, errors block, warnings are advisory. `fireforge build` and `fireforge test --build` run apply automatically — when apply wrote files during a build, the build prints a `Furnace: source → engine sync wrote N component(s) …` banner naming every component that was synced, so it is obvious whether engine/ was freshly updated. Use `fireforge doctor --repair-furnace` if the engine gets out of sync.
|
|
439
|
-
|
|
440
|
-
### Scaffolding top-level chrome documents
|
|
441
|
-
|
|
442
|
-
Custom elements live under `toolkit/content/widgets`, but a fork's top-level chrome document (`browser.xhtml` equivalents like `mybrowser.xhtml`, `about:*` panels, onboarding flows) lives at `browser/base/content/` and needs jar.mn, jar.inc.mn, and locales/jar.mn entries to reach the packaged bundle. `furnace chrome-doc create <name>` handles that boilerplate:
|
|
443
|
-
|
|
444
|
-
```bash
|
|
445
|
-
fireforge furnace chrome-doc create mybrowser # full chrome (titlebar + windowtype)
|
|
446
|
-
fireforge furnace chrome-doc create overlay --no-titlebar # frameless overlay
|
|
447
|
-
fireforge furnace chrome-doc create mybrowser --with-tests # + xpcshell packaging-verification test
|
|
448
|
-
fireforge furnace chrome-doc create mybrowser --dry-run # preview without writing
|
|
449
|
-
fireforge furnace chrome-doc remove mybrowser --dry-run # preview cleanup
|
|
450
|
-
fireforge furnace chrome-doc remove mybrowser --yes # remove files + registrations
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
The command writes:
|
|
454
|
-
|
|
455
|
-
- `engine/browser/base/content/<name>.xhtml` — XHTML shell. `data-l10n-id` is bound on the leaf `<title>` only (binding it on the root `<window>` would let Fluent's first-paint translation pass overwrite the entire body subtree, the standard `data-l10n-id`-on-non-leaf failure mode). The `<head>` loads `chrome://global/content/customElements.js` ahead of the per-doc subscript so any `<moz-*>` widget the author drops into the body resolves through the toolkit registry instead of silently degrading to `HTMLUnknownElement` — matches the `webrtcIndicator.xhtml` shape upstream uses for non-`browser.xhtml` chrome documents. Under `--with-titlebar` (the default) the root carries the `navigator:browser` minimum attribute set: `windowtype="navigator:browser"`, `customtitlebar="true"`, default `width="1024"` / `height="640"`, and `persist="screenX screenY width height sizemode"` so XULStore remembers geometry across restarts; without these a fork shipping the scaffold verbatim opens at the OS intrinsic minimum size on first launch and forgets the user's window position.
|
|
456
|
-
- `engine/browser/base/content/<name>.js` — startup-topic observer fired on first idle.
|
|
457
|
-
- `engine/browser/themes/shared/<name>-chrome.css` — scoped CSS. Under `--with-titlebar` the buttonbox container is a `-moz-window-dragging: drag` region and `.titlebar-buttonbox` opts into the platform-native `-moz-window-button-box` appearance so the OS renders traffic-light / minimize-maximize-close controls in their canonical positions; under `--no-titlebar` the macOS `.titlebar-button { display: none }` carve-out is emitted instead so frameless overlays don't inherit the platform window controls `global.css` applies by default.
|
|
458
|
-
- `engine/browser/locales/en-US/browser/<name>.ftl` — Fluent stub keyed on `<name>-window-title`.
|
|
459
|
-
- Appends the corresponding `jar.mn` / `jar.inc.mn` entries. The locales/jar.mn append is suppressed when the fork's existing `engine/browser/locales/jar.mn` already carries a `[localization] (%browser/**/*.ftl)` (or `(%browser/*.ftl)`) wildcard that would already pick up the scaffolded FTL — on those forks a per-file `locale/<name>.ftl` entry would be dead weight at best and an outright build break when the fork has dropped the `% locale browser …` registration the per-file entry depends on. Forks still on the legacy registration get the per-file entry as before.
|
|
460
|
-
- When `--with-tests` is set, also scaffolds an xpcshell test + `xpcshell.toml` under `engine/browser/base/content/test/<binary>-xpcshell/<name>/` that probes the packaged app directory (`Services.dirsvc.get("XCurProcD")/chrome/browser/...`) directly rather than going through `chrome://` URI resolution — see "Platform module compatibility" and the xpcshell chrome-URI note further down for why direct filesystem probing is the reliable way to verify chrome-doc packaging. Registration in `XPCSHELL_TESTS_MANIFESTS` is left to the operator because the owning moz.build depends on the fork layout.
|
|
461
|
-
|
|
462
|
-
Writes are transactional: a SIGINT mid-scaffold rolls back every touched file. `--dry-run` validates the same paths and registrations without acquiring the mutation lock or writing. `furnace chrome-doc remove <name>` removes the scaffolded source files, jar registrations, and optional xpcshell packaging-test directory; use its `--dry-run` first when cleaning an experimental document. Requires an existing engine — run `fireforge download` first.
|
|
463
|
-
|
|
464
|
-
#### Platform module compatibility
|
|
465
|
-
|
|
466
|
-
A custom chrome document with `windowtype="navigator:browser"` is treated as a main browser window by every upstream platform module that observes `browser-delayed-startup-finished` — `DevToolsStartup`, `PageActions`, `SessionStore`, `DownloadsButton`, Sync UI, and more. Those modules walk INTO the window assuming `browser.xhtml`'s DOM (`<menu>` entries, `window.BrowserPageActions`, the `cmd_*` command set, the tabbrowser, …) and throw a `TypeError` on anything else. The errors are non-fatal but noisy, and the matrix of "which modules walk in" grows with every Firefox release.
|
|
467
|
-
|
|
468
|
-
Every `furnace chrome-doc create`-scaffolded root element now carries a `data-furnace-chrome-doc="<name>"` sentinel attribute. Fork-side patches to the offending platform modules can guard on this attribute cheaply:
|
|
469
|
-
|
|
470
|
-
```js
|
|
471
|
-
// DevToolsStartup.sys.mjs (fork patch)
|
|
472
|
-
observe(subject, topic) {
|
|
473
|
-
if (topic === "browser-delayed-startup-finished") {
|
|
474
|
-
const win = subject.QueryInterface(Ci.nsIDOMWindow);
|
|
475
|
-
if (win.document.documentElement.hasAttribute("data-furnace-chrome-doc")) {
|
|
476
|
-
return; // fork's custom chrome doc — skip DevTools menubar wiring
|
|
477
|
-
}
|
|
478
|
-
// ... upstream body ...
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
The sentinel is fork-neutral (the attribute name is stable across projects) so a fork upgrading from one FireForge version to the next does not have to rewrite every guard. The name carried in the attribute value distinguishes multiple chrome docs in the same fork when a patch needs finer-grained routing.
|
|
484
|
-
|
|
485
|
-
#### Harness matrix gap
|
|
486
|
-
|
|
487
|
-
`furnace chrome-doc create` generates a top-level chrome document, but neither of the two existing test harnesses (`furnace create --test-style=mochikit`, `--test-style=browser-chrome`) covers it well: mochikit targets widgets loaded via `chrome://global/` (no tabbrowser, but also no chrome-doc-level interactive behaviors like titlebar drag / focus-ring / window sizing), and browser-chrome mochitest requires a working `tabbrowser` which a fork-authored chrome document that replaces `browser.xhtml` deliberately does not carry. Running a tabbrowser-less window through the mochikit harness crashes the harness itself (on `URILoadingHelper.openLinkIn`), not the chrome doc.
|
|
488
|
-
|
|
489
|
-
The packaging-verification test that `--with-tests` scaffolds is what FireForge can offer cleanly from inside the current harness matrix: it asserts the packaged files landed, not that they behave correctly at runtime. Interactive assertions (dot-grid background painting, titlebar drag region, focus ring) are out of scope for this scaffold and require manual verification against a built browser until the upstream harness matrix catches up.
|
|
490
|
-
|
|
491
|
-
### Picking a test harness for `furnace create`
|
|
492
|
-
|
|
493
|
-
`furnace create --with-tests` defaults to a **browser-chrome** mochitest: `browser_<binary>_<tag>.js` under `engine/browser/base/content/test/<binary-name>/`, registered via `browser.toml` (and `browser/base/moz.build` when the scaffolder appends there). Use this when the component participates in real browser chrome (tabs, `gBrowser`, and similar).
|
|
494
|
-
|
|
495
|
-
**MochiKit** (`--test-style=mochikit`) is opt-in: it emits `test_<tag>.html` under `engine/toolkit/content/tests/widgets/`, loads the module via `chrome://global/`, and does not need a `tabbrowser` — so it is the right choice for bespoke chrome documents that omit the upstream tab strip. On **macOS**, that harness can hit a long idle timeout (see [Known upstream build issues](#known-upstream-build-issues)); prefer browser-chrome tests when your fork has a normal tabbed window.
|
|
496
|
-
|
|
497
|
-
Three styles are available via `--test-style`:
|
|
498
|
-
|
|
499
|
-
| Style | When to use |
|
|
500
|
-
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
501
|
-
| `browser-chrome` | **Default** with `--with-tests`. Interactive chrome, tab strip, `gBrowser`. Requires a working `tabbrowser`. Emits `browser_<bin>_<tag>.js` and registers via `browser.toml` / `browser/base/moz.build`. |
|
|
502
|
-
| `mochikit` | Pure-UI custom elements on forks **without** a `tabbrowser`. Emits `test_<tag>.html` under `toolkit/content/tests/widgets/`. May be unreliable on macOS (mochitest-chrome / single-process). |
|
|
503
|
-
| `xpcshell` | Storage-layer, observer-driven, or ESM-loading code. Headless, no tabbrowser. Emits `test_<name>_module_loads.js` + `xpcshell.toml` (registration in `XPCSHELL_TESTS_MANIFESTS` is left to the operator). |
|
|
504
|
-
|
|
505
|
-
`--xpcshell` is preserved as an alias for `--test-style=xpcshell`; conflicting flag combinations (`--xpcshell --test-style=mochikit`) are rejected.
|
|
506
|
-
|
|
507
|
-
`furnace create --dry-run` previews the planned file set, test scaffold, and `furnace.json` mutation without writing anything. Every validation the real command runs (tag-name shape, name conflicts, engine pre-existence of the component, `--compose` target existence + cycle detection) fires BEFORE the plan is emitted, so a failed preview matches a failed real run.
|
|
508
|
-
|
|
509
|
-
`furnace create`, `furnace remove`, and `furnace rename` re-read `furnace.json` inside the mutation lock before writing, so concurrent component edits preserve sibling entries instead of writing back a stale outer snapshot. `furnace refresh --all` continues past per-component refresh failures, reports the failed count, and exits non-zero with the failed override names after finishing the rest of the selection.
|
|
510
|
-
|
|
511
|
-
`furnace preview` starts Firefox's upstream Storybook workspace. On the first run, `mach storybook` may install roughly a thousand npm packages under `engine/browser/components/storybook/` and may print npm audit counts from Storybook's transitive dependencies. FireForge frames that output as upstream Storybook dependency state; it does not mean FireForge's own package dependencies were installed into the project.
|
|
512
|
-
|
|
513
|
-
## Additional Commands
|
|
514
|
-
|
|
515
|
-
The commands below cover project configuration, patch queue management, build packaging and development utilities. Run `fireforge <command> --help` for full option details.
|
|
516
|
-
|
|
517
|
-
### Configuration
|
|
518
|
-
|
|
519
|
-
```bash
|
|
520
|
-
# Read a config value
|
|
521
|
-
fireforge config firefox.version
|
|
522
|
-
|
|
523
|
-
# Set a config value
|
|
524
|
-
fireforge config firefox.version 145.0.0esr
|
|
525
|
-
|
|
526
|
-
# Pin the resolved Firefox source archive checksum
|
|
527
|
-
fireforge config firefox.sha256 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
|
|
528
|
-
|
|
529
|
-
# Set a value at a non-standard path (requires --force)
|
|
530
|
-
fireforge config customKey "value" --force
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
Writes are serialised behind a sidecar lock — two concurrent `fireforge config` invocations against the same `fireforge.json` (for example, parallel automation steps) queue instead of racing the read-modify-write. The lock is released automatically on process exit; stale locks from a crashed earlier command are reclaimed on the next invocation via the PID-alive probe.
|
|
534
|
-
|
|
535
|
-
Re-setting a key to its current value is a no-op: `fireforge.json` is not rewritten, key ordering is preserved, and the success log surfaces `<key> = <value> (unchanged)` instead of a fresh `Set …` line. This means automation that idempotently runs `fireforge config <key> <value>` no longer produces spurious diffs in `fireforge.json`.
|
|
536
|
-
|
|
537
|
-
`firefox.sha256` is optional. When set, it must be a 64-character hex SHA-256 for the Firefox source archive resolved from `firefox.product` + `firefox.version`; cached archives and fresh downloads are verified against it before extraction. Omit it to keep the default cache-integrity-only behaviour.
|
|
538
|
-
|
|
539
|
-
### Patch queue management
|
|
540
|
-
|
|
541
|
-
```bash
|
|
542
|
-
# Delete a patch from the queue
|
|
543
|
-
fireforge patch delete 003-ui-sidebar-tweaks.patch
|
|
544
|
-
|
|
545
|
-
# Reorder a patch to a new position
|
|
546
|
-
fireforge patch reorder 003-ui-sidebar-tweaks.patch --to 1
|
|
547
|
-
|
|
548
|
-
# Move a patch before or after another
|
|
549
|
-
fireforge patch reorder 003-ui-sidebar.patch --before 001-branding-logo.patch
|
|
550
|
-
|
|
551
|
-
# Close ordinal gaps after deletes or splits (e.g. 1, 3, 7 → 1, 2, 3)
|
|
552
|
-
fireforge patch compact
|
|
553
|
-
|
|
554
|
-
# Set or clear the threshold-tier override on a single patch
|
|
555
|
-
# (no .patch body rewrite — manifest entry only)
|
|
556
|
-
fireforge patch tier 001-branding-assets.patch --tier branding
|
|
557
|
-
fireforge patch tier 001-branding-assets.patch --clear
|
|
558
|
-
|
|
559
|
-
# Edit the lintIgnore list on a single patch (one mode per invocation)
|
|
560
|
-
fireforge patch lint-ignore 001-branding-assets.patch --add large-patch-lines --add large-patch-files
|
|
561
|
-
fireforge patch lint-ignore 001-branding-assets.patch --remove large-patch-lines
|
|
562
|
-
fireforge patch lint-ignore 001-branding-assets.patch --clear
|
|
563
|
-
```
|
|
564
|
-
|
|
565
|
-
All `patch <verb>` subcommands accept three identifier forms for their target: the ordinal number (`fireforge patch reorder 3 --to 1`), the full filename (`003-ui-sidebar-tweaks.patch`), or the manifest `name` handle the patch was exported with (`ui-sidebar-tweaks`). Anchors passed to `--before` / `--after` accept the same three forms. All subcommands support `--dry-run` and `--yes`.
|
|
566
|
-
|
|
567
|
-
`patch tier` and `patch lint-ignore` are metadata-only edits: they update `patches/patches.json` under the patch directory lock and never rewrite the `.patch` body. Reach for them when an operator-visible advisory (e.g. `large-patch-files` firing on a 56-file fresh-fork branding bundle) needs the threshold-tier override or a per-patch lint suppression but the patch body is already correct — running `re-export` for that case wastes an engine read + diff regeneration. `patch lint-ignore` modes (`--add` / `--remove` / `--clear`) are mutually exclusive; `patch tier` rejects `--tier` and `--clear` together. The history log records each invocation under `operation: "patch-tier"` / `"patch-lint-ignore"` for audit.
|
|
568
|
-
|
|
569
|
-
### Additional workflow commands
|
|
570
|
-
|
|
571
|
-
```bash
|
|
572
|
-
# Package the built browser for distribution
|
|
573
|
-
fireforge package
|
|
574
|
-
|
|
575
|
-
# Watch for file changes and auto-rebuild
|
|
576
|
-
fireforge watch
|
|
577
|
-
|
|
578
|
-
# Add a CSS design token (requires `fireforge furnace init` first; see the Furnace/Tokens section below)
|
|
579
|
-
# The `--` separator is required because the token name itself starts with `--`,
|
|
580
|
-
# which Commander would otherwise read as an option flag. Bare names without `--`
|
|
581
|
-
# are accepted directly and get the configured `tokenPrefix` prepended.
|
|
582
|
-
fireforge token add --category 'Colors — General' --mode static -- --my-color 'light-dark(#fff, #000)'
|
|
583
|
-
fireforge token add --category 'Colors — General' --mode static my-color '#fff' # bare-name form
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
Tokens live in the Furnace-managed tokens CSS file (`engine/browser/themes/shared/<binaryName>-tokens.css`), scaffolded by `fireforge furnace init` alongside `furnace.json`. The scaffold seeds a default set of categories (`Colors — General`, `Colors — Canvas`, `Colors — Experiment`, `Spacing`); add a category by hand as a `/* = My Category = */` comment inside the `:root { … }` block if you need another. `fireforge furnace init` does three more things in the same step so the file is owned end-to-end by tooling: it registers the tokens CSS path in `patchLint.rawColorAllowlist` (so raw color literals inside it are not flagged by `fireforge lint`); it adds the matching `skin/classic/browser/<binaryName>-tokens.css (../shared/<binaryName>-tokens.css)` entry to `browser/themes/shared/jar.inc.mn` (so `fireforge status` does not flag the file as unmanaged or unregistered); and it derives `tokenPrefix: --<binaryName>-` from `fireforge.json`'s `binaryName` so `fireforge token coverage` has a prefix to key off on the very first run. Projects that prefer a different prefix can override it in `furnace.json` after init. When only the tokens CSS file is dirty or untracked, `fireforge token coverage` validates it as a token source file and does not count its expected literal token values as raw-color coverage failures.
|
|
587
|
-
|
|
588
|
-
### Diff-scoped lint (`lint --since`)
|
|
589
|
-
|
|
590
|
-
`fireforge lint --since <git-rev>` tags each issue as `[introduced]` or `[cumulative]` based on whether its file changed since `<git-rev>`:
|
|
591
|
-
|
|
592
|
-
```bash
|
|
593
|
-
fireforge lint --since HEAD~1 # just the current commit
|
|
594
|
-
fireforge lint --since main # everything since main
|
|
595
|
-
fireforge lint --since abc1234 # since a specific SHA
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
The summary line splits counts — e.g. `Lint: 2 introduced error(s), 0 introduced warning(s); 5 cumulative error(s), 1 cumulative warning(s)` — so triage of "did my diff introduce any of these?" is a one-glance check on a large patch series. Exit code still fails on any error (introduced or cumulative) unless `--only-introduced` is set; without `--since`, output is unchanged.
|
|
599
|
-
|
|
600
|
-
Pass `--only-introduced` together with `--since` to scope the exit code to issues the current diff introduced. Cumulative pre-existing errors still print, but do not fail lint — useful in CI when a branch's own diff is clean but the repo already carries unrelated errors from older patches:
|
|
601
|
-
|
|
602
|
-
```bash
|
|
603
|
-
fireforge lint --since main --only-introduced
|
|
604
37
|
```
|
|
605
38
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
Aggregate patch-size findings (`large-patch-files`, `large-patch-lines`) describe the whole diff rather than a single file. Under `--since` + a non-empty diff they tag `[introduced]` (the aggregate IS the introduced work); with an empty diff they tag `[cumulative]` (the finding describes drift accumulated across earlier commits).
|
|
609
|
-
|
|
610
|
-
### Post-build audit and auto-configure
|
|
611
|
-
|
|
612
|
-
`fireforge build` is a transactional step: after a successful mach build it audits the dist bundle against engine-relative paths touched since the last successful build, and warns per file that is packageable-by-convention (`.js`/`.mjs`/`.css`/`.ftl`/`.xhtml`/`app/profile/…`) but has no matching artifact or whose dist mtime is older than the source. Ends every build with a `Packaged: N updated, M stale, K missing, S skipped` summary. The audit is warn-only — it never fails a build that mach reported green.
|
|
613
|
-
|
|
614
|
-
`fireforge build --ui` is intentionally a fast rebuild path, not a bootstrap path. It now refuses before invoking `mach build faster` unless the current objdir has a completed launchable bundle; fresh imports and partial builds should run a full `fireforge build` first.
|
|
615
|
-
|
|
616
|
-
The audit applies seven routing rules to suppress false positives that previously trained operators to ignore its warnings:
|
|
617
|
-
|
|
618
|
-
- **jar.mn registrations are authoritative.** When the source under audit is claimed by a `(source)` reference in an ancestor `jar.mn`, the audit walks the registration to compute the expected target path (e.g. `content/browser/mybrowser.js`) and probes the dist tree for a candidate whose absolute path ends with that suffix. Picking the correct artifact from a same-basename collision no longer depends on path-similarity scoring. If the registration target is missing from dist, the warning names the `jar.mn` entry so "registration is intact, packaging dropped the file" is distinguishable from "source is unregistered". This is the fix for the class of false positive where `engine/browser/base/content/<name>.js` (registered in `browser/base/jar.mn`) collided with an unrelated `browser/defaults/preferences/<name>.js` added by a separate patch; the heuristic could not distinguish them, so the audit falsely reported the correctly-packaged chrome resource as missing.
|
|
619
|
-
- **Build inputs are excluded.** `jar.mn`, `moz.build`, `moz.configure`, `Makefile.in`, and `mozbuild.in` are consumed by the build to produce chrome registrations / make targets but never themselves ship. They are skipped before the dist lookup, so editing them no longer fires a "missing packaged artifact" warning.
|
|
620
|
-
- **Same-basename collisions in `dist/` are disambiguated by trailing-segment overlap.** A branding override at `engine/browser/branding/<name>/content/aboutDialog.css` ships at `chrome/<area>/content/branding/aboutDialog.css`. A naive basename match would tie that against the unrelated upstream `chrome/<area>/content/browser/aboutDialog.css`; the audit now scores candidates by trailing path-segment match plus a small bonus for non-generic source segments (`branding`, the branding directory name) appearing in the candidate path, so re-rooted artifacts win over coincidentally-named ones. Applies only to sources that are not registered in jar.mn (registration-aware lookup runs first).
|
|
621
|
-
- **Unrelated same-basename hits never surface as "stale".** When the best-scoring candidate shares only the basename with the source and no meaningful intermediate segment (common on sparsely-populated `_tests/` trees where an upstream helper like `head.js` is the only same-basename file left from a prior build), the audit classifies the file as `missing` rather than emitting a misleading stale-comparison warning against the unrelated candidate. The warning enumerates every same-basename hit so the operator can see the full set of confounders at a glance — not just the scorer's pick.
|
|
622
|
-
- **Test sources are looked up under `_tests/`, not `dist/`.** Anything under `/test(s)/` directories, plus `browser_*.js` / `test_*.js` / `xpcshell.toml` / `browser.ini`, is resolved against the `_tests/` tree under the active `obj-*` directory. Mochitest and xpcshell harnesses copy registered tests there, never into the packaged bundle. Misses still warn — but they point at `_tests/`, directing the operator to `BROWSER_CHROME_MANIFESTS` / `XPCSHELL_TESTS_MANIFESTS` instead of `package-manifest.in`.
|
|
623
|
-
- **Test-path audits are gated on `_tests/all-tests.json`.** Plain `mach build` populates a partial `_tests/` subtree and stops — full test packaging only runs under `mach package-tests` / `mach test <target>` (or `fireforge test <name>`). The audit now checks for the `all-tests.json` marker written by the packaged-tests make target and silently skips test-path sources when the marker is absent, so every registered mochitest / xpcshell source no longer false-flags as "missing" on the common build-only path. Run `cd engine && ./mach package-tests` (or a scoped `fireforge test`) after a build to green-check test registrations.
|
|
624
|
-
- **Files inside an `if CONFIG[…]:` block in their owning `moz.build` are skipped on hosts where the gate is off.** Windows-only stubinstaller CSS on a macOS build, Darwin-only artwork on Linux, etc. The detection walks up to the closest `moz.build`, scans for the basename inside a Python-style indented `if CONFIG[…]:` block, and matches the gate against the host platform. Negation expressions are conservatively NOT treated as single-OS gates so a warning is never wrongly suppressed for a file that should ship on the current host. Subtrees packaged through platform-specific `Makefile.in` recipes that live outside the `moz.build` graph — `/stubinstaller/` (NSIS), `browser/installer/windows/`, `browser/installer/macosx/`, `browser/installer/linux/` — are also gated by path convention so branding stubinstaller CSS no longer warns on every non-Windows build.
|
|
625
|
-
|
|
626
|
-
The build also auto-runs `mach configure` before the mach build step when any `moz.build`, `moz.configure`, or `Makefile.in` changed since the last successful build. Prevents incremental builds from silently skipping work against a stale recursive-make backend. Emits a `Backend config changed; running mach configure first...` banner when it fires.
|
|
627
|
-
|
|
628
|
-
Mach build failures with known-cryptic mozbuild errors now print actionable hints. Example: a `JS_PREFERENCE_PP_FILES` entry with no `#filter` / `#expand` directives now prints `Hint: ...use JS_PREFERENCE_FILES instead, or add at least one #filter / #expand directive to the file.` alongside the raw mach traceback.
|
|
629
|
-
|
|
630
|
-
When mach prints a post-build `config.status is out of date …` or `Config object not found by mach. / Configure complete!` banner on a successful build, FireForge surfaces a one-line annotation immediately before its own `Build completed in Xm Ys!` outro explaining that the banner is a known side effect of tool-managed branding edits applied before the build and that the build does not need to be re-run. The FireForge exit code remains authoritative regardless of the mach guard text.
|
|
631
|
-
|
|
632
|
-
The reported `Build completed in Xm Ys!` duration is wall-clock measured with `Date.now()`, so it includes any time the host spent suspended (laptop sleep, system idle) during the build. Treat it as wall-clock-with-sleep, not active CPU time, when comparing builds across machines or sessions.
|
|
633
|
-
|
|
634
|
-
### Relocated workspaces: `fireforge build --rewrite-mozinfo`
|
|
635
|
-
|
|
636
|
-
When a workspace is moved to a new path (e.g. the project directory was renamed or relocated on disk), `obj-*/mozinfo.json` still records the old `topsrcdir` / `topobjdir`. The pre-flight detects the mismatch and aborts with a "delete and rebuild" instruction — correct but expensive; a fresh clean build typically runs ~20 minutes and discards ~14 GB of intact obj artefacts on a moved checkout.
|
|
637
|
-
|
|
638
|
-
`fireforge build --rewrite-mozinfo` offers a shortcut for the pure path-relocation case. The rewriter patches `topsrcdir` / `topobjdir` / `mozconfig` inside `mozinfo.json` to match the current checkout, then runs `mach configure` so the recursive-make backend regenerates against the corrected paths. No obj-\* scrubbing, no fresh compile.
|
|
639
|
-
|
|
640
|
-
The rewriter refuses any change it cannot prove safe:
|
|
641
|
-
|
|
642
|
-
- `mozinfo.json` must record both `topsrcdir` and `topobjdir`.
|
|
643
|
-
- `topobjdir` must resolve to `<topsrcdir>/<objDir>` — out-of-tree builds are rejected.
|
|
644
|
-
- The detected `obj-*` directory name must match the one recorded in mozinfo — if the objdir name itself changed, the configure shape changed and a full rebuild is required.
|
|
645
|
-
- `mozinfo.json` must be valid JSON describing an object.
|
|
646
|
-
|
|
647
|
-
On any refusal the command falls back to the original clean-rebuild guidance with the refusal reason appended, so an unsafe relocation is never silently misrepaired.
|
|
648
|
-
|
|
649
|
-
## Configuration
|
|
650
|
-
|
|
651
|
-
`fireforge.json` at your project root:
|
|
652
|
-
|
|
653
|
-
```json
|
|
654
|
-
{
|
|
655
|
-
"name": "MyBrowser",
|
|
656
|
-
"vendor": "My Company",
|
|
657
|
-
"appId": "org.example.mybrowser",
|
|
658
|
-
"binaryName": "mybrowser",
|
|
659
|
-
"license": "EUPL-1.2",
|
|
660
|
-
"firefox": {
|
|
661
|
-
"version": "140.9.0esr",
|
|
662
|
-
"product": "firefox-esr"
|
|
663
|
-
},
|
|
664
|
-
"build": { "jobs": 8 },
|
|
665
|
-
"wire": { "subscriptDir": "browser/components/mybrowser" },
|
|
666
|
-
"patchLint": {
|
|
667
|
-
"checkJs": true,
|
|
668
|
-
"rawColorAllowlist": ["mybrowser-tokens.css"]
|
|
669
|
-
},
|
|
670
|
-
"markerComment": "MYBROWSER"
|
|
671
|
-
}
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
**`markerComment`** (optional). Appended as a ` // <marker>:` suffix to every line FireForge writes into upstream Firefox source files (starting with `customElements.js`). Keeps fork modifications discoverable and makes re-apply idempotent without hand-tagging entries after each `furnace apply`. Reject list: empty strings, leading/trailing whitespace, newlines, `*/` (would close an enclosing block comment), control characters.
|
|
675
|
-
|
|
676
|
-
**`furnace.json.tokenHostDocuments`** (optional). List of chrome XHTML documents the `missing-token-link` validator scans for the tokens CSS link. Forks with a second chrome host (e.g. `mybrowser.xhtml` alongside `browser.xhtml`) should list every document that may own the link — the rule fires only when NONE of them link the tokens CSS. Defaults to `["browser/base/content/browser.xhtml"]` when omitted. `fireforge doctor`'s engine-paths probe reads the same field when confirming the chrome document exists on disk, and `fireforge wire --dom` uses the first entry as the default target for its `#include` directive (override per-invocation with `--target <path>`). Forks that fully replaced `browser.xhtml` with a custom top-level chrome document configure this field once and both checks agree.
|
|
677
|
-
|
|
678
|
-
### `furnace create --localized` for `MozLitElement`
|
|
679
|
-
|
|
680
|
-
`fireforge furnace create <tag> --localized` scaffolds a Fluent-ready component. The generated `.mjs` uses the Mozilla-idiomatic `MozLitElement` pattern: a module-level `window.MozXULElement?.insertFTLIfNeeded("<chrome-uri>")` plus `this.ownerDocument.l10n?.connectRoot(this.shadowRoot)` / `disconnectRoot` in `connectedCallback` / `disconnectedCallback`. The chrome URI derives from `furnace.json.ftlBasePath` (default `toolkit/locales/en-US/toolkit/global` → `toolkit/global/<tag>.ftl`). `furnace apply` registers the `.ftl` in the matching locale jar.mn (default `toolkit/locales/jar.mn`) so the chrome URI resolves at runtime. If the locale jar.mn is missing in your fork (non-standard tree), apply surfaces a structured step error instead of aborting — the `.mjs`/`.css` still ship.
|
|
681
|
-
|
|
682
|
-
### `fireforge test --doctor`
|
|
39
|
+
Then initialise and build:
|
|
683
40
|
|
|
684
41
|
```bash
|
|
685
|
-
|
|
686
|
-
fireforge
|
|
687
|
-
fireforge
|
|
42
|
+
npx fireforge setup
|
|
43
|
+
npx fireforge download
|
|
44
|
+
npx fireforge bootstrap
|
|
45
|
+
npx fireforge import
|
|
46
|
+
npx fireforge build
|
|
47
|
+
npx fireforge run
|
|
688
48
|
```
|
|
689
49
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
The probe is a cascade of six layered checks — engine-present → mach-available → python-available → profile-creatable → browser-spawns → marionette-handshake. Each failure is tagged `[layer N/6: <name>]` so the first broken layer is surfaced immediately instead of the whole cascade blocking on the final socket poll. When the browser binary crashes at startup (missing dylib, wrong CPU arch, corrupt profile) the cascade fails at layer 5 within the settle window, not after the full socket timeout.
|
|
693
|
-
|
|
694
|
-
### Runtime CSS variables in Furnace
|
|
695
|
-
|
|
696
|
-
Design tokens imported from the fork's palette are enforced by `tokenPrefix`, but some components write and read CSS custom properties as runtime state channels (`--cam-x` per frame, `--tile-z` from a hit-test observer). Two escape hatches exist:
|
|
697
|
-
|
|
698
|
-
- **Auto-exempt** — a variable that is both declared (`--foo: 0;`) and consumed (`var(--foo)`) inside the same component's CSS file is recognised as a component-local runtime channel. No config entry required.
|
|
699
|
-
- **`furnace.json.runtimeVariables`** — explicit allowlist for names that are _written_ in JS and _read_ in a different file's CSS (cross-component runtime channels that the CSS-only auto-exempt cannot see). Entries must start with `--`.
|
|
700
|
-
|
|
701
|
-
Both rules compose with the existing `tokenPrefix` / `tokenAllowlist` checks and apply to both component validation and patch-stack lint.
|
|
702
|
-
|
|
703
|
-
### Platform variables in Furnace (token coverage)
|
|
50
|
+
After setup you should have a `fireforge.json`, an `engine/` directory containing Firefox source and a `patches/` directory containing the patch manifest. From there, the usual loop is to change code inside the `engine/` directory, export these changes, then verify the queue still applies cleanly.
|
|
704
51
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
```json
|
|
708
|
-
{
|
|
709
|
-
"platformPrefixes": ["--moz-", "--in-content-"]
|
|
710
|
-
}
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
Set this in `furnace.json` to extend the list (forks with additional platform prefixes) or pass an empty array to restore the pre-0.18 strict contract where `--moz-*` counted as unknown. Allowlisted usages are reported separately and are NOT counted toward the coverage denominator, so a 100% fork-owned project with a copied upstream baseline reads as `100%` instead of `1%`.
|
|
714
|
-
|
|
715
|
-
### Test harness options
|
|
716
|
-
|
|
717
|
-
`fireforge furnace create --with-tests` scaffolds a **browser-chrome mochitest** by default. Use this when the component renders UI that depends on the tab strip (`openLinkIn` → `URILoadingHelper`, `gBrowser`, etc.). On **macOS**, avoid relying on **`--test-style=mochikit`** (toolkit `test_*.html` under `toolkit/content/tests/widgets/`) for primary chrome/widget coverage: the **mochitest-chrome** flavor runs **single-process** (`e10s: false`) and has been observed to **idle ~370s** with no subtests (headless or headed compositing / SWGL). Prefer browser-chrome tests (multi-process); for forks that organize interactive tests under `engine/browser/modules/<fork>/test/`, extend that tree rather than adding new `test_<tag>.html` scaffolds when browser-chrome is sufficient.
|
|
718
|
-
|
|
719
|
-
`fireforge furnace create --xpcshell` scaffolds an **xpcshell test harness** instead. Use this when the component's code path is storage-only, observer-driven, or module-loading logic that does not touch a `tabbrowser`. xpcshell runs headless without browser chrome, so forks without an upstream tab strip can still cover these paths. The scaffolder writes `test_<name>_packaged.js` + `xpcshell.toml` into `engine/browser/base/content/test/<binary-name>-xpcshell/<component-name>/` and prints a note: registration in `XPCSHELL_TESTS_MANIFESTS` is the operator's call (the moz.build that should own the entry depends on where the component actually lives). `fireforge register <path>/xpcshell.toml` surfaces the same guidance when run directly rather than silently routing to a browser.toml-shaped advice.
|
|
720
|
-
|
|
721
|
-
The scaffolded xpcshell test is a **packaging probe**, not a module-load test. Lit-based components import `chrome://global/content/vendor/lit.all.mjs`, which references `window` at module-load — xpcshell has no `window` global, so an earlier scaffold that used `ChromeUtils.importESModule` reliably failed with `ReferenceError: window is not defined` for every Lit-based fork component. Instead, the test reads `XCurProcD` (`Services.dirsvc.get("XCurProcD", Ci.nsIFile)`) and probes two candidate layouts per asset — `<AppDir>/chrome/global/elements/<name>.{mjs,css}` (unpacked `dist/bin/browser/`) and `<AppDir>/browser/chrome/global/elements/<name>.{mjs,css}` (macOS .app-bundle / some ESR layouts). Either match passes; only when both miss does the assertion fail, which is the actual "stale build / missing jar.mn entry" case. Functional UI assertions still belong in a browser-chrome mochitest (`--test-style=browser-chrome`); the scaffolded test carries an inline comment pointing to that path so the constraint is obvious before the operator extends it.
|
|
722
|
-
|
|
723
|
-
xpcshell has a chrome-URI boundary that is worth knowing before writing assertions: `chrome://global/*` (toolkit chrome) IS registered and resolvable from the harness, but `chrome://browser/*` (browser chrome) is NOT — even when `firefox-appdir = "browser"` is set in the xpcshell.toml, the manifest set xpcshell loads lags what the real browser loads, so `NetUtil.asyncFetch("chrome://browser/content/…")` can still fail with `NS_ERROR_FILE_NOT_FOUND` against an artifact that IS present in `obj-*/dist/`. Assertions that need browser chrome URIs belong in a browser-chrome mochitest (`furnace create --test-style=browser-chrome`).
|
|
724
|
-
|
|
725
|
-
If you pass **`--with-tests` and `--xpcshell` together**, FireForge resolves the harness to **xpcshell only** (`--xpcshell` takes precedence). To get the default browser-chrome mochitest as well, run a second `furnace create` with `--with-tests` only or add the browser test files manually.
|
|
726
|
-
|
|
727
|
-
### Mochitest stalls and `--marionette-port`
|
|
728
|
-
|
|
729
|
-
When you pass `--marionette-port <n>`, FireForge uses that port for the **stale-listener probe** (before `mach test` runs) and for **`--doctor`**, forwards **`--setpref=marionette.port=<n>`** to `mach test` so the **browser Marionette listener** binds that port, and forwards **`--marionette=127.0.0.1:<n>`** so the **mochitest harness client** connects to the same endpoint (the client otherwise defaults to `127.0.0.1:2828`). If you already pass **`--mach-arg=--marionette=...`**, FireForge does not add a second client flag. If **`--mach-arg`** already forwards the port via **`--marionette-port=...`** or **`--setpref=marionette.port=...`**, FireForge skips duplicating the setpref (existing behaviour). The exception is an explicit **`--mach-arg --flavor=xpcshell`** (or **`--flavor=xpcshell-tests`**): that harness does not use the browser Marionette path, so FireForge skips both auto-forwarded flags and logs a short notice instead. Toolkit widget mochitests under `toolkit/content/tests/` (for example `test_*.html` next to `browser_*.js` suites) therefore stay aligned with the probe without duplicating **`--mach-arg=--setpref=marionette.port=…`**.
|
|
730
|
-
|
|
731
|
-
### Stale-build preflight on `fireforge test`
|
|
732
|
-
|
|
733
|
-
`fireforge test <path>` (without `--build`) now runs a preflight that diffs engine HEAD and the workdir against the last successful `fireforge build` (recorded at `.fireforge/last-build.json`). When packageable engine files have changed since that baseline, the command prints a single up-front warning naming the paths and pointing at `fireforge test --build`. This catches the class of failure where a newly scaffolded chrome resource or pref file is registered correctly but `obj-*/dist/` still holds the pre-edit bundle, so the test reads stale packaged artifacts and errors out with a cryptic `NS_ERROR_FILE_NOT_FOUND` inside xpcshell / mach test. The preflight is warn-only — a fork that rebuilt out-of-band (direct `./mach build`, IDE plugin, separate CI stage) is not blocked. Passing `--build` skips the preflight because the rebuild just refreshed the bundle.
|
|
734
|
-
|
|
735
|
-
### xpcshell appdir auto-injection on rebranded forks
|
|
736
|
-
|
|
737
|
-
`fireforge test` auto-resolves and injects `--app-path=<absolute>` into the underlying `mach test` invocation when the nearest `xpcshell.toml` sets `firefox-appdir = "browser"` and the active build's `appname` is anything other than `firefox`. Without this, every `resource:///modules/<name>.sys.mjs` import inside the harness throws `Failed to load resource:///modules/…` because the upstream xpcshell harness reads the appdir override under the appname-keyed manifest field (`<appname>-appdir`) — the literal `firefox-appdir = "browser"` directive is silently ignored on rebranded forks, `appPath` falls back to `xrePath`, and `resource:///` resolves one level above the real app root. The resolver walks each test path to its nearest manifest, reads `mozinfo.json` for the active appname, prefers any `<appname>-appdir` already in the manifest, and otherwise probes the platform-appropriate layout for the absolute target: on macOS, `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` is preferred over `<objDir>/dist/bin/<value>` (the `dist/bin` symlink on Darwin points at the binaries directory, not the Resources tree where modules live); on other platforms `dist/bin/<value>` is preferred and the macOS bundle layout is the fallback. Operator overrides via `--mach-arg=--app-path=…` always win and skip the resolver silently. Mismatches across multiple test paths and unresolvable manifest values surface as warnings rather than guesses, so triage reaches the underlying cause.
|
|
738
|
-
|
|
739
|
-
The durable fix is to add `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the manifest — the harness then reads the appname-keyed value directly without auto-injection. This is the most reliable fix on rebranded macOS builds where mach's xpcshell runner binds `-a` to the `.app/Contents/Resources` default and may not honour `--app-path` overrides. The xpcshell appdir hint that fires when the symptom persists despite injection lists this option first.
|
|
740
|
-
|
|
741
|
-
### Smoke-run mode (`fireforge run --smoke-exit`)
|
|
742
|
-
|
|
743
|
-
`fireforge run --smoke-exit <seconds>` launches the real built browser, streams the merged console line-by-line, sends `SIGTERM` to the entire child process group at the deadline, and exits non-zero when any `JavaScript error:` / `console.error:` / `[JavaScript Error]` / `###!!! [Parent]` line surfaces inside the smoke window without matching an allowlist. Closes the headless-vs-real-chrome gap that previously forced agents to choose between `fireforge run` (no exit hook, hangs on a human) and `--headless` (does not load the chrome document, so chrome-window constructor errors stay invisible).
|
|
52
|
+
## Regular Workflow
|
|
744
53
|
|
|
745
54
|
```bash
|
|
746
|
-
|
|
747
|
-
fireforge
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
fireforge
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
fireforge run --smoke-exit 60 --console-allow-file scripts/smoke-allow.txt --capture-console smoke.log
|
|
55
|
+
npx fireforge status
|
|
56
|
+
npx fireforge export browser/base/content/browser.js --name custom-toolbar --category ui
|
|
57
|
+
npx fireforge re-export custom-toolbar
|
|
58
|
+
npx fireforge lint --per-patch
|
|
59
|
+
npx fireforge verify
|
|
60
|
+
npx fireforge build
|
|
61
|
+
npx fireforge test browser/base/content/test/browser/
|
|
754
62
|
```
|
|
755
63
|
|
|
756
|
-
|
|
64
|
+
Use `fireforge --help` for the full set of commands.
|
|
757
65
|
|
|
758
|
-
|
|
759
|
-
| ---- | ---------------------------------------------------------------------- |
|
|
760
|
-
| 0 | Smoke window elapsed cleanly (or only allowlisted errors fired). |
|
|
761
|
-
| 12 | One or more unallowed console errors fired inside the window. |
|
|
762
|
-
| 13 | Browser exited non-clean before the window elapsed (launch-side fail). |
|
|
66
|
+
## Rebasing Firefox
|
|
763
67
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
The summary block reports two allowlist counters so operators can tell whether a pattern actually matched anything: `Allowlisted error hits (suppressed)` is the exit-contract number (errors that would have failed the window but were dropped by the allowlist), and `Allowlisted lines total` is the mental-model number (every console line that matched the allowlist, regardless of whether it was an error-class line). A non-zero `total` with a zero `suppressed` count means the allowlist patterns matched benign info/warn lines that never counted toward the exit contract to begin with.
|
|
767
|
-
|
|
768
|
-
### Furnace `--shared-ftl` for feature-scoped Fluent bundles
|
|
769
|
-
|
|
770
|
-
A feature with multiple components (e.g. an eight-component dock) typically wants one shared `.ftl` per feature rather than eight per-component stubs. `furnace create <tag> --localized --shared-ftl <chrome-uri>` participates in an existing feature-scoped bundle:
|
|
68
|
+
When Mozilla publishes a new ESR you need to update the configured Firefox version, download the new source code and reapply the patches:
|
|
771
69
|
|
|
772
70
|
```bash
|
|
773
|
-
fireforge
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
The generated `.mjs` calls `insertFTLIfNeeded("browser/mybrowser-dock.ftl")` instead of the per-component path. No `<tag>.ftl` stub is written. The `furnace.json` `custom` entry carries a new `sharedFtl` field so apply, validate, and remove all honour the participation:
|
|
777
|
-
|
|
778
|
-
- `furnace apply` does not copy a per-component `.ftl` into the FTL tree nor add a locale `jar.mn` entry — the shared file is registered by whoever owns the feature bundle.
|
|
779
|
-
- `furnace remove` early-returns from the locale `jar.mn` cleanup, so dropping our component's reference does not orphan the bundle for every other participant.
|
|
780
|
-
- `furnace validate`'s `missing-ftl` structural check is skipped — there is no per-component `.ftl` to require.
|
|
781
|
-
|
|
782
|
-
`--shared-ftl` implies `--localized`. `--no-localized + --shared-ftl` is rejected fast-fail. The value is interpolated verbatim into the generated template literal, so backticks, backslashes, and `${` are rejected at parse time. Setting `sharedFtl` does not auto-migrate previous per-component FTL state — flipping an existing component leaves the prior per-component entry in the engine tree and locale `jar.mn` until cleaned up explicitly.
|
|
783
|
-
|
|
784
|
-
### Furnace `keyboardCovered` for composed-button wrappers
|
|
785
|
-
|
|
786
|
-
`furnace validate`'s `no-keyboard-handler` rule is automatically suppressed when `@click` sits on a custom-element host whose `composes` lists a native-interactive child (e.g. `moz-button`, `moz-toggle`). The wrapper's click handler catches keyboard activation transitively because the inner element dispatches `click` on Enter/Space via the platform; a duplicate `@keydown` on the wrapper would either no-op or double-fire alongside the child's built-in path.
|
|
787
|
-
|
|
788
|
-
When the wrapped inner element is hand-authored or is a non-stock `moz-*` widget that does not appear in `composes`, the explicit `keyboardCovered: true` field on the component's `furnace.json` entry forces the same skip:
|
|
789
|
-
|
|
790
|
-
```json
|
|
791
|
-
"mybrowser-dock-button": {
|
|
792
|
-
"description": "Dock button wrapper",
|
|
793
|
-
"targetPath": "components/custom/mybrowser-dock-button",
|
|
794
|
-
"register": true,
|
|
795
|
-
"localized": false,
|
|
796
|
-
"composes": ["moz-button"],
|
|
797
|
-
"keyboardCovered": true
|
|
798
|
-
}
|
|
71
|
+
npx fireforge config firefox.version 145.0.0esr
|
|
72
|
+
npx fireforge download --force
|
|
73
|
+
npx fireforge rebase
|
|
799
74
|
```
|
|
800
75
|
|
|
801
|
-
|
|
76
|
+
If a patch fails, fix the reject inside the `engine/` directory, then run `npx fireforge rebase --continue`.
|
|
802
77
|
|
|
803
|
-
|
|
78
|
+
## Furnace
|
|
804
79
|
|
|
805
|
-
|
|
80
|
+
Furnace is our solution for UI components. It can track stock widgets, create new fork-owned widgets and override existing ones. The files still land inside the Firefox source directory and end up as part of the regular patches to export.
|
|
806
81
|
|
|
807
82
|
```bash
|
|
808
|
-
fireforge
|
|
809
|
-
fireforge
|
|
83
|
+
npx fireforge furnace scan
|
|
84
|
+
npx fireforge furnace override moz-button -t css-only
|
|
85
|
+
npx fireforge furnace create moz-my-widget
|
|
86
|
+
npx fireforge furnace deploy
|
|
87
|
+
npx fireforge furnace status
|
|
88
|
+
npx fireforge furnace preview
|
|
810
89
|
```
|
|
811
90
|
|
|
812
|
-
|
|
91
|
+
Use `fireforge furnace --help` for the full set of component commands.
|
|
813
92
|
|
|
814
93
|
## Roadmap
|
|
815
94
|
|
|
816
|
-
Planned but not yet implemented:
|
|
817
|
-
|
|
818
95
|
- **Docker builds** Reproducible builds using Docker containers.
|
|
819
96
|
- **CI mode** Automated setup for continuous integration pipelines.
|
|
820
97
|
- **Update manifests** Generate update server manifests for auto-updates.
|
|
821
98
|
- **Nightly support** Requires implementing `hg clone` support via mozilla-central. Currently fireforge only downloads from the archive.
|
|
822
|
-
- **E2E Github Actions** Requires either a higher tier of
|
|
99
|
+
- **E2E Github Actions** Requires either a higher tier of Githubs offering, an external VPS or another provider entirely. In any case, full end-to-end testing is currently run solely locally.
|
|
823
100
|
|
|
824
101
|
## Licence
|
|
825
102
|
|
|
826
|
-
[EUPL-1.2](LICENSE.md).
|
|
103
|
+
FireForge is licensed under [EUPL-1.2](LICENSE.md).
|
|
827
104
|
|
|
828
|
-
|
|
105
|
+
Firefox source downloaded into `engine/` is licensed under [MPL-2.0](https://www.mozilla.org/en-US/MPL/2.0/) and is not distributed by this repository. Firefox-derived files created through Furnace keep MPL-2.0 headers, regardless of the licence you choose for your own project files during `fireforge setup`.
|