@hominis/fireforge 0.15.9 → 0.16.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 +142 -0
- package/README.md +6 -2
- package/dist/src/cli.d.ts +4 -1
- package/dist/src/cli.js +6 -3
- package/dist/src/commands/config.js +16 -5
- package/dist/src/commands/download.js +31 -4
- package/dist/src/commands/export-all.js +96 -9
- package/dist/src/commands/export.js +10 -1
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +11 -1
- package/dist/src/commands/furnace/chrome-doc-templates.js +12 -2
- package/dist/src/commands/furnace/create.js +21 -3
- package/dist/src/commands/furnace/diff.js +22 -2
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/furnace/init.js +76 -2
- package/dist/src/commands/furnace/override.js +35 -12
- package/dist/src/commands/furnace/preview.js +46 -1
- package/dist/src/commands/furnace/rename.js +14 -3
- package/dist/src/commands/lint.js +26 -2
- package/dist/src/commands/package.js +16 -5
- package/dist/src/commands/re-export.js +25 -0
- package/dist/src/commands/rebase/patch-loop.js +19 -0
- package/dist/src/commands/register.js +2 -18
- package/dist/src/commands/run.js +23 -2
- package/dist/src/commands/status.js +42 -8
- package/dist/src/commands/test.js +6 -24
- package/dist/src/commands/token.js +14 -1
- package/dist/src/commands/watch.js +14 -2
- package/dist/src/commands/wire.js +35 -9
- package/dist/src/core/branding.d.ts +23 -0
- package/dist/src/core/branding.js +39 -0
- package/dist/src/core/browser-wire.js +68 -23
- package/dist/src/core/build-baseline.d.ts +14 -0
- package/dist/src/core/build-baseline.js +61 -1
- package/dist/src/core/config-mutate.d.ts +1 -1
- package/dist/src/core/config.d.ts +17 -0
- package/dist/src/core/config.js +35 -0
- package/dist/src/core/firefox.d.ts +16 -2
- package/dist/src/core/firefox.js +7 -2
- package/dist/src/core/furnace-config.d.ts +23 -0
- package/dist/src/core/furnace-config.js +38 -0
- package/dist/src/core/mach-build-artifacts.d.ts +41 -0
- package/dist/src/core/mach-build-artifacts.js +70 -0
- package/dist/src/core/mach-error-hints.js +38 -0
- package/dist/src/core/mach-mozconfig.d.ts +25 -0
- package/dist/src/core/mach-mozconfig.js +66 -0
- package/dist/src/core/mach.d.ts +12 -1
- package/dist/src/core/mach.js +14 -1
- package/dist/src/core/manifest-rules.js +22 -1
- package/dist/src/core/patch-lint.js +43 -20
- package/dist/src/core/test-stale-check.js +46 -1
- package/dist/src/core/token-manager.js +57 -4
- package/dist/src/core/token-scaffold.d.ts +36 -0
- package/dist/src/core/token-scaffold.js +74 -0
- package/dist/src/types/commands/options.d.ts +10 -0
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +12 -0
- package/dist/src/utils/paths.d.ts +19 -0
- package/dist/src/utils/paths.js +33 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,147 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
### Config — `--force`-written keys are now readable
|
|
6
|
+
|
|
7
|
+
- `fireforge config <key>` (read mode) now consults the raw `fireforge.json` document instead of the validated, typed config that `loadConfig` produces. Before this change, `fireforge config totallyUnknown value --force` succeeded (the key was persisted to disk via `writeConfigDocument`), but the corresponding `fireforge config totallyUnknown` read threw `Unknown config key: totallyUnknown` because `validateConfig` rebuilds a clean object containing only the schema-known fields — the forced key survived on disk but was invisible to the typed read path. A new `loadRawConfigDocument` helper in `src/core/config.ts` returns the raw JSON record, and the command's read branch now traverses that document. Writes are unaffected: schema validation still enforces shape for known keys, and `--force` continues to be the escape hatch for unknown keys.
|
|
8
|
+
- The set-mode `--force` branch also now seeds the mutation from `loadRawConfigDocument`, so writing a second forced key no longer drops previously-written forced keys. Before this, the sequence `config foo 1 --force && config bar 2 --force` silently lost `foo` because the intermediate `loadConfig` stripped it out of the in-memory config.
|
|
9
|
+
|
|
10
|
+
### Download — Extracting phase spinner
|
|
11
|
+
|
|
12
|
+
- `fireforge download` now switches its spinner message from `Downloading Firefox <ver>... 100%` to `Extracting Firefox <ver>... (decompressing ~600 MB of source; typically 30–90s)` when the byte transfer completes and `tar -xf` starts. Before this change, the download spinner stayed pinned at "Downloading… 100%" for the entire extraction window — on a 601 MB ESR archive that is ~30–90 seconds of silent tar decompression where the first-run setup looked network-stalled precisely when the payload was already on disk. The new `FirefoxSourcePhaseCallback` in `src/core/firefox.ts` fires `'extract'` right before `extractTarXz`, and the download command swaps spinners on that signal; downgrading the initial `const s = spinner(...)` to `let s` is the only mutation site in the command.
|
|
13
|
+
|
|
14
|
+
### Status — `--json` returns `[]` on a clean tree
|
|
15
|
+
|
|
16
|
+
- `fireforge status --json` now emits a valid JSON document (`[]\n`) when there are no modified files, instead of falling through to the human-readable `No modified files` / `Working tree clean` banner. Before this change, the clean-tree early-return ran before the `--json` branch and silently printed human text, so automation that piped the command through a JSON parser broke precisely on the most common clean-workspace invocation. The `--raw` mode gets the same guard: a clean tree writes nothing to stdout in raw mode, matching what native `git status --porcelain` does on a clean repo.
|
|
17
|
+
|
|
18
|
+
### Furnace init — tokens CSS scaffold + raw-color allowlist
|
|
19
|
+
|
|
20
|
+
- `fireforge furnace init` now scaffolds the Furnace-managed tokens CSS at `engine/browser/themes/shared/<binaryName>-tokens.css` whenever the engine directory exists, and registers that path in `fireforge.json`'s `patchLint.rawColorAllowlist`. Before this change, `furnace init` only wrote `furnace.json`, so every fresh project's first `fireforge token add` hit `Token CSS file not found: browser/themes/shared/<binaryName>-tokens.css`. The scaffold writes a `:root { … }` shell seeded with four default category headers (`Colors — General`, `Colors — Canvas`, `Colors — Experiment`, `Spacing`) plus a `@media (prefers-color-scheme: dark)` overrides block; all four are recognised by `assertTokenCategoryExists` so `token add --category 'Colors — General' …` works end-to-end on a fresh project. The allowlist registration is idempotent (no-op when the entry is already present) so a `furnace init --force` on an existing project doesn't duplicate it.
|
|
21
|
+
- `fireforge token add`'s "Category not found" error now lists the categories actually present in the file, along with a copy-pasteable header template (`/* = My Category = */`) and pointer to `fireforge furnace init --force`. Before this, the error told operators that "categories are defined by comment headers" without showing what was available — `token add` failed silently on any typo in the category name.
|
|
22
|
+
|
|
23
|
+
### README — `token add` syntax update
|
|
24
|
+
|
|
25
|
+
- The `Additional workflow commands` block in `README.md` now reflects the `token add <token> <value>` subcommand shape shipped in 0.14+. The previous example used a `--name/--value` form that no longer exists on the CLI surface and led operators through a broken copy-paste on their first try. The updated example also points at the Furnace tokens-CSS prerequisite so first-time operators know why `fireforge furnace init` runs before `token add`.
|
|
26
|
+
|
|
27
|
+
### Furnace create — prefix mismatch is a hard refusal
|
|
28
|
+
|
|
29
|
+
- `fireforge furnace create <name>` now refuses before any filesystem writes when `furnace.json`'s `componentPrefix` is set and `<name>` does not start with it. Before this change, the prefix check was a `warn()` that let the flow continue, producing runs where the command reported success, scaffolded files under `components/custom/<name>/`, registered tests in `browser/base/moz.build`, but the result was a second-class citizen of the fork's convention — subsequent follow-ups (list, status, rename) behaved inconsistently because the name didn't match what `fireforge furnace scan` / override workflows expected to see. A new `--allow-prefix-mismatch` flag is the intentional escape hatch when the mismatch is deliberate (e.g. a throwaway experiment); without it, the command throws `InvalidArgumentError` up-front with specific guidance about prefixing the name or editing `componentPrefix` in `furnace.json`.
|
|
30
|
+
|
|
31
|
+
### Wire — `--dom` engine-relative normalisation
|
|
32
|
+
|
|
33
|
+
- `fireforge wire --dom <path>` now accepts repo-root-relative forms (`engine/browser/base/content/foo.inc.xhtml`) and engine-relative forms (`browser/base/content/foo.inc.xhtml`) interchangeably, matching `lint`/`export`/`register`/`test`. Before this, passing the `engine/`-prefixed form from the repo root sailed through `pathExists` but then double-rooted through `toRootRelativePath(engineDir, 'engine/…')` — `resolve(engineDir, 'engine/…')` landed at `engineDir/engine/…`, which passed `isPathInsideRoot` but produced a `safeDomFilePath` of `engine/browser/base/content/foo.inc.xhtml`. The computed `#include` then read `#include ../../../engine/browser/base/content/foo.inc.xhtml`, nonsense that would never preprocess correctly. `stripEnginePrefix` normalises the input before the path probe so both forms produce `#include foo.inc.xhtml` in `browser.xhtml`. The `--target` option gets the same normalisation for symmetry.
|
|
34
|
+
|
|
35
|
+
### Lint — `token-prefix-violation` scoped to added lines
|
|
36
|
+
|
|
37
|
+
- `lintPatchedCss` now scopes its `token-prefix-violation` scan to the added/modified lines when diff context is available, mirroring what the `raw-color-value` rule already does. Before this change, a small CSS-only override of a stock component (e.g. `moz-card`) was flagged for every stock `var(--moz-card-*)` reference in the unchanged portion of the applied file, because the scanner saw the full applied CSS and treated every inherited reference as if the fork had introduced it. The fix ingests `addedLinesByFile.get(file)` as the scan source when present (with CSS comments stripped), de-duplicates per-prop so the same introduced var consumed five times produces one issue instead of five, and falls back to whole-file scanning only when no diff is available (matching the pre-existing contract for callers that pass raw CSS with no diff). `localDeclarations` continues to be collected from the full file so a var declared in an unchanged line is still recognised as a same-file runtime channel.
|
|
38
|
+
|
|
39
|
+
### Lint — aggregate multi-patch size rules are warnings, not errors
|
|
40
|
+
|
|
41
|
+
- `fireforge lint` running against an aggregate diff on a multi-patch queue (the default mode, `ctx.entries.length > 1`, no file paths supplied) now emits the two size rules (`large-patch-lines`, `large-patch-files`) as warnings rather than errors. Before this, a freshly-imported patch stack of 20+ patches failed the default lint on aggregate counts that are mathematically impossible to satisfy without splitting patches that were already split — the actionable unit is the individual patch, and `--per-patch` is the right mode. Per-patch mode (`lintPerPatch`) keeps the rules as errors because the size is genuinely per-patch there. The "aggregate mode" hint line is updated to note the severity downgrade so operators see the full picture.
|
|
42
|
+
|
|
43
|
+
### Furnace chrome-doc — locale jar.mn source path fix
|
|
44
|
+
|
|
45
|
+
- `localeJarMnEntryForChromeDoc` now emits `(%browser/<name>.ftl)` as the source-path column instead of `(%<name>.ftl)`. Before this fix, the first `fireforge build` after `fireforge furnace chrome-doc create <name>` failed with `jar.mn: Cannot find <name>.ftl` during backend generation because the FTL file is scaffolded under `engine/browser/locales/en-US/browser/<name>.ftl`, and the `%`-rooted jar path resolves relative to the per-locale root (e.g. `en-US/`), not the locale bundle root. The missing `browser/` subdirectory in the source path made the entry point at `en-US/<name>.ftl`, which doesn't exist. The extended docstring on the helper calls out the path resolution rule so a future refactor can't re-introduce the drift.
|
|
46
|
+
|
|
47
|
+
### Build — gecko-profiler bindgen hint
|
|
48
|
+
|
|
49
|
+
- The `MACH_ERROR_HINTS` table gains a pattern that matches the distinctive `cannot find type \`\_CharT\` in this scope`+`gecko-profiler-` co-occurrence emitted by upstream bindgen on some macOS libc++ SDK versions. The generated alias (`pub type basic_string**\_self_view = root::std::**1::basic_string_view<\_CharT>;`) references a type name that is not in scope at the landing site, so the Rust compile fails partway through the build. The new hint points operators at Hominis' `990-infra-bindgen-basic-string-workaround.patch` (which strips the offending line post-generation) and also prints the exact file to edit (`<objdir>/release/build/gecko-profiler-\*/out/gecko/bindings.rs`) for operators not on Hominis' patch queue. The hint lands via the existing `surfaceMachErrorHints`plumbing in`src/core/mach.ts`—`mach build`already captures stderr and feeds it through`explainMachError`, so no new wiring is needed.
|
|
50
|
+
|
|
51
|
+
### Export-all — `--exclude-furnace`
|
|
52
|
+
|
|
53
|
+
- `fireforge export-all --exclude-furnace` now filters Furnace-managed paths out of the aggregate diff instead of refusing the command when any are present. Before this, a mixed workspace (Furnace overrides + non-Furnace edits) could not use `export-all` at all — the operator had to fall back to `fireforge export <paths…>` with a hand-curated file list. The filter reuses the existing `collectFurnaceManagedPrefixes` helper to identify the Furnace-owned subset, rescopes the diff via `getDiffForFilesAgainstHead` over the remaining paths, and prints a one-line info (`Excluded N furnace-managed file(s) from export; exporting M remaining path(s).`). Without the flag, the existing refusal-with-guidance stays — the default still protects against accidentally capturing Furnace files as a regular patch.
|
|
54
|
+
|
|
55
|
+
### Furnace preview — accepts `.cargo/config.toml.in`
|
|
56
|
+
|
|
57
|
+
- `assertPreviewPrerequisites` now accepts either `engine/.cargo/config.toml` OR `engine/.cargo/config.toml.in` as proof that the Rust toolchain is registered. Before this, the preflight insisted on the plain file, but `fireforge bootstrap` alone produces only the `.in` template (the plain file is generated at `mach configure` time). Operators who followed the remediation instruction ("run bootstrap then rerun preview") hit the same refusal on the retry and had no in-surface recovery. The relaxed check still catches a completely un-bootstrapped engine (neither file exists), and the separate `hasBuildArtifacts` check above remains the authoritative "no dist" guard so this relaxation doesn't weaken the signal we care about.
|
|
58
|
+
|
|
59
|
+
### Rebase — Furnace override baseVersion stamped alongside patches
|
|
60
|
+
|
|
61
|
+
- After a successful rebase, `runPatchLoop` now calls `stampFurnaceOverrideBaseVersions` (new helper in `src/core/furnace-config.ts`) to update every override's `baseVersion` in `furnace.json` to `session.toVersion`, right after the existing `stampPatchVersions` call. Before this, a successful ESR bump from, say, `140.9.0esr` to `140.9.1esr` stamped every patch's `sourceEsrVersion` but left every override in `furnace.json` pointing at the old baseline, and the very next `fireforge doctor` failed `Furnace component validation` on every override until the operator hand-updated the file. The stamp emits a one-line info (`Stamped N Furnace override baseVersion(s) to <version>.`) and no-ops cleanly when there are zero overrides. Per-override content health is still the job of `fireforge furnace validate` / `doctor --repair-furnace`; the stamp only closes the version-drift reporting gap so a successful rebase doesn't leave the workspace in a doctor-failing state.
|
|
62
|
+
|
|
63
|
+
### Build baseline — packageable fingerprints for `test --doctor`
|
|
64
|
+
|
|
65
|
+
- `BuildBaseline` gains a `packageableFingerprints: Record<path, sha256>` field recorded at build completion for every packageable-dirty engine path. `checkStaleBuildForTest` re-hashes each current packageable-dirty file and flags only those whose live hash differs from the baseline entry (or paths new since the baseline). Before this change, the stale probe reported every workdir-dirty file as "changed since the last build" every time, because a project with imported patches + Furnace-applied components always has a persistent workdir diff against HEAD — the engine HEAD SHA doesn't move between builds even though the workdir does, so `git diff --name-only HEAD` always returned the full post-import / post-apply set. The result was a warning that fired immediately after a successful full + UI build with no edits in between. The fingerprint layer captures "these files had this content when the build ran", so the only paths that register as stale now are ones whose content actually changed.
|
|
66
|
+
- Baselines written by older FireForge versions do not carry `packageableFingerprints`; the stale check falls through to the pre-0.16.0 path-only comparison in that case, so upgrading does not silently flip the semantics for already-built workspaces until the next `fireforge build` re-records the baseline.
|
|
67
|
+
|
|
68
|
+
### Known limitations (unchanged in 0.16.0)
|
|
69
|
+
|
|
70
|
+
- **`fireforge test` against a browser-test harness fails with `ERROR_SIGNEDSTATE_REQUIRED`.** Gecko's add-on manager rejects the Marionette harness add-on as unsigned when the fork build's release settings enforce signing. Pref injection at the `fireforge test` boundary needs to plumb `--setpref xpinstall.signatures.required=false` (and a matching dev-root pref) through every harness invocation type; that work is tracked for 0.17.0. Workaround: toggle `MOZ_AUTOMATION=1` / relax signing in the launched build profile out-of-band.
|
|
71
|
+
- **`fireforge test --build` can loop on xpcshell tests that read packaged chrome resources.** The `--build` path drives `mach build faster`, which regenerates the fast-rebuild subset but not the chrome package xpcshell resolves through `chrome://branding/…` / `resource:///modules/…`. The stale-check then flags the same files on the retry even though the inline build ran. Workaround: run a full `fireforge build` (without `--ui`) before `fireforge test`; a proper test-type classifier + `--full-rebuild` flag is planned for 0.17.0.
|
|
72
|
+
- **`fireforge watch` can fail with `FasterBuildException: timed out waiting for response` while a direct `watchman watch-project <engine>` succeeds.** The timeout originates entirely inside `mozbuild.faster_daemon`; FireForge has no hook to extend or recover from it. Workaround: `watchman watch-del <engineDir>` followed by `watchman watch-project <engineDir>` to reset the watcher's internal state before rerunning `fireforge watch`.
|
|
73
|
+
|
|
74
|
+
### Wire — transactional rollback
|
|
75
|
+
|
|
76
|
+
- `fireforge wire` now snapshots every file the mutation sequence may touch (`browser/base/content/browser-main.js`, conditionally `browser/base/content/browser-init.js`, the chrome document the `#include` lands in, and `browser/base/jar.mn`) before any write, and restores them when any step fails. The evaluator hit the motivating case on `hominis/`: a `wire mock-wire --init … --destroy … --dom …` run threw `Could not find insertion point in chrome document` AFTER `browser-main.js`, `browser-init.js`, and `browser/base/jar.mn` had already been mutated — the operator had to hand-revert the partial mutation. The journal plumbing reuses `createRollbackJournal` / `snapshotFile` / `restoreRollbackJournal` from Furnace's rollback module; a rollback that itself fails surfaces both the original wire failure and the rollback diagnosis in a single `GeneralError` with `review "git status" under engine/` guidance so the operator knows the engine may need manual attention.
|
|
77
|
+
- The snapshot set is conditional on which option set the run actually uses — no unused snapshot cost on a `wire` invocation that doesn't pass `--init`/`--destroy` or `--dom`. Real-fs integration tests in `browser-wire-rollback.integration.test.ts` pin both the failure-rollback contract and the successful-run pass-through so a future refactor can't regress either branch silently.
|
|
78
|
+
|
|
79
|
+
### Branding/mozconfig preflight
|
|
80
|
+
|
|
81
|
+
- New `assertBrandingMozconfigAgreement` fires at the end of `generateMozconfig` and refuses to hand off to `mach` when the just-written `engine/mozconfig` sets `--with-branding=browser/branding/<X>` while FireForge's branding tree lives at `browser/branding/<Y>`. The new `BrandingMozconfigMismatchError` enumerates three reasons — `mozconfig-missing-branding`, `name-mismatch`, `branding-dir-missing` — each with an actionable recovery line: edit `configs/common.mozconfig` to use `${binaryName}`, or align `fireforge.json`'s `binaryName` with the baked-in value. Motivating case: the evaluator's real `fresh/` tree produced a branding scaffold at `browser/branding/freshforge/` while the rendered mozconfig still pointed `mach` at `browser/branding/freshtest/moz.build`, and every first build failed deep inside moz.build resolution with a confusing "path does not exist" message. The preflight turns that into a single-line refusal before `mach` runs.
|
|
82
|
+
- The extractor (`extractWithBrandingPath`) matches `/^\s*(?:ac_add_options\s+)?--with-branding\s*=\s*(\S+)/m` so both the bare form and the `ac_add_options`-prefixed on-disk convention are recognised, picks the last match (matches `mach`'s last-write-wins semantics for overlapping `ac_add_options` calls), and normalises backslash separators before comparing.
|
|
83
|
+
- The preflight only fires on values that would actually fail under `mach`; it does not prescribe a single shape for `configs/*.mozconfig`. A follow-up (tracked for 0.17.0) will flip `setup-support.ts` to keep templates unsubstituted and substitute at `generateMozconfig` time, removing the drift vector entirely. The 0.16.0 fix is preflight-only because existing projects already have post-substitution configs; reshaping those requires a migration we intend to ship with 0.17.
|
|
84
|
+
|
|
85
|
+
### Export-all — duplicate new-file-creation guard
|
|
86
|
+
|
|
87
|
+
- `fireforge export-all` now refuses before writing when the aggregate diff would newly-create (`new file mode`) a path some other patch in the queue already creates. Motivating case on `fresh/lab/`: exporting patch 1 with `browser/modules/labforge/Hello.sys.mjs` as a new file, then running `export-all --name bye-module --category infra` without scoping the change set, produced a second patch that also claimed the same path — `verify` then failed on `files-affected-mismatch`, a cross-patch `filesAffected` conflict, AND a `duplicate-new-file-creation` error all at once, and the fix required either `patch delete` or hand-edited `re-export --files`. The new guard slots in right after the existing branding and furnace refusals at `src/commands/export-all.ts:checkDuplicateNewFileCreations`; it lists every conflicting path and every existing owner, and points the operator at `fireforge export <path>` with explicit file scoping as the clean recovery.
|
|
88
|
+
- The guard reuses `detectNewFilesInDiff` + `collectNewFileCreatorsByPath` (already used by `verify` and `status --ownership`), so the pre-export check and the post-hoc detection report exactly the same conflict set. No change to `export` itself — single-path exports stay the surgical primitive for this case.
|
|
89
|
+
|
|
90
|
+
### Path normalization parity (`lint` / `export`)
|
|
91
|
+
|
|
92
|
+
- `fireforge lint <paths...>` and `fireforge export <paths...>` now accept both repo-root-relative forms (`engine/browser/base/content/foo.js`) and engine-relative forms (`browser/base/content/foo.js`), matching the normalization already implemented by `register` and `test`. Motivating cases on both `fresh/` and `fresh/lab/`: pasting `engine/browser/base/content/fresh-extra-a.js` into `fireforge export` produced `File "engine/..." has no changes to export.` and the same input to `fireforge lint` produced `No modified files found in the specified paths.` — both because the status lookup sees paths relative to `engine/` and the explicit prefix double-rooted the candidate. Re-running with the engine-relative form succeeded. The normalization is now shared: `stripEnginePrefix` in `src/utils/paths.ts` is the single source of truth, and `register`/`test` delegate to it so every command that takes an engine-relative path treats `Engine/`, `engine\\`, and leading whitespace identically.
|
|
93
|
+
|
|
94
|
+
### Furnace override — stock auto-promotion
|
|
95
|
+
|
|
96
|
+
- `fireforge furnace override <name>` no longer rejects the component when `name` is already present in `config.stock`. Motivating case on `hominis/`: every stock-discovered widget (populated by `furnace scan`) forced the operator to hand-edit `furnace.json` before the override could be created, with the error `"<name>" is already registered as a stock component. Remove it from config.stock before creating an override.` That's busywork — the whole point of `override` is to fork a stock component. The new contract splices the name out of `config.stock` in-memory and lets the existing mutation-phase `writeFurnaceConfig` persist the promotion atomically alongside the new override entry, under the same rollback journal. The collision check for `config.overrides[name]` and `config.custom[name]` stays — those are real conflicts. Promotion emits a one-line `Promoting "<name>" from stock to override.` so the operator sees that the stock entry is gone.
|
|
97
|
+
- The batch variant `furnaceBatchOverrideCommand` applies the same promotion per-name; a batch that overrides both `moz-button` and `moz-card` demotes both from `stock` in the same `writeFurnaceConfig` write.
|
|
98
|
+
|
|
99
|
+
### Furnace diff — FTL deployment path parity
|
|
100
|
+
|
|
101
|
+
- `fireforge furnace diff <name>` now resolves `.ftl` entries through the configured `ftlDir` instead of the component's `targetPath`, matching the path `furnace apply`'s `applyCustomFtlFile` writes to. Before the fix, a custom component with a `.ftl` always reported "`<name>.ftl`: not yet deployed to engine (new file)" after a clean apply, because `diff` probed `engine/<customConfig.targetPath>/<name>.ftl` while the deployed file was at `engine/<ftlDir>/<name>.ftl`. The diff header also now names the locale path (`--- engine/<ftlDir>/<name>.ftl`) so the rendered diff anchors at the same target on both sides. Motivating case on `hominis/`: `furnace diff moz-lab-pill` after a successful apply/deploy reported the `.ftl` as new even though `engine/toolkit/locales/en-US/toolkit/global/moz-lab-pill.ftl` was present on disk and `furnace status moz-lab-pill` reported clean — `diff`, `status`, and `validate` now agree on deployed state.
|
|
102
|
+
|
|
103
|
+
### Furnace rename — path-label fix
|
|
104
|
+
|
|
105
|
+
- `fireforge furnace rename <old> <new>` for a custom component now reports `components/custom/<new>/` instead of `components/customs/<new>/` in the guidance that follows a successful rename (and in the "directory not found" / "target directory already exists" error messages). The actual filesystem operations always used `furnacePaths.customDir` / `furnacePaths.overridesDir` correctly — this was a cosmetic mis-pluralisation produced by appending `s` to the `custom` / `override` furnace-state key — but operators who copied the path from the message to a `cd` or `ls` invocation hit "no such file or directory". Override renames continue to name `components/overrides/<new>/` (which was already correct by coincidence with the plural on-disk dir). A `componentDirLabel` helper centralises the singular/plural pick so a future refactor cannot re-introduce the drift.
|
|
106
|
+
|
|
107
|
+
### Furnace preview — build + toolchain preflight
|
|
108
|
+
|
|
109
|
+
- `fireforge furnace preview` now refuses before staging components or running the multi-minute `mach storybook upgrade` npm install when either (a) no `obj-*/dist/` tree exists, or (b) `.cargo/config.toml` is absent under the engine directory. Motivating case on `fresh/`: on an otherwise-unbuilt engine, preview staged workspace components into `engine/`, npm-installed 1023 packages into the upstream Storybook workspace, and only then failed deep in `mach storybook` with errors about `.cargo/config.toml` and `chrome-map.json` — both artefacts `fireforge build` / `fireforge bootstrap` would have produced. The new preflight catches the dominant failure mode (no `dist/`) fast via the existing `hasBuildArtifacts` helper and points the operator at `fireforge build`; the secondary check fires on a bootstrap-incomplete tree and points at `fireforge bootstrap`. Extracted into `assertPreviewPrerequisites` so `furnacePreviewCommand` stays under the per-function LOC budget as the preflight list grows.
|
|
110
|
+
|
|
111
|
+
### Register — `.inc.xhtml` fragments routed through `getUnregistrableAdvice`
|
|
112
|
+
|
|
113
|
+
- The browser-content registration pattern in `src/core/manifest-rules.ts` now excludes `.inc.xhtml` fragments under `browser/base/content/`. Motivating case on `fresh/`: wiring a `browser/base/content/fresh-fragment.inc.xhtml` into `browser.xhtml` with `fireforge wire ... --dom …` then running `fireforge status` flagged the fragment as `Potentially unregistered`, and `fireforge register <fragment>.inc.xhtml --dry-run` proposed a bogus `browser/base/jar.mn` entry. `.inc.xhtml` files are deliberately consumed via `#include` from a registered chrome document; they don't need a separate chrome URI entry. The narrowed regex now routes these paths through `getUnregistrableAdvice`, which emits specific guidance: "`.inc.xhtml` fragments are consumed via `#include` from a registered chrome document — run `fireforge wire <name> --dom <path>`, or add the `#include` directive manually in the top-level chrome document." Plain `.xhtml` files under `browser/base/content/` still match the registrable pattern.
|
|
114
|
+
|
|
115
|
+
### Stackless precondition errors
|
|
116
|
+
|
|
117
|
+
- `getProjectRoot` now throws a typed `ConfigNotFoundError` (exit code `CONFIG_ERROR` / 2) instead of a plain `Error` when no `fireforge.json` exists in any ancestor of the current working directory. Motivating case on `fresh/` before setup: every precondition-checking command (`doctor`, `status`, `download`, `import`, `build`, `run`, `test`, `lint`, `verify`) printed `Unexpected error: Could not find fireforge.json...` followed by a full JS stack trace — a routine user mistake surfaced as what looked like an internal crash. `withErrorHandling` now routes the `FireForgeError` subclass through `logError(error.userMessage)` and exits with the CONFIG_ERROR code; the stack-dump fallback stays in place for genuinely unexpected errors. The user-facing message is the already-defined `ConfigNotFoundError` copy — `This directory does not appear to be a FireForge project. Navigate to your project root directory, or run "fireforge setup" to initialize a new project.`
|
|
118
|
+
|
|
119
|
+
### Packager NoneType hint
|
|
120
|
+
|
|
121
|
+
- `fireforge package` now captures `mach package`'s streamed output (via the new `machPackageCapture` helper that layers over `runMachCapture`) and feeds the stderr tail through `explainMachError` on non-zero exit. A new hint pattern matches the `AttributeError: 'NoneType' object has no attribute 'open'` / `packager.py` co-occurrence (either ordering) and surfaces an actionable guidance line: "This usually means the packager was handed an incomplete `obj-*/dist/` tree — e.g. running `fireforge package` before a full `fireforge build` (not --ui) completed. Re-run `fireforge build` to completion … before rerunning `fireforge package`." Motivating case on a real `hominis/` tree that reached `mach package` but had not completed a full build: the raw mach traceback was surfaced but wrapped only in "Packaging failed with exit code 1"; operators had to read the Python traceback to learn that the `obj-*/dist/` tree was incomplete. The hint now lands in the thrown `BuildError` directly after the generic exit-code line, so the operator reads the recovery instruction before scrolling up through the traceback.
|
|
122
|
+
- The legacy `machPackage` helper stays exported for callers that only need the exit code; the new variant is additive. A negative-match test pins the pattern to the specific `packager.py` + `.open` pairing so unrelated `NoneType` errors elsewhere in mach output don't falsely trigger the hint.
|
|
123
|
+
|
|
124
|
+
### Run + watch — bundle-readiness agreement
|
|
125
|
+
|
|
126
|
+
- `fireforge run` now probes for the launchable binary (`obj-*/dist/<App>.app/Contents/MacOS/<binary>` on macOS, `obj-*/dist/bin/<binary>` on Linux, `obj-*/dist/bin/<binary>.exe` on Windows) via the new `hasRunnableBundle` helper in `src/core/mach-build-artifacts.ts`. Motivating case on a mid-build `hominis/` tree: `fireforge run` failed inside `mach run` because `dist/Hominis.app/Contents/MacOS/hominis` didn't exist yet, while `fireforge watch` happily announced "Using build artifacts from obj-…/" and entered watch mode — the two commands disagreed about whether the obj dir was usable. `run` now refuses up-front with a targeted error naming the missing path ("the expected binary at `obj-debug/dist/MyBrowser.app/Contents/MacOS/mybrowser` is missing — the build may have aborted or is still in progress"). `watch` stays permissive (it exists to drive rebuilds of partially-built trees) but now reports the bundle state in its startup banner: `Using build artifacts from obj-…/ (bundle: runnable)` or `Using build artifacts from obj-…/ (bundle: pending — watch will rebuild)`, so the operator can see at a glance why `run` would refuse right now.
|
|
127
|
+
- `hasRunnableBundle` is exported from `mach.ts` alongside `hasBuildArtifacts`, degrades to `runnable: false` on readdir failure (no throw), and reports `expectedPath` even on the not-runnable branch so error copy can always name what to look for on disk. The three-platform probe is covered by explicit unit tests that mock `getPlatform`.
|
|
128
|
+
|
|
129
|
+
### Token add — Furnace-initialized precondition
|
|
130
|
+
|
|
131
|
+
- `fireforge token add` now refuses before any normalization when `furnace.json` is missing under the project root. Motivating case on `fresh/`: running `token add surface-test '#abcdef' --category 'Colors — Canvas' --mode static --dry-run` on a brand-new project surfaced a warning about the missing `furnace.json` and then hard-failed with `Token CSS file not found: browser/themes/shared/freshforge-tokens.css` — technically correct, but the missing tokens CSS is a downstream artefact of Furnace not being initialized. The new guard throws a targeted `FurnaceError`: `Token management requires Furnace. Run "fireforge furnace init" first, then rerun "fireforge token add …"`. Projects with an initialized Furnace hit the existing "tokens CSS file not found" path unchanged when they're missing the rendered tokens file specifically (e.g. after a manual deletion).
|
|
132
|
+
|
|
133
|
+
### Re-export — stale-manifest advisory without `--scan`
|
|
134
|
+
|
|
135
|
+
- `fireforge re-export <patch>` now warns at the start of a single-patch run when `--scan`/`--files` is not set AND one or more files in `patches.json`'s `filesAffected` no longer exist on disk. Motivating case on `fresh/lab/`: after discarding a file and re-exporting the patch without `--scan`, the refreshed patch body targeted fewer files than the manifest claimed, and `fireforge verify` then failed on manifest-consistency. The warning names the missing paths and spells out both recovery modes: re-run with `--scan` to reconcile `filesAffected` with the worktree, or `--files <paths>` to set the list explicitly. Behaviour is unchanged when `--scan` / `--files` is set (those modes already reconcile the manifest); this is advisory-only.
|
|
136
|
+
|
|
137
|
+
### Download — indexing banner
|
|
138
|
+
|
|
139
|
+
- `fireforge download` now emits a one-line `Indexing downloaded source into git (one-time; typically 1–3 minutes on a ~600 MB Firefox tree)...` before starting the git-init spinner. Motivating case on `fresh/`: after the 609 MB archive download completed, the `git add -A` pass ran silently for minutes, long enough that a CI log tail or non-TTY shell looked like the command had hung. The banner lands via `info` (not the interactive spinner) so non-TTY wrappers still see the heads-up in log scrollback.
|
|
140
|
+
|
|
141
|
+
### Status — atomic-temp-file filter
|
|
142
|
+
|
|
143
|
+
- `fireforge status` no longer surfaces FireForge's own in-flight atomic-write temp files in any output mode. Motivating case on `hominis/`: a `status --json` that coincided with a `brand.ftl` or `mozconfig` write briefly listed paths like `.brand.ftl.fireforge-tmp-12345-<uuid>` and `.mozconfig.fireforge-tmp-12345-<uuid>` alongside real changes. `src/utils/fs.ts` now exports `FIREFORGE_TMP_PATH_PATTERN`, a regex anchored on the exact shape `createAtomicTempPath` produces (`<dir>/.<filename>.fireforge-tmp-<pid>-<uuid>`), and `status.ts` filters every status entry through it after `expandDirectoryEntries` and before classification. All status modes — default, raw, unmanaged, ownership, json — apply the same filter, so a late `status` call during a large write produces the same output regardless of which view the operator chose. The pattern is tight enough to let an operator-named `.notes.fireforge-tmp-backup` (no PID+UUID continuation) pass through unfiltered.
|
|
144
|
+
|
|
3
145
|
## 0.15.0
|
|
4
146
|
|
|
5
147
|
### Re-export — opt-in `--stamp` and per-patch `lintIgnore`
|
package/README.md
CHANGED
|
@@ -115,6 +115,8 @@ fireforge import --force
|
|
|
115
115
|
|
|
116
116
|
### Exporting changes
|
|
117
117
|
|
|
118
|
+
`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.
|
|
119
|
+
|
|
118
120
|
```bash
|
|
119
121
|
# Single file
|
|
120
122
|
fireforge export browser/base/content/browser.js
|
|
@@ -425,10 +427,12 @@ fireforge package
|
|
|
425
427
|
# Watch for file changes and auto-rebuild
|
|
426
428
|
fireforge watch
|
|
427
429
|
|
|
428
|
-
# Add a CSS design token
|
|
429
|
-
fireforge token --
|
|
430
|
+
# Add a CSS design token (requires `fireforge furnace init` first; see the Furnace/Tokens section below)
|
|
431
|
+
fireforge token add --category 'Colors — General' -- --my-color 'light-dark(#fff, #000)'
|
|
430
432
|
```
|
|
431
433
|
|
|
434
|
+
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` also registers the tokens CSS path in `patchLint.rawColorAllowlist` so raw color literals inside it are not flagged by `fireforge lint`.
|
|
435
|
+
|
|
432
436
|
### Diff-scoped lint (`lint --since`)
|
|
433
437
|
|
|
434
438
|
`fireforge lint --since <git-rev>` tags each issue as `[introduced]` or `[cumulative]` based on whether its file changed since `<git-rev>`:
|
package/dist/src/cli.d.ts
CHANGED
|
@@ -11,7 +11,10 @@ export declare function resetBrokenPipeHandlerForTests(): void;
|
|
|
11
11
|
/**
|
|
12
12
|
* Gets the project root directory.
|
|
13
13
|
* Walks up from the current working directory until a fireforge.json is found.
|
|
14
|
-
* Throws
|
|
14
|
+
* Throws a {@link ConfigNotFoundError} (code: CONFIG_ERROR) when no
|
|
15
|
+
* fireforge.json is found within the walk depth limit — the error is
|
|
16
|
+
* user-facing so `withErrorHandling` can print the guidance without
|
|
17
|
+
* the stack dump that a plain `Error` would trigger.
|
|
15
18
|
*/
|
|
16
19
|
export declare function getProjectRoot(): string;
|
|
17
20
|
/**
|
package/dist/src/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { Command, Help } from 'commander';
|
|
|
5
5
|
import { COMMAND_MANIFEST } from './commands/manifest.js';
|
|
6
6
|
import { CancellationError, CommandError, FireForgeError } from './errors/base.js';
|
|
7
7
|
import { ExitCode } from './errors/codes.js';
|
|
8
|
+
import { ConfigNotFoundError } from './errors/config.js';
|
|
8
9
|
import { toError } from './utils/errors.js';
|
|
9
10
|
import { cancel, error as logError, setVerbose } from './utils/logger.js';
|
|
10
11
|
import { getPackageVersion } from './utils/package-root.js';
|
|
@@ -64,7 +65,10 @@ const MAX_PROJECT_ROOT_WALK_DEPTH = 50;
|
|
|
64
65
|
/**
|
|
65
66
|
* Gets the project root directory.
|
|
66
67
|
* Walks up from the current working directory until a fireforge.json is found.
|
|
67
|
-
* Throws
|
|
68
|
+
* Throws a {@link ConfigNotFoundError} (code: CONFIG_ERROR) when no
|
|
69
|
+
* fireforge.json is found within the walk depth limit — the error is
|
|
70
|
+
* user-facing so `withErrorHandling` can print the guidance without
|
|
71
|
+
* the stack dump that a plain `Error` would trigger.
|
|
68
72
|
*/
|
|
69
73
|
export function getProjectRoot() {
|
|
70
74
|
const start = resolve(process.cwd());
|
|
@@ -78,8 +82,7 @@ export function getProjectRoot() {
|
|
|
78
82
|
break;
|
|
79
83
|
current = parent;
|
|
80
84
|
}
|
|
81
|
-
throw new
|
|
82
|
-
'Are you inside a FireForge project?');
|
|
85
|
+
throw new ConfigNotFoundError('fireforge.json');
|
|
83
86
|
}
|
|
84
87
|
/**
|
|
85
88
|
* Wraps a command handler with error handling.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { configExists, loadConfig, mutateConfig, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, writeConfig, writeConfigDocument, } from '../core/config.js';
|
|
1
|
+
import { configExists, loadConfig, loadRawConfigDocument, mutateConfig, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, writeConfig, writeConfigDocument, } from '../core/config.js';
|
|
2
2
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
3
3
|
import { toError } from '../utils/errors.js';
|
|
4
4
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
@@ -83,10 +83,15 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
83
83
|
if (!(await configExists(projectRoot))) {
|
|
84
84
|
throw new GeneralError('No fireforge.json found. Run "fireforge setup" to create a project.');
|
|
85
85
|
}
|
|
86
|
-
const config = await loadConfig(projectRoot);
|
|
87
86
|
if (value === undefined) {
|
|
88
|
-
// Get mode
|
|
89
|
-
|
|
87
|
+
// Get mode — read the raw document rather than the validated config so
|
|
88
|
+
// keys persisted via `fireforge config <key> --force` remain readable.
|
|
89
|
+
// `validateConfig` builds a typed clone containing only the known
|
|
90
|
+
// schema fields; relying on it here would silently hide forced-write
|
|
91
|
+
// keys and surface "Unknown config key" on the read even though the
|
|
92
|
+
// key is sitting plainly inside fireforge.json.
|
|
93
|
+
const rawConfig = await loadRawConfigDocument(projectRoot);
|
|
94
|
+
const currentValue = getNestedValue(rawConfig, key);
|
|
90
95
|
if (currentValue === undefined) {
|
|
91
96
|
throw new InvalidArgumentError(`Unknown config key: ${key}`);
|
|
92
97
|
}
|
|
@@ -113,10 +118,16 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
113
118
|
// listed in SUPPORTED_CONFIG_PATHS, regardless of --force, and only
|
|
114
119
|
// skip validation for genuinely unknown key paths.
|
|
115
120
|
if (options.force && !keyIsKnown) {
|
|
116
|
-
|
|
121
|
+
// Seed mutation from the raw on-disk document so previously-forced
|
|
122
|
+
// keys (which `validateConfig` would strip) survive the round-trip.
|
|
123
|
+
// Without this, writing a second --force key would silently drop
|
|
124
|
+
// every earlier forced key from fireforge.json.
|
|
125
|
+
const rawConfig = await loadRawConfigDocument(projectRoot);
|
|
126
|
+
const updatedConfig = mutateConfig(rawConfig, key, parsedValue, true);
|
|
117
127
|
await writeConfigDocument(projectRoot, updatedConfig);
|
|
118
128
|
}
|
|
119
129
|
else {
|
|
130
|
+
const config = await loadConfig(projectRoot);
|
|
120
131
|
const updatedConfig = mutateConfig(config, key, parsedValue);
|
|
121
132
|
await writeConfig(projectRoot, updatedConfig);
|
|
122
133
|
}
|
|
@@ -172,9 +172,16 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
172
172
|
// Ensure cache directory exists
|
|
173
173
|
const cacheDir = join(paths.fireforgeDir, 'cache');
|
|
174
174
|
await ensureDir(cacheDir);
|
|
175
|
-
//
|
|
176
|
-
|
|
175
|
+
// Phase-switched spinners: the download phase runs with the byte-count
|
|
176
|
+
// progress callbacks below; the extract phase is blocking tar-xz and
|
|
177
|
+
// has no incremental progress, but it can take 30–90s on a ~600 MB
|
|
178
|
+
// Firefox tree, so it gets its own spinner message. Before the phase
|
|
179
|
+
// split, a single "Downloading Firefox … 100%" spinner covered both
|
|
180
|
+
// — the first-run setup looked hung precisely when the archive had
|
|
181
|
+
// already reached disk and `tar` was the long pole.
|
|
182
|
+
let s = spinner(`Downloading Firefox ${version}...`);
|
|
177
183
|
let lastPercent = 0;
|
|
184
|
+
const phaseState = { value: 'download' };
|
|
178
185
|
try {
|
|
179
186
|
await downloadFirefoxSource(version, config.firefox.product, paths.engine, cacheDir, (downloaded, total) => {
|
|
180
187
|
if (total <= 0)
|
|
@@ -184,13 +191,33 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
184
191
|
s.message(`Downloading Firefox ${version}... ${percent}% (${formatBytes(downloaded)} / ${formatBytes(total)})`);
|
|
185
192
|
lastPercent = percent;
|
|
186
193
|
}
|
|
194
|
+
}, (phase) => {
|
|
195
|
+
if (phase === 'extract' && phaseState.value === 'download') {
|
|
196
|
+
s.stop(`Firefox ${version} downloaded`);
|
|
197
|
+
phaseState.value = 'extract';
|
|
198
|
+
s = spinner(`Extracting Firefox ${version}... (decompressing ~600 MB of source; typically 30–90s)`);
|
|
199
|
+
}
|
|
187
200
|
});
|
|
188
|
-
|
|
201
|
+
if (phaseState.value === 'extract') {
|
|
202
|
+
s.stop(`Firefox ${version} extracted`);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
s.stop(`Firefox ${version} downloaded`);
|
|
206
|
+
}
|
|
189
207
|
}
|
|
190
208
|
catch (error) {
|
|
191
|
-
s.error('Download failed');
|
|
209
|
+
s.error(phaseState.value === 'extract' ? 'Extraction failed' : 'Download failed');
|
|
192
210
|
throw error;
|
|
193
211
|
}
|
|
212
|
+
// Finding #17: the git indexing phase of `download` can block for
|
|
213
|
+
// minutes on a ~600 MB Firefox tree — the spinner updates less often
|
|
214
|
+
// than operators expect during the monolithic `git add -A` pass, and
|
|
215
|
+
// non-TTY shells see long stretches of silence. Emit a one-line
|
|
216
|
+
// heads-up banner BEFORE the spinner starts so even a log-scraping
|
|
217
|
+
// CI job notes the expected duration. The progress callbacks below
|
|
218
|
+
// still fire as usual; this is an additional up-front signal, not a
|
|
219
|
+
// replacement.
|
|
220
|
+
info('Indexing downloaded source into git (one-time; typically 1–3 minutes on a ~600 MB Firefox tree)...');
|
|
194
221
|
// Initialize git repository
|
|
195
222
|
const gitSpinner = spinner('Initializing git repository (this may take a few minutes)...');
|
|
196
223
|
let baseCommit;
|
|
@@ -4,10 +4,11 @@ import { isBrandingManagedPath } from '../core/branding.js';
|
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
5
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
6
6
|
import { hasChanges, isGitRepository } from '../core/git.js';
|
|
7
|
-
import { getAllDiff } from '../core/git-diff.js';
|
|
7
|
+
import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
8
8
|
import { getWorkingTreeStatus } from '../core/git-status.js';
|
|
9
9
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
10
10
|
import { commitExportedPatch } from '../core/patch-export.js';
|
|
11
|
+
import { buildPatchQueueContext, collectNewFileCreatorsByPath, detectNewFilesInDiff, } from '../core/patch-lint.js';
|
|
11
12
|
import { GeneralError } from '../errors/base.js';
|
|
12
13
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
13
14
|
import { info, intro, outro, spinner } from '../utils/logger.js';
|
|
@@ -24,18 +25,76 @@ async function checkBrandingManagedFiles(paths, config) {
|
|
|
24
25
|
'Review these files with "fireforge status" first. If you intentionally want a branding patch, export the specific branding paths explicitly with "fireforge export ...".');
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Policy around Furnace-managed files in the aggregate diff.
|
|
30
|
+
*
|
|
31
|
+
* Default behavior refuses the export (Furnace paths belong to
|
|
32
|
+
* `furnace apply`). `--exclude-furnace` flips the policy from refusal to
|
|
33
|
+
* filtering: the command still runs, but the Furnace-managed paths are
|
|
34
|
+
* dropped from the diff and counted in an info line so operators in
|
|
35
|
+
* mixed workspaces can capture only the non-Furnace subset.
|
|
36
|
+
*
|
|
37
|
+
* Returns the set of Furnace-managed paths to exclude (empty when the
|
|
38
|
+
* policy is "refuse" and nothing is in the working tree).
|
|
39
|
+
*/
|
|
40
|
+
async function resolveFurnaceExclusionPolicy(paths, projectRoot, excludeFurnace) {
|
|
28
41
|
const prefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
29
42
|
if (prefixes.size === 0)
|
|
30
|
-
return;
|
|
43
|
+
return new Set();
|
|
31
44
|
const changedFiles = await getWorkingTreeStatus(paths.engine);
|
|
32
45
|
const furnaceManagedFiles = changedFiles
|
|
33
46
|
.flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
|
|
34
47
|
.filter((file) => [...prefixes].some((prefix) => file.startsWith(prefix)));
|
|
35
|
-
if (furnaceManagedFiles.length
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
if (furnaceManagedFiles.length === 0)
|
|
49
|
+
return new Set();
|
|
50
|
+
if (excludeFurnace) {
|
|
51
|
+
return new Set(furnaceManagedFiles);
|
|
38
52
|
}
|
|
53
|
+
throw new GeneralError('Export-all refuses to capture Furnace-managed component changes.\n\n' +
|
|
54
|
+
'These files are deployed by "fireforge furnace apply" and should be managed through the Furnace workflow. ' +
|
|
55
|
+
'Review them with "fireforge status" or "fireforge furnace status", ' +
|
|
56
|
+
'or pass --exclude-furnace to export the non-Furnace subset of the diff.');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Refuses the export when the aggregate diff would create (new-file-mode) a
|
|
60
|
+
* path that some existing patch in the queue already creates. `verify`
|
|
61
|
+
* detects this post-hoc via `collectNewFileCreatorsByPath`, but by the time
|
|
62
|
+
* `verify` runs the operator has already landed a patch that irreversibly
|
|
63
|
+
* sits atop the queue — resolving the conflict from there requires a
|
|
64
|
+
* `patch delete` or hand-surgery on `re-export --files`. Catching it here,
|
|
65
|
+
* pre-write, keeps the queue clean and gives the operator a specific path
|
|
66
|
+
* to narrow with `export` + explicit file scoping (which lets them drop
|
|
67
|
+
* the already-claimed path without losing other edits).
|
|
68
|
+
*
|
|
69
|
+
* Slots in alongside the existing branding and furnace guards so the three
|
|
70
|
+
* "export-all refuses" branches remain the single, symmetric fence around
|
|
71
|
+
* unintended captures.
|
|
72
|
+
*/
|
|
73
|
+
async function checkDuplicateNewFileCreations(paths, diff) {
|
|
74
|
+
if (!(await pathExists(paths.patches)))
|
|
75
|
+
return;
|
|
76
|
+
const pendingNewFiles = detectNewFilesInDiff(diff);
|
|
77
|
+
if (pendingNewFiles.size === 0)
|
|
78
|
+
return;
|
|
79
|
+
const ctx = await buildPatchQueueContext(paths.patches);
|
|
80
|
+
if (ctx.entries.length === 0)
|
|
81
|
+
return;
|
|
82
|
+
const creators = collectNewFileCreatorsByPath(ctx);
|
|
83
|
+
const conflicts = [];
|
|
84
|
+
for (const path of pendingNewFiles) {
|
|
85
|
+
const owners = creators.get(path);
|
|
86
|
+
if (owners && owners.length > 0) {
|
|
87
|
+
conflicts.push({ path, owners });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (conflicts.length === 0)
|
|
91
|
+
return;
|
|
92
|
+
const conflictList = conflicts
|
|
93
|
+
.map(({ path, owners }) => ` • ${path} — already created by ${owners.join(', ')}`)
|
|
94
|
+
.join('\n');
|
|
95
|
+
throw new GeneralError('Export-all refuses to capture new-file creations that are already claimed by existing patches.\n\n' +
|
|
96
|
+
`Conflicting creations:\n${conflictList}\n\n` +
|
|
97
|
+
'Only one patch may create a given path. Run "fireforge export <path> [...]" with an explicit file list that omits the already-claimed path(s), or resolve the conflict via "fireforge patch delete" / "fireforge re-export --files" before retrying export-all.');
|
|
39
98
|
}
|
|
40
99
|
/**
|
|
41
100
|
* Runs the export-all command to export all changes as a patch.
|
|
@@ -61,14 +120,41 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
61
120
|
}
|
|
62
121
|
const config = await loadConfig(projectRoot);
|
|
63
122
|
await checkBrandingManagedFiles(paths, config);
|
|
64
|
-
await
|
|
65
|
-
// Get the full diff
|
|
66
|
-
|
|
123
|
+
const furnaceExcluded = await resolveFurnaceExclusionPolicy(paths, projectRoot, options.excludeFurnace);
|
|
124
|
+
// Get the full diff. When --exclude-furnace is set and furnaceExcluded
|
|
125
|
+
// is non-empty, rescope the diff to the non-Furnace path subset so the
|
|
126
|
+
// resulting patch does not contain any Furnace-managed hunks. We use
|
|
127
|
+
// `getDiffForFilesAgainstHead` over the filtered path list rather than
|
|
128
|
+
// post-hoc string surgery on the aggregate diff, which keeps the
|
|
129
|
+
// output shape aligned with the single-file `export` command.
|
|
130
|
+
let diff;
|
|
131
|
+
if (furnaceExcluded.size > 0) {
|
|
132
|
+
const allChanged = await getWorkingTreeStatus(paths.engine);
|
|
133
|
+
const nonFurnacePaths = [
|
|
134
|
+
...new Set(allChanged
|
|
135
|
+
.flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
|
|
136
|
+
.filter((file) => !furnaceExcluded.has(file))),
|
|
137
|
+
].sort();
|
|
138
|
+
if (nonFurnacePaths.length === 0) {
|
|
139
|
+
info(`Excluded ${furnaceExcluded.size} furnace-managed file(s) from export; no non-Furnace changes remain.`);
|
|
140
|
+
outro('Nothing to export');
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
diff = await getDiffForFilesAgainstHead(paths.engine, nonFurnacePaths);
|
|
144
|
+
info(`Excluded ${furnaceExcluded.size} furnace-managed file(s) from export; exporting ${nonFurnacePaths.length} remaining path(s).`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
diff = await getAllDiff(paths.engine);
|
|
148
|
+
}
|
|
67
149
|
if (!diff.trim()) {
|
|
68
150
|
info('No diff content to export');
|
|
69
151
|
outro('Nothing to export');
|
|
70
152
|
return;
|
|
71
153
|
}
|
|
154
|
+
// Duplicate-creation preflight needs the diff in hand to see which paths
|
|
155
|
+
// the aggregate would newly create, so it runs here instead of alongside
|
|
156
|
+
// the branding / furnace guards that operate on the raw status list.
|
|
157
|
+
await checkDuplicateNewFileCreations(paths, diff);
|
|
72
158
|
// Check for non-interactive mode
|
|
73
159
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
74
160
|
// Auto-fix missing license headers on new files (interactive only)
|
|
@@ -126,6 +212,7 @@ export function registerExportAll(program, { getProjectRoot, withErrorHandling }
|
|
|
126
212
|
.option('-d, --description <desc>', 'Description of the patch')
|
|
127
213
|
.option('--supersede', 'Allow superseding multiple existing patches')
|
|
128
214
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
215
|
+
.option('--exclude-furnace', 'Export the non-Furnace subset of the aggregate diff instead of refusing when Furnace-managed files are modified. Furnace-managed files are still deployed by "fireforge furnace apply"; this flag only changes whether export-all aborts or filters in their presence.')
|
|
129
216
|
.action(withErrorHandling(async (options) => {
|
|
130
217
|
const { category, ...rest } = options;
|
|
131
218
|
await exportAllCommand(getProjectRoot(), {
|
|
@@ -16,6 +16,7 @@ import { toError } from '../utils/errors.js';
|
|
|
16
16
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
17
17
|
import { info, intro, outro, spinner, verbose, warn } from '../utils/logger.js';
|
|
18
18
|
import { pickDefined } from '../utils/options.js';
|
|
19
|
+
import { stripEnginePrefix } from '../utils/paths.js';
|
|
19
20
|
import { parsePositiveIntegerFlag, PATCH_CATEGORIES } from '../utils/validation.js';
|
|
20
21
|
import { commitPlacementExport, placementSummary, projectPlacementForLint, renderDryRunPreview, resolvePlacementPlan, } from './export-flow.js';
|
|
21
22
|
import { autoFixLicenseHeaders, confirmSupersedePatches, promptExportPatchMetadata, runPatchLint, } from './export-shared.js';
|
|
@@ -23,7 +24,15 @@ async function collectExportFiles(paths, files) {
|
|
|
23
24
|
const collectedFiles = new Set();
|
|
24
25
|
let fileStatuses;
|
|
25
26
|
let untrackedFiles;
|
|
26
|
-
|
|
27
|
+
// Accept both repo-root-relative (`engine/browser/...`) and engine-relative
|
|
28
|
+
// (`browser/...`) paths for every input, matching `register`/`test`/`lint`.
|
|
29
|
+
// Previously, an `engine/`-prefixed path fell through to
|
|
30
|
+
// `File "engine/..." has no changes to export.` because the status lookup
|
|
31
|
+
// sees paths relative to `paths.engine` and the explicit prefix double-
|
|
32
|
+
// rooted the candidate. `stripEnginePrefix` makes that user-facing form
|
|
33
|
+
// a no-op for the lookup pipeline.
|
|
34
|
+
for (const rawInputPath of files) {
|
|
35
|
+
const inputPath = stripEnginePrefix(rawInputPath);
|
|
27
36
|
const fullInputPath = join(paths.engine, inputPath);
|
|
28
37
|
let isDirectory = false;
|
|
29
38
|
try {
|
|
@@ -62,5 +62,15 @@ export declare function generateChromeDocFtl(name: string, licenseHeader: string
|
|
|
62
62
|
export declare function jarMnEntriesForChromeDoc(name: string): string[];
|
|
63
63
|
/** jar.inc.mn entry that registers the scoped CSS under `content/browser/`. */
|
|
64
64
|
export declare function jarIncMnEntryForChromeDoc(name: string): string;
|
|
65
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* locales/jar.mn entry that registers the `.ftl` under the browser locale
|
|
67
|
+
* bundle. The source path is resolved by mach-locale-jar relative to the
|
|
68
|
+
* per-locale root (e.g. `engine/browser/locales/en-US/`), and the FTL
|
|
69
|
+
* file is scaffolded at `browser/${name}.ftl` under that root — the `%`
|
|
70
|
+
* prefix means "per-locale content" and the `browser/` subdirectory
|
|
71
|
+
* matches the subdir the scaffolder writes into. Before this fix the
|
|
72
|
+
* entry emitted `(%${name}.ftl)`, which pointed at `en-US/${name}.ftl`
|
|
73
|
+
* and broke the first post-scaffold `fireforge build` with
|
|
74
|
+
* "jar.mn: Cannot find ${name}.ftl".
|
|
75
|
+
*/
|
|
66
76
|
export declare function localeJarMnEntryForChromeDoc(name: string): string;
|
|
@@ -162,8 +162,18 @@ export function jarMnEntriesForChromeDoc(name) {
|
|
|
162
162
|
export function jarIncMnEntryForChromeDoc(name) {
|
|
163
163
|
return ` content/browser/${name}-chrome.css (shared/${name}-chrome.css)`;
|
|
164
164
|
}
|
|
165
|
-
/**
|
|
165
|
+
/**
|
|
166
|
+
* locales/jar.mn entry that registers the `.ftl` under the browser locale
|
|
167
|
+
* bundle. The source path is resolved by mach-locale-jar relative to the
|
|
168
|
+
* per-locale root (e.g. `engine/browser/locales/en-US/`), and the FTL
|
|
169
|
+
* file is scaffolded at `browser/${name}.ftl` under that root — the `%`
|
|
170
|
+
* prefix means "per-locale content" and the `browser/` subdirectory
|
|
171
|
+
* matches the subdir the scaffolder writes into. Before this fix the
|
|
172
|
+
* entry emitted `(%${name}.ftl)`, which pointed at `en-US/${name}.ftl`
|
|
173
|
+
* and broke the first post-scaffold `fireforge build` with
|
|
174
|
+
* "jar.mn: Cannot find ${name}.ftl".
|
|
175
|
+
*/
|
|
166
176
|
export function localeJarMnEntryForChromeDoc(name) {
|
|
167
|
-
return ` locale/browser/${name}.ftl (
|
|
177
|
+
return ` locale/browser/${name}.ftl (%browser/${name}.ftl)`;
|
|
168
178
|
}
|
|
169
179
|
//# sourceMappingURL=chrome-doc-templates.js.map
|
|
@@ -400,9 +400,27 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
400
400
|
throw new FurnaceError(`"${componentName}" already exists in the engine source tree. Use "fireforge furnace override" instead.`, componentName);
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
|
-
//
|
|
404
|
-
|
|
405
|
-
|
|
403
|
+
// Refuse if name doesn't match componentPrefix, unless
|
|
404
|
+
// --allow-prefix-mismatch was explicitly passed.
|
|
405
|
+
//
|
|
406
|
+
// Pre-0.16.0 this was a bare `warn()` and the create flow continued,
|
|
407
|
+
// which produced a class of validation runs where the command reported
|
|
408
|
+
// success, scaffolded files under components/custom/<name>/, and
|
|
409
|
+
// registered tests in browser/base/moz.build, but the component
|
|
410
|
+
// wasn't a good citizen of the fork's convention — subsequent
|
|
411
|
+
// follow-up commands (list, status, rename) behaved inconsistently.
|
|
412
|
+
// Refusing up-front leaves the workspace untouched on a bad name and
|
|
413
|
+
// forces an intentional `--allow-prefix-mismatch` for the rare case
|
|
414
|
+
// where the mismatch is deliberate.
|
|
415
|
+
if (config.componentPrefix &&
|
|
416
|
+
!componentName.startsWith(config.componentPrefix) &&
|
|
417
|
+
!options.allowPrefixMismatch) {
|
|
418
|
+
throw new InvalidArgumentError(`Name "${componentName}" does not start with the configured prefix "${config.componentPrefix}". ` +
|
|
419
|
+
'Use a prefixed name (e.g. "' +
|
|
420
|
+
config.componentPrefix +
|
|
421
|
+
componentName +
|
|
422
|
+
'"), update `componentPrefix` in furnace.json, ' +
|
|
423
|
+
'or pass --allow-prefix-mismatch to create the component anyway.', 'name');
|
|
406
424
|
}
|
|
407
425
|
// --- Resolve description ---
|
|
408
426
|
const description = await resolveDescription(isInteractive, options);
|