@hominis/fireforge 0.47.1 → 0.49.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.
Files changed (167) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/bin/fireforge.js +5 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli.js +16 -28
  5. package/dist/src/commands/build.js +30 -4
  6. package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
  7. package/dist/src/commands/doctor-engine-eslint.js +12 -0
  8. package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
  9. package/dist/src/commands/doctor-macos-sdk.js +158 -0
  10. package/dist/src/commands/doctor-source-pin.js +9 -2
  11. package/dist/src/commands/doctor.js +4 -0
  12. package/dist/src/commands/download.js +64 -3
  13. package/dist/src/commands/furnace/apply.js +10 -4
  14. package/dist/src/commands/furnace/create-templates.js +0 -3
  15. package/dist/src/commands/import.js +5 -2
  16. package/dist/src/commands/lint-per-patch.js +44 -6
  17. package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
  18. package/dist/src/commands/lint-per-run-checkjs.js +1 -0
  19. package/dist/src/commands/lint.js +14 -0
  20. package/dist/src/commands/manifest.d.ts +1 -2
  21. package/dist/src/commands/patch/delete.js +18 -4
  22. package/dist/src/commands/re-export-lint.d.ts +17 -2
  23. package/dist/src/commands/re-export-lint.js +33 -4
  24. package/dist/src/commands/re-export-options.js +7 -0
  25. package/dist/src/commands/re-export-register.js +1 -0
  26. package/dist/src/commands/re-export-removed.d.ts +4 -0
  27. package/dist/src/commands/re-export-removed.js +49 -0
  28. package/dist/src/commands/re-export-single.js +4 -1
  29. package/dist/src/commands/re-export.js +8 -2
  30. package/dist/src/commands/rebase/confirm.d.ts +12 -1
  31. package/dist/src/commands/rebase/confirm.js +6 -3
  32. package/dist/src/commands/rebase/index.js +77 -13
  33. package/dist/src/commands/run.js +43 -7
  34. package/dist/src/commands/status.js +5 -3
  35. package/dist/src/commands/test-browser-preflight.d.ts +28 -0
  36. package/dist/src/commands/test-browser-preflight.js +181 -0
  37. package/dist/src/commands/test-build-phase.js +30 -7
  38. package/dist/src/commands/test-checkjs.d.ts +3 -0
  39. package/dist/src/commands/test-checkjs.js +27 -0
  40. package/dist/src/commands/test-diagnose.js +3 -0
  41. package/dist/src/commands/test-harness-env.d.ts +5 -6
  42. package/dist/src/commands/test-harness-env.js +27 -7
  43. package/dist/src/commands/test-modes.d.ts +2 -4
  44. package/dist/src/commands/test-register.js +11 -5
  45. package/dist/src/commands/test-run.d.ts +2 -0
  46. package/dist/src/commands/test-run.js +21 -1
  47. package/dist/src/commands/test-verdict.d.ts +2 -2
  48. package/dist/src/commands/test.js +83 -211
  49. package/dist/src/commands/typecheck.d.ts +5 -0
  50. package/dist/src/commands/typecheck.js +7 -1
  51. package/dist/src/core/build-baseline-types.d.ts +12 -14
  52. package/dist/src/core/build-baseline-validate.d.ts +3 -0
  53. package/dist/src/core/build-baseline-validate.js +53 -0
  54. package/dist/src/core/build-baseline.d.ts +4 -3
  55. package/dist/src/core/build-baseline.js +23 -11
  56. package/dist/src/core/build-prepare.d.ts +2 -0
  57. package/dist/src/core/build-prepare.js +67 -35
  58. package/dist/src/core/component-source-guard.d.ts +20 -0
  59. package/dist/src/core/component-source-guard.js +68 -0
  60. package/dist/src/core/config-paths.d.ts +1 -1
  61. package/dist/src/core/config-paths.js +1 -0
  62. package/dist/src/core/config-validate.js +7 -0
  63. package/dist/src/core/coverage-extend.d.ts +5 -34
  64. package/dist/src/core/coverage-extend.js +26 -37
  65. package/dist/src/core/discard-baseline.js +2 -1
  66. package/dist/src/core/engine-write-boundary.d.ts +5 -0
  67. package/dist/src/core/engine-write-boundary.js +41 -0
  68. package/dist/src/core/forward-registrations.d.ts +33 -0
  69. package/dist/src/core/forward-registrations.js +16 -0
  70. package/dist/src/core/furnace-apply-helpers.js +8 -2
  71. package/dist/src/core/furnace-apply.js +7 -0
  72. package/dist/src/core/furnace-validate-accessibility.js +9 -12
  73. package/dist/src/core/furnace-validate.js +0 -43
  74. package/dist/src/core/gecko-dom-types.d.ts +2 -0
  75. package/dist/src/core/gecko-dom-types.js +17 -0
  76. package/dist/src/core/git-file-ops.d.ts +21 -0
  77. package/dist/src/core/git-file-ops.js +75 -0
  78. package/dist/src/core/git-status.d.ts +14 -0
  79. package/dist/src/core/git-status.js +41 -2
  80. package/dist/src/core/lit-template-regions.d.ts +4 -0
  81. package/dist/src/core/lit-template-regions.js +50 -0
  82. package/dist/src/core/mach-build-artifacts.d.ts +6 -6
  83. package/dist/src/core/mach-build-artifacts.js +13 -7
  84. package/dist/src/core/mach-mozconfig.d.ts +2 -1
  85. package/dist/src/core/mach-mozconfig.js +2 -0
  86. package/dist/src/core/mach-resource-shim.d.ts +1 -1
  87. package/dist/src/core/mach-resource-shim.js +2 -0
  88. package/dist/src/core/marionette-port.d.ts +2 -0
  89. package/dist/src/core/marionette-port.js +38 -53
  90. package/dist/src/core/mochitest-server-port.d.ts +1 -1
  91. package/dist/src/core/mochitest-server-port.js +20 -17
  92. package/dist/src/core/objdir-maintenance.d.ts +10 -0
  93. package/dist/src/core/objdir-maintenance.js +151 -0
  94. package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
  95. package/dist/src/core/patch-apply-fuzz.js +27 -6
  96. package/dist/src/core/patch-apply.d.ts +13 -0
  97. package/dist/src/core/patch-apply.js +27 -2
  98. package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
  99. package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
  100. package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
  101. package/dist/src/core/patch-lint-checkjs.js +10 -2
  102. package/dist/src/core/patch-lint-cross.d.ts +7 -1
  103. package/dist/src/core/patch-lint-cross.js +13 -12
  104. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
  105. package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
  106. package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
  107. package/dist/src/core/patch-lint-forward-registration.js +83 -37
  108. package/dist/src/core/patch-lint.d.ts +11 -0
  109. package/dist/src/core/patch-lint.js +20 -2
  110. package/dist/src/core/patch-transform.d.ts +14 -13
  111. package/dist/src/core/patch-transform.js +29 -17
  112. package/dist/src/core/preflight-wait.d.ts +2 -0
  113. package/dist/src/core/preflight-wait.js +24 -0
  114. package/dist/src/core/process-owner.d.ts +9 -0
  115. package/dist/src/core/process-owner.js +43 -0
  116. package/dist/src/core/rebase-dry-run.d.ts +48 -0
  117. package/dist/src/core/rebase-dry-run.js +105 -0
  118. package/dist/src/core/run-profile.d.ts +50 -0
  119. package/dist/src/core/run-profile.js +131 -0
  120. package/dist/src/core/status-binary.d.ts +1 -1
  121. package/dist/src/core/status-binary.js +3 -1
  122. package/dist/src/core/status-classify.js +29 -2
  123. package/dist/src/core/test-harness-crash.d.ts +1 -1
  124. package/dist/src/core/test-harness-crash.js +5 -5
  125. package/dist/src/core/test-host-state.d.ts +14 -0
  126. package/dist/src/core/test-host-state.js +80 -0
  127. package/dist/src/core/test-profile-files.d.ts +17 -0
  128. package/dist/src/core/test-profile-files.js +75 -0
  129. package/dist/src/core/test-stale-check.d.ts +6 -7
  130. package/dist/src/core/test-stale-check.js +9 -32
  131. package/dist/src/core/token-category.d.ts +7 -26
  132. package/dist/src/core/token-category.js +68 -121
  133. package/dist/src/core/token-inventory.js +12 -7
  134. package/dist/src/core/toolchain-preflight.d.ts +6 -0
  135. package/dist/src/core/toolchain-preflight.js +1 -1
  136. package/dist/src/core/typecheck.d.ts +11 -1
  137. package/dist/src/core/typecheck.js +86 -12
  138. package/dist/src/errors/rebase.d.ts +9 -0
  139. package/dist/src/errors/rebase.js +20 -0
  140. package/dist/src/index.d.ts +3 -1
  141. package/dist/src/index.js +2 -1
  142. package/dist/src/types/commands/options.d.ts +29 -3
  143. package/dist/src/types/config.d.ts +10 -0
  144. package/dist/src/utils/assert.js +2 -7
  145. package/dist/src/utils/child-env.d.ts +0 -3
  146. package/dist/src/utils/child-env.js +0 -3
  147. package/dist/src/utils/concurrency.d.ts +2 -2
  148. package/dist/src/utils/concurrency.js +15 -4
  149. package/dist/src/utils/fs.js +11 -1
  150. package/dist/src/utils/options.d.ts +16 -1
  151. package/dist/src/utils/options.js +5 -3
  152. package/dist/src/utils/paths.js +3 -2
  153. package/dist/src/utils/process-group.d.ts +1 -2
  154. package/dist/src/utils/process-group.js +1 -2
  155. package/dist/src/utils/process-input.d.ts +27 -0
  156. package/dist/src/utils/process-input.js +46 -0
  157. package/dist/src/utils/process.d.ts +5 -2
  158. package/dist/src/utils/process.js +21 -8
  159. package/dist/src/utils/validation.d.ts +11 -0
  160. package/dist/src/utils/validation.js +43 -0
  161. package/docs/configuration.md +82 -0
  162. package/docs/exit-codes.md +1 -1
  163. package/docs/lifecycle-invariants.md +13 -8
  164. package/docs/machine-output.md +36 -19
  165. package/docs/patch-workflow.md +50 -1
  166. package/docs/testing.md +77 -12
  167. package/package.json +10 -5
