@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/README.md CHANGED
@@ -3,826 +3,103 @@
3
3
  [![npm version](https://img.shields.io/npm/v/@hominis/fireforge)](https://www.npmjs.com/package/@hominis/fireforge)
4
4
  [![license](https://img.shields.io/npm/l/@hominis/fireforge)](LICENSE.md)
5
5
  [![node](https://img.shields.io/node/v/@hominis/fireforge)](package.json)
6
- [![types](https://img.shields.io/npm/types/@hominis/fireforge)](https://www.npmjs.com/package/@hominis/fireforge)
7
- [![npm downloads](https://img.shields.io/npm/dm/@hominis/fireforge)](https://www.npmjs.com/package/@hominis/fireforge)
8
6
 
9
- **Build and maintain your own Firefox-based browser with a patch-first workflow**
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
- ## Features
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
- - **Design token management** Track CSS custom property coverage across your modified files.
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
- - **Quality checks** `fireforge lint` catches fork-specific issues (raw colours, missing licence headers, relative imports, large patches, cross-patch ordering problems) before you export. `fireforge typecheck` runs CI-grade JS type checking against project-supplied jsconfig.json files (web components, chrome scripts) — separate from `lint`'s patch-hygiene checkJs pass. `fireforge verify` runs a read-only integrity check over the whole patch queue. `fireforge doctor` diagnoses project health including Furnace component validation.
21
+ ## Requirements
28
22
 
29
- - **Built and validated against real Firefox code** Developed by editing a real Firefox ESR codebase, learning from existing patch tools, observing the breakages and edge cases that surfaced and turning those findings into a realistic test suite. In-repo tests are thus grounded in actual development 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, as they require about 30 GB of disk and significant compute for multiple full builds. Full end-to-end via Github Actions will be added soonishlyTM.
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
- ## Quick Start
29
+ ## Getting Started
32
30
 
33
- ### Requirements
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
- The failure message reports how many cumulative errors were suppressed by the flag so a branch that passed only because of the flag still tells the operator what was hidden. Without `--since`, `--only-introduced` is rejected up-front — there is no introduced-vs-cumulative distinction to scope to.
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
- # Sub-minute marionette handshake probe; bails out of mach test on FAIL
686
- fireforge test --doctor
687
- fireforge test --doctor browser/base/content/test/foo/browser_bar.js
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
- Spawns the built browser headless, waits for a marionette handshake on `127.0.0.1:2828`, and reports PASS/FAIL with the tail of the browser's stderr on FAIL. The success output also names the objdir, binary, app path, port, and elapsed probe time so CI logs show exactly what was probed. Distinguishes "marionette wedged" (socket silent) from "mach test discovery failed" — both otherwise surface as a silent 360-second hang followed by `Passed: 0, Failed: 0`. Useful as a prefix on routine `fireforge test` invocations when marionette has been flaky.
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
- `fireforge token coverage` partitions `var(--…)` usages into three buckets: fork-owned tokens (matching `tokenPrefix`), allowlisted exceptions (explicit `tokenAllowlist` entries, plus platform prefixes), and unknown. Platform prefixes default to `['--moz-']` so upstream Firefox variables in copied baselines (e.g. the CSS that lands under `toolkit/content/widgets/<name>/` after `furnace override <name> -t css-only`) don't drag the fork-owned coverage percentage down.
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
- # Launch, wait 60s, exit 0 unless an unallowed error fired
747
- fireforge run --smoke-exit 60
748
-
749
- # Same, but ignore a known async-shutdown blocker we've already triaged
750
- fireforge run --smoke-exit 60 --console-allow 'AsyncShutdown blocker timed out'
751
-
752
- # Allowlist file (one regex per line, # comments and blanks skipped) + capture
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
- Exit codes are wired distinct from `BUILD_ERROR`:
64
+ Use `fireforge --help` for the full set of commands.
757
65
 
758
- | Code | Meaning |
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
- POSIX only process-group semantics do not map cleanly onto Windows. A smoke window shorter than 30 s warns up-front because cold-start time alone can consume that budget on a debug build; `--capture-console <file>` mirrors the captured stream so post-exit inspection has the raw log without re-running.
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 furnace create mybrowser-dock-button --localized --shared-ftl browser/mybrowser-dock.ftl
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
- `keyboardCovered` is operator-asserted it does not re-check the component, so it can be used to silence a genuine finding. Prefer adding the wrapped tag to `composes` when that field applies (it carries semantic value beyond a11y).
76
+ If a patch fails, fix the reject inside the `engine/` directory, then run `npx fireforge rebase --continue`.
802
77
 
803
- ### Test escape valves
78
+ ## Furnace
804
79
 
805
- `fireforge test --mach-arg <arg>` (repeatable) forwards a single argument verbatim to `mach test` after FireForge-managed flags. Escape valve for upstream xpcshell/mochitest options FireForge does not model directly:
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 test browser/base/content/test/foo --mach-arg=--keep-going --mach-arg=--verbose
809
- fireforge test browser/components/tests/unit/test_x.js --mach-arg=--app-path=/abs/override
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
- Operator overrides for `--app-path` always win over the auto-injection described above.
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 Github offering, an external VPS or similar, or another provider entirely. In either case, full end-to-end testing is currently run solely locally.
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). Firefox source in `engine/` is under [MPL-2.0](https://www.mozilla.org/en-US/MPL/2.0/) and is not distributed by this repository.
103
+ FireForge is licensed under [EUPL-1.2](LICENSE.md).
827
104
 
828
- During `fireforge setup`, you choose a licence for your project files. Options: EUPL-1.2 (default), MPL-2.0, 0BSD, GPL-2.0-or-later. Firefox-derived files from Furnace always carry MPL-2.0 headers, because that is what the upstream licence requires regardless of your project-level choice.
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`.