@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
@@ -18,11 +18,15 @@
18
18
  * without installing it, the function returns a clear error pointing
19
19
  * at `npm install typescript`.
20
20
  */
21
- import { dirname, isAbsolute, relative, resolve } from 'node:path';
21
+ import { mkdir, readdir, rm } from 'node:fs/promises';
22
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
22
23
  import { toError } from '../utils/errors.js';
23
24
  import { pathExists } from '../utils/fs.js';
25
+ import { sha256Hex } from '../utils/hash.js';
24
26
  import { verbose } from '../utils/logger.js';
27
+ import { getPackageVersion } from '../utils/package-root.js';
25
28
  import { normalizePathSlashes } from '../utils/paths.js';
29
+ import { withGeckoDomTypes } from './gecko-dom-types.js';
26
30
  import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
27
31
  /**
28
32
  * Sentinel string surfaced as a `TypecheckIssue.message` when the
@@ -60,7 +64,7 @@ export const CHECK_JS_DISABLED_NOTICE = 'Project sets "checkJs: false" — skipp
60
64
  * in declared order. The CLI is responsible for printing the issues
61
65
  * and computing the exit code.
62
66
  */
63
- export async function runTypecheck(projectRoot, cfg) {
67
+ export async function runTypecheck(projectRoot, cfg, runOptions = {}) {
64
68
  // Dynamic import, so typescript stays a dev dependency. Same pattern
65
69
  // as `patch-lint-checkjs.ts`. The empty-projects-array case is
66
70
  // already rejected by config-validate, so we don't gate the import
@@ -133,7 +137,9 @@ export async function runTypecheck(projectRoot, cfg) {
133
137
  });
134
138
  continue;
135
139
  }
136
- results.push(await runTypecheckForProject(ts, projectRoot, projectPath, shimSource, cfg.undefinedIdentifiers ?? 'warning'));
140
+ results.push(await runTypecheckForProject(ts, projectRoot, projectPath, shimSource, cfg.undefinedIdentifiers ?? 'warning', runOptions.noCache === true
141
+ ? undefined
142
+ : (runOptions.cacheDir ?? join(projectRoot, '.fireforge', 'typecheck'))));
137
143
  }
138
144
  return results;
139
145
  }
@@ -151,7 +157,7 @@ function resolveProjectExtraShim(cfg, projectPath) {
151
157
  return cfg.extraShim;
152
158
  }
153
159
  /** Runs typecheck for a single jsconfig path, isolating its failures. */
