@hominis/fireforge 0.16.2 → 0.16.3
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 +73 -0
- package/README.md +9 -2
- package/dist/bin/fireforge.js +11 -2
- package/dist/src/commands/doctor-furnace.js +83 -1
- package/dist/src/commands/doctor.js +18 -0
- package/dist/src/commands/download.js +16 -1
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +21 -3
- package/dist/src/commands/furnace/chrome-doc-templates.js +23 -5
- package/dist/src/commands/furnace/chrome-doc-tests.js +42 -17
- package/dist/src/commands/furnace/create-templates.d.ts +17 -7
- package/dist/src/commands/furnace/create-templates.js +85 -31
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +1 -1
- package/dist/src/commands/import.js +63 -11
- package/dist/src/commands/patch/delete.js +10 -1
- package/dist/src/commands/setup-support.js +60 -7
- package/dist/src/commands/status.js +28 -1
- package/dist/src/commands/test.js +20 -4
- package/dist/src/commands/token.js +7 -1
- package/dist/src/core/branding.d.ts +10 -0
- package/dist/src/core/branding.js +7 -9
- package/dist/src/core/build-prepare.js +8 -1
- package/dist/src/core/file-lock.js +49 -15
- package/dist/src/core/furnace-operation.d.ts +17 -0
- package/dist/src/core/furnace-operation.js +30 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +33 -1
- package/dist/src/core/furnace-validate-helpers.js +53 -2
- package/dist/src/core/git.js +39 -10
- package/dist/src/core/manifest-rules.js +16 -0
- package/dist/src/core/marionette-preflight.js +43 -12
- package/dist/src/core/patch-files.d.ts +12 -1
- package/dist/src/core/patch-files.js +14 -11
- package/dist/src/core/patch-lint.js +62 -11
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -149,6 +149,79 @@
|
|
|
149
149
|
|
|
150
150
|
- `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.
|
|
151
151
|
|
|
152
|
+
### Lifecycle — `test --doctor` exits cleanly on passing preflight
|
|
153
|
+
|
|
154
|
+
- `src/core/marionette-preflight.ts` now spawns the browser in its own process group (`detached: true`) and sends SIGTERM / SIGKILL via `process.kill(-pid, …)` in the finally block, with an explicit `child.stderr?.destroy()` to close the local end of the stderr pipe. Before this, `fireforge test --doctor` routinely printed `Marionette preflight: PASS` and then hung indefinitely in `uv__io_poll` — the Python mach wrapper exited under SIGTERM but Firefox (a grandchild) inherited the stderr FD and kept Node's event loop alive. A process-group kill takes down the whole tree; destroying the stderr stream closes the local handle regardless of what the grandchild does with its copy. The non-win32 guard falls back to `child.kill(signal)` on Windows, where negative-PID signalling is not a supported kernel primitive.
|
|
155
|
+
|
|
156
|
+
### Lifecycle — `download` progress visibility during git indexing and patch-touched restore
|
|
157
|
+
|
|
158
|
+
- `src/core/git.ts` adds a 15-second heartbeat during the monolithic `git add -A`: every tick reports elapsed seconds through `onProgress` so a spinner or non-TTY log-scraping CI job both see that indexing is still making progress. A ~600 MB Firefox tree takes 60–120 seconds for the monolithic add, during which git emits nothing on stdout/stderr; without the heartbeat the CLI looked hung precisely during the expected work window and an eval run consistently SIGINT'd mid-way assuming the process had stalled. `src/commands/download.ts` also wraps the post-commit `cleanPatchTouchedFiles` pass in its own spinner so the phase is visibly distinct from the preceding git-add window. Neither change alters the underlying timing budget — they surface progress that was always there.
|
|
159
|
+
|
|
160
|
+
### Doctor — post-interrupt engine state check and watchman preflight
|
|
161
|
+
|
|
162
|
+
- `fireforge status` now surfaces a single recovery banner when the engine git repository has no HEAD, pointing at `fireforge download --force`. Before this, interrupting a `fireforge download` during the initial git-add left engine/ extracted but with an unborn HEAD; `status` then flooded the output with hundreds of thousands of untracked entries plus a truncation warning — correct, but not actionable. `src/commands/status.ts` now probes HEAD up-front (via `getHead` + `isMissingHeadError`) and throws a `GeneralError` with the recovery guidance, which matches doctor's existing row for the same state. `--raw` / `--json` modes get the same error message but no banner, so their consumers still see the structural failure.
|
|
163
|
+
- `src/commands/doctor.ts` adds a `Watchman available` check (warning severity when watchman is not on `PATH`). Before this, operators got through setup → download → bootstrap → build without ever seeing the requirement, then hit it only when `fireforge watch` refused to start. A warning row is the right shape: most projects never run watch, so a missing watchman should not fail `doctor` outright — but the gap is now visible during the normal onboarding sweep. The README setup requirements section lists watchman explicitly, and the check runs alongside `mach available` so the location is ergonomic.
|
|
164
|
+
|
|
165
|
+
### Setup — package.json license kept in sync with fireforge.json
|
|
166
|
+
|
|
167
|
+
- `src/commands/setup-support.ts` now rewrites an existing root `package.json`'s `license` field to match the project license picked during `fireforge setup` (instead of only writing a new minimal `package.json` when none existed). Every other field is preserved — `name`, `description`, `dependencies`, `scripts`, `private`, author metadata. Before this, `fireforge setup --force` that selected a new license (e.g. EUPL-1.2 → 0BSD) updated `fireforge.json` but left the package.json license stale, so the two files described different projects. A malformed existing `package.json` is left alone rather than rewritten; the file's trailing-newline state is preserved so a hand-edited convention survives the sync.
|
|
168
|
+
|
|
169
|
+
### Branding — generated files carry the project license header
|
|
170
|
+
|
|
171
|
+
- `src/core/branding.ts` now stamps `configure.sh`, `brand.properties`, and `brand.ftl` with the license header that matches the project's `fireforge.json` `license` field (via `getLicenseHeader(config.license, 'hash')` from `license-headers.ts`). Before this, the three generated files hard-coded the Mozilla MPL-2.0 header regardless of the project license, so a 0BSD / EUPL-1.2 / GPL-2.0-or-later fork's first export failed `patch-lint`'s `missing-license-header` on its own generated branding with no actionable fix. The fix threads the license through `BrandingConfig` (optional, defaults to `DEFAULT_LICENSE` for pre-0.16 callers) and `build-prepare.ts` sets it from the active project config. Copied upstream branding assets under `browser/branding/<binary>/` still carry Mozilla's MPL-2.0 headers (those files are Mozilla-copyrighted template material) and are auto-exempted by the lint rules below.
|
|
172
|
+
|
|
173
|
+
### Patch lint — branding tier for `large-patch-lines`, auto-exemption for branding headers and colors
|
|
174
|
+
|
|
175
|
+
- `src/core/patch-lint.ts` adds a `branding` threshold tier to `lintPatchSize` (notice 3000 / warning 8000 / error 20000) and selects it when every file in a patch lives under `browser/branding/`. Before this, a first-export of setup-generated branding landed at 15,904 lines (localized `brand.ftl` across many locales + SVG path data + copied upstream CSS) and fired the general hard limit of 3000 as an error — even though the patch was already the minimum branding diff. The branding tier keeps the soft warning (8000) visible but moves the hard limit to a threshold that genuinely suggests "something other than branding is bundled in here too". Mixed patches (branding + other trees) still fall through to the general tier so an operator bundling unrelated edits into a branding change still sees the warning.
|
|
176
|
+
- `lintPatchedCss` now auto-exempts files under `browser/branding/` from the `raw-color-value` check. Copied-from-`unofficial` branding CSS contains hex literals (about dialogs, installer pages, branded chrome) that are legitimate Mozilla design decisions, not fork-editorial choices — listing every copied path in `patchLint.rawColorAllowlist` would add dozens of entries that the operator did not author. The narrower exemption applies by path prefix, so a fork that authors an entirely new branding tree under a different top-level directory still sees the lint fire. `lintNewFileHeaders` adds a parallel carve-out: a new file under `browser/branding/` that starts with any recognised license header (MPL-2.0, EUPL-1.2, 0BSD, GPL-2.0-or-later) passes the check, even when the header does not match the project license — an operator forking Mozilla's branding template inherits Mozilla's MPL-2.0 header and cannot legitimately rewrite it to another license without misrepresenting authorship. A file with no header at all still fails.
|
|
177
|
+
|
|
178
|
+
### Patch manifest — binary file paths survive verify, repair, and rebuild
|
|
179
|
+
|
|
180
|
+
- `src/core/patch-files.ts` now delegates `getAllTargetFilesFromPatch` to `extractAffectedFiles` from `patch-parse.ts`, which matches both `diff --git a/… b/…` and `+++ b/…` lines. Before this, the custom `+++ b/…`-only regex missed every file in a `GIT binary patch` section (binary diffs have no `+++` line, only a diff header), so `fireforge verify` reported `files-affected-mismatch` against branding patches and `fireforge doctor --repair-patches-manifest` "repaired" the mismatch by rewriting the manifest down to the text-only subset — hiding the true scope of the patch. Three downstream callers (`patch-manifest-query.ts`, `patch-manifest-consistency.ts`, `patch-apply.ts`) inherit the fix without local changes. The returned list is alphabetically sorted (matching `extractAffectedFiles`); callers already compare `filesAffected` as a set, so the order change is API-safe.
|
|
181
|
+
|
|
182
|
+
### Import — `--until` scopes patch-integrity checks to the targeted range
|
|
183
|
+
|
|
184
|
+
- `fireforge import --until <filename>` now filters the patch-integrity and manifest-consistency issues to patches at or before the target, so a malformed later patch does not block replaying an earlier good subset. Before this, `validatePatchIntegrity` returned issues for every patch on disk and the block / force-prompt pathway fired on all of them — an operator with `--until 001-foo.patch` who wanted to step around a broken `002-bar.patch` got "Refusing to import while 1 patch integrity issue" even though patch 2 was out of scope. The new `buildUntilFilenameSet` helper (in `src/commands/import.ts`) resolves the `--until` target via the same `.patch`-suffix-tolerant lookup `applyPatchesWithContinue` already uses, and the filter applies to version-compatibility warnings, the "Found N patches to apply" banner, and the dry-run listing too. Structural manifest issues (missing / unparseable `patches.json`) remain global — those block any import regardless of scope, because the manifest has to be valid to resolve filenames against in the first place.
|
|
185
|
+
|
|
186
|
+
### Patch delete — dependency warning clarifies runtime-only impact
|
|
187
|
+
|
|
188
|
+
- `src/commands/patch/delete.ts`'s refusal message now reads `"N later patch(es) contain import statements that reference files created by <target>. Patch application itself will still succeed, but runtime imports will fail at browser startup until those files are re-introduced."` instead of the previous "later patches depend on files created by X". The old phrasing implied patch application itself would fail, which is incorrect — `git apply` does not resolve `ChromeUtils.importESModule` specifiers and will happily apply a queue whose imports point at deleted files. An eval run confirmed this directly: forcing `patch delete` with `--force-unsafe` and then re-importing the mutated 20-patch queue produced zero rejects. The reworded message lets operators planning a rename / refactor (the legitimate "I'll re-introduce the imported files under a new name") make the call without being scared off by a phantom apply-time risk; the runtime risk is still clearly named.
|
|
189
|
+
|
|
190
|
+
### Token add — `--mode` option marked `(required)` in help output
|
|
191
|
+
|
|
192
|
+
- `src/commands/token.ts` appends `(required)` to the `--mode` description. Commander's `.makeOptionMandatory(true)` enforces the option at runtime, but it does not render a `(required)` marker in `--help` output the way `.requiredOption` does — a real invocation derived from the built-in help failed with `error: required option '--mode <mode>' not specified` despite help listing the option alongside normal optional flags. Switching to `.requiredOption` would have lost the `.choices(['auto', 'static', 'override'])` enforcement, so keeping the Option-object form with an explicit description suffix is the minimal fix that makes help honest. Runtime validation is unchanged.
|
|
193
|
+
|
|
194
|
+
### Furnace validate — customized built-ins accepted as valid components
|
|
195
|
+
|
|
196
|
+
- `classExtendsMozLitElement` in `src/core/furnace-validate-helpers.ts` now accepts a class that extends `HTMLAnchorElement`, `HTMLButtonElement`, or any other `HTML<Something>Element` **when** the same module calls `customElements.define(..., ..., { extends: "<tagname>" })` with a literal `extends:` option. Before this, `fireforge furnace override moz-support-link --type full` wrote the upstream source verbatim (the toolkit's `moz-support-link` extends `HTMLAnchorElement` with `customElements.define("moz-support-link", ..., { extends: "a" })`) and `furnace validate` then rejected it with `[not-moz-lit-element] Component class must extend MozLitElement` — blocking deploy on a legitimate upstream pattern. Both halves of the shape are required: a class that extends `HTMLButtonElement` without the matching `extends:` option is almost certainly an author mistake and still fires `not-moz-lit-element`. The autonomous-element path (`extends MozLitElement`) is unchanged.
|
|
197
|
+
|
|
198
|
+
### Furnace chrome-doc — jar.inc.mn shared path prefix and XHTML preprocessor flag fixes
|
|
199
|
+
|
|
200
|
+
- `src/commands/furnace/chrome-doc-templates.ts` emits `../shared/<name>-chrome.css` (was `shared/<name>-chrome.css`) in the `jar.inc.mn` entry. `jar.inc.mn` is included from each theme-specific manifest (`browser/themes/osx/jar.mn`, `linux/jar.mn`, `windows/jar.mn`) where every existing entry resolves source paths relative to the including manifest's directory — a bare `shared/` produced `obj-.../browser/themes/osx/shared/<name>-chrome.css`, which does not exist. The new `../shared/` prefix climbs out of the theme-specific directory and lands on the real `browser/themes/shared/` tree.
|
|
201
|
+
- `jarMnEntriesForChromeDoc` no longer marks the scaffolded XHTML or JS entries with the `*` preprocessor flag. The generated XHTML / JS contain no `#filter` / `#expand` / `#include` directives, and mach's `process_install_manifest.py` fails the whole package step with "no preprocessor directives found" when a `*`-flagged entry has nothing for the preprocessor to do. A fork that later needs brand substitution can reintroduce `*` alongside a top-of-file `#filter substitution` directive.
|
|
202
|
+
|
|
203
|
+
### Furnace packaging test — probes both dist/bin/browser and app-bundle layouts
|
|
204
|
+
|
|
205
|
+
- `src/commands/furnace/chrome-doc-tests.ts`'s scaffolded packaging test now probes both candidate packaged-tree layouts per asset via a `probeEither(primary, fallback, description)` helper: `<AppDir>/chrome/browser/…` (the unpacked layout when `XCurProcD` honours `firefox-appdir = "browser"` and resolves into `dist/bin/browser/`) AND `<AppDir>/browser/chrome/browser/…` (the macOS .app-bundle and some ESR layouts where `XCurProcD` sits one level above `browser/` even when the appdir directive is set). The assertion only fails when both candidates miss, which is the actual stale-build / missing jar.mn case. Before this, the eval on macOS consistently showed the test reporting "missing" against a file that was packaged correctly, just at the bundle-layout path the probe did not walk.
|
|
206
|
+
|
|
207
|
+
### Furnace register — xpcshell.toml manifests get correct wiring guidance
|
|
208
|
+
|
|
209
|
+
- `src/core/manifest-rules.ts`'s `getUnregistrableAdvice` now emits xpcshell-specific guidance when the path ends in `xpcshell.toml`, pointing at `XPCSHELL_TESTS_MANIFESTS` in the nearest moz.build. Before this, the generic `testMatch` branch caught the path and suggested registering a non-existent `browser.toml` — wrong manifest type, and a path that did not exist in the generated tree. The new branch mirrors the intentional design in `create-xpcshell.ts`, which scaffolds the manifest and prints the same warning so the operator knows wiring is deliberately manual. Browser-chrome test registration (`browser.toml`) is unchanged and still auto-registered.
|
|
210
|
+
|
|
211
|
+
### Furnace xpcshell scaffold — filesystem probe replaces browser-global module-load test
|
|
212
|
+
|
|
213
|
+
- `fireforge furnace create --xpcshell` now scaffolds `test_<name>_packaged.js` (was `test_<name>_module_loads.js`) with a filesystem-probe test instead of a `ChromeUtils.importESModule` call. Lit-based components import `chrome://global/content/vendor/lit.all.mjs`, which references `window` at module-load time — xpcshell has no `window` global, so the old module-load path reliably failed with `ReferenceError: window is not defined` for every Lit-based fork component (the eval scenario). The replacement probes `XCurProcD` at both candidate layouts for `<name>.mjs` and `<name>.css`, matching the chrome-doc packaging scaffold's pattern. This tests what xpcshell CAN test (packaging) without tripping on browser-only globals; functional UI assertions still belong in a browser-chrome mochitest (`furnace create --test-style browser-chrome`) and the scaffolded test carries an inline comment pointing there.
|
|
214
|
+
|
|
215
|
+
### Furnace lock — stale lock cleanup via PID-first check, signal handler, and doctor repair
|
|
216
|
+
|
|
217
|
+
- `src/core/file-lock.ts`'s `removeIfStaleLock` now checks the PID file BEFORE the age gate. If the lock's PID file says its owner is no longer alive, the lock is removed immediately regardless of age. Before this ordering change the age gate (5 min default) fired first, so a lock written by a process the user had just SIGINT'd sat undisturbed for the full window even though its owner was explicitly gone — the next `fireforge furnace …` / `fireforge test --build` then timed out waiting, and the only operator recovery was to `rm -rf .fireforge/furnace.lock` manually. The age-only fallback still applies when the PID file is missing (older release locks, externally-created lock directories).
|
|
218
|
+
- `bin/fireforge.ts`'s signal handler calls a new `forceReleaseFurnaceLocksForActiveOperations()` sweep after `rollbackActiveOperationsForSignal` and before `process.exit`. `withFileLock`'s `finally { rm }` never runs when the handler calls `process.exit`, so without the sweep a SIGINT during `furnace preview` left the lock behind and subsequent commands stalled. Errors are logged but swallowed — a slow I/O failure at shutdown cannot prevent the process from exiting.
|
|
219
|
+
- `src/commands/doctor-furnace.ts` adds a `Furnace lock` check that detects and (under `--repair-furnace`) removes a stale lock directory. Two signals flag a lock as stale: (1) PID file present but owner dead, (2) PID file absent AND directory older than 60s. Without `--repair-furnace` the check is warning-only; with it, the lock is `rm -rf`'d. This is the recovery path when the signal-handler sweep misses (SIGKILL, older FireForge release lock, externally-created directory).
|
|
220
|
+
|
|
221
|
+
### Diagnostics — xpcshell-appdir wins over stale-build-artifact on generic resource failures
|
|
222
|
+
|
|
223
|
+
- `src/commands/test.ts`'s `hasStaleBuildArtifactsSignal` no longer matches `resource:///modules/distribution.sys.mjs` — the signal now requires a branding-specific path (`chrome://branding/locale/brand.properties`, `browser/branding/<name>/moz.build`). Before this narrowing, any `Failed to load resource:///modules/…` failure routed to the "rebuild" advice, which was wrong for the eval's Hominis case (`HominisStore.sys.mjs` missed because of an appdir / packaging issue, not stale artifacts — rebuilding did nothing). Branding-specific failures still win ahead of the xpcshell-appdir hint; cases that used to match the distribution literal now fall through to xpcshell-appdir, which is the right first guess for generic `resource:///modules/…` module-load failures.
|
|
224
|
+
|
|
152
225
|
## 0.15.0
|
|
153
226
|
|
|
154
227
|
### Re-export — opt-in `--stamp` and per-patch `lintIgnore`
|
package/README.md
CHANGED
|
@@ -36,6 +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
40
|
|
|
40
41
|
### Setup
|
|
41
42
|
|
|
@@ -54,6 +55,10 @@ npx fireforge run # launch it
|
|
|
54
55
|
|
|
55
56
|
Your project now has `fireforge.json`, an `engine/` directory with Firefox source and a `patches/` directory with an empty `patches.json` manifest ready for your first customisation.
|
|
56
57
|
|
|
58
|
+
#### Known upstream build issues
|
|
59
|
+
|
|
60
|
+
- **macOS 15 (Darwin 25+) — `gecko-profiler` bindgen error `cannot find type _CharT in this scope`.** An Apple toolchain update changed `std::__CharT_pointer` to `_CharT_pointer` in the libc++ headers Firefox's bindgen walks, so `toolkit/library/rust/target-objects` fails during `mach build` even on a clean `fireforge bootstrap`. This is an upstream Firefox issue, not a FireForge bug. Two workarounds: pin Xcode's command line tools to a pre-September-2025 release via `xcode-select --install` / [Apple developer downloads](https://developer.apple.com/download/all/), or apply a one-line bindgen-basic-string-workaround patch (Hominis ships one in its patch queue). If you interrupt the resulting `fireforge build` and re-run `fireforge doctor`, the download/engine state is unaffected — the failure is isolated to the Rust compile phase.
|
|
61
|
+
|
|
57
62
|
### Workflow Overview
|
|
58
63
|
|
|
59
64
|
1. Make changes inside the `engine/` directory.
|
|
@@ -544,9 +549,11 @@ Both rules compose with the existing `tokenPrefix` / `tokenAllowlist` checks and
|
|
|
544
549
|
|
|
545
550
|
`fireforge furnace create --with-tests` scaffolds a **browser-chrome mochitest**. Use this when the component renders UI that depends on the tab strip (`openLinkIn` → `URILoadingHelper`, `gBrowser`, etc.).
|
|
546
551
|
|
|
547
|
-
`fireforge furnace create --xpcshell` scaffolds an **xpcshell test harness** instead. Use this when the component's code path is storage-only, observer-driven, or module-loading logic that does not touch a `tabbrowser`. xpcshell runs headless without browser chrome, so forks without an upstream tab strip can still cover these paths. The scaffolder writes `test_<name>
|
|
552
|
+
`fireforge furnace create --xpcshell` scaffolds an **xpcshell test harness** instead. Use this when the component's code path is storage-only, observer-driven, or module-loading logic that does not touch a `tabbrowser`. xpcshell runs headless without browser chrome, so forks without an upstream tab strip can still cover these paths. The scaffolder writes `test_<name>_packaged.js` + `xpcshell.toml` into `engine/browser/base/content/test/<binary-name>-xpcshell/<component-name>/` and prints a note: registration in `XPCSHELL_TESTS_MANIFESTS` is the operator's call (the moz.build that should own the entry depends on where the component actually lives). `fireforge register <path>/xpcshell.toml` surfaces the same guidance when run directly rather than silently routing to a browser.toml-shaped advice.
|
|
553
|
+
|
|
554
|
+
The scaffolded xpcshell test is a **packaging probe**, not a module-load test. Lit-based components import `chrome://global/content/vendor/lit.all.mjs`, which references `window` at module-load — xpcshell has no `window` global, so an earlier scaffold that used `ChromeUtils.importESModule` reliably failed with `ReferenceError: window is not defined` for every Lit-based fork component. Instead, the test reads `XCurProcD` (`Services.dirsvc.get("XCurProcD", Ci.nsIFile)`) and probes two candidate layouts per asset — `<AppDir>/chrome/global/elements/<name>.{mjs,css}` (unpacked `dist/bin/browser/`) and `<AppDir>/browser/chrome/global/elements/<name>.{mjs,css}` (macOS .app-bundle / some ESR layouts). Either match passes; only when both miss does the assertion fail, which is the actual "stale build / missing jar.mn entry" case. Functional UI assertions still belong in a browser-chrome mochitest (`--test-style=browser-chrome`); the scaffolded test carries an inline comment pointing to that path so the constraint is obvious before the operator extends it.
|
|
548
555
|
|
|
549
|
-
xpcshell has a chrome-URI boundary that is worth knowing before writing assertions: `chrome://global/*` (toolkit chrome) IS registered and resolvable from the harness, but `chrome://browser/*` (browser chrome) is NOT — even when `firefox-appdir = "browser"` is set in the xpcshell.toml, the manifest set xpcshell loads lags what the real browser loads, so `NetUtil.asyncFetch("chrome://browser/content/…")` can still fail with `NS_ERROR_FILE_NOT_FOUND` against an artifact that IS present in `obj-*/dist/`. Assertions that need browser chrome URIs belong in a browser-chrome mochitest (`furnace create --test-style=browser-chrome`).
|
|
556
|
+
xpcshell has a chrome-URI boundary that is worth knowing before writing assertions: `chrome://global/*` (toolkit chrome) IS registered and resolvable from the harness, but `chrome://browser/*` (browser chrome) is NOT — even when `firefox-appdir = "browser"` is set in the xpcshell.toml, the manifest set xpcshell loads lags what the real browser loads, so `NetUtil.asyncFetch("chrome://browser/content/…")` can still fail with `NS_ERROR_FILE_NOT_FOUND` against an artifact that IS present in `obj-*/dist/`. Assertions that need browser chrome URIs belong in a browser-chrome mochitest (`furnace create --test-style=browser-chrome`).
|
|
550
557
|
|
|
551
558
|
The two flags can be combined — `--with-tests --xpcshell` writes both harnesses.
|
|
552
559
|
|
package/dist/bin/fireforge.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
*/
|
|
11
11
|
import { installBrokenPipeHandler, main } from '../src/cli.js';
|
|
12
|
-
import { isSignalRollbackInFlight, rollbackActiveOperationsForSignal, } from '../src/core/furnace-operation.js';
|
|
12
|
+
import { forceReleaseFurnaceLocksForActiveOperations, isSignalRollbackInFlight, rollbackActiveOperationsForSignal, } from '../src/core/furnace-operation.js';
|
|
13
13
|
import { waitForActiveCriticalSections } from '../src/core/signal-critical.js';
|
|
14
14
|
import { CommandError } from '../src/errors/base.js';
|
|
15
15
|
/**
|
|
@@ -51,7 +51,16 @@ function installFurnaceSignalHandler(signal, exitCode) {
|
|
|
51
51
|
console.error(`Furnace rollback after ${signal} failed:`, error instanceof Error ? error.message : error);
|
|
52
52
|
}),
|
|
53
53
|
waitForActiveCriticalSections(SIGNAL_CRITICAL_SECTION_TIMEOUT_MS),
|
|
54
|
-
])
|
|
54
|
+
])
|
|
55
|
+
// Force-release the furnace lock directory after rollback completes.
|
|
56
|
+
// `withFileLock`'s `finally { rm }` never runs when we `process.exit`
|
|
57
|
+
// the handler below, so without this sweep the lock survives the
|
|
58
|
+
// process and wedges the next `fireforge furnace …` / `fireforge
|
|
59
|
+
// test --build` command until the staleness window elapses. See
|
|
60
|
+
// `forceReleaseFurnaceLocksForActiveOperations` for why the sweep is
|
|
61
|
+
// best-effort (errors are logged, not thrown).
|
|
62
|
+
.then(() => forceReleaseFurnaceLocksForActiveOperations())
|
|
63
|
+
.finally(() => {
|
|
55
64
|
process.exit(exitCode);
|
|
56
65
|
});
|
|
57
66
|
});
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { readFile, rm, stat } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { applyAllComponents } from '../core/furnace-apply.js';
|
|
4
5
|
import { hasCustomEngineDrift, hasOverrideEngineDrift } from '../core/furnace-apply-helpers.js';
|
|
5
6
|
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, } from '../core/furnace-config.js';
|
|
6
7
|
import { CUSTOM_ELEMENTS_JS, JAR_MN, resolveFtlDir } from '../core/furnace-constants.js';
|
|
7
|
-
import { runFurnaceMutation } from '../core/furnace-operation.js';
|
|
8
|
+
import { getFurnaceLockPath, runFurnaceMutation } from '../core/furnace-operation.js';
|
|
8
9
|
import { validateAllComponents } from '../core/furnace-validate.js';
|
|
9
10
|
import { toError } from '../utils/errors.js';
|
|
10
11
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -412,6 +413,86 @@ const furnaceComponentValidationCheck = {
|
|
|
412
413
|
}
|
|
413
414
|
},
|
|
414
415
|
};
|
|
416
|
+
/**
|
|
417
|
+
* Reads the owner PID from a furnace lock directory. Returns `null` when
|
|
418
|
+
* the PID file is missing, unreadable, or does not parse as a finite
|
|
419
|
+
* integer — the caller then falls back to an age-only heuristic.
|
|
420
|
+
*/
|
|
421
|
+
async function readFurnaceLockPid(lockPath) {
|
|
422
|
+
try {
|
|
423
|
+
const pidContent = await readFile(join(lockPath, 'pid'), 'utf-8');
|
|
424
|
+
const pid = parseInt(pidContent.trim(), 10);
|
|
425
|
+
return Number.isFinite(pid) ? pid : null;
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function isProcessStillRunning(pid) {
|
|
432
|
+
try {
|
|
433
|
+
process.kill(pid, 0);
|
|
434
|
+
return true;
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* "Furnace stale lock" check: detect and (under `--repair-furnace`)
|
|
442
|
+
* remove a `.fireforge/furnace.lock` directory whose owner process is no
|
|
443
|
+
* longer alive.
|
|
444
|
+
*
|
|
445
|
+
* This is the recovery path when the signal-handler sweep
|
|
446
|
+
* (`forceReleaseFurnaceLocksForActiveOperations` in `bin/fireforge.ts`)
|
|
447
|
+
* misses — e.g. a SIGKILL'd process that never got to run the handler, or
|
|
448
|
+
* a lock created by an older FireForge release without a PID file. The
|
|
449
|
+
* motivating eval scenario: SIGINT'ing `furnace preview` left the lock
|
|
450
|
+
* behind, and the next `fireforge test --build` timed out waiting for it.
|
|
451
|
+
* `doctor --repair-furnace` now clears the lock explicitly so the next
|
|
452
|
+
* command runs immediately.
|
|
453
|
+
*/
|
|
454
|
+
const furnaceStaleLockCheck = {
|
|
455
|
+
name: 'Furnace lock',
|
|
456
|
+
dependsOn: ['Furnace configuration'],
|
|
457
|
+
skipIf: (ctx) => !ctx.furnaceConfigExists,
|
|
458
|
+
run: async (ctx) => {
|
|
459
|
+
const lockPath = getFurnaceLockPath(ctx.projectRoot);
|
|
460
|
+
if (!(await pathExists(lockPath))) {
|
|
461
|
+
return ok('Furnace lock');
|
|
462
|
+
}
|
|
463
|
+
const pid = await readFurnaceLockPid(lockPath);
|
|
464
|
+
const lockStat = await stat(lockPath).catch(() => null);
|
|
465
|
+
const ageMs = lockStat ? Date.now() - lockStat.mtimeMs : undefined;
|
|
466
|
+
const ageSuffix = ageMs !== undefined ? ` (age: ${Math.round(ageMs / 1000)}s)` : '';
|
|
467
|
+
// Two signals mark a lock as stale:
|
|
468
|
+
// 1. PID file says owner is dead → unambiguous, remove immediately.
|
|
469
|
+
// 2. PID file absent AND lock is older than 60s → older FireForge
|
|
470
|
+
// releases (or an externally-created lock directory) fall into
|
|
471
|
+
// this bucket; the age gate avoids false positives on a lock
|
|
472
|
+
// that was just acquired by a concurrent process that hadn't
|
|
473
|
+
// written its PID yet.
|
|
474
|
+
const ownerDead = pid !== null && !isProcessStillRunning(pid);
|
|
475
|
+
const pidMissingAndOld = pid === null && (ageMs ?? 0) > 60_000;
|
|
476
|
+
const isStale = ownerDead || pidMissingAndOld;
|
|
477
|
+
if (!isStale) {
|
|
478
|
+
// Lock is held by a running FireForge process — nothing to report.
|
|
479
|
+
return ok('Furnace lock');
|
|
480
|
+
}
|
|
481
|
+
const description = ownerDead
|
|
482
|
+
? `Stale furnace lock at ${lockPath}: owner PID ${pid} is no longer running${ageSuffix}.`
|
|
483
|
+
: `Stale furnace lock at ${lockPath}: no PID file and lock directory is older than 60s${ageSuffix}.`;
|
|
484
|
+
if (!ctx.options.repairFurnace) {
|
|
485
|
+
return warning('Furnace lock', description, 'Run "fireforge doctor --repair-furnace" to remove the stale lock.');
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
489
|
+
return warning('Furnace lock', `Removed stale furnace lock at ${lockPath}${ageSuffix}.`);
|
|
490
|
+
}
|
|
491
|
+
catch (err) {
|
|
492
|
+
return failure('Furnace lock', `Could not remove stale furnace lock at ${lockPath}: ${toError(err).message}`, 'Remove the directory manually (rm -rf .fireforge/furnace.lock) and retry.');
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
};
|
|
415
496
|
/**
|
|
416
497
|
* The ordered furnace check group. Exported as an array so `doctor.ts`
|
|
417
498
|
* can splice it into the main registry at the right position. The order
|
|
@@ -423,6 +504,7 @@ export const FURNACE_DOCTOR_CHECKS = [
|
|
|
423
504
|
furnaceStateConsistencyCheck,
|
|
424
505
|
furnaceEnginePathsCheck,
|
|
425
506
|
furnaceStorybookCheck,
|
|
507
|
+
furnaceStaleLockCheck,
|
|
426
508
|
furnaceEngineStateCheck,
|
|
427
509
|
furnaceComponentValidationCheck,
|
|
428
510
|
];
|
|
@@ -10,6 +10,7 @@ import { ExitCode } from '../errors/codes.js';
|
|
|
10
10
|
import { toError } from '../utils/errors.js';
|
|
11
11
|
import { pathExists } from '../utils/fs.js';
|
|
12
12
|
import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
13
|
+
import { executableExists } from '../utils/process.js';
|
|
13
14
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
14
15
|
/**
|
|
15
16
|
* Builds a DoctorCheck object representing a successful "OK" check.
|
|
@@ -245,6 +246,23 @@ const DOCTOR_CHECKS = [
|
|
|
245
246
|
},
|
|
246
247
|
fix: 'Firefox source may be corrupted. Re-download with "fireforge download --force"',
|
|
247
248
|
},
|
|
249
|
+
{
|
|
250
|
+
// `fireforge watch` has an undeclared hard dependency on watchman —
|
|
251
|
+
// neither `bootstrap` nor `doctor` used to surface it, so operators
|
|
252
|
+
// got through setup → download → build → and only discovered the gap
|
|
253
|
+
// when they tried to start watch mode. A warning-severity doctor row
|
|
254
|
+
// is the right shape: most projects never run watch, so a missing
|
|
255
|
+
// watchman should not fail `doctor` outright, but the information
|
|
256
|
+
// needs to be visible ahead of time rather than at the watch-mode
|
|
257
|
+
// failure site.
|
|
258
|
+
name: 'Watchman available',
|
|
259
|
+
run: async () => {
|
|
260
|
+
const present = await executableExists('watchman');
|
|
261
|
+
if (present)
|
|
262
|
+
return ok('Watchman available');
|
|
263
|
+
return warning('Watchman available', 'watchman is not installed or not on PATH. "fireforge watch" requires it.', 'Install watchman (brew install watchman / dnf install watchman / https://facebook.github.io/watchman/), then re-run doctor.');
|
|
264
|
+
},
|
|
265
|
+
},
|
|
248
266
|
{
|
|
249
267
|
name: 'Patches directory exists',
|
|
250
268
|
run: async (ctx) => {
|
|
@@ -242,13 +242,28 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
242
242
|
// commit (e.g. line-ending normalisation or extraction artefacts) so that
|
|
243
243
|
// a subsequent `fireforge import` works without --force.
|
|
244
244
|
//
|
|
245
|
+
// Wrapped in a dedicated spinner because the restore can itself take
|
|
246
|
+
// tens of seconds on a ~600 MB Firefox tree: it walks every file in the
|
|
247
|
+
// patch manifest, calls `git status` / `git checkout` for each, and the
|
|
248
|
+
// eval's "download looks hung" report landed at least partly on this
|
|
249
|
+
// post-commit window. An operator watching the CLI needs to see that
|
|
250
|
+
// this phase is distinct from the preceding git-add work.
|
|
251
|
+
//
|
|
245
252
|
// This runs BEFORE updateState so a restore failure keeps the previous
|
|
246
253
|
// downloadedVersion in state.json. The invariant we preserve is
|
|
247
254
|
// "state.downloadedVersion matches a clean engine": stamping the new
|
|
248
255
|
// version only after the restore succeeds means a failed clean-up will
|
|
249
256
|
// re-enter the resume path on the next `fireforge download` rather than
|
|
250
257
|
// reporting success against a dirty engine.
|
|
251
|
-
|
|
258
|
+
const restoreSpinner = spinner('Restoring patch-touched files to baseline...');
|
|
259
|
+
try {
|
|
260
|
+
await cleanPatchTouchedFiles(paths.engine, paths.patches);
|
|
261
|
+
restoreSpinner.stop('Patch-touched files restored');
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
restoreSpinner.error('Failed to restore patch-touched files');
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
252
267
|
await updateState(projectRoot, {
|
|
253
268
|
downloadedVersion: version,
|
|
254
269
|
baseCommit,
|
|
@@ -56,11 +56,29 @@ export declare function generateChromeDocCss(name: string, withTitlebar: boolean
|
|
|
56
56
|
export declare function generateChromeDocFtl(name: string, licenseHeader: string): string;
|
|
57
57
|
/**
|
|
58
58
|
* Single-line jar.mn entry that registers an xhtml + js pair under
|
|
59
|
-
* `content/browser/`.
|
|
60
|
-
*
|
|
59
|
+
* `content/browser/`.
|
|
60
|
+
*
|
|
61
|
+
* Neither emitted line carries the `*` preprocessor flag. The scaffolded
|
|
62
|
+
* XHTML and JS contain no `#filter` / `#expand` / `#include` directives,
|
|
63
|
+
* and mach's `process_install_manifest.py` fails the whole package step
|
|
64
|
+
* with "no preprocessor directives found" when a preprocessed entry has
|
|
65
|
+
* nothing for the preprocessor to do. A fork that later needs brand
|
|
66
|
+
* substitution can re-introduce `*` and add a top-of-file
|
|
67
|
+
* `#filter substitution` directive itself.
|
|
61
68
|
*/
|
|
62
69
|
export declare function jarMnEntriesForChromeDoc(name: string): string[];
|
|
63
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* jar.inc.mn entry that registers the scoped CSS under `content/browser/`.
|
|
72
|
+
*
|
|
73
|
+
* The source path is `../shared/<name>-chrome.css` because `jar.inc.mn`
|
|
74
|
+
* is included from each theme-specific manifest (`browser/themes/osx/jar.mn`,
|
|
75
|
+
* `browser/themes/linux/jar.mn`, `browser/themes/windows/jar.mn`), and every
|
|
76
|
+
* existing entry in those manifests resolves paths relative to the including
|
|
77
|
+
* manifest's directory. A bare `(shared/…)` path produced
|
|
78
|
+
* `obj-.../browser/themes/osx/shared/<name>-chrome.css` which does not exist;
|
|
79
|
+
* `(../shared/…)` matches the upstream pattern and resolves under
|
|
80
|
+
* `browser/themes/shared/`.
|
|
81
|
+
*/
|
|
64
82
|
export declare function jarIncMnEntryForChromeDoc(name: string): string;
|
|
65
83
|
/**
|
|
66
84
|
* locales/jar.mn entry that registers the `.ftl` under the browser locale
|
|
@@ -149,18 +149,36 @@ ${name}-window-title = ${name}
|
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
151
|
* Single-line jar.mn entry that registers an xhtml + js pair under
|
|
152
|
-
* `content/browser/`.
|
|
153
|
-
*
|
|
152
|
+
* `content/browser/`.
|
|
153
|
+
*
|
|
154
|
+
* Neither emitted line carries the `*` preprocessor flag. The scaffolded
|
|
155
|
+
* XHTML and JS contain no `#filter` / `#expand` / `#include` directives,
|
|
156
|
+
* and mach's `process_install_manifest.py` fails the whole package step
|
|
157
|
+
* with "no preprocessor directives found" when a preprocessed entry has
|
|
158
|
+
* nothing for the preprocessor to do. A fork that later needs brand
|
|
159
|
+
* substitution can re-introduce `*` and add a top-of-file
|
|
160
|
+
* `#filter substitution` directive itself.
|
|
154
161
|
*/
|
|
155
162
|
export function jarMnEntriesForChromeDoc(name) {
|
|
156
163
|
return [
|
|
157
|
-
|
|
164
|
+
` content/browser/${name}.xhtml (content/${name}.xhtml)`,
|
|
158
165
|
` content/browser/${name}.js (content/${name}.js)`,
|
|
159
166
|
];
|
|
160
167
|
}
|
|
161
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* jar.inc.mn entry that registers the scoped CSS under `content/browser/`.
|
|
170
|
+
*
|
|
171
|
+
* The source path is `../shared/<name>-chrome.css` because `jar.inc.mn`
|
|
172
|
+
* is included from each theme-specific manifest (`browser/themes/osx/jar.mn`,
|
|
173
|
+
* `browser/themes/linux/jar.mn`, `browser/themes/windows/jar.mn`), and every
|
|
174
|
+
* existing entry in those manifests resolves paths relative to the including
|
|
175
|
+
* manifest's directory. A bare `(shared/…)` path produced
|
|
176
|
+
* `obj-.../browser/themes/osx/shared/<name>-chrome.css` which does not exist;
|
|
177
|
+
* `(../shared/…)` matches the upstream pattern and resolves under
|
|
178
|
+
* `browser/themes/shared/`.
|
|
179
|
+
*/
|
|
162
180
|
export function jarIncMnEntryForChromeDoc(name) {
|
|
163
|
-
return ` content/browser/${name}-chrome.css (shared/${name}-chrome.css)`;
|
|
181
|
+
return ` content/browser/${name}-chrome.css (../shared/${name}-chrome.css)`;
|
|
164
182
|
}
|
|
165
183
|
/**
|
|
166
184
|
* locales/jar.mn entry that registers the `.ftl` under the browser locale
|
|
@@ -69,32 +69,57 @@ export function generateChromeDocPackagingTest(name, header) {
|
|
|
69
69
|
add_task(async function test_${taskSuffix}_files_packaged() {
|
|
70
70
|
const appDir = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
// Probes a pair of candidate layouts for the same packaged file:
|
|
73
|
+
// 1) \`<AppDir>/chrome/browser/…\` — the unpacked layout when
|
|
74
|
+
// XCurProcD honours \`firefox-appdir = "browser"\` and resolves
|
|
75
|
+
// into \`dist/bin/browser/\`.
|
|
76
|
+
// 2) \`<AppDir>/browser/chrome/browser/…\` — the macOS .app bundle
|
|
77
|
+
// layout and some ESR configurations, where XCurProcD sits one
|
|
78
|
+
// level above \`browser/\` even when the appdir directive is set.
|
|
79
|
+
// If either path exists the file is packaged; the assertion only fails
|
|
80
|
+
// when BOTH layouts miss, which is the actual stale-build / missing
|
|
81
|
+
// jar.mn entry case. Before this dual probe, the eval on macOS
|
|
82
|
+
// consistently failed against layout (2) even though the file was
|
|
83
|
+
// packaged correctly.
|
|
84
|
+
function probeEither(primary, fallback, description) {
|
|
85
|
+
const primaryFile = appDir.clone();
|
|
86
|
+
for (const segment of primary) {
|
|
87
|
+
primaryFile.append(segment);
|
|
76
88
|
}
|
|
89
|
+
const fallbackFile = appDir.clone();
|
|
90
|
+
for (const segment of fallback) {
|
|
91
|
+
fallbackFile.append(segment);
|
|
92
|
+
}
|
|
93
|
+
const found = primaryFile.exists() ? primaryFile : fallbackFile.exists() ? fallbackFile : null;
|
|
77
94
|
Assert.ok(
|
|
78
|
-
|
|
79
|
-
description +
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
file.fileSize,
|
|
86
|
-
0,
|
|
87
|
-
description + " is zero-length at " + file.path +
|
|
88
|
-
" — packaging copied an empty file, check the source template.",
|
|
95
|
+
found !== null,
|
|
96
|
+
description +
|
|
97
|
+
" missing at both " +
|
|
98
|
+
primaryFile.path +
|
|
99
|
+
" and " +
|
|
100
|
+
fallbackFile.path +
|
|
101
|
+
' — run "fireforge build --ui" and retry. If one of those paths IS populated, the xpcshell harness is probing a stale build tree; the post-build audit should flag the same miss.',
|
|
89
102
|
);
|
|
103
|
+
if (found !== null) {
|
|
104
|
+
Assert.greater(
|
|
105
|
+
found.fileSize,
|
|
106
|
+
0,
|
|
107
|
+
description +
|
|
108
|
+
" is zero-length at " +
|
|
109
|
+
found.path +
|
|
110
|
+
" — packaging copied an empty file, check the source template.",
|
|
111
|
+
);
|
|
112
|
+
}
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
probeEither(
|
|
93
116
|
["chrome", "browser", "content", "browser", "${name}.xhtml"],
|
|
117
|
+
["browser", "chrome", "browser", "content", "browser", "${name}.xhtml"],
|
|
94
118
|
"${name}.xhtml",
|
|
95
119
|
);
|
|
96
|
-
|
|
120
|
+
probeEither(
|
|
97
121
|
["chrome", "browser", "skin", "classic", "browser", "${name}-chrome.css"],
|
|
122
|
+
["browser", "chrome", "browser", "skin", "classic", "browser", "${name}-chrome.css"],
|
|
98
123
|
"${name}-chrome.css",
|
|
99
124
|
);
|
|
100
125
|
});
|
|
@@ -33,13 +33,23 @@ export declare function xpcshellTestFileName(name: string): string;
|
|
|
33
33
|
/**
|
|
34
34
|
* Generates an xpcshell test file for a custom component.
|
|
35
35
|
*
|
|
36
|
-
* xpcshell
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
36
|
+
* xpcshell cannot execute a component module that imports
|
|
37
|
+
* `chrome://global/content/vendor/lit.all.mjs` — the Lit bundle touches
|
|
38
|
+
* `window` at module-load time and the xpcshell harness has no `window`
|
|
39
|
+
* global. Before 0.16.0 the scaffold called `ChromeUtils.importESModule`
|
|
40
|
+
* on the component's MJS, which reliably failed with
|
|
41
|
+
* `ReferenceError: window is not defined` for every Lit-based fork
|
|
42
|
+
* component. FireForge's diagnostics then misrouted the failure to the
|
|
43
|
+
* "stale build artifacts" branch, sending operators on a rebuild loop
|
|
44
|
+
* that couldn't fix a runtime-environment incompatibility.
|
|
45
|
+
*
|
|
46
|
+
* The rewrite here mirrors the chrome-doc packaging test: XCurProcD is
|
|
47
|
+
* probed at a pair of candidate layouts (dist/bin/browser and the macOS
|
|
48
|
+
* .app-bundle / ESR layout) to confirm the `.mjs` and `.css` files
|
|
49
|
+
* landed where jar.mn promised. That's the assertion xpcshell CAN make.
|
|
50
|
+
* Functional tests that need DOM/shadow-root/keyboard behaviour belong
|
|
51
|
+
* in a browser-chrome mochitest — scaffolded via
|
|
52
|
+
* `fireforge furnace create --test-style browser-chrome`.
|
|
43
53
|
*/
|
|
44
54
|
export declare function generateXpcshellTestContent(name: string, header: string): string;
|
|
45
55
|
/**
|