@@ -4,6 +4,7 @@ import { constants as osConstants } from 'node:os';
4
4
  import { StringDecoder } from 'node:string_decoder';
5
5
  import { ExecTimeoutError } from '../errors/base.js';
6
6
  import { buildChildEnv } from './child-env.js';
7
+ import { toError } from './errors.js';
7
8
  import { dispatchCompleteLines } from './line-dispatch.js';
8
9
  import { killProcessTree, sweepProcessGroup } from './process-group.js';
9
10
  // 50 MB cap per stream to prevent OOM. Large toolchain builds (e.g. Firefox, Chromium)
@@ -77,7 +78,8 @@ function collectedOutput(out, err) {
77
78
  truncated: out.wasTruncated() || err.wasTruncated(),
78
79
  };
79
80
  }
80
- function exitCodeFromClose(code, signal) {
81
+ /** A child's exit code, with a signal death mapped to 128 + its number. */
82
+ export function exitCodeFromClose(code, signal) {
81
83
  if (code !== null) {
82
84
  return code;
83
85
  }
@@ -168,6 +170,7 @@ async function spawnTracked(spec) {
168
170
  ? installGracefulShutdownForwarder(child, spec.graceMs, killTarget)
169
171
  : undefined;
170
172
  const closure = trackChildClosure();
173
+ let rejection;
171
174
  child.on('error', (error) => {
172
175
  // Abort/startup failure: make sure a partially-started tree does not
173
176
  // outlive the dispatch (a mach dying at startup used to strand
@@ -175,15 +178,24 @@ async function spawnTracked(spec) {
175
178
  if (options.processGroup === true) {
176
179
  killProcessTree(child, 'SIGKILL', usesProcessGroup);
177
180
  }
178
- forwarder?.dispose();
179
- closure.settle();
180
- reject(toExecRejection(error, command, args, options.timeout, spec.rejectionOutput?.()));
181
+ // An abort emits error before close. Keep the dispatch tracked until
182
+ // the child closes and both teardown sweeps finish, including on timeout.
183
+ rejection = toExecRejection(error, command, args, options.timeout, spec.rejectionOutput?.());
181
184
  });
182
185
  child.on('close', (code, signal) => {
183
186
  forwarder?.dispose();
184
187
  const finish = () => {
185
188
  closure.settle();
186
- resolve(spec.result(code, signal));
189
+ if (rejection)
190
+ reject(rejection);
191
+ else {
192
+ try {
193
+ resolve(spec.result(code, signal));
194
+ }
195
+ catch (error) {
196
+ reject(toError(error));
197
+ }
198
+ }
187
199
  };
188
200
  // Group sweep first (it takes the helpers that stayed in the group),
189
201
  // then the caller's own sweep (the ones that did not), then settle.
@@ -193,7 +205,7 @@ async function spawnTracked(spec) {
193
205
  finish();
194
206
  return;
195
207
  }
196
- sweep().then(finish, finish);
208
+ Promise.resolve().then(sweep).then(finish, finish);
197
209
  };
198
210
  if (groupPid !== undefined) {
199
211
  sweepProcessGroup(groupPid).then(callerSweep, callerSweep);
@@ -264,8 +276,9 @@ const activeChildClosures = new Set();
264
276
  * Registers a child whose shutdown {@link waitForActiveChildShutdown} must
265
277
  * wait for, and returns the handle that deregisters it.
266
278
  *
267
- * @returns `settle()`. Call from both the child's `close` and `error`
268
- * handlers so a dead child never holds the shutdown wait open
279
+ * @returns `settle()`. Call after child closure and required teardown finish.
280
+ * An abort's `error` event does not prove closure; do not settle there.
281
+ * A failed start may settle when no live child or teardown remains.
269
282
  */
270
283
  export function trackChildClosure() {
271
284
  let resolveClosed;
@@ -68,6 +68,17 @@ export declare function isJsonObject(value: JsonValue | undefined): value is Jso
68
68
  * Accepts formats like "140.9.0", "140.9.1", "140.9.0esr", "147.0b1"
69
69
  */
70
70
  export declare function isValidFirefoxVersion(version: string): boolean;
71
+ /**
72
+ * Orders two Firefox version strings semantically: major, minor and patch
73
+ * numerically, with a beta (`153.0b7`) before the release it precedes
74
+ * (`153.0`). An `esr` suffix does not change the order: `153.3.0esr` and
75
+ * `153.3.0` compare equal, since the product says which line it is. A
76
+ * lexical sort, which this replaces in `rebase`, put `153.10.0esr` before
77
+ * `153.2.0esr` and `99.0` after `140.0esr`. A string that is not a Firefox
78
+ * version sorts after every one that is, then lexically.
79
+ * @returns Negative when `a` is older, positive when newer, 0 when equal
80
+ */
81
+ export declare function compareFirefoxVersions(a: string, b: string): number;
71
82
  /**
72
83
  * Validates a release-candidate build directory name.
73
84
  * Accepts formats like "build1", "build2", "build12" (no leading zero).
@@ -100,6 +100,49 @@ export function isValidFirefoxVersion(version) {
100
100
  // Beta: 147.0b1, 147.0b2
101
101
  return /^[1-9]\d{0,2}\.\d+(?:b[1-9]\d*|\.\d+(?:esr)?|esr)?$/.test(version);
102
102
  }
103
+ /**
104
+ * Orders two Firefox version strings semantically: major, minor and patch
105
+ * numerically, with a beta (`153.0b7`) before the release it precedes
106
+ * (`153.0`). An `esr` suffix does not change the order: `153.3.0esr` and
107
+ * `153.3.0` compare equal, since the product says which line it is. A
108
+ * lexical sort, which this replaces in `rebase`, put `153.10.0esr` before
109
+ * `153.2.0esr` and `99.0` after `140.0esr`. A string that is not a Firefox
110
+ * version sorts after every one that is, then lexically.
111
+ * @returns Negative when `a` is older, positive when newer, 0 when equal
112
+ */
113
+ export function compareFirefoxVersions(a, b) {
114
+ const left = parseFirefoxVersionOrder(a);
115
+ const right = parseFirefoxVersionOrder(b);
116
+ if (left === undefined || right === undefined) {
117
+ if (left !== undefined)
118
+ return -1;
119
+ if (right !== undefined)
120
+ return 1;
121
+ return a < b ? -1 : a > b ? 1 : 0;
122
+ }
123
+ for (let i = 0; i < left.length; i++) {
124
+ const delta = (left[i] ?? 0) - (right[i] ?? 0);
125
+ if (delta !== 0)
126
+ return delta;
127
+ }
128
+ return 0;
129
+ }
130
+ /**
131
+ * `[major, minor, patch, betaRank]`, where a release ranks above every beta
132
+ * of the same version.
133
+ */
134
+ function parseFirefoxVersionOrder(version) {
135
+ const match = /^(\d+)\.(\d+)(?:\.(\d+))?(?:b(\d+))?(?:esr)?$/i.exec(version.trim());
136
+ if (!match)
137
+ return undefined;
138
+ const beta = match[4];
139
+ return [
140
+ Number(match[1]),
141
+ Number(match[2]),
142
+ Number(match[3] ?? 0),
143
+ beta === undefined ? Number.MAX_SAFE_INTEGER : Number(beta),
144
+ ];
145
+ }
103
146
  /**
104
147
  * Validates a release-candidate build directory name.
105
148
  * Accepts formats like "build1", "build2", "build12" (no leading zero).
@@ -59,6 +59,19 @@ tool at
59
59
  `/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/ictool`,
60
60
  plus `actool` via `xcrun`, `sips`, and `iconutil`.
61
61
 
62
+ On macOS, `fireforge doctor` also checks that the bootstrapped clang
63
+ (`~/.mozbuild/clang`, or under `MOZBUILD_STATE_PATH`) can link against the
64
+ SDK the build will use. That is the SDK `engine/mozconfig` selects with
65
+ `--with-macos-sdk`, resolved by sourcing the mozconfig the way mach does,
66
+ or `xcrun --show-sdk-path` when it names none. The check links a trivial C
67
+ program through the toolchain's lld in a temp directory. When that fails, it
68
+ reports a warning with clang's first error and lists the SDKs installed
69
+ under the mozbuild state directory, the Command Line Tools and the active
70
+ Xcode. An Xcode update can ship an SDK the pinned toolchain cannot read, and
71
+ `mach configure` then dies at its linker probe with nothing but `config.log`
72
+ saying why. The check does nothing on other platforms and when no clang has
73
+ been bootstrapped.
74
+
62
75
  ## `patchLint`
63
76
 
64
77
  Per-patch lint type-checks plain Firefox JS against a built-in
@@ -84,6 +97,28 @@ Firefox-globals shim that tracks upstream WebIDL additions per release.
84
97
  project's, then `npx --no-install`. Left at `'off'`, formatting is out of
85
98
  scope for the per-patch tier, and no other tier checks `.sys.mjs`
86
99
  formatting either.
100
+ - `patchLint.forwardRegistration` (`'support-files'` or `'extended'`,
101
+ default `'support-files'`) picks which carriers the `forward-registration`
102
+ queue rule checks for a line naming a file that only a later patch
103
+ creates. The default checks test-manifest `support-files`. `'extended'`
104
+ also checks:
105
+ - jar.mn pair lines (the source in parentheses, relative to the jar.mn);
106
+ - moz.build path tokens (a quoted string with a `.` or `/`, relative to
107
+ the moz.build);
108
+ - test-manifest section headers and `head`;
109
+ - `customElements.js` `chrome://<pkg>/content/…` URLs, resolved through
110
+ the jar.mn lines the same queue adds.
111
+
112
+ A line whose target cannot be resolved without the build system is left
113
+ alone: a `%` or `@AB_CD@` substitution, `**`, `!`, a path that leaves the
114
+ tree, or a chrome URL no queue jar.mn line packages. Each finding prints
115
+ the same paste-and-run `patch staged-dependency --add --kind registration`
116
+ command as the `support-files` arm. The setting is opt-in because a queue
117
+ that predates it can carry many such edges, and a new error would red a
118
+ `--max-warnings 0` gate on install. The same census is on the public API as
119
+ `findForwardRegistrations(patchesDir, { scope })`, which defaults to
120
+ `'extended'`, for a consumer that keeps its own ratchet over these edges.
121
+
87
122
  - `patchLint.fileSizeThresholds` tunes the `file-too-large` line counts per
88
123
  file class:
89
124
 
@@ -105,6 +140,28 @@ Firefox-globals shim that tracks upstream WebIDL additions per release.
105
140
  is a hard failure rather than a soft limit, which is what these thresholds
106
141
  exist to let you move.
107
142
 
143
+ ## `typecheck`
144
+
145
+ `typecheck.projects` lists the jsconfig files `fireforge typecheck` checks
146
+ as whole projects. `typecheck.extraShim` and `typecheck.projectOverrides`
147
+ add declarations to the Firefox-globals shim, for every project or for one.
148
+ When `patchLint.checkJs` is on, the same command also runs the per-patch
149
+ checkJs pass that `export` enforces, so a green `typecheck` means
150
+ export-clean types.
151
+
152
+ Project build info is written in `.fireforge/typecheck/`, outside `engine/`,
153
+ with a key covering the shim, TypeScript version, compiler options and
154
+ FireForge version. Diagnostics always use a fresh project
155
+ program: prior incremental diagnostic state is never restored. Cached and
156
+ `--no-cache` runs therefore check the same complete file set. `--no-cache`
157
+ also disables build-info writes. A jsconfig's own `incremental` and
158
+ `tsBuildInfoFile` remain ignored.
159
+
160
+ Both project and per-patch checkJs provide Gecko DOM constructor
161
+ `isInstance(value)` predicates with instance narrowing, plus
162
+ `MouseEvent.MOZ_SOURCE_PEN`. These declarations retain normal DOM member
163
+ checking; they do not turn constructors or instances into `any`.
164
+
108
165
  ## `patchPolicy`
109
166
 
110
167
  `patchPolicy.ranges` makes `patch compact` range-aware, so closing ordinal
@@ -225,6 +282,31 @@ non-CI launches print a warning saying so. Exit codes 12 and 13 separate a
225
282
  console regression from a launch failure. See
226
283
  [`exit-codes.md`](exit-codes.md).
227
284
 
285
+ A smoke run starts on a fresh temporary profile by default (since 0.48.0).
286
+ The step exists to prove what the build does on a clean start. The
287
+ developer's `obj-*/tmp/profile-default` carries whatever the last session
288
+ left (a pre-reset database, a restored hand-made applet), and those reds
289
+ say nothing about the tree. FireForge creates the directory under the OS
290
+ temp dir, seeds the two prefs `mach run` writes into profiles it makes
291
+ itself (`browser.shell.checkDefaultBrowser` and
292
+ `browser.aboutConfig.showWarning`), and passes it to Firefox as
293
+ `-profile`. The directory is removed when the run ends. That includes a
294
+ failed run and a SIGINT/SIGTERM, which the signal handler removes after
295
+ the browser tree has shut down. A FireForge that was SIGKILLed cannot clean
296
+ up after itself, so the next `run` removes any temporary profile whose
297
+ owning process is gone.
298
+
299
+ - `--profile <path>` launches with a named profile directory and leaves it
300
+ in place. Under `--smoke-exit` it replaces the fresh default.
301
+ - `--temp-profile` gives a plain `run` the same fresh, removed-afterwards
302
+ profile.
303
+
304
+ A plain `run` without either flag keeps mach's `profile-default`. `mach run`'s
305
+ desktop parser has no `--profile` option (its `--profile/-P` belongs to the
306
+ Android parser), so the profile is always passed as a program parameter.
307
+ With an explicit profile, mach neither writes its `runprefs` from
308
+ `~/.mozbuild/machrc` nor accepts `--setpref`.
309
+
228
310
  The allowlist (`--console-allow-file`) is count-blind by default: a line that
229
311
  matches an entry never counts, however often it fires. A `# max-hits: N`
230
312
  comment directly above an entry (other comments may sit between) caps how
@@ -15,7 +15,7 @@ which error class produces each one.
15
15
  | 3 | `DOWNLOAD_ERROR` | Could not download or extract Firefox source. | `DownloadError` and subclasses |
16
16
  | 4 | `GIT_ERROR` | A git operation failed. | `GitError` and subclasses, except `GitNotFoundError` |
17
17
  | 5 | `BUILD_ERROR` | `mach build` failed, or a test suite went red. | `BuildError` and subclasses, `TestFailureError` |
18
- | 6 | `PATCH_ERROR` | A patch failed to apply, or the patch queue could not be mutated. | `PatchError`, `PatchManifestCorruptError`, `RebaseError` and subclasses |
18
+ | 6 | `PATCH_ERROR` | A patch failed to apply (including a patch `rebase --dry-run` predicts will reject), or the patch queue could not be mutated. | `PatchError`, `PatchManifestCorruptError`, `RebaseError` and subclasses |
19
19
  | 7 | `MISSING_DEPENDENCY` | A required tool (python3, git, tar) was not found. | `PythonNotFoundError`, `GitNotFoundError`, `MachNotFoundError` |
20
20
  | 8 | `INVALID_ARGUMENT` | The flags were wrong. A usage problem, not an environment one. | `InvalidArgumentError`, plus commander usage errors (unknown command or option, missing argument) |
21
21
  | 9 | `FURNACE_ERROR` | A Furnace component operation failed. | `FurnaceError` |
@@ -113,11 +113,15 @@ command, read the decision table at the bottom first.
113
113
  carry primary shebangs, and mach rebuilds them in-tree), and runs
114
114
  `mach configure` inside the tree so the configure-generated root files are
115
115
  regenerated against the tree's paths rather than keeping the primary's. It
116
- then verifies exactly that set afterwards (config.status, backend.mk,
117
- Makefile, config/autoconf.mk, via `findObjdirRelocationViolation`, pinned
118
- by `mach-objdir-relocation.test.ts`). Nested Makefiles are products of the
119
- verified config.status, and `.deps` build products are out of scope as
120
- read-only staleness that the first in-tree rebuild corrects. Finally it
116
+ Before configure, relocation rewrites old-source symlinks, `.pp`/`.d`,
117
+ FasterMake and Cargo fingerprint metadata, and removes copied
118
+ `config.statusd/*/config.track` inventories. Those inventories contain
119
+ absolute WRITE destinations and must never be consumed from a clone.
120
+ Aliased objdirs or partial-config directories are refused. Afterwards
121
+ `findObjdirRelocationViolation` verifies config.status, backend.mk,
122
+ Makefile, config/autoconf.mk and every remaining objdir symlink for the
123
+ old source prefix. External toolchain links are allowed; source links
124
+ must no longer consult the old checkout. Finally it
121
125
  copies `last-build.json` so the in-tree stale-build gates anchor
122
126
  correctly.
123
127
 
@@ -190,9 +194,10 @@ command, read the decision table at the bottom first.
190
194
  stages and unstages for real. Each of those commands therefore runs its
191
195
  whole body inside `withPrivateGitIndex`, which seeds a temp
192
196
  `GIT_INDEX_FILE` from the repository's own index so that every refresh
193
- lands there and is discarded. `typecheck` additionally forces
194
- `incremental: false` and drops `tsBuildInfoFile`, so no sidecar is written
195
- under `engine/`. The scope fails open, meaning an unresolvable git dir
197
+ lands there and is discarded. `typecheck` additionally ignores a
198
+ jsconfig's own `incremental` and `tsBuildInfoFile`, so no sidecar is
199
+ written under `engine/`. Its incremental build info (0.48.0) lives in the
200
+ project's `.fireforge/typecheck/`, outside the engine. The scope fails open, meaning an unresolvable git dir
196
201
  simply does not install it, and it is not reentrant. This exists so that
197
202
  invariant 8 stays evidential: a concurrent `fireforge test` must not be
198
203
  invalidated by FireForge's own read-only lane. Enforced by
@@ -86,15 +86,16 @@ moved.
86
86
  `FIREFORGE-VERDICT: FAIL reason=<reason>` uses a closed set
87
87
  (`FireforgeVerdictReason`, `src/commands/test-verdict.ts`):
88
88
 
89
- | Reason | Meaning |
90
- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
- | `crash` | The harness or the browser died, so there is no trustworthy suite result. |
92
- | `no-tests` | The run dispatched but nothing ran. |
93
- | `test-failures` | The suite ran and reported unexpected results. |
94
- | `preflight` | The run was refused before the harness was reached. |
95
- | `inconclusive` | A result exists, but `engine/` moved under it, so the result was discarded. |
96
- | `lock-timeout` | The run never started: the engine session lock stayed contended. |
97
- | `killed` | A signal terminated the run, or FireForge ended it because its parent process vanished. Written from the signal handler (or the parent-exit watchdog) so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. `signal=` names the cause: a signal name, or `parent-exit`. |
89
+ | Reason | Meaning |
90
+ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `crash` | The harness or the browser died, so there is no trustworthy suite result. |
92
+ | `harness-arguments` | Profile or other harness argument setup failed before any tests could run; a rebuild is not the repair. |
93
+ | `no-tests` | The run dispatched but nothing ran. |
94
+ | `test-failures` | The suite ran and reported unexpected results. |
95
+ | `preflight` | The run was refused before the harness was reached. |
96
+ | `inconclusive` | A result exists, but `engine/` moved under it or a perf run changed power source, so the result was discarded. |
97
+ | `lock-timeout` | The run never started: the engine session lock stayed contended. |
98
+ | `killed` | A signal terminated the run, or FireForge ended it because its parent process vanished. Written from the signal handler (or the parent-exit watchdog) so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. `signal=` names the cause: a signal name, or `parent-exit`. |
98
99
 
99
100
  ### The additive `note=` key
100
101
 
@@ -109,20 +110,19 @@ unchanged and still comes from the closed set above, so a consumer that
109
110
  tokenises `key=value` pairs is unaffected, and a gate that names no class
110
111
  emits no `note=` at all. The classes are not a closed set and may grow, so
111
112
  consumers should treat an unrecognised one as opaque. Current values:
112
- `stale-browser`, `stale-browser-kill-failed`, `marionette-port-busy`,
113
+ `browser-busy`, `stale-browser`, `stale-browser-kill-failed`, `marionette-port-busy`, `mochitest-port-busy`,
113
114
  `coverage-replaced`, `stale-build`, `stale-components`.
114
115
 
115
116
  ### The additive `shuffle=` key
116
117
 
117
- A `fireforge test --shuffle [seed]` run forwards mach's `--shuffle` (an
118
- unseeded shuffle of file order in the harness) and exports the seed FireForge
119
- drew or was given as `FIREFORGE_SHUFFLE_SEED` in the harness environment. The
120
- seed is appended to the verdict line as `shuffle=<seed>` on every emission
121
- form, so a red found by shuffling carries its own reproduction
122
- (`fireforge test --shuffle=<seed> …`) on the one line a pipe keeps. Reordering
123
- tasks _inside_ a file is harness code that reads the exported seed; FireForge
124
- owns the seed, the plumbing and the record. Absent when the run did not
125
- shuffle.
118
+ A `fireforge test --shuffle [seed]` run records the seed used to permute
119
+ FireForge's isolated path-argument shards. Repeating the seed and selection
120
+ repeats that shard order. It also forwards native mach `--shuffle`, which
121
+ randomizes files within an invocation without using this seed. `--no-shard`
122
+ disables FireForge's seeded argument permutation. The seed is exported as
123
+ `FIREFORGE_SHUFFLE_SEED` for custom in-file task code and appended as
124
+ `shuffle=<seed>` to every verdict form. This field does not claim replayable
125
+ native harness file order. Absent when shuffle was not requested.
126
126
 
127
127
  ### The additive `orphans-reaped=` key
128
128
 
@@ -141,3 +141,20 @@ A preflight refusal's own text is written to stdout before the verdict line,
141
141
  and into the run log, so the verdict stays the last stdout write while the
142
142
  reason survives both a redirect and a `tail`. See
143
143
  [`run-logs.md`](run-logs.md).
144
+
145
+ ### Additive host and perf keys
146
+
147
+ `host-load=<number>` records the one-minute load at suite startup. Each suite
148
+ also samples the highest CPU process before and after dispatch. A contention
149
+ warning appends `host-cpu-warning=true`; the diagnostic names the process
150
+ and measured CPU. These diagnostics annotate a test failure without changing
151
+ its classifier reason.
152
+
153
+ Perf runs append `power-source=ac|battery|unknown`. On macOS the source is
154
+ read from `pmset -g ps`, so charging on AC is correctly `ac`. A known source
155
+ transition appends `power-changed=true` and refuses the run as
156
+ `reason=inconclusive`. The producer's sample JSON retains its fields and
157
+ gains `fireforgeHost: { start, end, powerSourceChanged }`, where each host
158
+ state includes `load`, `power` and an optional `topProcess` with CPU and
159
+ command. Consumers should tolerate this additive object. Unknown power
160
+ state is visible but never treated as a proven transition.
@@ -56,6 +56,21 @@ requested path produces no hunk. That is checked against the full requested
56
56
  list rather than the subset left after filtering, so nothing dropped upstream
57
57
  can pass as a silent success.
58
58
 
59
+ An absent file originally created by a selected patch can be retired with
60
+ repeatable `--expect-removed <path>` on scan-less re-export:
61
+
62
+ ```sh
63
+ fireforge re-export 202 --refuse-adjacent-unmanaged --refuse-foreign-drift --expect browser/themes/shared/hominis/widgets.css --expect-removed browser/themes/shared/hominis/stop.svg
64
+ ```
65
+
66
+ The removal must have exactly one selected owner, be absent from disk and
67
+ engine HEAD, and have a `new file mode` section in that owner's patch.
68
+ Every removal is validated before writes, in dry-run too. Other paths keep
69
+ both refusal checks; retirement grants no unmanaged or foreign-drift
70
+ exception. Removing every file would empty the patch and requires `patch
71
+ drop` instead. `--expect-removed` cannot combine with `--scan`, `--files` or
72
+ `--all`.
73
+
59
74
  ## Queue maintenance
60
75
 
61
76
  Under `fireforge patch`:
@@ -81,11 +96,23 @@ slugs, and bare slugs.
81
96
  `lint --per-patch --max-warnings 0` is the warning-clean form for a release
82
97
  gate.
83
98
 
99
+ `lint --per-patch --notices summary` prints one line per check in place of
100
+ every NOTICE row, for example `NOTICE [large-patch-lines]: 180 across 172
101
+ patch(es), 150 of them suppressed by lintIgnore (see --report)`. On a green
102
+ queue those rows are reviewed waivers and under-threshold measurements, and
103
+ there can be hundreds of them. The default, `--notices full`, prints them
104
+ as before. Errors and warnings always print in full, and the `--report`
105
+ JSON is the same in both modes.
106
+
84
107
  `lint --per-patch --report <path>` writes a machine-readable JSON report with
85
108
  each patch's line count, tier, active size thresholds, issues, and issues
86
109
  suppressed by `lintIgnore`. The size metrics (`countNonBinaryDiffLines`,
87
110
  `resolvePatchSizeTier`, `getPatchSizeThresholds`) are also exported on the
88
- programmatic API.
111
+ programmatic API. So is `filesMeasuredByFileTooLarge(patchText)`, which
112
+ returns the files `file-too-large` measures in a patch: the JS files it
113
+ creates, and never a modified file or a new file of another kind. The rule
114
+ selects its files through that same function, so an audit that uses it
115
+ cannot drift from the rule.
89
116
 
90
117
  Per-patch lint type-checks plain Firefox JS against a built-in
91
118
  Firefox-globals shim that tracks upstream WebIDL additions per release. See
@@ -116,6 +143,28 @@ npx fireforge download --force
116
143
  npx fireforge rebase
117
144
  ```
118
145
 
146
+ `download --force` is the destructive step of the three, not `rebase`. It
147
+ replaces `engine/` wholesale, so applied patches, unexported edits and
148
+ commits made in `engine/` are gone once the new tree is active. When the
149
+ engine it is about to replace has changed paths against HEAD, or HEAD has
150
+ moved off the recorded base commit, it names the loss (how many changed
151
+ paths match the patch queue and how many do not) and asks first. A
152
+ non-interactive run refuses unless `--yes` is given. A clean engine is
153
+ replaced without a prompt.
154
+
155
+ `npx fireforge rebase --dry-run` replays the queue onto the engine's HEAD
156
+ without touching `engine/`. It seeds a private index from HEAD and applies
157
+ each patch there with `git apply --cached`, in queue order, walking the same
158
+ context-reduction ladder as the real run (exact, then `-C2`, `-C1`, `-C0`,
159
+ bounded by `--max-fuzz`). Each patch is reported as `clean`, `reduced
160
+ context` (with the step it needed) or `reject` (with the files git named).
161
+ It exits 6 when anything rejects. A reject after an earlier one is marked
162
+ as possibly cascading from it, since the replay continues without the
163
+ rejected patch. Run it after the download, when HEAD is the new source. The
164
+ header names the oldest patch stamp as "from", compared as versions rather
165
+ than strings, prints the spread when the queue carries several stamps, and
166
+ warns when `engine/` is not yet on the pinned version.
167
+
119
168
  If a patch fails, fix the reject inside `engine/`, then run
120
169
  `npx fireforge rebase --continue`. A rebase leaves the engine mutated on
121
170
  purpose, so its apply-and-persist pairs are held across a signal rather than
package/docs/testing.md CHANGED
@@ -34,9 +34,59 @@ build, `test` fails before launching stale artifacts. Use `--build` to
34
34
  refresh, or `--allow-stale-build` only for intentional out-of-band rebuilds.
35
35
 
36
36
  `--build-only` packages mixed-harness paths once, and then each harness half
37
- can run without `--build`. `--extend-coverage` retains prior scoped coverage
38
- instead of replacing it. It is refused when the build anchor moved (engine
39
- HEAD, `engine/mozconfig`, or a previously fingerprinted packageable file).
37
+ can run without `--build`. Scoped builds retain prior coverage automatically
38
+ when engine HEAD, `engine/mozconfig` and recorded staging inputs outside the
39
+ rebuilt scope are unchanged. Changed files explicitly included in this
40
+ build are allowed. The fingerprints include test scripts and support files,
41
+ not only packageable chrome sources. A failed anchor narrows the claim and
42
+ prints the dropped paths; `--extend-coverage` instead refuses. Baselines
43
+ without complete staging-input fingerprints require a new build before they
44
+ can retain prior coverage.
45
+ New dirty inputs outside the rebuilt scope invalidate retention conservatively,
46
+ including shared fixtures outside test directories.
47
+
48
+ When `patchLint.checkJs` and `patchLint.checkJsTestFiles` are enabled, named
49
+ pre-test builds check the selected test scripts before deployment or mach.
50
+ This includes new scripts not yet owned by an exported patch.
51
+
52
+ ### Shared browsers, ports and host state
53
+
54
+ Browser ownership is checked before a pre-test deployment and before full
55
+ or UI builds. A live or unreadable parent means busy, with its owner named
56
+ when available. `--kill-stale-marionette` requires a dead parent plus harness
57
+ arguments; a profile argument alone does not prove abandonment. A plain
58
+ launchd-parented app remains unattributed and is not automatically killed.
59
+
60
+ `test --wait-browser [seconds]` waits for this objdir's browser to exit;
61
+ `test --wait-port [seconds]` waits for mochitest and Marionette listeners.
62
+ `build --wait-browser [seconds]` queues a full or UI build behind the browser.
63
+ Bare flags wait 60 seconds; explicit values accept 1–3600 seconds. They are
64
+ independent of the FireForge lock budget. On expiry the normal preflight
65
+ refusal and its remedy are preserved.
66
+
67
+ Every suite samples host load and the highest CPU process before and after
68
+ dispatch. Load at least 4 or a process at least 90% CPU produces a warning;
69
+ verdicts include `host-load` and, when needed, `host-cpu-warning=true`.
70
+ Perf runs include `power-source=ac|battery|unknown`; on macOS this comes from
71
+ `pmset -g ps`, including charging on AC. Sample JSON gains a `fireforgeHost`
72
+ object with start/end states and `powerSourceChanged`. A known power-source
73
+ transition produces `FAIL reason=inconclusive power-changed=true`.
74
+ Unknown power state is reported without inventing a transition.
75
+
76
+ ### Profile overlays
77
+
78
+ Use repeatable `--profile-file source=relative/destination` to add files to
79
+ the test profile, for example:
80
+
81
+ ```sh
82
+ fireforge test --perf-samples sample.json --profile-file arm.css=chrome/userChrome.css browser/base/content/test/perf/browser_perf.js
83
+ ```
84
+
85
+ The source resolves from the project root. Destinations must stay relative
86
+ with no traversal. FireForge stages copies and merges only those staged
87
+ directories into the harness profile, preserving existing chrome files.
88
+ A raw `--extra-profile-file` directory collision is classified as
89
+ `reason=harness-arguments` and names this remedy rather than a rebuild.
40
90
 
41
91
  ### What a pre-test build costs
42
92
 
@@ -183,15 +233,16 @@ redirected run keeps the reason as well as the verdict. See
183
233
 
184
234
  ## Seeded shuffle
185
235
 
186
- `fireforge test --shuffle` runs a mochitest selection in a seeded random file
187
- order and prints the seed; `--shuffle=<seed>` replays it. FireForge forwards
188
- mach's own `--shuffle`, exports `FIREFORGE_SHUFFLE_SEED=<seed>` to the harness
189
- so in-file task reordering (harness `head.js` code, not FireForge) can key off
190
- the same number, and stamps `shuffle=<seed>` on the verdict line. It is
191
- mochitest-only: the xpcshell harness has no shuffle, and generic `mach test`
192
- dispatch would hand the flag to every suite. Write the flag as
193
- `--shuffle=<seed>` or after the paths, because a bare `--shuffle <path>` reads
194
- the path as the seed.
236
+ `fireforge test --shuffle` deterministically permutes isolated mochitest path
237
+ arguments and prints the seed; `--shuffle=<seed>` replays that shard order for
238
+ the same input selection. A directory remains one invocation. `--no-shard`
239
+ disables this argument permutation. FireForge also forwards native mach
240
+ `--shuffle` within each invocation, whose file order remains unseeded and is
241
+ not replayed by the FireForge seed. `FIREFORGE_SHUFFLE_SEED=<seed>` is available
242
+ to custom harness `head.js` code for in-file task ordering, and `shuffle=<seed>`
243
+ is recorded on the verdict. The option is mochitest-only. Write
244
+ `--shuffle=<seed>` or place it after the paths because a bare
245
+ `--shuffle <path>` consumes the path as its optional seed.
195
246
 
196
247
  Exit code 14 (`INCONCLUSIVE`) is not red: it means `engine/` moved while the
197
248
  harness ran and the result was thrown away. Exit 15 (`LOCK_TIMEOUT`) means
@@ -256,3 +307,17 @@ instead, use a [verification tree](verification-trees.md).
256
307
  In dev builds, files under `obj-*/dist/bin` may be symlinks back into the
257
308
  source tree (prefs especially), so edit source prefs directly and keep a
258
309
  backup before bisection experiments.
310
+
311
+ ## Repository release gate
312
+
313
+ Run `npm run release:check` with the exact Node version in `.nvmrc` and npm
314
+ version in `package.json`'s `packageManager`. The gate verifies these pins
315
+ before formatting, whitespace, zero-warning lint, strict typecheck, development
316
+ and production dead-code checks, import cycles, coverage floors, and installed
317
+ package smoke tests. `prepublishOnly` and the CI quality job run the same gate.
318
+ Supported Node-version and OS matrices continue to run ordinary tests separately.
319
+
320
+ The full-Firefox integration runner is opt-in. It snapshots project files,
321
+ engine changes and the index before arming recovery; a refusal cannot erase an
322
+ existing workspace. Failed recovery fails the run, preserves affected paths and
323
+ writes recovery evidence for manual repair.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.47.1",
3
+ "version": "0.49.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -39,8 +39,9 @@
39
39
  "format:check": "prettier --check --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"eslint-rules/**/*.js\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"vitest.pack.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\" \"docs/**/*.md\" \"knip.json\" \"knip.production.json\"",
40
40
  "prepare": "node -e \"import('./scripts/prepare.mjs').catch((error) => { if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') process.exit(0); throw error; })\"",
41
41
  "prepack": "npm run build && node ./scripts/generate-build-info.mjs --loud",
42
- "release:check": "npm run format:check && npm run whitespace:check && npm run lint:ci && npm run typecheck && npm run deadcode:check && npm run deadcode:check:production && npm run cycles:check && npm run test:coverage && npm run pack:verify",
43
- "prepublishOnly": "npm run release:check"
42
+ "release:check": "npm run toolchain:check && npm run format:check && npm run whitespace:check && npm run lint:ci && npm run typecheck && npm run deadcode:check && npm run deadcode:check:production && npm run cycles:check && npm run test:coverage && npm run pack:verify",
43
+ "prepublishOnly": "npm run release:check",
44
+ "toolchain:check": "node ./scripts/check-release-toolchain.mjs"
44
45
  },
45
46
  "files": [
46
47
  "dist/",
@@ -75,9 +76,9 @@
75
76
  "eslint-plugin-simple-import-sort": "^14.0.0",
76
77
  "fast-check": "^4.6.0",
77
78
  "husky": "^9.1.7",
78
- "knip": "6.35.1",
79
+ "knip": "6.37.0",
79
80
  "lint-staged": "^17.0.4",
80
- "prettier": "3.9.6",
81
+ "prettier": "3.9.8",
81
82
  "tsx": "^4.7.0",
82
83
  "typescript": "~6.0.0",
83
84
  "typescript-eslint": "^8.0.0",
@@ -108,5 +109,9 @@
108
109
  "publishConfig": {
109
110
  "access": "public",
110
111
  "provenance": true
112
+ },
113
+ "allowScripts": {
114
+ "esbuild@0.28.1": true,
115
+ "fsevents@2.3.3": true
111
116
  }
112
117
  }