154
- async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource, undefinedIdentifiers = 'warning') {
160
+ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource, undefinedIdentifiers = 'warning', cacheDir) {
155
161
  const absConfig = resolve(projectRoot, projectPath);
156
162
  if (!(await pathExists(absConfig))) {
157
163
  return {
@@ -223,12 +229,12 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
223
229
  noEmit: true,
224
230
  allowJs: parsed.options.allowJs ?? true,
225
231
  checkJs: parsed.options.checkJs ?? true,
226
- // No incremental sidecar, ever. A user jsconfig under `engine/` that sets
227
- // `incremental` (or names a `tsBuildInfoFile`) would have this command
228
- // drop a `.tsbuildinfo` inside the primary engine checkout: a second
229
- // writer that invalidates a concurrent `fireforge test`'s engine
230
- // fingerprint. This command emits nothing, so the sidecar buys nothing
231
- // either.
232
+ // Never the jsconfig's own incremental sidecar: a user jsconfig under
233
+ // `engine/` that sets `incremental` (or names a `tsBuildInfoFile`) would
234
+ // have this command drop a `.tsbuildinfo` inside the primary engine
235
+ // checkout, a second writer that invalidates a concurrent
236
+ // `fireforge test`'s engine fingerprint. FireForge's own build info
237
+ // lives under `.fireforge/typecheck/` instead (see below).
232
238
  incremental: false,
233
239
  // skipLibCheck is not forced. The user owns it via their jsconfig.
234
240
  };
@@ -247,7 +253,7 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
247
253
  // filtered out below, so the pass reports failures that do not exist.
248
254
  const shimPath = normalizePathSlashes(resolve(projectDir, `.fireforge-${SHIM_FILENAME}`));
249
255
  const rootFiles = [...parsed.fileNames, shimPath];
250
- const defaultHost = ts.createCompilerHost(options);
256
+ const defaultHost = withGeckoDomTypes(ts, ts.createCompilerHost(options));
251
257
  const host = {
252
258
  ...defaultHost,
253
259
  getSourceFile(fileName, languageVersion, onError, shouldCreate) {
@@ -267,7 +273,37 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
267
273
  return defaultHost.readFile(fileName);
268
274
  },
269
275
  };
270
- const program = ts.createProgram(rootFiles, options, host);
276
+ // Incremental build info, keyed by everything outside the program's own
277
+ // files that can change a verdict: the composed shim, the TypeScript
278
+ // version, the compiler options, the undefined-identifier gate and the
279
+ // FireForge version. A change to any of them selects a different file,
280
+ // so a stale verdict is never replayed. Files inside the program are
281
+ // tracked by TypeScript's own per-file hashes in the build info.
282
+ const buildInfoFile = cacheDir === undefined
283
+ ? undefined
284
+ : await prepareBuildInfoFile(cacheDir, projectPath, {
285
+ shim: sha256Hex(shimSource),
286
+ typescript: ts.version,
287
+ fireforge: getPackageVersion(),
288
+ undefinedIdentifiers,
289
+ options,
290
+ });
291
+ let program;
292
+ let writeBuildInfo = () => undefined;
293
+ if (buildInfoFile === undefined) {
294
+ program = ts.createProgram(rootFiles, options, host);
295
+ }
296
+ else {
297
+ // Always check the entire project in root order. Restoring TypeScript's
298
+ // per-file semantic cache makes checkJs inference depend on the first
299
+ // invalidated importer and can both invent and hide diagnostics.
300
+ const builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootFiles, { ...options, incremental: true, tsBuildInfoFile: buildInfoFile }, withSourceFileVersions(host));
301
+ program = builder;
302
+ // Under noEmit, emit() writes the build info and nothing else.
303
+ writeBuildInfo = () => {
304
+ builder.emit();
305
+ };
306
+ }
271
307
  // Collect the full diagnostic set. patchLint reads only semantic +
272
308
  // syntactic, which is fine for hygiene but wrong for CI: a misconfigured
273
309
  // `lib: ["es2015"]` or a missing `paths` target should fail
@@ -298,6 +334,7 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
298
334
  }
299
335
  issues.push(diagnosticToIssue(ts, diag, absConfig, projectPath));
300
336
  }
337
+ writeBuildInfo();
301
338
  verbose(`typecheck: ${projectPath} — analyzed ${parsed.fileNames.length} file(s), found ${issues.length} issue(s)`);
302
339
  return {
303
340
  project: projectPath,
@@ -305,6 +342,43 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
305
342
  filesChecked: parsed.fileNames.length,
306
343
  };
307
344
  }
