@hominis/fireforge 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -2
- package/README.md +20 -13
- package/dist/src/commands/doctor.js +13 -1
- package/dist/src/commands/export-all.js +63 -1
- package/dist/src/commands/furnace/create-xpcshell.js +4 -2
- package/dist/src/commands/furnace/preview.js +38 -0
- package/dist/src/commands/furnace/remove.js +67 -1
- package/dist/src/commands/furnace/rename-xpcshell.d.ts +35 -0
- package/dist/src/commands/furnace/rename-xpcshell.js +97 -0
- package/dist/src/commands/furnace/rename.js +9 -0
- package/dist/src/commands/rebase/index.js +19 -1
- package/dist/src/commands/status.js +44 -5
- package/dist/src/commands/test.js +27 -16
- package/dist/src/commands/verify.js +81 -6
- package/dist/src/commands/watch.js +43 -7
- package/dist/src/core/furnace-constants.d.ts +14 -0
- package/dist/src/core/furnace-constants.js +16 -0
- package/dist/src/core/furnace-validate.js +67 -1
- package/dist/src/core/git-base.d.ts +27 -2
- package/dist/src/core/git-base.js +41 -3
- package/dist/src/core/git.js +53 -14
- package/dist/src/core/mach.d.ts +14 -2
- package/dist/src/core/mach.js +12 -2
- package/dist/src/core/marionette-preflight.d.ts +16 -0
- package/dist/src/core/marionette-preflight.js +19 -0
- package/dist/src/core/patch-lint-diff-tag.d.ts +20 -0
- package/dist/src/core/patch-lint-diff-tag.js +25 -0
- package/dist/src/core/patch-lint.js +5 -4
- package/dist/src/core/patch-registration-refs.d.ts +42 -0
- package/dist/src/core/patch-registration-refs.js +117 -0
- package/dist/src/core/xpcshell-appdir.d.ts +19 -5
- package/dist/src/core/xpcshell-appdir.js +46 -20
- package/dist/src/errors/git.d.ts +20 -0
- package/dist/src/errors/git.js +39 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
- **`fireforge test --doctor` — triple-path PASS footer survives non-TTY clack rendering.** The eval's `test --doctor` captures on both fresh and hominis projects showed `● Running marionette preflight...` and then exited 0 with nothing else — the 0.17.0 `outro(\`Marionette preflight: PASS (...)\`)`was dropped by clack somewhere in the non-TTY render pipeline (exact root cause could not be isolated from captures alone). Rather than chase the rendering bug, the fix triple-writes the result:`success(\`Marionette preflight: PASS (...)\`)`, then `outro('Test completed')`closes the intro frame, then`process.stdout.write(\`${summary}\n\`)`guarantees the line reaches stdout regardless of clack's buffering. A test in`test.test.ts` asserts all three emissions.
|
|
21
21
|
- **`fireforge test` — fork-owned module import failures beat the branding stale-build diagnosis.** The eval's hominis xpcshell test failed loading `resource:///modules/hominis/HominisStore.sys.mjs`; FireForge reported "stale build artifacts, run fireforge build --ui" because the harness teardown also printed a branding warning that matched the narrower stale-build pattern. Re-running the build did nothing — the real failure was that the new module wasn't registered in `browser/modules/hominis/moz.build`. `handleNonZeroTestExit` now checks for `Failed to load resource:///modules/<binaryName>/` BEFORE the branding stale-build branch and surfaces a fork-module-specific diagnostic pointing at `browser/modules/<binary>/moz.build` + `fireforge register`, so the operator sees the correct recovery path on the first try. Existing branding-stale diagnosis is untouched.
|
|
22
22
|
- **`fireforge token coverage` — `--moz-*` platform variables allowlisted by default; untracked CSS directories now scanned.** Two independent token-coverage fixes:
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
- **Platform allowlist** — `src/core/token-coverage.ts` seeds a default `platformPrefixes: ['--moz-']` and treats matches as `allowlisted` rather than `unknown`. A CSS-only Furnace override of `moz-button` with one fork token previously reported 1% coverage because the copied upstream baseline referenced 84 `--moz-*` platform vars; coverage now reports 100% on the same diff, and the one unknown was correctly declared by the fork. Forks can override through a new optional `platformPrefixes?: string[]` field on `furnace.json` — extending the list (e.g. `['--moz-', '--in-content-']`) for forks with additional platform prefixes, or setting an explicit empty array to restore the pre-0.18 strict contract. The field is added to the public `FurnaceConfig` type.
|
|
24
|
+
- **Untracked-directory expansion** — `src/commands/token-coverage.ts` now reads `getWorkingTreeStatus` + `expandUntrackedDirectoryEntries` instead of `getStatusWithCodes` so CSS files inside a `?? dir/` untracked directory (the common shape for a freshly-imported patch stack adding a new theme tree) show up in the scan. The eval's hominis run saw `?? browser/branding/hominis/content/aboutDialog.css` and `?? browser/themes/shared/hominis-tokens.css` collapse to directory entries that didn't end in `.css`; coverage reported "No modified CSS files". Both CSS files are now scanned.
|
|
25
25
|
- **`fireforge patch reorder` + `fireforge patch delete` — accept manifest `name` handle in addition to filename and ordinal.** `resolvePatchIdentifier` in `src/core/patch-manifest-resolve.ts` previously rejected the `name` field even though CLI help says `<name>` and the manifest stores a `name` on every entry. Operators had to copy the full filename from `patches.json` before every queue mutation. The resolver now matches, in order: ordinal number → exact filename → filename-with-`.patch`-appended → `name` field. Filename lookup still wins when a manifest happens to have a `name` identical to a different filename — legacy scripts passing filenames keep working. Error messages on both commands now list filenames AND their `name` counterparts so operators see both forms as valid identifiers.
|
|
26
26
|
- **`fireforge patch reorder` — postcondition assert on each Phase 2 rename.** The eval reported that a two-patch swap updated `patches.json` but left the files at their pre-rename names, and `fireforge verify` then failed ENOENT opening the manifest-renamed file. The code path in `renumberPatchesInManifest` (two-phase staging: stage → final + manifest write) appears correct against both the integration test in `patch-mutations.integration.test.ts` and the shipped 0.17.0 dist — but the eval's operation log shows the manifest was updated while the rename was missing, so a filesystem-specific failure mode exists that the existing integration test does not exercise. Defense-in-depth lands a postcondition `pathExists(newFilename)` assert after every `rename()` in Phase 2, so any silent rename failure aborts the reorder before the manifest write and triggers the Phase 2 rollback (which itself reverses completed final-name moves and unwinds the staging). A second integration test reproduces the exact eval scenario (`001-infra` + `002-ui` swap via `--to 1 --yes`) and asserts both filenames exist on disk after completion.
|
|
27
27
|
- **`fireforge register --dry-run` — honours the rule-level idempotency decision.** `registerFile` in `src/core/manifest-rules.ts` returned `{ skipped: true }` when the rule's `isRegistered(...)` check found the file already present in its manifest, but `registerCommand` in `src/commands/register.ts` ignored the flag on the dry-run branch and always printed `[dry-run] Would register <path>`. The next real invocation correctly reported `Already registered: <path>`. Automation that consumed the dry-run as a plan mis-predicted unnecessary work. Dry-run now prints `[dry-run] Already registered: <path> in <manifest>` when `result.skipped` is true — mirroring the real-run copy — and otherwise prints the registration plan as before.
|
|
@@ -30,9 +30,25 @@
|
|
|
30
30
|
- **`fireforge register` — empty single-line `EXTRA_JS_MODULES += []` lists expand before insertion.** `tokenizeMozBuildList` in `src/core/manifest-tokenizers.ts` tracked opening `[` and closing `]` on separate lines. A freshly-scaffolded `browser/modules/<fork>/moz.build` that wrote `EXTRA_JS_MODULES += []` on one line returned `null` from the tokenizer because the scanner never saw a line STARTING with `]`, and `registerFireForgeModule` failed with `Could not find EXTRA_JS_MODULES in moz.build` — blocking the documented `browser/modules/<fork>/*.sys.mjs` workflow at the first module. The tokenizer now detects the single-line empty-list form `...+= []` at open time, rewrites `lines[i]` to `...+= [` and splices in a `]` at `i + 1`, and emits matched `list-open` / `list-close` tokens for the canonical multi-line shape. Downstream `findAlphabeticalMozBuildPosition` and the register helpers then place the new entry inside the brackets as normal.
|
|
31
31
|
- **`fireforge wire --dry-run` — notice when the subscript file is absent.** The dry-run branch in `src/commands/wire.ts` deliberately skips the subscript-file existence check to support the "wire first, create the file after" scaffolding workflow. Pre-0.18 that deliberate skip produced a plausible plan even when the subscript was genuinely missing, and the real `wire` invocation then refused with `Subscript file not found`. The skip stays (the workflow is legitimate) but dry-run now emits `Note: <subscriptDir>/<name>.js does not exist yet — the real wire command will require it before writing.` so the operator sees the absence in the preview and can create the file before re-running without `--dry-run`.
|
|
32
32
|
- **`fireforge status --unmanaged` + `fireforge register` — browser-chrome test files route to browser.toml, not jar.mn.** The browser-content pattern in `src/core/manifest-rules.ts` matched every `browser/base/content/**/*.{js,mjs,xhtml,css}` path, including test implementation files at `browser/base/content/test/<dir>/browser_*.js`. `status --unmanaged` therefore proposed `fireforge register browser/base/content/test/forgeqa/browser_forgeqa_qa_browser.js` which, if run, would cluttered jar.mn with a test-file chrome URI (the right place for those files is the sibling `browser.toml`). The pattern now excludes the `test/` subtree via a negative lookahead, so these paths fall through to `getUnregistrableAdvice`, which already returns the correct browser.toml-centric guidance for individual test files. `status`, `register`, and `register --dry-run` all now agree that test JS belongs in the test manifest.
|
|
33
|
+
- **`fireforge export-all --exclude-furnace` — refuses patches that would register a furnace component without carrying its source files; `fireforge verify` now flags dangling registrations that slipped through earlier versions.** Finding 1 (High). The evaluator's fresh-project smoke sequence (`furnace init` → `token add` → `furnace override moz-button -t css-only` → `furnace create moz-qa-panel --localized --with-tests --test-style mochikit` → `furnace deploy` → `export-all --exclude-furnace`) produced a patch whose `toolkit/content/customElements.js`, `toolkit/content/jar.mn`, and `toolkit/locales/jar.mn` hunks registered `moz-qa-panel` even though the widget sources were filtered out. `fireforge verify` reported "Verify clean" for the broken queue. The fix lands in two layers: a new `collectPatchRegistrationReferences` scanner in `src/core/patch-registration-refs.ts` extracts component-shaped registration references from any patch body, and `verifyCommand` in `src/commands/verify.ts` now cross-checks each extracted reference against both the aggregate `filesAffected` coverage and the engine working tree — any miss fires a `dangling-registration` error naming the specific patch + target path. `export-all --exclude-furnace` in `src/commands/export-all.ts` calls the same scanner before writing and refuses with a clear message when the hunks would register a furnace-managed component not included in the patch. Tests in `patch-registration-refs.test.ts` + `verify.integration.test.ts` pin both halves.
|
|
34
|
+
- **`fireforge status --unmanaged` — tolerates a missing parent moz.build instead of exiting non-zero.** Finding 2 (Medium). `printUnregisteredWarnings` in `src/commands/status.ts` awaited `Promise.all` on `isFileRegistered` checks, and the manifest-rules layer threw `GeneralError("Manifest not found: ...")` synchronously when the parent `moz.build` was absent — exactly the state an operator hits after scaffolding a new engine module and before running `register`. Status is a read-only reporter; the non-zero exit broke `status --unmanaged` as a discovery tool. The fix wraps each `isFileRegistered` call in a narrowly scoped try/catch, buckets missing-manifest cases into a distinct "registration manifest does not exist yet" warning list, and lets the command exit cleanly. Other error shapes still propagate. A regression test in `status.test.ts` pins the new contract.
|
|
35
|
+
- **`fireforge lint --since HEAD --only-introduced` — aggregate patch-size findings tag as `[introduced]` when the diff set is non-empty.** Finding 4 (Low). The `large-patch-files` and `large-patch-lines` rules emit findings with the synthetic `file: '(patch)'` placeholder, which never matched any entry in the `diffFiles` set. `tagLintIssues` in `src/core/patch-lint-diff-tag.ts` therefore tagged them `[cumulative]` even when the aggregate was entirely the operator's own diff — reading as "pre-existing drift" to an operator asking "what did this task introduce?" The tagger now recognises the synthetic placeholder (exported as `AGGREGATE_PATCH_FILE`) and promotes it to `introduced` whenever the diff set is non-empty. Cumulative semantics are preserved for the empty-diff case. Tests in `patch-lint-diff-tag.test.ts` pin both branches.
|
|
36
|
+
- **`fireforge furnace remove` / `rename` / `validate` — xpcshell scaffolds are cleaned up, renamed, and flagged as orphans.** Finding 5 (Medium). `furnace create --with-tests --xpcshell` scaffolds at `browser/base/content/test/<binary>-xpcshell/<name>/`, but the pre-0.18.1 `furnace remove` only touched the sibling browser-mochitest tree and `furnace rename` never reached the scaffold at all. Operators who ran create → rename → remove were left with an orphan scaffold whose filenames still referenced the original pre-rename component. The path template is now centralised in `xpcshellTestParentDir` in `src/core/furnace-constants.ts`; `create-xpcshell.ts` consumes it, a new `cleanupCustomXpcshellTestFiles` helper in `src/commands/furnace/remove.ts` removes the scaffold under the journal contract, a new `renameXpcshellTestFiles` helper in `src/commands/furnace/rename-xpcshell.ts` (extracted to keep `rename.ts` under the per-file LOC budget) updates the directory, test filename, TOML section header, and word-boundary tag references in the test body, and `findOrphanXpcshellScaffolds` in `src/core/furnace-validate.ts` walks the parent directory reporting any entry whose name is not in furnace.json as an `orphan-xpcshell-scaffold` error. The validator degrades silently on projects that never used xpcshell scaffolding. Test coverage lands in `furnace-validate-xpcshell-orphan.test.ts`.
|
|
37
|
+
- **`fireforge doctor --repair-patches-manifest` — recovery guidance no longer contradicts the docs by telling operators to hand-edit patches.json.** Finding 6 (Low). The per-filename warning emitted on recovered manifest entries ended with "Edit patches.json to restore the original description if you have it backed up." — which directly contradicts the README and Hominis docs that treat the manifest as FireForge-owned. The reworded warning now points at `fireforge re-export <filename> --description "<your description>"` (or the equivalent `fireforge export` invocation) to overwrite the reconstructed metadata through the tool, and explicitly warns against hand-editing. Test coverage in `doctor.test.ts` pins both the presence of the new guidance and the absence of the old "Edit patches.json" string.
|
|
38
|
+
- **`fireforge test --doctor` — PASS/FAIL line written directly to stdout, bypassing any clack flush races.** Finding 7 (Medium). The 0.18.0 "belt-and-suspenders" approach still reproducibly dropped the PASS footer under non-TTY capture — the eval log showed only the intro and `Running marionette preflight...` banner before exit. A new `formatMarionettePreflightLine` helper in `src/core/marionette-preflight.ts` returns the raw banner as a plain string, and `testCommand` in `src/commands/test.ts` writes both the "Running marionette preflight..." intro and the final PASS/FAIL summary via `process.stdout.write` as the authoritative emissions so non-TTY captures always see the summary. The clack `success()` + `outro()` calls are retained for TTY framing. Test coverage in `test.test.ts` now spies on `process.stdout.write` and asserts both the intro and PASS lines are emitted.
|
|
39
|
+
- **`fireforge test` — xpcshell appdir probe prefers the macOS `.app/Contents/Resources/<value>` layout on Darwin.** Finding 8 (High). `resolveAbsoluteAppPath` in `src/core/xpcshell-appdir.ts` probed `dist/bin/<value>` first on every platform. On macOS `dist/bin` is a symlink to `<App>.app/Contents/MacOS/`, so `dist/bin/browser` resolved to the _binaries_ directory rather than the Resources tree where `resource:///modules/` is rooted — the auto-injection logged success, but the injected path did not match where modules actually live. The probe now branches on `process.platform`: on Darwin it prefers `<App>.app/Contents/Resources/<value>` first and falls back to `dist/bin/<value>`; other platforms keep the historical order. The operator-facing `buildXpcshellAppdirMessage` in `src/commands/test.ts` also gains a macOS-specific note recommending the `<appname>-appdir = "browser"` xpcshell.toml migration, which is the most reliable fix on rebranded macOS builds. Test coverage in `xpcshell-appdir.test.ts` now exercises both probe orders via `process.platform` branches.
|
|
40
|
+
- **`fireforge download` — git-indexing timeouts raise a typed `GitIndexingTimeoutError` with env-var recovery guidance, and the monolithic → chunked transition is visible in non-TTY logs.** Finding 10 (High). On a loaded filesystem the fresh Firefox source indexing legitimately exceeded the 10-minute monolithic `git add -A` budget; the chunked fallback then hit its own `AbortSignal.timeout` and surfaced a generic `AbortError: The operation was aborted` with no recovery direction. `src/core/git-base.ts` now exposes `FIREFORGE_GIT_ADD_TIMEOUT_MS` and `FIREFORGE_GIT_ADD_CHUNK_TIMEOUT_MS` environment variables that override the monolithic (default 10 min) and chunked (default grew to 30 min) timeouts respectively; the chunked pass's timeout is wrapped into a typed `GitIndexingTimeoutError` in `src/errors/git.ts` that names the elapsed budget, points at the environment variable override, and explains the `fireforge download --force` resume path. The fallback-transition progress banner now names the specific timeout so operators watching a non-TTY log see exactly when the monolithic attempt lost. Test coverage in `git-performance.test.ts` pins both the new error type and the refreshed transition banner.
|
|
41
|
+
- **`fireforge rebase --dry-run` — refuses when the engine has no baseline commit.** Finding 11 (Medium). A partially-initialised engine (e.g. the aftermath of an aborted `download --force`) has `.git/` in place but no valid HEAD; the real `rebase --yes` immediately failed with `fatal: ambiguous argument 'HEAD'` even though dry-run had previously reported "Dry run complete" suggesting the rebase was ready to run. `handleFreshStart` in `src/commands/rebase/index.ts` now calls `getHead(paths.engine)` and, on `isMissingHeadError`, throws a clear `GeneralError` pointing at `fireforge download --force`. The check runs ahead of the dry-run early-exit so dry-run and real-run preconditions stay in sync. Test coverage in `rebase.test.ts` pins both the dry-run and real-run refusal paths.
|
|
42
|
+
- **`fireforge watch` — resolved watchman directory is prepended to the mach subprocess PATH.** Finding 12 (Medium). `fireforge watch` located watchman via the parent shell PATH but the `mach watch` subprocess inherited the Node parent's PATH, which on macOS frequently omits `/opt/homebrew/bin`. `mach watch` then failed at the `watch-project` subscription step with a confusing `FasterBuildException: timed out`. `watch.ts` now resolves watchman's absolute path via `findExecutable`, prepends the containing directory to the subprocess PATH, and threads the composed env through a new optional `options.env` parameter on `watchWithOutput` in `src/core/mach.ts`. The watch-failure diagnostic also gains a line naming the resolved watchman path so operators can distinguish "FireForge didn't find watchman" from "FireForge found watchman but mach still failed to reach it." Test coverage in `watch.test.ts` pins the env plumbing and the de-duplication branch.
|
|
43
|
+
- **`fireforge furnace preview` — first-run banner frames the unavoidable npm noise from `mach storybook`.** Finding 13 (Low). `mach storybook` internally drives a ~1000-package `npm install` when the Storybook workspace's `node_modules/` is absent and emits `npm error code ELSPROBLEMS` + `UNMET DEPENDENCY` lines verbatim. Operators on a fresh Firefox checkout read the npm block as a fatal error even though the command recovers and Storybook starts. `furnacePreviewCommand` in `src/commands/furnace/preview.ts` now checks for the absent `node_modules` before spawning mach and emits a clear pre-banner explaining the npm output is an expected one-time first-run cost; an explicit "Storybook stopped cleanly." line fires on clean exits so the npm noise is visually terminated. The `--install` path skips the banner because `mach storybook upgrade` already runs its install before `mach storybook` launches. Test coverage in `furnace-preview.test.ts` pins both the presence of the banner when node_modules is absent and its absence when node_modules is already populated.
|
|
33
44
|
|
|
34
45
|
### Internal
|
|
35
46
|
|
|
47
|
+
- **Module extractions preserve the per-file LOC budget.** `doctor-working-tree.ts` hosts `inspectEngineWorkingTree` (the ownership-aware working-tree check), `doctor-furnace-manifest-sync.ts` hosts the orphan override / custom detection + repair, `patch-registration-refs.ts` hosts the dangling-registration scanner shared between `verify` and `export-all`, and `rename-xpcshell.ts` hosts the xpcshell scaffold rename path. All spliced into their respective orchestrators from the hosting file, keeping `doctor.ts`, `doctor-furnace.ts`, `rename.ts`, and `preview.ts` under the 500-line `max-lines` rule. The drift test in `manifest.test.ts` classifies each new file as a helper so the top-level command drift check stays clean.
|
|
48
|
+
- **Expanded test coverage.** Every eval3 fix ships with at least one regression test exercising the failing path: `patch-registration-refs.test.ts` + `verify.integration.test.ts` (Finding 1), `status.test.ts` (Finding 2), `patch-lint-diff-tag.test.ts` (Finding 4), `furnace-validate-xpcshell-orphan.test.ts` (Finding 5), `doctor.test.ts` (Finding 6), `test.test.ts` (Finding 7), `xpcshell-appdir.test.ts` (Finding 8), `git-performance.test.ts` (Finding 10), `rebase.test.ts` (Finding 11), `watch.test.ts` (Finding 12), `furnace-preview.test.ts` (Finding 13).
|
|
49
|
+
|
|
50
|
+
### Internal (0.18.0 original)
|
|
51
|
+
|
|
36
52
|
- **Two small module extractions preserve the per-file LOC budget.** `doctor-working-tree.ts` hosts `inspectEngineWorkingTree` (the ownership-aware working-tree check), and `doctor-furnace-manifest-sync.ts` hosts the orphan override / custom detection + repair. Both are spliced into their respective registries from the hosting file, keeping `doctor.ts` and `doctor-furnace.ts` under the 500-line `max-lines` rule. The drift test in `manifest.test.ts` is extended to classify both new files as helpers so the top-level command drift check stays clean.
|
|
37
53
|
- **Expanded test coverage — 34 new assertions.** Every fix above ships with at least one new test exercising the regression path: `src/core/__tests__/git-diff.test.ts` gains a `?? dir/` expansion case; `src/commands/__tests__/doctor.test.ts` gains patch-backed / conflict / orphan-override / manifest-sync-repair cases; `furnace-remove.test.ts` + `furnace-rename.test.ts` pin the locale jar.mn re-wire / removal; `furnace-override.test.ts` pins the concurrent-writer read-modify-write; `build.test.ts` pins the stale-configure annotation; `test.test.ts` pins the fork-module diagnostic + triple-path PASS footer; `token-coverage.test.ts` pins the `--moz-*` allowlist + untracked-dir expansion; `patch-manifest-helpers.test.ts` pins name-field resolution; `patch-mutations.integration.test.ts` pins the two-patch swap postcondition; `register.test.ts` pins dry-run idempotency; `wire-init.test.ts` pins the marker; `manifest-tokenizers.test.ts` + `register-module.test.ts` pin the empty-list expansion; `wire.test.ts` pins the dry-run missing-subscript notice; `manifest-register.test.ts` pins the test-file pattern exclusion.
|
|
38
54
|
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
|
|
|
36
36
|
- **Python 3** (required by Firefox's `mach` build system).
|
|
37
37
|
- **Git**
|
|
38
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` so the dependency is visible during the usual onboarding sweep rather than at the watch-mode failure site.
|
|
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` so the dependency is visible during the usual onboarding sweep rather than at the watch-mode failure site. `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.
|
|
40
40
|
|
|
41
41
|
### Setup
|
|
42
42
|
|
|
@@ -83,6 +83,10 @@ npx fireforge download --force
|
|
|
83
83
|
npx fireforge rebase
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
+
`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.
|
|
87
|
+
|
|
88
|
+
`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.
|
|
89
|
+
|
|
86
90
|
## Patch Workflow
|
|
87
91
|
|
|
88
92
|
Patches live in `patches/`, applied by numeric filename prefix and tracked in `patches/patches.json`:
|
|
@@ -259,17 +263,18 @@ fireforge status --json # machine-readable classified output
|
|
|
259
263
|
|
|
260
264
|
Then fix with the appropriate primitive:
|
|
261
265
|
|
|
262
|
-
| Problem | Fix
|
|
263
|
-
| ---------------------------------------------- |
|
|
264
|
-
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files`
|
|
265
|
-
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>`
|
|
266
|
-
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>`
|
|
267
|
-
| Ordinal gaps after deletes/splits | `fireforge patch compact`
|
|
268
|
-
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>`
|
|
269
|
-
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest`
|
|
270
|
-
|
|
|
266
|
+
| Problem | Fix |
|
|
267
|
+
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
268
|
+
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files` |
|
|
269
|
+
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>` |
|
|
270
|
+
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>` |
|
|
271
|
+
| Ordinal gaps after deletes/splits | `fireforge patch compact` |
|
|
272
|
+
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>` |
|
|
273
|
+
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
|
|
274
|
+
| Dangling widget / locale registration in patch | Re-run `fireforge export` without `--exclude-furnace` to capture the source files, or revert furnace changes |
|
|
275
|
+
| Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset` |
|
|
271
276
|
|
|
272
|
-
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.
|
|
277
|
+
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.
|
|
273
278
|
|
|
274
279
|
## Wiring Custom Code
|
|
275
280
|
|
|
@@ -472,6 +477,8 @@ fireforge lint --since main --only-introduced
|
|
|
472
477
|
|
|
473
478
|
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.
|
|
474
479
|
|
|
480
|
+
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).
|
|
481
|
+
|
|
475
482
|
### Post-build audit and auto-configure
|
|
476
483
|
|
|
477
484
|
`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.
|
|
@@ -589,9 +596,9 @@ The two flags can be combined — `--with-tests --xpcshell` writes both harnesse
|
|
|
589
596
|
|
|
590
597
|
### xpcshell appdir auto-injection on rebranded forks
|
|
591
598
|
|
|
592
|
-
`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 `<objDir>/dist/
|
|
599
|
+
`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.
|
|
593
600
|
|
|
594
|
-
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. The xpcshell appdir hint that fires when the symptom persists despite injection lists this option first.
|
|
601
|
+
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.
|
|
595
602
|
|
|
596
603
|
### Smoke-run mode (`fireforge run --smoke-exit`)
|
|
597
604
|
|
|
@@ -322,7 +322,19 @@ const DOCTOR_CHECKS = [
|
|
|
322
322
|
// (only `filesAffected` / ordering drifted) are not flagged.
|
|
323
323
|
if (repaired.recoveredFilenames.length > 0) {
|
|
324
324
|
for (const filename of repaired.recoveredFilenames) {
|
|
325
|
-
|
|
325
|
+
// 2026-04-24 eval Finding 6: the repair path used to tell the
|
|
326
|
+
// operator to hand-edit patches.json, which contradicts the
|
|
327
|
+
// README + Hominis docs that treat the manifest as
|
|
328
|
+
// FireForge-owned. Point at the existing `re-export` /
|
|
329
|
+
// `export` workflow instead so the fix stays inside the tool:
|
|
330
|
+
// re-exporting the same files with an explicit `--description`
|
|
331
|
+
// overwrites the recovered entry with operator-supplied
|
|
332
|
+
// metadata and supersedes the mtime-based createdAt stamp.
|
|
333
|
+
warn(`Recovered manifest entry for ${filename} with generic description and mtime-based createdAt. ` +
|
|
334
|
+
'Re-export the affected files with `fireforge re-export <filename> --description "<your description>"` ' +
|
|
335
|
+
'(or `fireforge export <paths...> --name <name> --category <category> --description "<your description>"`) ' +
|
|
336
|
+
'to overwrite the reconstructed metadata, or accept the generic description if the original text is not recoverable. ' +
|
|
337
|
+
'Avoid hand-editing patches.json — FireForge owns that file and will regenerate it on the next manifest consistency pass.');
|
|
326
338
|
}
|
|
327
339
|
}
|
|
328
340
|
return warning('Patch manifest consistency', `Rebuilt patches.json from ${repaired.manifest.patches.length} patch${repaired.manifest.patches.length === 1 ? '' : 'es'}${repaired.recoveredFilenames.length > 0 ? ` (${repaired.recoveredFilenames.length} with reconstructed metadata — see warnings above)` : ''}. Review recovered metadata before release.`);
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { Option } from 'commander';
|
|
3
3
|
import { isBrandingManagedPath } from '../core/branding.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
-
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
5
|
+
import { collectFurnaceManagedPrefixes, furnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
|
|
6
6
|
import { hasChanges, isGitRepository } from '../core/git.js';
|
|
7
7
|
import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
8
8
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
9
9
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
10
10
|
import { commitExportedPatch, findAllPatchesForFiles } from '../core/patch-export.js';
|
|
11
11
|
import { buildPatchQueueContext, collectNewFileCreatorsByPath, detectNewFilesInDiff, } from '../core/patch-lint.js';
|
|
12
|
+
import { collectPatchRegistrationReferences } from '../core/patch-registration-refs.js';
|
|
12
13
|
import { GeneralError } from '../errors/base.js';
|
|
13
14
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
14
15
|
import { info, intro, outro, spinner } from '../utils/logger.js';
|
|
@@ -62,6 +63,62 @@ async function resolveFurnaceExclusionPolicy(paths, projectRoot, excludeFurnace)
|
|
|
62
63
|
'Review them with "fireforge status" or "fireforge furnace status", ' +
|
|
63
64
|
'or pass --exclude-furnace to export the non-Furnace subset of the diff.');
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Refuses the export when the resulting patch would register furnace
|
|
68
|
+
* component source files it does not itself carry. 2026-04-24 eval
|
|
69
|
+
* Finding 1: operators running `export-all --exclude-furnace` after
|
|
70
|
+
* `furnace create --localized --with-tests` ended up with patches that
|
|
71
|
+
* added `toolkit/content/widgets/moz-qa-panel/*` via jar.mn /
|
|
72
|
+
* customElements.js / locale jar.mn but excluded the component source
|
|
73
|
+
* files themselves. The resulting patch queue was structurally broken
|
|
74
|
+
* and `fireforge verify` stayed silent. We now detect the condition
|
|
75
|
+
* pre-write and ask the operator to either include the component
|
|
76
|
+
* sources (skip `--exclude-furnace`) or revert the furnace changes
|
|
77
|
+
* before exporting.
|
|
78
|
+
*
|
|
79
|
+
* The check runs against the synthesised patch body before
|
|
80
|
+
* `commitExportedPatch` writes anything, so no broken patch is left on
|
|
81
|
+
* disk when the refusal fires.
|
|
82
|
+
*/
|
|
83
|
+
async function checkDanglingFurnaceRegistrations(projectRoot, diff, furnaceExcluded) {
|
|
84
|
+
if (furnaceExcluded.size === 0)
|
|
85
|
+
return;
|
|
86
|
+
if (!(await furnaceConfigExists(projectRoot)))
|
|
87
|
+
return;
|
|
88
|
+
const refs = collectPatchRegistrationReferences(diff);
|
|
89
|
+
if (refs.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
const config = await loadFurnaceConfig(projectRoot);
|
|
92
|
+
// Build the set of furnace-managed component names so we can tell
|
|
93
|
+
// "registers moz-qa-panel (furnace-managed)" apart from "registers
|
|
94
|
+
// moz-button (an upstream widget this patch legitimately touches)".
|
|
95
|
+
const furnaceComponentNames = new Set([
|
|
96
|
+
...Object.keys(config.custom),
|
|
97
|
+
...Object.keys(config.overrides),
|
|
98
|
+
...config.stock,
|
|
99
|
+
]);
|
|
100
|
+
const dangling = [];
|
|
101
|
+
for (const ref of refs) {
|
|
102
|
+
if (!furnaceExcluded.has(ref.targetPath))
|
|
103
|
+
continue;
|
|
104
|
+
const tagMatch = /toolkit\/content\/widgets\/([a-z][a-z0-9-]*)\//.exec(ref.targetPath);
|
|
105
|
+
const ftlMatch = /toolkit\/locales\/en-US\/toolkit\/global\/([a-z][a-z0-9-]*)\.ftl$/.exec(ref.targetPath);
|
|
106
|
+
const component = tagMatch?.[1] ?? ftlMatch?.[1];
|
|
107
|
+
if (!component || !furnaceComponentNames.has(component))
|
|
108
|
+
continue;
|
|
109
|
+
dangling.push({ component, targetPath: ref.targetPath, source: ref.source });
|
|
110
|
+
}
|
|
111
|
+
if (dangling.length === 0)
|
|
112
|
+
return;
|
|
113
|
+
const summary = dangling
|
|
114
|
+
.map((d) => ` • ${d.component} — registered via ${d.source} → ${d.targetPath}`)
|
|
115
|
+
.join('\n');
|
|
116
|
+
throw new GeneralError('Export-all --exclude-furnace would produce a patch that registers furnace-managed components without including their source files.\n\n' +
|
|
117
|
+
`Dangling registrations:\n${summary}\n\n` +
|
|
118
|
+
'To proceed, either:\n' +
|
|
119
|
+
' 1. Drop the --exclude-furnace flag so the source files are captured alongside the registration edits.\n' +
|
|
120
|
+
' 2. Revert the registration hunks (or the whole furnace workflow) before re-running export-all — registrations belong with their components, and splitting them across separate patches is what "verify" catches post-hoc as a dangling-registration error.');
|
|
121
|
+
}
|
|
65
122
|
/**
|
|
66
123
|
* Refuses the export when the aggregate diff would create (new-file-mode) a
|
|
67
124
|
* path that some existing patch in the queue already creates. `verify`
|
|
@@ -163,6 +220,11 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
163
220
|
// the aggregate would newly create, so it runs here instead of alongside
|
|
164
221
|
// the branding / furnace guards that operate on the raw status list.
|
|
165
222
|
await checkDuplicateNewFileCreations(paths, diff);
|
|
223
|
+
// Dangling-furnace-registration preflight (Finding 1). Runs after the
|
|
224
|
+
// diff is assembled so we can inspect the exact hunks the operator is
|
|
225
|
+
// about to land; runs BEFORE any write so a refusal leaves the
|
|
226
|
+
// patches directory untouched.
|
|
227
|
+
await checkDanglingFurnaceRegistrations(projectRoot, diff, furnaceExcluded);
|
|
166
228
|
// Check for non-interactive mode
|
|
167
229
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
168
230
|
// Auto-fix missing license headers on new files (interactive only)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* per-file LOC budget and the scaffolder is unit-testable in isolation.
|
|
6
6
|
*/
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
+
import { xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
8
9
|
import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
9
10
|
import { getLicenseHeader } from '../../core/license-headers.js';
|
|
10
11
|
import { ensureDir, pathExists, writeText } from '../../utils/fs.js';
|
|
@@ -26,8 +27,9 @@ import { generateXpcshellManifestContent, generateXpcshellTestContent, xpcshellT
|
|
|
26
27
|
* auto-insertion that guessed wrong would be worse than a note.
|
|
27
28
|
*/
|
|
28
29
|
export async function scaffoldXpcshellTestFiles(componentName, license, forgeConfig, paths, journal) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
30
|
+
const parentRelDir = xpcshellTestParentDir(forgeConfig.binaryName);
|
|
31
|
+
const parentDirName = parentRelDir.split('/').slice(-1)[0] ?? `${forgeConfig.binaryName}-xpcshell`;
|
|
32
|
+
const testDir = join(paths.engine, parentRelDir, componentName);
|
|
31
33
|
if (journal && !(await pathExists(testDir))) {
|
|
32
34
|
recordCreatedDir(journal, testDir);
|
|
33
35
|
}
|
|
@@ -163,6 +163,38 @@ async function assertPreviewPrerequisites(engineDir) {
|
|
|
163
163
|
'Run "fireforge bootstrap" (or the underlying `mach bootstrap` in the engine) to populate the toolchain config, then rerun "fireforge furnace preview".');
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Emits a framing banner when the Storybook workspace has not yet had
|
|
168
|
+
* its npm dependencies installed. `mach storybook` will drive the
|
|
169
|
+
* install internally and print ELSPROBLEMS / UNMET DEPENDENCY lines
|
|
170
|
+
* verbatim; without this banner operators reliably read the npm output
|
|
171
|
+
* as a failure (2026-04-24 eval Finding 13).
|
|
172
|
+
*
|
|
173
|
+
* Skipped when `--install` was explicitly requested — that path already
|
|
174
|
+
* runs `mach storybook upgrade` before the preview launches, so the npm
|
|
175
|
+
* output for the subsequent `mach storybook` invocation is a no-op.
|
|
176
|
+
*/
|
|
177
|
+
async function announceStorybookFirstRunIfNeeded(engineDir, installRequested) {
|
|
178
|
+
if (installRequested)
|
|
179
|
+
return;
|
|
180
|
+
const storybookNodeModules = join(engineDir, 'browser', 'components', 'storybook', 'node_modules');
|
|
181
|
+
const storybookDepsMissing = !(await pathExists(storybookNodeModules));
|
|
182
|
+
if (!storybookDepsMissing)
|
|
183
|
+
return;
|
|
184
|
+
info('Storybook workspace dependencies are not yet installed. The next step will install ~1000 npm packages via `mach storybook`; expect npm error-style output below. This is a one-time first-run cost — Storybook will start once the install finishes.');
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Surfaces an explicit success banner after a clean mach-storybook
|
|
188
|
+
* exit so the operator's scrollback visually terminates the npm noise
|
|
189
|
+
* from the first-run install. Only fires on expected exit codes — non-
|
|
190
|
+
* zero cases fall through to the existing
|
|
191
|
+
* `buildStorybookFailureMessage` classification.
|
|
192
|
+
*/
|
|
193
|
+
function announceStorybookCleanExitIfApplicable(exitCode) {
|
|
194
|
+
if (exitCode === 0 || exitCode === 130 || exitCode === 143) {
|
|
195
|
+
info('Storybook stopped cleanly.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
166
198
|
/**
|
|
167
199
|
* Runs the furnace preview command to start Storybook for component preview.
|
|
168
200
|
* @param projectRoot - Root directory of the project
|
|
@@ -269,10 +301,16 @@ export async function furnacePreviewCommand(projectRoot, options = {}) {
|
|
|
269
301
|
}
|
|
270
302
|
installSpinner.stop('Storybook dependencies reinstalled');
|
|
271
303
|
}
|
|
304
|
+
// 2026-04-24 eval Finding 13: frame the npm noise that `mach
|
|
305
|
+
// storybook` emits on first-run as expected progress rather than a
|
|
306
|
+
// failure. The banner-before / banner-after helpers are extracted
|
|
307
|
+
// so the command body stays under the per-function LOC budget.
|
|
308
|
+
await announceStorybookFirstRunIfNeeded(paths.engine, options.install ?? false);
|
|
272
309
|
// Start Storybook
|
|
273
310
|
info('Starting Storybook...');
|
|
274
311
|
info('Press Ctrl+C to stop\n');
|
|
275
312
|
previewResult = await runMachCapture(['storybook'], paths.engine);
|
|
313
|
+
announceStorybookCleanExitIfApplicable(previewResult.exitCode);
|
|
276
314
|
}
|
|
277
315
|
catch (error) {
|
|
278
316
|
primaryError = error;
|
|
@@ -6,7 +6,7 @@ import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
|
6
6
|
import { removeCustomFtlJarMnEntry } from '../../core/furnace-apply-ftl.js';
|
|
7
7
|
import { extractComponentChecksums, getOverrideEngineTargetPath, isOverrideCopyCandidate, restoreOverrideFileToBaseline, } from '../../core/furnace-apply-helpers.js';
|
|
8
8
|
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
9
|
-
import { resolveFtlDir } from '../../core/furnace-constants.js';
|
|
9
|
+
import { resolveFtlDir, xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
10
10
|
import { recordFurnaceRollbackFailure, runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
11
11
|
import { removeCustomElementRegistration, removeJarMnEntries, } from '../../core/furnace-registration.js';
|
|
12
12
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
@@ -212,6 +212,64 @@ async function cleanupCustomTestFiles(name, projectRoot, journal) {
|
|
|
212
212
|
}
|
|
213
213
|
return { partialFailures };
|
|
214
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Removes generated xpcshell test scaffolds associated with a custom
|
|
217
|
+
* component. 2026-04-24 eval Finding 5: `furnace remove` handled
|
|
218
|
+
* browser mochitests via `cleanupCustomTestFiles` but never touched the
|
|
219
|
+
* xpcshell scaffold tree, so an operator who ran
|
|
220
|
+
* `furnace create --with-tests --xpcshell` followed by `furnace remove`
|
|
221
|
+
* was left with orphan `xpcshell.toml` + `test_<name>_packaged.js`
|
|
222
|
+
* files still referencing the removed component. This cleanup pass
|
|
223
|
+
* mirrors the mochitest one — snapshot before removal, warn-and-
|
|
224
|
+
* continue semantics, explicit summary when partial failures occur.
|
|
225
|
+
*/
|
|
226
|
+
async function cleanupCustomXpcshellTestFiles(name, projectRoot, journal) {
|
|
227
|
+
const partialFailures = [];
|
|
228
|
+
let forgeConfig;
|
|
229
|
+
try {
|
|
230
|
+
forgeConfig = await loadConfig(projectRoot);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const msg = `Could not load config for xpcshell test cleanup — ${toError(error).message}. Remove xpcshell test files manually if needed.`;
|
|
234
|
+
warn(msg);
|
|
235
|
+
partialFailures.push(msg);
|
|
236
|
+
return { partialFailures };
|
|
237
|
+
}
|
|
238
|
+
const paths = getProjectPaths(projectRoot);
|
|
239
|
+
const xpcshellRoot = join(paths.engine, xpcshellTestParentDir(forgeConfig.binaryName));
|
|
240
|
+
const componentXpcshellDir = join(xpcshellRoot, name);
|
|
241
|
+
if (!(await pathExists(componentXpcshellDir)))
|
|
242
|
+
return { partialFailures };
|
|
243
|
+
try {
|
|
244
|
+
await snapshotDir(journal, componentXpcshellDir);
|
|
245
|
+
await removeDir(componentXpcshellDir);
|
|
246
|
+
info(`Deleted xpcshell test scaffold directory: ${componentXpcshellDir.replace(paths.engine + '/', 'engine/')}`);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
const msg = `Could not delete xpcshell test scaffold — ${toError(error).message}. Remove it manually if needed.`;
|
|
250
|
+
warn(msg);
|
|
251
|
+
partialFailures.push(msg);
|
|
252
|
+
}
|
|
253
|
+
// If the xpcshell parent directory is now empty (no other components
|
|
254
|
+
// had scaffolds), drop it too so `furnace validate` stays quiet about
|
|
255
|
+
// the empty per-binary tree. Warn-and-continue on any failure.
|
|
256
|
+
try {
|
|
257
|
+
if (await pathExists(xpcshellRoot)) {
|
|
258
|
+
const remaining = await readdir(xpcshellRoot);
|
|
259
|
+
if (remaining.length === 0) {
|
|
260
|
+
await snapshotDir(journal, xpcshellRoot);
|
|
261
|
+
await removeDir(xpcshellRoot);
|
|
262
|
+
info(`Deleted empty xpcshell parent directory: ${xpcshellRoot.replace(paths.engine + '/', 'engine/')}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const msg = `Could not clean up xpcshell parent directory — ${toError(error).message}. Remove it manually if needed.`;
|
|
268
|
+
warn(msg);
|
|
269
|
+
partialFailures.push(msg);
|
|
270
|
+
}
|
|
271
|
+
return { partialFailures };
|
|
272
|
+
}
|
|
215
273
|
function dropChecksumsByPrefix(state, prefix) {
|
|
216
274
|
const result = { ...state };
|
|
217
275
|
if (state.appliedChecksums) {
|
|
@@ -367,6 +425,14 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
367
425
|
if (type === 'custom') {
|
|
368
426
|
const result = await cleanupCustomTestFiles(name, projectRoot, journal);
|
|
369
427
|
testCleanupFailures = result.partialFailures;
|
|
428
|
+
// 2026-04-24 eval Finding 5: also clean up xpcshell scaffolds
|
|
429
|
+
// generated by `furnace create --with-tests --xpcshell`. The
|
|
430
|
+
// mochitest cleanup above covers `browser/base/content/test/
|
|
431
|
+
// <binary>/`, but xpcshell scaffolds live in the sibling
|
|
432
|
+
// `<binary>-xpcshell/` directory and were orphaned by prior
|
|
433
|
+
// versions.
|
|
434
|
+
const xpcshellResult = await cleanupCustomXpcshellTestFiles(name, projectRoot, journal);
|
|
435
|
+
testCleanupFailures.push(...xpcshellResult.partialFailures);
|
|
370
436
|
}
|
|
371
437
|
// Remove entry from furnace.json
|
|
372
438
|
if (type === 'stock') {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xpcshell scaffold rename helper extracted from `rename.ts`.
|
|
3
|
+
*
|
|
4
|
+
* 2026-04-24 eval Finding 5: `furnace create --with-tests --xpcshell`
|
|
5
|
+
* writes a scaffold at `browser/base/content/test/<binary>-xpcshell/
|
|
6
|
+
* <name>/` and `furnace rename` did not update it. The helper below
|
|
7
|
+
* renames the directory, updates the test filename, rewrites the
|
|
8
|
+
* `xpcshell.toml` section header, and re-writes the test body so word-
|
|
9
|
+
* boundary occurrences of the old tag / underscored name map to the new
|
|
10
|
+
* ones.
|
|
11
|
+
*
|
|
12
|
+
* Extracted to keep `rename.ts` under the per-file LOC budget —
|
|
13
|
+
* `rename.ts` already carries mochikit + browser-mochitest + FTL
|
|
14
|
+
* handling, and tacking xpcshell onto that tree pushed the file past
|
|
15
|
+
* the limit.
|
|
16
|
+
*/
|
|
17
|
+
import { type RollbackJournal } from '../../core/furnace-rollback.js';
|
|
18
|
+
/**
|
|
19
|
+
* Renames an xpcshell test scaffold in place. Moves the directory,
|
|
20
|
+
* rewrites the test filename, updates the `[test_name]` section header
|
|
21
|
+
* in `xpcshell.toml`, and word-boundary-rewrites occurrences of the
|
|
22
|
+
* old tag / old underscored name inside the test body.
|
|
23
|
+
*
|
|
24
|
+
* Best-effort: any failure logs a warning through the shared logger
|
|
25
|
+
* but never throws — the component rename itself has already succeeded
|
|
26
|
+
* at this point, and blocking on a test rewrite would leave the
|
|
27
|
+
* operator with a half-renamed component.
|
|
28
|
+
*
|
|
29
|
+
* @param engineDir - Absolute path to the engine directory under the project.
|
|
30
|
+
* @param projectRoot - Absolute path to the project root, used to load the binary name.
|
|
31
|
+
* @param oldName - Pre-rename component tag name.
|
|
32
|
+
* @param newName - Post-rename component tag name.
|
|
33
|
+
* @param journal - Rollback journal that the rename mutation writes to before touching files.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renameXpcshellTestFiles(engineDir: string, projectRoot: string, oldName: string, newName: string, journal: RollbackJournal): Promise<void>;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* xpcshell scaffold rename helper extracted from `rename.ts`.
|
|
4
|
+
*
|
|
5
|
+
* 2026-04-24 eval Finding 5: `furnace create --with-tests --xpcshell`
|
|
6
|
+
* writes a scaffold at `browser/base/content/test/<binary>-xpcshell/
|
|
7
|
+
* <name>/` and `furnace rename` did not update it. The helper below
|
|
8
|
+
* renames the directory, updates the test filename, rewrites the
|
|
9
|
+
* `xpcshell.toml` section header, and re-writes the test body so word-
|
|
10
|
+
* boundary occurrences of the old tag / underscored name map to the new
|
|
11
|
+
* ones.
|
|
12
|
+
*
|
|
13
|
+
* Extracted to keep `rename.ts` under the per-file LOC budget —
|
|
14
|
+
* `rename.ts` already carries mochikit + browser-mochitest + FTL
|
|
15
|
+
* handling, and tacking xpcshell onto that tree pushed the file past
|
|
16
|
+
* the limit.
|
|
17
|
+
*/
|
|
18
|
+
import { readdir } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { loadConfig } from '../../core/config.js';
|
|
21
|
+
import { xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
22
|
+
import { snapshotFile } from '../../core/furnace-rollback.js';
|
|
23
|
+
import { toError } from '../../utils/errors.js';
|
|
24
|
+
import { ensureDir, pathExists, readText, removeDir, removeFile, writeText, } from '../../utils/fs.js';
|
|
25
|
+
import { info, warn } from '../../utils/logger.js';
|
|
26
|
+
/** Escapes regex metacharacters so a user-supplied name stays literal. */
|
|
27
|
+
function escapeRegex(input) {
|
|
28
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Renames an xpcshell test scaffold in place. Moves the directory,
|
|
32
|
+
* rewrites the test filename, updates the `[test_name]` section header
|
|
33
|
+
* in `xpcshell.toml`, and word-boundary-rewrites occurrences of the
|
|
34
|
+
* old tag / old underscored name inside the test body.
|
|
35
|
+
*
|
|
36
|
+
* Best-effort: any failure logs a warning through the shared logger
|
|
37
|
+
* but never throws — the component rename itself has already succeeded
|
|
38
|
+
* at this point, and blocking on a test rewrite would leave the
|
|
39
|
+
* operator with a half-renamed component.
|
|
40
|
+
*
|
|
41
|
+
* @param engineDir - Absolute path to the engine directory under the project.
|
|
42
|
+
* @param projectRoot - Absolute path to the project root, used to load the binary name.
|
|
43
|
+
* @param oldName - Pre-rename component tag name.
|
|
44
|
+
* @param newName - Post-rename component tag name.
|
|
45
|
+
* @param journal - Rollback journal that the rename mutation writes to before touching files.
|
|
46
|
+
*/
|
|
47
|
+
export async function renameXpcshellTestFiles(engineDir, projectRoot, oldName, newName, journal) {
|
|
48
|
+
let forgeConfig;
|
|
49
|
+
try {
|
|
50
|
+
forgeConfig = await loadConfig(projectRoot);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return; // Cannot determine scaffold path without config.
|
|
54
|
+
}
|
|
55
|
+
const parentDir = join(engineDir, xpcshellTestParentDir(forgeConfig.binaryName));
|
|
56
|
+
if (!(await pathExists(parentDir)))
|
|
57
|
+
return;
|
|
58
|
+
const oldScaffoldDir = join(parentDir, oldName);
|
|
59
|
+
const newScaffoldDir = join(parentDir, newName);
|
|
60
|
+
if (!(await pathExists(oldScaffoldDir)))
|
|
61
|
+
return;
|
|
62
|
+
const oldUnderscored = oldName.replace(/-/g, '_');
|
|
63
|
+
const newUnderscored = newName.replace(/-/g, '_');
|
|
64
|
+
const oldTestFileName = `test_${oldUnderscored}_packaged.js`;
|
|
65
|
+
const newTestFileName = `test_${newUnderscored}_packaged.js`;
|
|
66
|
+
try {
|
|
67
|
+
await ensureDir(newScaffoldDir);
|
|
68
|
+
const entries = await readdir(oldScaffoldDir, { withFileTypes: true });
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!entry.isFile())
|
|
71
|
+
continue;
|
|
72
|
+
const oldFilePath = join(oldScaffoldDir, entry.name);
|
|
73
|
+
const renamedFileName = entry.name === oldTestFileName ? newTestFileName : entry.name;
|
|
74
|
+
const newFilePath = join(newScaffoldDir, renamedFileName);
|
|
75
|
+
await snapshotFile(journal, oldFilePath);
|
|
76
|
+
const body = await readText(oldFilePath);
|
|
77
|
+
let updated = body;
|
|
78
|
+
if (entry.name === 'xpcshell.toml') {
|
|
79
|
+
updated = updated.replace(new RegExp(`\\[${escapeRegex(`"${oldTestFileName}"`)}\\]`, 'g'), `["${newTestFileName}"]`);
|
|
80
|
+
}
|
|
81
|
+
else if (entry.name === oldTestFileName) {
|
|
82
|
+
const oldTagPattern = new RegExp(`(?<![\\w-])${escapeRegex(oldName)}(?![\\w-])`, 'g');
|
|
83
|
+
updated = updated.replace(oldTagPattern, newName);
|
|
84
|
+
const oldUnderscoredPattern = new RegExp(`(?<![\\w])${escapeRegex(oldUnderscored)}(?![\\w])`, 'g');
|
|
85
|
+
updated = updated.replace(oldUnderscoredPattern, newUnderscored);
|
|
86
|
+
}
|
|
87
|
+
await writeText(newFilePath, updated);
|
|
88
|
+
await removeFile(oldFilePath);
|
|
89
|
+
}
|
|
90
|
+
await removeDir(oldScaffoldDir);
|
|
91
|
+
info(`Renamed xpcshell scaffold directory: ${xpcshellTestParentDir(forgeConfig.binaryName)}/${oldName} → ${xpcshellTestParentDir(forgeConfig.binaryName)}/${newName}`);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
warn(`Could not rename xpcshell scaffold — ${toError(error).message}. Rename the scaffold files manually if needed.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=rename-xpcshell.js.map
|
|
@@ -14,6 +14,7 @@ import { FurnaceError } from '../../errors/furnace.js';
|
|
|
14
14
|
import { toError } from '../../utils/errors.js';
|
|
15
15
|
import { copyFile, ensureDir, pathExists, readText, removeDir, removeFile, writeText, } from '../../utils/fs.js';
|
|
16
16
|
import { info, intro, note, outro, warn } from '../../utils/logger.js';
|
|
17
|
+
import { renameXpcshellTestFiles } from './rename-xpcshell.js';
|
|
17
18
|
/** Escapes regex metacharacters so a user-supplied name is literal inside a RegExp. */
|
|
18
19
|
function escapeRegex(input) {
|
|
19
20
|
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -299,6 +300,14 @@ async function performRenameMutations(args) {
|
|
|
299
300
|
// either failed the test run outright or (worse) passed for the
|
|
300
301
|
// wrong component.
|
|
301
302
|
await renameMochikitTestFiles(args.engineDir, oldName, newName, journal);
|
|
303
|
+
// 2026-04-24 eval Finding 5: xpcshell scaffolds live in yet
|
|
304
|
+
// another tree (`browser/base/content/test/<binary>-xpcshell/
|
|
305
|
+
// <name>/`). Before this call, renaming a component scaffolded
|
|
306
|
+
// with `--with-tests --xpcshell` left a directory whose name
|
|
307
|
+
// still referenced the pre-rename component, plus a test file
|
|
308
|
+
// whose underscored name referenced the old tag — both of
|
|
309
|
+
// which then failed to match the new component.
|
|
310
|
+
await renameXpcshellTestFiles(args.engineDir, projectRoot, oldName, newName, journal);
|
|
302
311
|
// Clear the stale deployed component directory so the next
|
|
303
312
|
// `furnace apply` is the single writer of the new name's
|
|
304
313
|
// deployment. Without this, eval runs showed the old widget
|