@hominis/fireforge 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +79 -26
- package/dist/src/commands/bootstrap-checks.d.ts +16 -0
- package/dist/src/commands/bootstrap-checks.js +66 -0
- package/dist/src/commands/bootstrap.js +27 -9
- package/dist/src/commands/doctor.d.ts +8 -0
- package/dist/src/commands/doctor.js +7 -1
- package/dist/src/commands/export-flow.js +3 -11
- package/dist/src/commands/export-shared.d.ts +2 -1
- package/dist/src/commands/export-shared.js +7 -2
- package/dist/src/commands/furnace/create.js +1 -1
- package/dist/src/commands/furnace/deploy.js +1 -1
- package/dist/src/commands/furnace/override.js +1 -1
- package/dist/src/commands/furnace/refresh.js +1 -1
- package/dist/src/commands/furnace/remove.js +1 -1
- package/dist/src/commands/furnace/rename.js +1 -1
- package/dist/src/commands/furnace/scan.js +1 -1
- package/dist/src/commands/lint.js +12 -3
- package/dist/src/commands/patch/delete.js +1 -15
- package/dist/src/commands/patch/reorder.js +1 -9
- package/dist/src/commands/re-export.js +1 -17
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/core/ast-utils.d.ts +10 -0
- package/dist/src/core/ast-utils.js +18 -0
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +3 -0
- package/dist/src/core/config-validate.js +21 -3
- package/dist/src/core/file-lock.js +39 -2
- package/dist/src/core/furnace-apply.js +2 -1
- package/dist/src/core/furnace-config.js +6 -2
- package/dist/src/core/patch-apply.js +26 -4
- package/dist/src/core/patch-lint-checkjs.d.ts +21 -0
- package/dist/src/core/patch-lint-checkjs.js +225 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -0
- package/dist/src/core/patch-lint-cross.js +7 -0
- package/dist/src/core/patch-lint-jsdoc.d.ts +21 -0
- package/dist/src/core/patch-lint-jsdoc.js +259 -0
- package/dist/src/core/patch-lint-ownership.d.ts +25 -0
- package/dist/src/core/patch-lint-ownership.js +43 -0
- package/dist/src/core/patch-lint.d.ts +14 -3
- package/dist/src/core/patch-lint.js +116 -47
- package/dist/src/core/patch-manifest-resolve.d.ts +5 -0
- package/dist/src/core/patch-manifest-resolve.js +12 -0
- package/dist/src/core/patch-manifest.d.ts +1 -0
- package/dist/src/core/patch-manifest.js +1 -0
- package/dist/src/types/commands/patches.d.ts +2 -2
- package/dist/src/types/config.d.ts +11 -0
- package/dist/src/utils/paths.js +3 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0
|
|
4
|
+
|
|
5
|
+
### Setup
|
|
6
|
+
|
|
7
|
+
- **`fireforge bootstrap` now runs targeted post-bootstrap checks** instead of pattern-matching output text. When `mach bootstrap` exits successfully but sub-downloads fail (e.g. HTTP 403 from Apple's CDN), FireForge validates actual system state — checking whether a macOS SDK is available via Xcode — and reports actionable results using the same `✓`/`!`/`✗` severity rendering as `fireforge doctor`. Non-critical issues (SDK download failed but Xcode provides one) are reported as warnings rather than alarming "did not complete successfully" errors.
|
|
8
|
+
|
|
9
|
+
### Lint fixes
|
|
10
|
+
|
|
11
|
+
- **`file-too-large` now uses tiered severity thresholds.** The old single 650-line warning is replaced with a three-tier system (notice / warning / error) that distinguishes general files from test files. General files: 500–749 lines notice, 750–899 warning, 900+ error. Test files (paths containing `/test/`, or filenames matching `browser_*.js`, `test_*.js`, `xpcshell_*.js`): 1200–1399 notice, 1400–1599 warning, 1600+ error. Messages include the applicable thresholds so users know where they stand. The new `notice` severity is displayed but does not count toward warning or error totals and does not block export.
|
|
12
|
+
- **`observer-topic-naming` no longer matches across newlines.** The regex that extracts topic strings from `notifyObservers`/`addObserver`/`removeObserver` calls now anchors to a single line, preventing false positives when the call spans multiple lines and an unrelated string literal appears later.
|
|
13
|
+
- **`raw-color-value` now supports a file allowlist and inline suppression.** New `patchLint.rawColorAllowlist` config array in `fireforge.json` exempts file paths (exact or basename match) from the raw-color check — intended for design token files that must contain raw color values. Individual declarations can also be suppressed with an inline `/* fireforge-ignore: raw-color-value */` comment.
|
|
14
|
+
- **`large-patch-lines` now uses tiered severity thresholds.** The old single >300-line warning is replaced with a three-tier system matching the `file-too-large` pattern. General patches: 800+ lines notice, 1500+ warning, 3000+ error. Test-only patches (all files match test patterns): 1500+ notice, 3000+ warning, 6000+ error. The previous threshold was too restrictive relative to file LOC limits — creating a single new file at the `file-too-large` notice tier (500 LOC) already exceeded it. Messages now include the applicable soft and hard limits.
|
|
15
|
+
|
|
16
|
+
### General Improvements
|
|
17
|
+
|
|
18
|
+
- **Minor Refactor**
|
|
19
|
+
|
|
20
|
+
## 0.12.0
|
|
21
|
+
|
|
22
|
+
### JSDoc validation (breaking)
|
|
23
|
+
|
|
24
|
+
- **JSDoc enforcement is now AST-based and severity `error`.** The previous heuristic (walk backwards from `export` to find `*/`) has been replaced with Acorn-based AST analysis. Exported functions must have a JSDoc block with `@param` for each parameter (names must match) and `@returns` when returning a value. Exported classes require a JSDoc block. Exported constants require `@type`. This is a breaking change: projects that previously passed with incomplete JSDoc will now see lint errors.
|
|
25
|
+
- **Patch-owned scope.** JSDoc enforcement now applies to all patch-owned `.sys.mjs` files, not just files new in the current diff. A file is patch-owned if it was created by the current diff or by any existing patch in the queue.
|
|
26
|
+
- New check: **`jsdoc-param-mismatch`** (error) — flags `@param` tags that are missing or have the wrong name.
|
|
27
|
+
- New check: **`jsdoc-missing-returns`** (error) — flags functions that return a value but lack `@returns`.
|
|
28
|
+
- Exported constants and classes require a JSDoc block but do not require specific tags like `@type`.
|
|
29
|
+
|
|
30
|
+
### Optional checkJs pass
|
|
31
|
+
|
|
32
|
+
- **`patchLint.checkJs`** — new opt-in config field in `fireforge.json`. When enabled, runs TypeScript's `checkJs` pass (`allowJs + checkJs + noEmit`) on patch-owned `.sys.mjs` files only. Firefox globals are shimmed automatically. Diagnostics are filtered to patch-owned files so upstream noise is suppressed.
|
|
33
|
+
- New check: **`checkjs-type-error`** (error/warning) — surfaces type errors from the TypeScript compiler.
|
|
34
|
+
|
|
35
|
+
### Hardening
|
|
36
|
+
|
|
37
|
+
- **Path validation.** `binaryName` in `fireforge.json` now rejects null bytes and absolute paths (including Windows drive letters). `isContainedRelativePath` and `isPathInsideRoot` reject null bytes. Furnace custom component `targetPath` rejects null bytes and absolute paths.
|
|
38
|
+
- **Symlink traversal protection.** Patch target validation now checks whether existing paths are symlinks resolving outside the engine tree before applying.
|
|
39
|
+
- **PID-aware stale lock recovery.** The file lock writes the owning PID into the lock directory. Stale lock recovery checks whether the PID is still alive before removing, preventing premature removal when a slow operation legitimately holds the lock.
|
|
40
|
+
- **Forward-import detection** now catches `ChromeUtils.importESModule()` calls in addition to static/dynamic ES imports and `defineESModuleGetters`.
|
|
41
|
+
- **Furnace rollback failure markers** now include the component name and operation context, improving diagnostics in `fireforge doctor`.
|
|
42
|
+
- New lint check in README: **`modified-file-missing-header`** (warning) was implemented but not documented; now listed in the lint checks table.
|
|
43
|
+
|
|
3
44
|
## 0.11.0
|
|
4
45
|
|
|
5
46
|
### New commands
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**Build and maintain your own Firefox-based browser with a patch-first workflow**
|
|
10
10
|
|
|
11
|
-
FireForge gives you a toolkit for forking Firefox: download a specific ESR release, manage your customisations as a series of patches, survive version upgrades with semi-automated rebase, wire custom code into Mozilla's startup paths
|
|
11
|
+
FireForge gives you a toolkit for forking Firefox: download a specific ESR release, manage your customisations as a series of patches, survive version upgrades with semi-automated rebase, wire custom code into Mozilla's startup paths and build the result. It also ships **Furnace**, a component system for creating and overriding Firefox custom elements under `toolkit/content/widgets`.
|
|
12
12
|
|
|
13
13
|
Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon](https://github.com/dothq/melon).
|
|
14
14
|
|
|
@@ -18,7 +18,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
|
|
|
18
18
|
|
|
19
19
|
- **Semi-automated ESR rebase** `fireforge rebase` replays your patch stack onto new Firefox source with escalating fuzz matching. When a patch fails, you fix it manually and `--continue`. The full stack gets re-exported with updated version stamps.
|
|
20
20
|
|
|
21
|
-
- **Wiring and registration** `fireforge wire` and `fireforge register` inject your code into Mozilla's startup paths, build manifests
|
|
21
|
+
- **Wiring and registration** `fireforge wire` and `fireforge register` inject your code into Mozilla's startup paths, build manifests and JAR files with a single command. The injection is AST-based (via Acorn), so it survives formatting changes applied between versions.
|
|
22
22
|
|
|
23
23
|
- **Furnace component system** Override existing Firefox custom elements or create new ones under `toolkit/content/widgets` (CSS-only restyles, full behavioural forks, or entirely new widgets).
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
|
|
|
26
26
|
|
|
27
27
|
- **Quality checks** `fireforge lint` catches fork-specific issues (raw colours, missing licence headers, relative imports, large patches, cross-patch ordering problems) before you export. `fireforge verify` runs a read-only integrity check over the whole patch queue. `fireforge doctor` diagnoses project health including Furnace component validation.
|
|
28
28
|
|
|
29
|
-
- **Built and validated against real Firefox code** Developed by editing a real Firefox ESR codebase, learning from existing patch tools, observing the breakages and edge cases that surfaced
|
|
29
|
+
- **Built and validated against real Firefox code** Developed by editing a real Firefox ESR codebase, learning from existing patch tools, observing the breakages and edge cases that surfaced and turning those findings into a realistic test suite. In-repo tests are thus grounded in actual development scenarios. Yes, we mock quite a bit, but when building a tool that modifies a separate code base, I think it's a solid compromise for the time being. Full end-to-end runs are currently run locally, as they require about 30 GB of disk and significant compute for multiple full builds. Full end-to-end via Github Actions will be added soonishlyTM.
|
|
30
30
|
|
|
31
31
|
## Quick Start
|
|
32
32
|
|
|
@@ -52,7 +52,7 @@ npx fireforge build # build the browser
|
|
|
52
52
|
npx fireforge run # launch it
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Your project now has `fireforge.json`, an `engine/` directory with Firefox source
|
|
55
|
+
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
56
|
|
|
57
57
|
### Workflow Overview
|
|
58
58
|
|
|
@@ -71,14 +71,14 @@ npx fireforge export browser/base/content/browser.js \
|
|
|
71
71
|
npx fireforge reset --yes
|
|
72
72
|
npx fireforge import # --dry-run to preview without applying
|
|
73
73
|
|
|
74
|
-
# 5. When Firefox releases a new ESR, update fireforge.json, re-download
|
|
74
|
+
# 5. When Firefox releases a new ESR, update fireforge.json, re-download and rebase
|
|
75
75
|
npx fireforge download --force
|
|
76
76
|
npx fireforge rebase
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
## Patch Workflow
|
|
80
80
|
|
|
81
|
-
Patches live in `patches/`, applied by numeric filename prefix
|
|
81
|
+
Patches live in `patches/`, applied by numeric filename prefix and tracked in `patches/patches.json`:
|
|
82
82
|
|
|
83
83
|
```
|
|
84
84
|
patches/
|
|
@@ -186,22 +186,30 @@ If the manifest drifts after an interrupted export or manual edits, `fireforge i
|
|
|
186
186
|
<details>
|
|
187
187
|
<summary>Patch lint checks</summary>
|
|
188
188
|
|
|
189
|
-
`fireforge lint` runs automatically during export, export-all
|
|
190
|
-
|
|
191
|
-
| Check | Scope
|
|
192
|
-
| ------------------------------ |
|
|
193
|
-
| `missing-license-header` | New files (JS/CSS/FTL)
|
|
194
|
-
| `relative-import` | JS/MJS files
|
|
195
|
-
| `token-prefix-violation` | CSS files (with furnace)
|
|
196
|
-
| `raw-color-value` | Introduced CSS color values
|
|
197
|
-
| `duplicate-new-file-creation` | Same path created by multiple patches
|
|
198
|
-
| `forward-import` | Patch imports from a later-patch file
|
|
199
|
-
| `missing-
|
|
200
|
-
| `
|
|
201
|
-
| `missing-
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
| `
|
|
189
|
+
`fireforge lint` runs automatically during export, export-all and re-export. Use `--skip-lint` to downgrade errors to warnings. Errors block the export; warnings are printed but do not block.
|
|
190
|
+
|
|
191
|
+
| Check | Scope | Severity |
|
|
192
|
+
| ------------------------------ | --------------------------------------------------------------------- | ------------------------ |
|
|
193
|
+
| `missing-license-header` | New files (JS/CSS/FTL) | error |
|
|
194
|
+
| `relative-import` | JS/MJS files | error |
|
|
195
|
+
| `token-prefix-violation` | CSS files (with furnace) | error |
|
|
196
|
+
| `raw-color-value` | Introduced CSS color values | error |
|
|
197
|
+
| `duplicate-new-file-creation` | Same path created by multiple patches | error |
|
|
198
|
+
| `forward-import` | Patch imports from a later-patch file | error |
|
|
199
|
+
| `missing-jsdoc` | Exports in patch-owned `.sys.mjs` | error |
|
|
200
|
+
| `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs` | error |
|
|
201
|
+
| `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs` | error |
|
|
202
|
+
| `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in) | error |
|
|
203
|
+
| `missing-modification-comment` | Modified upstream JS/MJS | warning |
|
|
204
|
+
| `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL) | warning |
|
|
205
|
+
| `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test) | notice / warning / error |
|
|
206
|
+
| `observer-topic-naming` | Observer topics with binaryName | warning |
|
|
207
|
+
| `large-patch-files` | Patches affecting >5 files | warning |
|
|
208
|
+
| `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test) | notice / warning / error |
|
|
209
|
+
|
|
210
|
+
**JSDoc validation** uses AST-based analysis (Acorn) to validate exported APIs in patch-owned `.sys.mjs` files. A file is "patch-owned" if it was newly created by the current diff or by an existing patch in the queue. Functions must document every `@param` (names must match) and include `@returns` when the function returns a value. Exported constants and classes require a JSDoc block.
|
|
211
|
+
|
|
212
|
+
**Optional `checkJs` pass.** Enable a TypeScript-esque bastardization of type checking for patch-owned `.sys.mjs` files by adding `"patchLint": { "checkJs": true }` to `fireforge.json`. This uses the TypeScript compiler API with `allowJs + checkJs + noEmit`, scoped only to patch-owned files. Firefox globals (`Services`, `ChromeUtils`, `lazy`, etc.) are shimmed automatically. Module-resolution errors from Firefox's `resource://` and `chrome://` URL schemes are suppressed since TypeScript cannot follow these. This pass solely focuses on type errors within the patch-owned code itself (mismatched JSDoc types, wrong argument counts, unreachable code, etc.).
|
|
205
213
|
|
|
206
214
|
The two cross-patch rules (`duplicate-new-file-creation` and `forward-import`) run over the whole patch queue rather than a single diff, catching ordering issues that only surface during `import`. Forward-import detection compares leaf filenames, so a false positive is theoretically possible when two patches create files with the same basename in different directories. Suppress with an inline `// fireforge-ignore: forward-import` comment on or above the import line. This is currently the only lint rule that supports inline suppression.
|
|
207
215
|
|
|
@@ -209,7 +217,7 @@ The two cross-patch rules (`duplicate-new-file-creation` and `forward-import`) r
|
|
|
209
217
|
|
|
210
218
|
### Repairing a broken patch queue
|
|
211
219
|
|
|
212
|
-
When a patch queue drifts
|
|
220
|
+
When a patch queue drifts, e.g. due to overlapping new-file creations, forward imports, manifest desync, etc. start with diagnosing the root cause:
|
|
213
221
|
|
|
214
222
|
```bash
|
|
215
223
|
fireforge verify # fsck: manifest + cross-patch lint
|
|
@@ -229,7 +237,7 @@ Then fix with the appropriate primitive:
|
|
|
229
237
|
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
|
|
230
238
|
| Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset` |
|
|
231
239
|
|
|
232
|
-
Every destructive command defaults to an interactive confirmation with a change summary. `--dry-run` previews without writing; `--yes` skips the prompt for CI; `--force-unsafe` bypasses structural refusals when you have context the linter cannot see. Do not hand-edit `patches.json`
|
|
240
|
+
Every destructive command defaults to an interactive confirmation with a change summary. `--dry-run` previews without writing; `--yes` skips the prompt for CI; `--force-unsafe` bypasses structural refusals when you have context the linter cannot see. Do not hand-edit `patches.json` as the file is owned by FireForge.
|
|
233
241
|
|
|
234
242
|
## Wiring Custom Code
|
|
235
243
|
|
|
@@ -270,7 +278,7 @@ fireforge register browser/modules/mybrowser/MyStore.sys.mjs
|
|
|
270
278
|
|
|
271
279
|
## Furnace (UI Component System)
|
|
272
280
|
|
|
273
|
-
Furnace manages Firefox custom elements (`MozLitElement`) under `toolkit/content/widgets`. You can override existing components or create new ones. Changes feed into the same patch workflow as everything else
|
|
281
|
+
Furnace manages Firefox custom elements (`MozLitElement`) under `toolkit/content/widgets`. You can override existing components or create new ones. Changes feed into the same patch workflow as everything else, Furnace is not a separate persistence layer.
|
|
274
282
|
|
|
275
283
|
There are three component types:
|
|
276
284
|
|
|
@@ -289,7 +297,52 @@ fireforge furnace status # workspace vs engine drift
|
|
|
289
297
|
fireforge furnace diff moz-button # unified diff against baseline
|
|
290
298
|
```
|
|
291
299
|
|
|
292
|
-
`furnace deploy` validates components before applying
|
|
300
|
+
`furnace deploy` validates components before applying. As always, errors block, warnings are advisory. `fireforge build` and `fireforge test --build` run apply automatically. Use `fireforge doctor --repair-furnace` if the engine gets out of sync.
|
|
301
|
+
|
|
302
|
+
## Additional Commands
|
|
303
|
+
|
|
304
|
+
The commands below cover project configuration, patch queue management, build packaging and development utilities. Run `fireforge <command> --help` for full option details.
|
|
305
|
+
|
|
306
|
+
### Configuration
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
# Read a config value
|
|
310
|
+
fireforge config firefox.version
|
|
311
|
+
|
|
312
|
+
# Set a config value
|
|
313
|
+
fireforge config firefox.version 145.0.0esr
|
|
314
|
+
|
|
315
|
+
# Set a value at a non-standard path (requires --force)
|
|
316
|
+
fireforge config customKey "value" --force
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Patch queue management
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
# Delete a patch from the queue
|
|
323
|
+
fireforge patch delete 003-ui-sidebar-tweaks.patch
|
|
324
|
+
|
|
325
|
+
# Reorder a patch to a new position
|
|
326
|
+
fireforge patch reorder 003-ui-sidebar-tweaks.patch --to 1
|
|
327
|
+
|
|
328
|
+
# Move a patch before or after another
|
|
329
|
+
fireforge patch reorder 003-ui-sidebar.patch --before 001-branding-logo.patch
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Both subcommands support `--dry-run` and `--yes`.
|
|
333
|
+
|
|
334
|
+
### Additional workflow commands
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
# Package the built browser for distribution
|
|
338
|
+
fireforge package
|
|
339
|
+
|
|
340
|
+
# Watch for file changes and auto-rebuild
|
|
341
|
+
fireforge watch
|
|
342
|
+
|
|
343
|
+
# Add a CSS design token
|
|
344
|
+
fireforge token --name "--my-color" --value "light-dark(#fff, #000)"
|
|
345
|
+
```
|
|
293
346
|
|
|
294
347
|
## Configuration
|
|
295
348
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DoctorCheck } from '../types/commands/index.js';
|
|
2
|
+
/** Tags representing distinct issues detected in bootstrap output. */
|
|
3
|
+
export type BootstrapIssue = 'sdk-fetch-403' | 'python-traceback' | 'missing-origin-remote';
|
|
4
|
+
/**
|
|
5
|
+
* Scans bootstrap output for known failure patterns and returns structured
|
|
6
|
+
* issue tags. A Python traceback paired with an HTTP 403 is collapsed into
|
|
7
|
+
* a single `sdk-fetch-403` tag since the traceback is just the stack trace
|
|
8
|
+
* from the HTTP error.
|
|
9
|
+
*/
|
|
10
|
+
export declare function detectBootstrapIssues(output: string): BootstrapIssue[];
|
|
11
|
+
/**
|
|
12
|
+
* Runs targeted post-bootstrap checks based on the detected issues.
|
|
13
|
+
* Returns doctor-compatible check results so the caller can render them
|
|
14
|
+
* with the standard `reportDoctorResults` display.
|
|
15
|
+
*/
|
|
16
|
+
export declare function runPostBootstrapChecks(issues: BootstrapIssue[]): Promise<DoctorCheck[]>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { failure, warning } from './doctor.js';
|
|
4
|
+
/**
|
|
5
|
+
* Scans bootstrap output for known failure patterns and returns structured
|
|
6
|
+
* issue tags. A Python traceback paired with an HTTP 403 is collapsed into
|
|
7
|
+
* a single `sdk-fetch-403` tag since the traceback is just the stack trace
|
|
8
|
+
* from the HTTP error.
|
|
9
|
+
*/
|
|
10
|
+
export function detectBootstrapIssues(output) {
|
|
11
|
+
const normalized = output.replace(/\r\n/g, '\n');
|
|
12
|
+
const issues = [];
|
|
13
|
+
const hasTraceback = /traceback \(most recent call last\):/i.test(normalized);
|
|
14
|
+
const has403 = /\bhttp(?:\s+error)?\s*403\b/i.test(normalized) || /\b403\b.*forbidden/i.test(normalized);
|
|
15
|
+
if (has403) {
|
|
16
|
+
// The traceback is just the stack trace from the HTTP error — report once.
|
|
17
|
+
issues.push('sdk-fetch-403');
|
|
18
|
+
}
|
|
19
|
+
else if (hasTraceback) {
|
|
20
|
+
issues.push('python-traceback');
|
|
21
|
+
}
|
|
22
|
+
if (/no such remote ['"]origin['"]/i.test(normalized) ||
|
|
23
|
+
/remote ['"]origin['"] does not exist/i.test(normalized) ||
|
|
24
|
+
/missing git remote ['"]origin['"]/i.test(normalized)) {
|
|
25
|
+
issues.push('missing-origin-remote');
|
|
26
|
+
}
|
|
27
|
+
return issues;
|
|
28
|
+
}
|
|
29
|
+
/** Checks whether `xcrun --show-sdk-path` returns a valid macOS SDK path. */
|
|
30
|
+
async function hasMacOsSdk() {
|
|
31
|
+
return new Promise((resolve) => {
|
|
32
|
+
execFile('xcrun', ['--show-sdk-path'], { timeout: 10_000 }, (err, stdout) => {
|
|
33
|
+
resolve(!err && stdout.trim().length > 0);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Runs targeted post-bootstrap checks based on the detected issues.
|
|
39
|
+
* Returns doctor-compatible check results so the caller can render them
|
|
40
|
+
* with the standard `reportDoctorResults` display.
|
|
41
|
+
*/
|
|
42
|
+
export async function runPostBootstrapChecks(issues) {
|
|
43
|
+
const checks = [];
|
|
44
|
+
for (const issue of issues) {
|
|
45
|
+
switch (issue) {
|
|
46
|
+
case 'sdk-fetch-403': {
|
|
47
|
+
const sdkAvailable = await hasMacOsSdk();
|
|
48
|
+
if (sdkAvailable) {
|
|
49
|
+
checks.push(warning('macOS SDK download', "SDK download from Apple's CDN failed (HTTP 403), but a macOS SDK was found via Xcode. This is safe to ignore."));
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
checks.push(failure('macOS SDK', 'SDK download failed and no macOS SDK found on your system.', 'Install Xcode Command Line Tools with "xcode-select --install"'));
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case 'python-traceback':
|
|
57
|
+
checks.push(warning('Python traceback', 'Bootstrap emitted a Python traceback. This may indicate a non-critical issue.', 'Review the bootstrap output above for details.'));
|
|
58
|
+
break;
|
|
59
|
+
case 'missing-origin-remote':
|
|
60
|
+
checks.push(failure('Git remote', 'Bootstrap expected an "origin" git remote in the Firefox source checkout.', 'Run "git remote add origin <url>" in the engine directory.'));
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return checks;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=bootstrap-checks.js.map
|
|
@@ -4,7 +4,13 @@ import { bootstrapWithOutput } from '../core/mach.js';
|
|
|
4
4
|
import { GeneralError } from '../errors/base.js';
|
|
5
5
|
import { BootstrapError } from '../errors/build.js';
|
|
6
6
|
import { pathExists } from '../utils/fs.js';
|
|
7
|
-
import { error, info, intro, outro } from '../utils/logger.js';
|
|
7
|
+
import { error, info, intro, outro, warn } from '../utils/logger.js';
|
|
8
|
+
import { detectBootstrapIssues, runPostBootstrapChecks } from './bootstrap-checks.js';
|
|
9
|
+
import { reportDoctorResults } from './doctor.js';
|
|
10
|
+
/**
|
|
11
|
+
* Builds a human-readable failure message for hard failures (non-zero exit).
|
|
12
|
+
* Used only when mach bootstrap itself reports failure.
|
|
13
|
+
*/
|
|
8
14
|
function buildBootstrapFailureMessage(output) {
|
|
9
15
|
const normalized = output.replace(/\r\n/g, '\n');
|
|
10
16
|
const issues = [];
|
|
@@ -52,15 +58,27 @@ export async function bootstrapCommand(projectRoot) {
|
|
|
52
58
|
throw new BootstrapError();
|
|
53
59
|
}
|
|
54
60
|
// mach bootstrap may exit 0 even when sub-downloads fail (e.g. HTTP 403).
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
// Instead of guessing from output text, detect what went wrong and run
|
|
62
|
+
// targeted checks to determine whether the issues are actually actionable.
|
|
63
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
64
|
+
const issues = detectBootstrapIssues(output);
|
|
65
|
+
if (issues.length > 0) {
|
|
66
|
+
const checks = await runPostBootstrapChecks(issues);
|
|
67
|
+
const hasErrors = checks.some((c) => c.severity === 'error' || (!c.passed && !c.warning));
|
|
58
68
|
info('');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
if (hasErrors) {
|
|
70
|
+
warn('Bootstrap completed with issues:');
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
warn('Bootstrap completed with warnings:');
|
|
74
|
+
}
|
|
75
|
+
reportDoctorResults(checks);
|
|
76
|
+
if (hasErrors) {
|
|
77
|
+
outro('Build dependencies installed with errors');
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
outro('Build dependencies installed with warnings');
|
|
81
|
+
}
|
|
64
82
|
return;
|
|
65
83
|
}
|
|
66
84
|
outro('Build dependencies installed successfully!');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { ExitCode } from '../errors/codes.js';
|
|
2
3
|
import type { CommandContext } from '../types/cli.js';
|
|
3
4
|
import type { DoctorCheck, DoctorOptions } from '../types/commands/index.js';
|
|
4
5
|
import type { FireForgeConfig, FireForgeState, ProjectPaths } from '../types/config.js';
|
|
@@ -116,6 +117,13 @@ export declare function failure(name: string, message: string, fix?: string): Do
|
|
|
116
117
|
* think through the consequences.
|
|
117
118
|
*/
|
|
118
119
|
export declare const DOCTOR_CHECK_ORDER: readonly string[];
|
|
120
|
+
/**
|
|
121
|
+
* Renders a list of doctor checks to the console and returns the
|
|
122
|
+
* appropriate exit code (success when no errors, general error otherwise).
|
|
123
|
+
* @param checks - The check results to display
|
|
124
|
+
* @returns The exit code reflecting the overall result
|
|
125
|
+
*/
|
|
126
|
+
export declare function reportDoctorResults(checks: DoctorCheck[]): ExitCode;
|
|
119
127
|
/**
|
|
120
128
|
* Result of the doctor command, carrying the exit code so the caller
|
|
121
129
|
* (or test) can inspect it without relying on process.exitCode.
|
|
@@ -342,7 +342,13 @@ validateCheckDependencies(DOCTOR_CHECKS);
|
|
|
342
342
|
* think through the consequences.
|
|
343
343
|
*/
|
|
344
344
|
export const DOCTOR_CHECK_ORDER = DOCTOR_CHECKS.map((check) => check.name);
|
|
345
|
-
|
|
345
|
+
/**
|
|
346
|
+
* Renders a list of doctor checks to the console and returns the
|
|
347
|
+
* appropriate exit code (success when no errors, general error otherwise).
|
|
348
|
+
* @param checks - The check results to display
|
|
349
|
+
* @returns The exit code reflecting the overall result
|
|
350
|
+
*/
|
|
351
|
+
export function reportDoctorResults(checks) {
|
|
346
352
|
info('');
|
|
347
353
|
let passedCount = 0;
|
|
348
354
|
let warningCount = 0;
|
|
@@ -11,7 +11,7 @@ import { join } from 'node:path';
|
|
|
11
11
|
import { findAllPatchesForFilesWithDetails, planExport } from '../core/patch-export.js';
|
|
12
12
|
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../core/patch-lint.js';
|
|
13
13
|
import { withPatchDirectoryLock } from '../core/patch-lock.js';
|
|
14
|
-
import { addPatchToManifest, loadPatchesManifest, renumberPatchesInManifest, savePatchesManifest, } from '../core/patch-manifest.js';
|
|
14
|
+
import { addPatchToManifest, loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
|
|
15
15
|
import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
|
|
16
16
|
import { InvalidArgumentError } from '../errors/base.js';
|
|
17
17
|
import { toError } from '../utils/errors.js';
|
|
@@ -32,14 +32,6 @@ function buildFilenameForPlacement(category, name, order, width) {
|
|
|
32
32
|
const padded = String(order).padStart(Math.max(3, width), '0');
|
|
33
33
|
return `${padded}-${category}-${sanitizeExportName(name)}.patch`;
|
|
34
34
|
}
|
|
35
|
-
function resolvePatchByIdentifier(identifier, patches) {
|
|
36
|
-
if (/^\d+$/.test(identifier)) {
|
|
37
|
-
const order = parseInt(identifier, 10);
|
|
38
|
-
return patches.find((p) => p.order === order) ?? null;
|
|
39
|
-
}
|
|
40
|
-
const normalized = identifier.endsWith('.patch') ? identifier : `${identifier}.patch`;
|
|
41
|
-
return patches.find((p) => p.filename === normalized) ?? null;
|
|
42
|
-
}
|
|
43
35
|
function getSortedRenameEntries(renameMap) {
|
|
44
36
|
return Array.from(renameMap.entries()).sort((a, b) => a[1].newOrder - b[1].newOrder);
|
|
45
37
|
}
|
|
@@ -115,7 +107,7 @@ export async function resolvePlacementPlan(patchesDir, options, category, name)
|
|
|
115
107
|
targetOrder = options.order;
|
|
116
108
|
}
|
|
117
109
|
else if (options.before !== undefined) {
|
|
118
|
-
const anchor =
|
|
110
|
+
const anchor = resolvePatchIdentifier(options.before, existingPatches);
|
|
119
111
|
if (!anchor) {
|
|
120
112
|
throw new InvalidArgumentError(`--before anchor "${options.before}" not found.`, '--before');
|
|
121
113
|
}
|
|
@@ -126,7 +118,7 @@ export async function resolvePlacementPlan(patchesDir, options, category, name)
|
|
|
126
118
|
if (afterAnchorId === undefined) {
|
|
127
119
|
throw new InvalidArgumentError('Placement flag resolver reached --after branch with no value set.', '--after');
|
|
128
120
|
}
|
|
129
|
-
const anchor =
|
|
121
|
+
const anchor = resolvePatchIdentifier(afterAnchorId, existingPatches);
|
|
130
122
|
if (!anchor) {
|
|
131
123
|
throw new InvalidArgumentError(`--after anchor "${afterAnchorId}" not found.`, '--after');
|
|
132
124
|
}
|
|
@@ -10,8 +10,9 @@ import type { SpinnerHandle } from '../utils/logger.js';
|
|
|
10
10
|
* @param diffContent - Raw unified diff string
|
|
11
11
|
* @param config - Project configuration
|
|
12
12
|
* @param skipLint - If true, downgrade errors to warnings
|
|
13
|
+
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
13
14
|
*/
|
|
14
|
-
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean): Promise<void>;
|
|
15
|
+
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext): Promise<void>;
|
|
15
16
|
/**
|
|
16
17
|
* Resolves patch metadata interactively or from flags, with shared validation.
|
|
17
18
|
* @param options - Export command options
|
|
@@ -17,13 +17,18 @@ import { isValidPatchCategory, PATCH_CATEGORIES, validatePatchName } from '../ut
|
|
|
17
17
|
* @param diffContent - Raw unified diff string
|
|
18
18
|
* @param config - Project configuration
|
|
19
19
|
* @param skipLint - If true, downgrade errors to warnings
|
|
20
|
+
* @param patchQueueCtx - Optional cross-patch context for ownership resolution
|
|
20
21
|
*/
|
|
21
|
-
export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint) {
|
|
22
|
-
const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config);
|
|
22
|
+
export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx) {
|
|
23
|
+
const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config, patchQueueCtx);
|
|
23
24
|
if (issues.length === 0)
|
|
24
25
|
return;
|
|
25
26
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
26
27
|
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
28
|
+
const notices = issues.filter((i) => i.severity === 'notice');
|
|
29
|
+
for (const issue of notices) {
|
|
30
|
+
info(`NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
31
|
+
}
|
|
27
32
|
for (const issue of warnings) {
|
|
28
33
|
warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
|
|
29
34
|
}
|
|
@@ -315,7 +315,7 @@ async function performCreateMutations(args) {
|
|
|
315
315
|
await restoreRollbackJournalOrThrow(journal, `Failed to create custom component "${args.componentName}"`);
|
|
316
316
|
}
|
|
317
317
|
catch (rollbackError) {
|
|
318
|
-
await recordFurnaceRollbackFailure(args.projectRoot, 'create-rollback', toError(rollbackError).message);
|
|
318
|
+
await recordFurnaceRollbackFailure(args.projectRoot, 'create-rollback', `component "${args.componentName}": ${toError(rollbackError).message}`);
|
|
319
319
|
throw rollbackError;
|
|
320
320
|
}
|
|
321
321
|
throw error;
|
|
@@ -125,7 +125,7 @@ async function restoreNamedDeployRollback(rollbackJournal, name, projectRoot) {
|
|
|
125
125
|
}
|
|
126
126
|
catch (rollbackError) {
|
|
127
127
|
if (projectRoot) {
|
|
128
|
-
await recordFurnaceRollbackFailure(projectRoot, 'deploy-rollback', toError(rollbackError).message);
|
|
128
|
+
await recordFurnaceRollbackFailure(projectRoot, 'deploy-rollback', `component "${name}": ${toError(rollbackError).message}`);
|
|
129
129
|
}
|
|
130
130
|
throw rollbackError;
|
|
131
131
|
}
|
|
@@ -116,7 +116,7 @@ async function performOverrideMutations(args) {
|
|
|
116
116
|
await restoreRollbackJournalOrThrow(journal, `Failed to override component "${args.componentName}"`);
|
|
117
117
|
}
|
|
118
118
|
catch (rollbackError) {
|
|
119
|
-
await recordFurnaceRollbackFailure(args.projectRoot, 'override-rollback', toError(rollbackError).message);
|
|
119
|
+
await recordFurnaceRollbackFailure(args.projectRoot, 'override-rollback', `component "${args.componentName}": ${toError(rollbackError).message}`);
|
|
120
120
|
throw rollbackError;
|
|
121
121
|
}
|
|
122
122
|
throw error;
|
|
@@ -149,7 +149,7 @@ async function refreshSingleOverride(projectRoot, name, options = {}) {
|
|
|
149
149
|
await restoreRollbackJournalOrThrow(journal, `Failed to refresh override "${name}"`);
|
|
150
150
|
}
|
|
151
151
|
catch (rollbackError) {
|
|
152
|
-
await recordFurnaceRollbackFailure(projectRoot, 'refresh-rollback', toError(rollbackError).message);
|
|
152
|
+
await recordFurnaceRollbackFailure(projectRoot, 'refresh-rollback', `override "${name}": ${toError(rollbackError).message}`);
|
|
153
153
|
throw rollbackError;
|
|
154
154
|
}
|
|
155
155
|
}
|
|
@@ -343,7 +343,7 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
343
343
|
await restoreRollbackJournalOrThrow(journal, `Failed to remove component "${name}"`);
|
|
344
344
|
}
|
|
345
345
|
catch (rollbackError) {
|
|
346
|
-
await recordFurnaceRollbackFailure(projectRoot, 'remove-rollback', toError(rollbackError).message);
|
|
346
|
+
await recordFurnaceRollbackFailure(projectRoot, 'remove-rollback', `component "${name}": ${toError(rollbackError).message}`);
|
|
347
347
|
throw rollbackError;
|
|
348
348
|
}
|
|
349
349
|
throw error;
|
|
@@ -185,7 +185,7 @@ async function performRenameMutations(args) {
|
|
|
185
185
|
await restoreRollbackJournalOrThrow(journal, `Failed to rename component "${oldName}" to "${newName}"`);
|
|
186
186
|
}
|
|
187
187
|
catch (rollbackError) {
|
|
188
|
-
await recordFurnaceRollbackFailure(projectRoot, 'rename-rollback', toError(rollbackError).message);
|
|
188
|
+
await recordFurnaceRollbackFailure(projectRoot, 'rename-rollback', `rename "${oldName}" → "${newName}": ${toError(rollbackError).message}`);
|
|
189
189
|
throw rollbackError;
|
|
190
190
|
}
|
|
191
191
|
throw error;
|
|
@@ -67,7 +67,7 @@ async function promptAddComponents(components, tracked, projectRoot) {
|
|
|
67
67
|
await restoreRollbackJournalOrThrow(journal, 'Failed to update furnace.json during scan');
|
|
68
68
|
}
|
|
69
69
|
catch (rollbackError) {
|
|
70
|
-
await recordFurnaceRollbackFailure(projectRoot, 'scan-rollback', toError(rollbackError).message);
|
|
70
|
+
await recordFurnaceRollbackFailure(projectRoot, 'scan-rollback', `furnace.json update during scan: ${toError(rollbackError).message}`);
|
|
71
71
|
throw rollbackError;
|
|
72
72
|
}
|
|
73
73
|
throw error;
|
|
@@ -85,14 +85,19 @@ export async function lintCommand(projectRoot, files) {
|
|
|
85
85
|
}
|
|
86
86
|
const config = await loadConfig(projectRoot);
|
|
87
87
|
const filesAffected = extractAffectedFiles(diff);
|
|
88
|
+
// Build patch queue context once so it can be shared between the
|
|
89
|
+
// per-patch ownership resolver and the cross-patch rules.
|
|
90
|
+
let ctx;
|
|
91
|
+
if (await pathExists(paths.patches)) {
|
|
92
|
+
ctx = await buildPatchQueueContext(paths.patches);
|
|
93
|
+
}
|
|
88
94
|
const issues = [
|
|
89
|
-
...(await lintExportedPatch(paths.engine, filesAffected, diff, config)),
|
|
95
|
+
...(await lintExportedPatch(paths.engine, filesAffected, diff, config, ctx)),
|
|
90
96
|
];
|
|
91
97
|
// Cross-patch rules operate over the whole queue, so run them whenever a
|
|
92
98
|
// patches directory exists — they surface duplicate /dev/null creations
|
|
93
99
|
// and forward-import chains that the per-patch orchestrator cannot see.
|
|
94
|
-
if (
|
|
95
|
-
const ctx = await buildPatchQueueContext(paths.patches);
|
|
100
|
+
if (ctx) {
|
|
96
101
|
issues.push(...lintPatchQueue(ctx));
|
|
97
102
|
}
|
|
98
103
|
if (issues.length === 0) {
|
|
@@ -102,6 +107,10 @@ export async function lintCommand(projectRoot, files) {
|
|
|
102
107
|
}
|
|
103
108
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
104
109
|
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
110
|
+
const notices = issues.filter((i) => i.severity === 'notice');
|
|
111
|
+
for (const issue of notices) {
|
|
112
|
+
info(`NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
113
|
+
}
|
|
105
114
|
for (const issue of warnings) {
|
|
106
115
|
warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
|
|
107
116
|
}
|
|
@@ -12,26 +12,12 @@ import { getProjectPaths } from '../../core/config.js';
|
|
|
12
12
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
13
13
|
import { buildPatchQueueContext, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, isForwardImportableFile, } from '../../core/patch-lint.js';
|
|
14
14
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
15
|
-
import { loadPatchesManifest, removePatchFileAndManifest } from '../../core/patch-manifest.js';
|
|
15
|
+
import { loadPatchesManifest, removePatchFileAndManifest, resolvePatchIdentifier, } from '../../core/patch-manifest.js';
|
|
16
16
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
17
17
|
import { toError } from '../../utils/errors.js';
|
|
18
18
|
import { pathExists } from '../../utils/fs.js';
|
|
19
19
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
20
20
|
import { pickDefined } from '../../utils/options.js';
|
|
21
|
-
/**
|
|
22
|
-
* Resolves `<name>` (ordinal number or filename) to a manifest entry.
|
|
23
|
-
* Mirrors re-export's `resolvePatchIdentifier` so the two resolvers behave
|
|
24
|
-
* consistently — future work can lift this into a shared helper once a
|
|
25
|
-
* third consumer appears.
|
|
26
|
-
*/
|
|
27
|
-
function resolvePatchIdentifier(identifier, patches) {
|
|
28
|
-
if (/^\d+$/.test(identifier)) {
|
|
29
|
-
const order = parseInt(identifier, 10);
|
|
30
|
-
return patches.find((p) => p.order === order) ?? null;
|
|
31
|
-
}
|
|
32
|
-
const normalized = identifier.endsWith('.patch') ? identifier : `${identifier}.patch`;
|
|
33
|
-
return patches.find((p) => p.filename === normalized) ?? null;
|
|
34
|
-
}
|
|
35
21
|
/**
|
|
36
22
|
* Runs the `patch delete` command: removes a patch file and its manifest
|
|
37
23
|
* row atomically, refusing when a later patch imports a leaf owned by the
|