345
+ /**
346
+ * Stamps every source file with a content-hash version, which the
347
+ * incremental builder requires and a plain compiler host never sets.
348
+ * `ts.createIncrementalCompilerHost` does the same internally, but the
349
+ * shim here is served by a custom host rather than read from disk.
350
+ */
351
+ function withSourceFileVersions(host) {
352
+ const getSourceFile = host.getSourceFile.bind(host);
353
+ return {
354
+ ...host,
355
+ getSourceFile(...args) {
356
+ const file = getSourceFile(...args);
357
+ if (file !== undefined) {
358
+ file.version = sha256Hex(file.text);
359
+ }
360
+ return file;
361
+ },
362
+ };
363
+ }
364
+ /**
365
+ * Resolves the build-info path for one project and key, creating the cache
366
+ * directory and removing the project's build info for any other key: a
367
+ * shim or TypeScript change leaves the old file unreachable anyway.
368
+ * @returns Absolute build-info path
369
+ */
370
+ async function prepareBuildInfoFile(cacheDir, projectPath, keyInputs) {
371
+ const slug = projectPath.replace(/[^A-Za-z0-9._-]+/g, '_');
372
+ const key = sha256Hex(JSON.stringify(keyInputs)).slice(0, 16);
373
+ const fileName = `${slug}-${key}.tsbuildinfo`;
374
+ await mkdir(cacheDir, { recursive: true });
375
+ for (const entry of await readdir(cacheDir)) {
376
+ if (entry !== fileName && entry.startsWith(`${slug}-`) && entry.endsWith('.tsbuildinfo')) {
377
+ await rm(join(cacheDir, entry), { force: true });
378
+ }
379
+ }
380
+ return join(cacheDir, fileName);
381
+ }
308
382
  /**
309
383
  * Converts a TS Diagnostic to a TypecheckIssue. The `fallbackFile`
310
384
  * is used when the diagnostic carries no source file (typical for
@@ -31,3 +31,12 @@ export declare class NoRebaseSessionError extends RebaseError {
31
31
  export declare class CorruptRebaseSessionError extends RebaseError {
32
32
  constructor(sessionPath: string, reason: string);
33
33
  }
34
+ /**
35
+ * Thrown by `rebase --dry-run` when the replay rejects at least one patch.
36
+ * A dry run that exited 0 regardless used to read as "the rebase is ready".
37
+ */
38
+ export declare class RebaseDryRunRejectError extends RebaseError {
39
+ readonly rejected: readonly string[];
40
+ constructor(rejected: readonly string[]);
41
+ get userMessage(): string;
42
+ }
@@ -49,3 +49,23 @@ export class CorruptRebaseSessionError extends RebaseError {
49
49
  'or delete the file to start over.');
50
50
  }
51
51
  }
52
+ /**
53
+ * Thrown by `rebase --dry-run` when the replay rejects at least one patch.
54
+ * A dry run that exited 0 regardless used to read as "the rebase is ready".
55
+ */
56
+ export class RebaseDryRunRejectError extends RebaseError {
57
+ rejected;
58
+ constructor(rejected) {
59
+ super(`Dry run: ${rejected.length} patch(es) would not apply: ${rejected.join(', ')}. ` +
60
+ 'engine/ was not modified.');
61
+ this.rejected = rejected;
62
+ }
63
+ get userMessage() {
64
+ return (`Rebase Error: ${this.message}` +
65
+ remedies([
66
+ 'The per-patch lines above name the files that did not apply',
67
+ 'Run "fireforge rebase" to start the real rebase; it stops at the first reject for manual resolution',
68
+ 'Later rejects may cascade from an earlier one; resolve in queue order',
69
+ ]));
70
+ }
71
+ }
@@ -16,12 +16,14 @@
16
16
  * @packageDocumentation
17
17
  */
18
18
  export { loadConfig, validateConfig } from './core/config.js';
19
+ export type { FindForwardRegistrationsOptions, ForwardRegistration, ForwardRegistrationKind, ForwardRegistrationScope, } from './core/forward-registrations.js';
20
+ export { findForwardRegistrations } from './core/forward-registrations.js';
19
21
  export type { ApplyAllComponentsResult } from './core/furnace-apply.js';
20
22
  export { applyAllComponents } from './core/furnace-apply.js';
21
23
  export { ensureFurnaceConfig, loadFurnaceConfig, loadFurnaceState, saveFurnaceState, validateFurnaceConfig, } from './core/furnace-config.js';
22
24
  export { validateAllComponents, validateComponent } from './core/furnace-validate.js';
23
25
  export type { PatchSizeTierDecision } from './core/patch-lint.js';
24
- export { countNonBinaryDiffLines, getPatchSizeThresholds, resolvePatchSizeTier, } from './core/patch-lint.js';
26
+ export { countNonBinaryDiffLines, filesMeasuredByFileTooLarge, getPatchSizeThresholds, resolvePatchSizeTier, } from './core/patch-lint.js';
25
27
  export type { AddTokenOptions, AddTokenResult, TokenMode } from './core/token-manager.js';
26
28
  export { addToken, getTokensCssPath, validateTokenAdd } from './core/token-manager.js';
27
29
  export { CancellationError, CommandError, FireForgeError, GeneralError, InvalidArgumentError, ResolutionError, } from './errors/base.js';
package/dist/src/index.js CHANGED
@@ -17,10 +17,11 @@
17
17
  * @packageDocumentation
18
18
  */
19
19
  export { loadConfig, validateConfig } from './core/config.js';
20
+ export { findForwardRegistrations } from './core/forward-registrations.js';
20
21
  export { applyAllComponents } from './core/furnace-apply.js';
21
22
  export { ensureFurnaceConfig, loadFurnaceConfig, loadFurnaceState, saveFurnaceState, validateFurnaceConfig, } from './core/furnace-config.js';
22
23
  export { validateAllComponents, validateComponent } from './core/furnace-validate.js';
23
- export { countNonBinaryDiffLines, getPatchSizeThresholds, resolvePatchSizeTier, } from './core/patch-lint.js';
24
+ export { countNonBinaryDiffLines, filesMeasuredByFileTooLarge, getPatchSizeThresholds, resolvePatchSizeTier, } from './core/patch-lint.js';
24
25
  export { addToken, getTokensCssPath, validateTokenAdd } from './core/token-manager.js';
25
26
  export { CancellationError, CommandError, FireForgeError, GeneralError, InvalidArgumentError, ResolutionError, } from './errors/base.js';
26
27
  export { ExitCode } from './errors/codes.js';
@@ -85,6 +85,8 @@ export interface SetupOptions {
85
85
  export interface DownloadOptions {
86
86
  /** Force re-download, deleting existing engine/ */
87
87
  force?: boolean;
88
+ /** With `force`: skip the confirmation for an engine with changes to lose */
89
+ yes?: boolean;
88
90
  }
89
91
  /**
90
92
  * Options for the source command.
@@ -107,6 +109,8 @@ export interface SourceSetOptions {
107
109
  * Options for the build command.
108
110
  */
109
111
  export interface BuildOptions extends EngineLockWaitable {
112
+ /** Wait for the objdir browser before deploying/building. */
113
+ waitBrowser?: number | boolean;
110
114
  /** Fast UI-only rebuild */
111
115
  ui?: boolean;
112
116
  /** Number of parallel jobs */
@@ -366,6 +370,8 @@ export interface ReExportOptions extends EngineLockWaitable {
366
370
  * rather than hidden.
367
371
  */
368
372
  expectUnmanaged?: string[];
373
+ /** Explicitly retire named absent patch-created files in a scan-less export. */
374
+ expectRemoved?: string[];
369
375
  }
370
376
  /**
371
377
  * Options for the `fireforge patch tier` subcommand. Sets or clears the
@@ -497,6 +503,13 @@ export interface RebaseOptions {
497
503
  export interface RunOptions {
498
504
  /** Additional arguments to pass to the browser */
499
505
  args?: string[];
506
+ /** Launch with this profile directory, passed to Firefox as `-profile`. */
507
+ profile?: string;
508
+ /**
509
+ * Launch with a fresh temporary profile, removed when the run ends.
510
+ * Implied by `smokeExit` unless `profile` is given.
511
+ */
512
+ tempProfile?: boolean;
500
513
  /**
501
514
  * Enable smoke-run mode. Launches the browser, streams the console,
502
515
  * sends SIGTERM to the whole process group after `smokeExit` seconds,
@@ -542,6 +555,12 @@ export interface RunOptions {
542
555
  * Options for the test command.
543
556
  */
544
557
  export interface TestOptions extends EngineLockWaitable {
558
+ /** Queue behind a browser using this objdir; bare flag means 60 seconds. */
559
+ waitBrowser?: number | boolean;
560
+ /** Queue behind the mochitest/Marionette port holder. */
561
+ waitPort?: number | boolean;
562
+ /** Copy a file into the profile at a relative destination (repeatable source=destination). */
563
+ profileFile?: string[];
545
564
  /** Run tests in headless mode */
546
565
  headless?: boolean;
547
566
  /**
@@ -666,9 +685,10 @@ export interface TestOptions extends EngineLockWaitable {
666
685
  */
667
686
  perfSamples?: string;
668
687
  /**
669
- * Seeded file-order shuffle for mochitest runs. `true` draws a fresh seed;
670
- * a number replays one. Forwarded as mach `--shuffle` with the seed
671
- * exported as `FIREFORGE_SHUFFLE_SEED` and stamped on the verdict line.
688
+ * Seeded isolated path-argument order for mochitest runs. `true` draws a
689
+ * fresh seed; a number replays the shard permutation. Native mach
690
+ * `--shuffle` within a shard remains unseeded. Also exported as
691
+ * `FIREFORGE_SHUFFLE_SEED` for custom task code and recorded on the verdict.
672
692
  */
673
693
  shuffle?: number | boolean;
674
694
  }
@@ -1115,4 +1135,10 @@ export interface LintCommandOptions {
1115
1135
  * {@link perPatch}.
1116
1136
  */
1117
1137
  report?: string;
1138
+ /**
1139
+ * With `--per-patch`: `'full'` (default) prints every NOTICE line;
1140
+ * `'summary'` prints one line per check with its count and patch count.
1141
+ * Errors and warnings always print in full.
1142
+ */
1143
+ notices?: 'summary' | 'full';
1118
1144
  }
@@ -355,6 +355,16 @@ export interface PatchLintConfig {
355
355
  * scope for the per-patch tier.
356
356
  */
357
357
  prettier?: PatchLintSeverityGate;
358
+ /**
359
+ * Which carriers the `forward-registration` queue rule checks for a line
360
+ * that names a file only a later patch creates. `'support-files'` (the
361
+ * default) checks test-manifest `support-files`. `'extended'` also checks
362
+ * jar.mn pair lines, moz.build path tokens, test-manifest sections and
363
+ * `head`, and customElements.js chrome URLs resolved through the queue's
364
+ * own jar.mn lines. Opt-in: a queue that predates the wider rule can carry
365
+ * such edges, and a new error on install would red its gate.
366
+ */
367
+ forwardRegistration?: 'support-files' | 'extended';
358
368
  /** Enforce JSDoc on class-method exports in patch-owned .sys.mjs files. Default: 'off'. */
359
369
  jsdocClassMethods?: PatchLintSeverityGate;
360
370
  /** Require ≥1 assertion in any patch-touched browser_*.js test file (new or modified). Default: 'off'. */
@@ -11,13 +11,8 @@
11
11
  * says the opposite: nothing the operator could have done would have
12
12
  * prevented it.
13
13
  *
14
- * Every check routes through a helper here rather than being open-coded
15
- * as `if (!condition) throw`. An inline throw adds a
16
- * permanently-uncovered branch to every function it appears in, and the
17
- * per-module coverage floors in `scripts/check-coverage-thresholds.mjs`
18
- * would pay for it in ~40 places. Routed through a call, the branch lives
19
- * here once and is covered here once, and the `complexity` ceiling in
20
- * `eslint.config.js` does not see it at all.
14
+ * Shared helpers provide consistent classification and TypeScript narrowing,
15
+ * and keep expensive failure descriptions lazy on successful paths.
21
16
  *
22
17
  * This module is a leaf: it imports the error class and nothing else, per
23
18
  * the dependency-direction note in `src/utils/errors.ts`.
@@ -1,8 +1,5 @@
1
1
  /**
2
2
  * Environment construction for spawned children.
3
- *
4
- * Its own module rather than a helper inside `process.ts` only because that
5
- * file sits on a 500-line budget. The logic belongs to the exec layer.
6
3
  */
7
4
  /** The `env` / `envUnset` slice of exec options this module needs. */
8
5
  interface ChildEnvOptions {
@@ -1,9 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * Environment construction for spawned children.
4
- *
5
- * Its own module rather than a helper inside `process.ts` only because that
6
- * file sits on a 500-line budget. The logic belongs to the exec layer.
7
4
  */
8
5
  /**
9
6
  * The child's environment: `process.env`, with `env` merged over it, then
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Bounded-concurrency helpers shared by commands that fan out per-file IO
3
3
  * (status classification, import's unmanaged-dirty guard, re-export scans,
4
- * the dry-run purity guard). Kept tiny: one order-preserving mapper, no
5
- * queues, no cancellation.
4
+ * the dry-run purity guard). On failure, stop scheduling new items and await
5
+ * every started operation before rejecting, so callers can safely clean up.
6
6
  */
7
7
  /** Maps items with at most `limit` in-flight promises. Preserves order. */
8
8
  export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
@@ -1,21 +1,32 @@
1
1
  /**
2
2
  * Bounded-concurrency helpers shared by commands that fan out per-file IO
3
3
  * (status classification, import's unmanaged-dirty guard, re-export scans,
4
- * the dry-run purity guard). Kept tiny: one order-preserving mapper, no
5
- * queues, no cancellation.
4
+ * the dry-run purity guard). On failure, stop scheduling new items and await
5
+ * every started operation before rejecting, so callers can safely clean up.
6
6
  */
7
7
  /** Maps items with at most `limit` in-flight promises. Preserves order. */
8
8
  export async function mapWithConcurrency(items, limit, fn) {
9
+ if (!Number.isSafeInteger(limit) || limit < 1) {
10
+ throw new RangeError('Concurrency limit must be a positive integer');
11
+ }
9
12
  const results = new Array(items.length);
10
13
  let next = 0;
14
+ const state = {};
11
15
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
12
- for (;;) {
16
+ while (!state.failure) {
13
17
  const index = next++;
14
18
  if (index >= items.length)
15
19
  return;
16
- results[index] = await fn(items[index], index);
20
+ try {
21
+ results[index] = await fn(items[index], index);
22
+ }
23
+ catch (error) {
24
+ state.failure ??= { error };
25
+ }
17
26
  }
18
27
  });
19
28
  await Promise.all(workers);
29
+ if (state.failure)
30
+ throw state.failure.error;
20
31
  return results;
21
32
  }
@@ -99,7 +99,17 @@ export async function isSymlink(path) {
99
99
  */
100
100
  export async function copyFile(src, dest) {
101
101
  await ensureParentDir(dest);
102
- await fsCopyFile(src, dest);
102
+ // Never follow a destination symlink or truncate a shared hardlink:
103
+ // build/deploy copies must not write through into components/ or a peer.
104
+ const tempPath = createAtomicTempPath(dest);
105
+ try {
106
+ await fsCopyFile(src, tempPath);
107
+ await renameWithRetries(tempPath, dest);
108
+ await syncParentDir(dest);
109
+ }
110
+ finally {
111
+ await rm(tempPath, { force: true });
112
+ }
103
113
  }
104
114
  /**
105
115
  * Reads a JSON file and parses it.
@@ -89,7 +89,22 @@ export declare function resolveWaitLockSeconds(value: string | number | boolean
89
89
  */
90
90
  export declare function sessionWaitLockMs(): number | undefined;
91
91
  /**
92
- * Recursively gives every command in the tree a `--wait-lock` flag: the
92
+ * Registers `--wait-lock [seconds]` on a command that takes no lock, so a
93
+ * scripted sequence can blanket-append the flag without hitting a usage
94
+ * error. A command that rejects it with "unknown option" kills the whole
95
+ * sequence with a usage error instead of a lock message.
96
+ *
97
+ * The flag is accepted and ignored here, never silently repurposed, and
98
+ * the help text says so, so nobody reads its presence as evidence that this
99
+ * command waits for anything.
100
+ *
101
+ * The value is parsed and validated exactly as the honoring variant does:
102
+ * `--wait-lock nonsense` must still be a usage error everywhere, or the
103
+ * uniformity would be a lie.
104
+ */
105
+ export declare function addAcceptedWaitLockOption(command: Command): Command;
106
+ /**
107
+ * Recursively gives every leaf command in the tree a `--wait-lock` flag: the
93
108
  * honoring registration where one already exists, the accept-and-ignore
94
109
  * one everywhere else.
95
110
  */
@@ -182,7 +182,7 @@ export function sessionWaitLockMs() {
182
182
  * `--wait-lock nonsense` must still be a usage error everywhere, or the
183
183
  * uniformity would be a lie.
184
184
  */
185
- function addAcceptedWaitLockOption(command) {
185
+ export function addAcceptedWaitLockOption(command) {
186
186
  return command.option('--wait-lock [seconds]', 'Accepted for scripting uniformity and ignored: this command takes no FireForge lock', commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
187
187
  }
188
188
  /**
@@ -194,13 +194,15 @@ function hasWaitLockOption(command) {
194
194
  return command.options.some((option) => option.long === '--wait-lock');
195
195
  }
196
196
  /**
197
- * Recursively gives every command in the tree a `--wait-lock` flag: the
197
+ * Recursively gives every leaf command in the tree a `--wait-lock` flag: the
198
198
  * honoring registration where one already exists, the accept-and-ignore
199
199
  * one everywhere else.
200
200
  */
201
201
  export function ensureWaitLockOptionEverywhere(command) {
202
202
  for (const sub of command.commands) {
203
- if (!hasWaitLockOption(sub))
203
+ // Commander groups otherwise capture a trailing leaf option before the
204
+ // action can see it. Register the uniform flag only on actionable leaves.
205
+ if (sub.commands.length === 0 && !hasWaitLockOption(sub))
204
206
  addAcceptedWaitLockOption(sub);
205
207
  ensureWaitLockOptionEverywhere(sub);
206
208
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { isAbsolute, relative, resolve } from 'node:path';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
3
  const WINDOWS_ABSOLUTE_PATH = /^[a-zA-Z]:[\\/]/;
4
4
  const RELATIVE_PATH_ROOT = resolve('/__fireforge_path_root__');
5
5
  /**
@@ -55,7 +55,8 @@ export function isPathInsideRoot(root, candidate) {
55
55
  : resolve(resolvedRoot, candidate);
56
56
  const relativePath = relative(resolvedRoot, resolvedCandidate);
57
57
  return (relativePath === '' ||
58
- (!relativePath.startsWith('..') &&
58
+ (relativePath !== '..' &&
59
+ !relativePath.startsWith(`..${sep}`) &&
59
60
  !isAbsolute(relativePath) &&
60
61
  !WINDOWS_ABSOLUTE_PATH.test(relativePath)));
61
62
  }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * Process-group kill and post-run sweep helpers for the exec layer. Split
3
- * out of `process.ts` to keep that file within the per-file line budget.
2
+ * Process-group kill and post-run sweep helpers for the exec layer.
4
3
  * Spawn-based rather than `exec`-based so the two modules do not import
5
4
  * each other cyclically.
6
5
  */
@@ -1,7 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Process-group kill and post-run sweep helpers for the exec layer. Split
4
- * out of `process.ts` to keep that file within the per-file line budget.
3
+ * Process-group kill and post-run sweep helpers for the exec layer.
5
4
  * Spawn-based rather than `exec`-based so the two modules do not import
6
5
  * each other cyclically.
7
6
  */
@@ -0,0 +1,27 @@
1
+ /** Options for {@link execWithInput}. */
2
+ export interface ExecWithInputOptions {
3
+ /** Working directory for the command */
4
+ cwd?: string;
5
+ /** Environment variables merged over `process.env` */
6
+ env?: Record<string, string>;
7
+ }
8
+ /** Result of {@link execWithInput}: stdout stays raw bytes. */
9
+ export interface ExecBufferResult {
10
+ /** Standard output, uncapped and undecoded */
11
+ stdout: Buffer;
12
+ /** Standard error, decoded as UTF-8 */
13
+ stderr: string;
14
+ /** Process exit code */
15
+ exitCode: number;
16
+ }
17
+ /**
18
+ * Runs a command, writes `input` to its stdin, and returns stdout as raw
19
+ * bytes. For batch protocols (`git cat-file --batch`) whose framing counts
20
+ * bytes: the decoded, 50 MB-capped collector `exec` uses would corrupt
21
+ * both. The caller bounds the input, and so the output.
22
+ * @param command - Command to execute
23
+ * @param args - Command arguments
24
+ * @param input - Bytes written to stdin, which is then closed
25
+ * @param options - Execution options
26
+ */
27
+ export declare function execWithInput(command: string, args: string[], input: string | Buffer, options?: ExecWithInputOptions): Promise<ExecBufferResult>;
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Stdin-fed command execution with byte-exact stdout.
4
+ */
5
+ import { spawn } from 'node:child_process';
6
+ import { buildChildEnv } from './child-env.js';
7
+ import { exitCodeFromClose } from './process.js';
8
+ /**
9
+ * Runs a command, writes `input` to its stdin, and returns stdout as raw
10
+ * bytes. For batch protocols (`git cat-file --batch`) whose framing counts
11
+ * bytes: the decoded, 50 MB-capped collector `exec` uses would corrupt
12
+ * both. The caller bounds the input, and so the output.
13
+ * @param command - Command to execute
14
+ * @param args - Command arguments
15
+ * @param input - Bytes written to stdin, which is then closed
16
+ * @param options - Execution options
17
+ */
18
+ export async function execWithInput(command, args, input, options = {}) {
19
+ return new Promise((resolve, reject) => {
20
+ const child = spawn(command, args, {
21
+ cwd: options.cwd,
22
+ env: buildChildEnv(options),
23
+ stdio: ['pipe', 'pipe', 'pipe'],
24
+ });
25
+ const out = [];
26
+ const err = [];
27
+ child.stdout.on('data', (chunk) => {
28
+ out.push(chunk);
29
+ });
30
+ child.stderr.on('data', (chunk) => {
31
+ err.push(chunk);
32
+ });
33
+ // A child that exits before reading all of stdin (a bad ref, say) makes
34
+ // the write fail with EPIPE. The close handler reports the real outcome.
35
+ child.stdin.on('error', () => undefined);
36
+ child.stdin.end(input);
37
+ child.on('error', reject);
38
+ child.on('close', (code, signal) => {
39
+ resolve({
40
+ stdout: Buffer.concat(out),
41
+ stderr: Buffer.concat(err).toString('utf8'),
42
+ exitCode: exitCodeFromClose(code, signal),
43
+ });
44
+ });
45
+ });
46
+ }
@@ -104,6 +104,8 @@ export interface ProcessGroupOptions {
104
104
  */
105
105
  postCloseSweep?: () => Promise<void>;
106
106
  }
107
+ /** A child's exit code, with a signal death mapped to 128 + its number. */
108
+ export declare function exitCodeFromClose(code: number | null, signal: NodeJS.Signals | null): number;
107
109
  /**
108
110
  * Executes a command and returns its output.
109
111
  * @param command - Command to execute
@@ -148,8 +150,9 @@ export declare function execStream(command: string, args: string[], options?: St
148
150
  * Registers a child whose shutdown {@link waitForActiveChildShutdown} must
149
151
  * wait for, and returns the handle that deregisters it.
150
152
  *
151
- * @returns `settle()`. Call from both the child's `close` and `error`
152
- * handlers so a dead child never holds the shutdown wait open
153
+ * @returns `settle()`. Call after child closure and required teardown finish.
154
+ * An abort's `error` event does not prove closure; do not settle there.
155
+ * A failed start may settle when no live child or teardown remains.
153
156
  */
154
157
  export declare function trackChildClosure(): {
155
158
  settle: () => void;