@hominis/fireforge 0.36.0 → 0.38.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 (87) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +1 -1
  3. package/dist/src/commands/build.js +6 -6
  4. package/dist/src/commands/config.js +1 -0
  5. package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
  6. package/dist/src/commands/doctor-orphaned-harness.js +132 -0
  7. package/dist/src/commands/doctor.js +2 -0
  8. package/dist/src/commands/download.js +5 -2
  9. package/dist/src/commands/export-flow.d.ts +5 -1
  10. package/dist/src/commands/export-flow.js +10 -7
  11. package/dist/src/commands/export-placement-gate.js +3 -5
  12. package/dist/src/commands/export-placement-policy.d.ts +11 -3
  13. package/dist/src/commands/export-placement-policy.js +32 -29
  14. package/dist/src/commands/export.js +12 -5
  15. package/dist/src/commands/furnace/index.js +23 -18
  16. package/dist/src/commands/patch/move-files-create.d.ts +19 -0
  17. package/dist/src/commands/patch/move-files-create.js +116 -0
  18. package/dist/src/commands/patch/move-files.d.ts +3 -1
  19. package/dist/src/commands/patch/move-files.js +49 -4
  20. package/dist/src/commands/patch/split.d.ts +11 -1
  21. package/dist/src/commands/patch/split.js +6 -5
  22. package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
  23. package/dist/src/commands/patch/staged-dependency.js +113 -51
  24. package/dist/src/commands/re-export-files.js +4 -0
  25. package/dist/src/commands/re-export-scan.d.ts +17 -0
  26. package/dist/src/commands/re-export-scan.js +51 -0
  27. package/dist/src/commands/re-export.js +25 -6
  28. package/dist/src/commands/source.js +28 -3
  29. package/dist/src/commands/test-diagnose.js +20 -1
  30. package/dist/src/commands/test-register.js +6 -5
  31. package/dist/src/commands/test-stale-gate.d.ts +44 -0
  32. package/dist/src/commands/test-stale-gate.js +81 -0
  33. package/dist/src/commands/test.js +40 -23
  34. package/dist/src/core/build-audit.d.ts +12 -0
  35. package/dist/src/core/build-audit.js +14 -0
  36. package/dist/src/core/build-baseline-types.d.ts +58 -0
  37. package/dist/src/core/build-baseline.d.ts +10 -2
  38. package/dist/src/core/build-baseline.js +51 -7
  39. package/dist/src/core/config-paths.d.ts +1 -1
  40. package/dist/src/core/config-paths.js +1 -0
  41. package/dist/src/core/config-validate.js +6 -1
  42. package/dist/src/core/engine-session-lock.d.ts +10 -1
  43. package/dist/src/core/engine-session-lock.js +28 -2
  44. package/dist/src/core/file-lock.d.ts +33 -0
  45. package/dist/src/core/file-lock.js +32 -6
  46. package/dist/src/core/firefox-archive.d.ts +2 -1
  47. package/dist/src/core/firefox-archive.js +32 -8
  48. package/dist/src/core/firefox.d.ts +8 -3
  49. package/dist/src/core/firefox.js +11 -6
  50. package/dist/src/core/furnace-config-custom.js +10 -0
  51. package/dist/src/core/furnace-stale-export.d.ts +59 -0
  52. package/dist/src/core/furnace-stale-export.js +123 -0
  53. package/dist/src/core/furnace-validate-compatibility.js +9 -2
  54. package/dist/src/core/furnace-validate-helpers.js +28 -1
  55. package/dist/src/core/furnace-validate-structure.js +3 -2
  56. package/dist/src/core/license-headers.d.ts +12 -0
  57. package/dist/src/core/license-headers.js +76 -1
  58. package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
  59. package/dist/src/core/mach-known-noise-filter.js +193 -0
  60. package/dist/src/core/mach.d.ts +8 -0
  61. package/dist/src/core/mach.js +36 -3
  62. package/dist/src/core/patch-lint-cross.js +80 -2
  63. package/dist/src/core/patch-lint.d.ts +3 -0
  64. package/dist/src/core/patch-lint.js +25 -7
  65. package/dist/src/core/patch-manifest-io.d.ts +3 -2
  66. package/dist/src/core/patch-manifest-io.js +27 -16
  67. package/dist/src/core/patch-manifest-validate.js +24 -0
  68. package/dist/src/core/status-classify.d.ts +11 -2
  69. package/dist/src/core/status-classify.js +49 -25
  70. package/dist/src/core/test-harness-crash.d.ts +16 -0
  71. package/dist/src/core/test-harness-crash.js +55 -0
  72. package/dist/src/core/test-stale-check.d.ts +69 -1
  73. package/dist/src/core/test-stale-check.js +134 -1
  74. package/dist/src/types/commands/index.d.ts +1 -1
  75. package/dist/src/types/commands/options.d.ts +84 -7
  76. package/dist/src/types/commands/patches.d.ts +29 -0
  77. package/dist/src/types/config.d.ts +7 -0
  78. package/dist/src/types/furnace.d.ts +13 -0
  79. package/dist/src/utils/options.d.ts +20 -0
  80. package/dist/src/utils/options.js +39 -0
  81. package/dist/src/utils/process-group.d.ts +33 -0
  82. package/dist/src/utils/process-group.js +161 -0
  83. package/dist/src/utils/process.d.ts +15 -0
  84. package/dist/src/utils/process.js +88 -44
  85. package/dist/src/utils/validation.d.ts +5 -0
  86. package/dist/src/utils/validation.js +7 -0
  87. package/package.json +3 -3
@@ -294,6 +294,58 @@ function realUnexpectedFailureLines(output) {
294
294
  }
295
295
  return [...new Set(matches.map((line) => line.trim()))].filter((line) => !SHUTDOWN_REENTRY_PATTERN.test(line));
296
296
  }
297
+ /**
298
+ * Non-global copies of the unexpected/assertion patterns for per-line use —
299
+ * the `g`-flagged module patterns carry `lastIndex` state across `.test()`
300
+ * calls and must never be reused line-by-line.
301
+ */
302
+ const UNEXPECTED_LINE_SINGLE = /\bTEST-UNEXPECTED-[A-Z-]+\b/;
303
+ /**
304
+ * Context lines the chrome/xpcshell harnesses print directly under a
305
+ * TEST-UNEXPECTED line: `Got …` / `Expected …` / `Actual …`, Assert
306
+ * messages, diff markers, stack-trace heads, and indented continuation
307
+ * lines.
308
+ */
309
+ const FAILURE_CONTEXT_LINE_PATTERN = /^(?:\s*(?:Got\b|Expected\b|Actual\b|Assert\w*\b|Stack trace:|[-+]\s)|\s{2,}\S)/;
310
+ /** Cap on context lines echoed under a single TEST-UNEXPECTED line. */
311
+ const FAILURE_BLOCK_CONTEXT_LIMIT = 6;
312
+ /**
313
+ * Collects the first `limit` TEST-UNEXPECTED-* lines with their trailing
314
+ * assertion/diff context, one string per block. The shutdown-reentry
315
+ * artifact is excluded (it is harness noise, not a test result). When more
316
+ * than `limit` blocks exist, a final `…(+N more …)` note is appended so the
317
+ * operator knows the echo was truncated. Pure; exported for unit tests.
318
+ */
319
+ export function collectUnexpectedFailureBlocks(output, limit = 5) {
320
+ const lines = output.split(/\r?\n/);
321
+ const blocks = [];
322
+ let skipped = 0;
323
+ for (let i = 0; i < lines.length; i++) {
324
+ const line = lines[i] ?? '';
325
+ if (!UNEXPECTED_LINE_SINGLE.test(line) || SHUTDOWN_REENTRY_PATTERN.test(line))
326
+ continue;
327
+ if (blocks.length >= limit) {
328
+ skipped++;
329
+ continue;
330
+ }
331
+ const block = [line.trim()];
332
+ let contextCount = 0;
333
+ for (let j = i + 1; j < lines.length && contextCount < FAILURE_BLOCK_CONTEXT_LIMIT; j++) {
334
+ const next = lines[j] ?? '';
335
+ if (UNEXPECTED_LINE_SINGLE.test(next))
336
+ break;
337
+ if (!FAILURE_CONTEXT_LINE_PATTERN.test(next))
338
+ break;
339
+ block.push(next.trimEnd());
340
+ contextCount++;
341
+ }
342
+ blocks.push(block.join('\n'));
343
+ }
344
+ if (skipped > 0) {
345
+ blocks.push(`…(+${skipped} more TEST-UNEXPECTED line${skipped === 1 ? '' : 's'} not shown)`);
346
+ }
347
+ return blocks;
348
+ }
297
349
  function detectSecondaryHarnessNoise(output) {
298
350
  const evidence = stripNonSignalNoise(output);
299
351
  if (TRACEBACK_PATTERN.test(evidence)) {
@@ -397,6 +449,7 @@ export function detectHarnessCrashSignature(output) {
397
449
  export function classifyHarnessRun(exitCode, output, requestedPaths) {
398
450
  const realFailures = realUnexpectedFailureLines(output);
399
451
  const firstRealFailure = realFailures[0];
452
+ const failureBlocks = collectUnexpectedFailureBlocks(output);
400
453
  const secondaryHarnessSignature = firstRealFailure !== undefined ? detectSecondaryHarnessNoise(output) : undefined;
401
454
  const signature = detectHarnessCrashSignature(output);
402
455
  if (signature) {
@@ -431,6 +484,7 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
431
484
  return {
432
485
  kind: 'test-failures',
433
486
  ...(firstRealFailure !== undefined ? { realFailureLine: firstRealFailure } : {}),
487
+ ...(failureBlocks.length > 0 ? { realFailureBlocks: failureBlocks } : {}),
434
488
  ...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
435
489
  greenSummaryRejected: {
436
490
  ...(crashLine !== undefined ? { crashLine } : {}),
@@ -444,6 +498,7 @@ export function classifyHarnessRun(exitCode, output, requestedPaths) {
444
498
  return {
445
499
  kind: 'test-failures',
446
500
  ...(firstRealFailure !== undefined ? { realFailureLine: firstRealFailure } : {}),
501
+ ...(failureBlocks.length > 0 ? { realFailureBlocks: failureBlocks } : {}),
447
502
  ...(secondaryHarnessSignature !== undefined ? { secondaryHarnessSignature } : {}),
448
503
  };
449
504
  }
@@ -1,4 +1,6 @@
1
- import type { BuildBaseline } from './build-baseline-types.js';
1
+ import { isXpcomManifestPath } from './build-audit.js';
2
+ import type { BuildBaseline, TestPackagingCoverage } from './build-baseline-types.js';
3
+ export { isXpcomManifestPath };
2
4
  /** Result of the stale-build preflight probe. */
3
5
  export interface StaleBuildResult {
4
6
  /** True when at least one packageable engine file changed since the baseline. */
@@ -40,3 +42,69 @@ export declare function checkStaleBuildForTest(projectRoot: string, engineDir: s
40
42
  * result without matching the rendered copy.
41
43
  */
42
44
  export declare function formatStaleBuildWarning(result: StaleBuildResult): string;
45
+ /**
46
+ * Sentinel returned by {@link findUncoveredRequestPaths} when a full-suite
47
+ * request (no paths) is checked against a scoped coverage record.
48
+ */
49
+ export declare const FULL_SUITE_REQUEST = "(entire suite)";
50
+ /**
51
+ * Compares the requested test paths against the packaged runtime's coverage
52
+ * claim recorded in the baseline. Returns the requested paths the recorded
53
+ * packaging does NOT cover — the runs that would dispatch against missing
54
+ * `_tests/` support fixtures and hang rather than fail.
55
+ *
56
+ * Coverage semantics: `undefined` (pre-0.37.0 baseline) and `'full'` cover
57
+ * everything. A scoped list covers a request path when the request equals a
58
+ * covered entry, sits beneath a covered directory entry, or shares a
59
+ * manifest granule with a covered entry ({@link toManifestGranule} — a
60
+ * scoped `test --build` packages the whole manifest directory, so a
61
+ * same-manifest sibling of a covered file is packaged too). Both sides are
62
+ * normalized to forward slashes so Windows-style CLI input cannot defeat
63
+ * the prefix rule (baseline paths are POSIX by convention). A request with
64
+ * no paths is a full-suite run and is never covered by a scoped list — the
65
+ * {@link FULL_SUITE_REQUEST} sentinel is returned so the refusal can name it.
66
+ */
67
+ export declare function findUncoveredRequestPaths(coverage: TestPackagingCoverage | undefined, requestedPaths: readonly string[]): string[];
68
+ /**
69
+ * Formats the refusal shown when a non-`--build` run requests paths the
70
+ * packaged runtime's scoped `test --build` coverage does not include —
71
+ * enforced on every such run, with or without `--allow-stale-build`. Kept
72
+ * separate from the matcher so tests can pin structure and copy
73
+ * independently (same split as {@link formatStaleBuildWarning}).
74
+ */
75
+ export declare function formatTestCoverageRefusal(uncovered: string[], coverage: string[]): string;
76
+ /** Result of the compiled-StaticComponents staleness probe. */
77
+ export interface StaticComponentsStaleResult {
78
+ /** True when at least one `components.conf` genuinely diverged from the anchor. */
79
+ stale: boolean;
80
+ /**
81
+ * Engine-relative `components.conf` paths changed since the last FULL
82
+ * build. Sorted; NOT capped — {@link formatStaticComponentsRefusal}
83
+ * applies the render cap.
84
+ */
85
+ changedManifests: string[];
86
+ }
87
+ /**
88
+ * Probes whether any `components.conf` changed since the last FULL
89
+ * `fireforge build` — i.e. since the compiled StaticComponents table was
90
+ * last regenerated. `components.conf` entries bake into compiled code; a
91
+ * scoped `test --build` packages the file but the child process resolves
92
+ * the OLD table and fails with `NS_ERROR_MALFORMED_URI` that reads as a
93
+ * test bug.
94
+ *
95
+ * The diff anchors to the baseline's `staticComponentsBaseline` (the last
96
+ * full build's engine HEAD SHA), NOT the baseline's own `engineHeadSha`
97
+ * which a scoped `test --build` advances. Dirty candidates are hash-checked
98
+ * against the anchor's fingerprints so only genuine content divergence
99
+ * counts. No baseline / no anchor (pre-0.38.0 marker) → fresh. Never
100
+ * throws — the probes it composes degrade to verbose lines and empty
101
+ * results on git failure, matching {@link checkStaleBuildForTest}.
102
+ */
103
+ export declare function checkStaticComponentsStale(engineDir: string, baseline: BuildBaseline | undefined): Promise<StaticComponentsStaleResult>;
104
+ /**
105
+ * Formats the refusal shown when a run would dispatch against a stale
106
+ * compiled StaticComponents table. Same probe/copy split as
107
+ * {@link formatStaleBuildWarning} so tests can pin structure and wording
108
+ * independently.
109
+ */
110
+ export declare function formatStaticComponentsRefusal(changedManifests: string[]): string;
@@ -28,9 +28,10 @@ import { readFile } from 'node:fs/promises';
28
28
  import { join } from 'node:path';
29
29
  import { toError } from '../utils/errors.js';
30
30
  import { verbose } from '../utils/logger.js';
31
- import { isPackageablePath } from './build-audit.js';
31
+ import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
32
32
  import { readBuildBaseline } from './build-baseline.js';
33
33
  import { collectChangedEnginePaths } from './engine-changes.js';
34
+ export { isXpcomManifestPath };
34
35
  /** Cap on the number of changed paths rendered inline. */
35
36
  const STALE_PATHS_LIMIT = 10;
36
37
  /**
@@ -112,3 +113,135 @@ export function formatStaleBuildWarning(result) {
112
113
  'packaged chrome / jar.mn resources, rerun with "fireforge test --build" (or ' +
113
114
  '"fireforge build --ui") first. Passing --build skips this check.');
114
115
  }
116
+ /**
117
+ * Sentinel returned by {@link findUncoveredRequestPaths} when a full-suite
118
+ * request (no paths) is checked against a scoped coverage record.
119
+ */
120
+ export const FULL_SUITE_REQUEST = '(entire suite)';
121
+ /**
122
+ * Compares the requested test paths against the packaged runtime's coverage
123
+ * claim recorded in the baseline. Returns the requested paths the recorded
124
+ * packaging does NOT cover — the runs that would dispatch against missing
125
+ * `_tests/` support fixtures and hang rather than fail.
126
+ *
127
+ * Coverage semantics: `undefined` (pre-0.37.0 baseline) and `'full'` cover
128
+ * everything. A scoped list covers a request path when the request equals a
129
+ * covered entry, sits beneath a covered directory entry, or shares a
130
+ * manifest granule with a covered entry ({@link toManifestGranule} — a
131
+ * scoped `test --build` packages the whole manifest directory, so a
132
+ * same-manifest sibling of a covered file is packaged too). Both sides are
133
+ * normalized to forward slashes so Windows-style CLI input cannot defeat
134
+ * the prefix rule (baseline paths are POSIX by convention). A request with
135
+ * no paths is a full-suite run and is never covered by a scoped list — the
136
+ * {@link FULL_SUITE_REQUEST} sentinel is returned so the refusal can name it.
137
+ */
138
+ export function findUncoveredRequestPaths(coverage, requestedPaths) {
139
+ if (coverage === undefined || coverage === 'full') {
140
+ return [];
141
+ }
142
+ const covered = coverage.map(normalizeCoveragePath).filter((p) => p.length > 0);
143
+ if (requestedPaths.length === 0) {
144
+ return [FULL_SUITE_REQUEST];
145
+ }
146
+ return requestedPaths.filter((requested) => {
147
+ const path = normalizeCoveragePath(requested);
148
+ const granule = toManifestGranule(path);
149
+ return !covered.some((c) => path === c || path.startsWith(`${c}/`) || granule === toManifestGranule(c));
150
+ });
151
+ }
152
+ /**
153
+ * Maps a normalized request/coverage path to the "manifest granule" the
154
+ * packaged runtime actually staged: an extension-bearing basename (a test
155
+ * FILE) maps to its containing directory, a directory (no dot in the
156
+ * basename) maps to itself. Purely lexical — the directory-as-manifest
157
+ * approximation holds because xpcshell/mochitest manifests live next to
158
+ * their test files and a scoped `test --build` stages the whole manifest
159
+ * directory into `obj-*`/`_tests/`, not single files. Caveat: a DIRECTORY
160
+ * whose basename contains a dot is misread as a file and mapped to its
161
+ * parent, which widens (never narrows) the covered granule.
162
+ */
163
+ function toManifestGranule(path) {
164
+ const slash = path.lastIndexOf('/');
165
+ const base = slash === -1 ? path : path.slice(slash + 1);
166
+ if (!base.includes('.')) {
167
+ return path;
168
+ }
169
+ return slash === -1 ? '' : path.slice(0, slash);
170
+ }
171
+ /** Normalizes a path for coverage comparison: forward slashes, no trailing slash. */
172
+ function normalizeCoveragePath(path) {
173
+ return path.trim().replace(/\\/g, '/').replace(/\/+$/, '');
174
+ }
175
+ /**
176
+ * Formats the refusal shown when a non-`--build` run requests paths the
177
+ * packaged runtime's scoped `test --build` coverage does not include —
178
+ * enforced on every such run, with or without `--allow-stale-build`. Kept
179
+ * separate from the matcher so tests can pin structure and copy
180
+ * independently (same split as {@link formatStaleBuildWarning}).
181
+ */
182
+ export function formatTestCoverageRefusal(uncovered, coverage) {
183
+ const cap = (paths) => {
184
+ const head = paths.slice(0, STALE_PATHS_LIMIT);
185
+ const truncated = paths.length - head.length;
186
+ return head.join(', ') + (truncated > 0 ? `, … (+${truncated} more)` : '');
187
+ };
188
+ const rebuildTargets = uncovered.filter((p) => p !== FULL_SUITE_REQUEST);
189
+ const rebuildHint = rebuildTargets.length > 0
190
+ ? `Rerun "fireforge test --build ${rebuildTargets.slice(0, STALE_PATHS_LIMIT).join(' ')}" to package them, or run "fireforge build" for full coverage.`
191
+ : 'Run "fireforge build" (or a path-less "fireforge test --build") for full coverage first.';
192
+ return (`The packaged test runtime was produced by a scoped "fireforge test --build" covering only: ${cap(coverage)}.\n` +
193
+ `The requested run needs ${cap(uncovered)}, which that packaging does not cover — support ` +
194
+ 'fixtures for those manifests may be missing from obj-*/_tests/, and the run can hang ' +
195
+ `rather than fail. ${rebuildHint} ` +
196
+ '(--allow-stale-build does not bypass this check — it accepts stale content, not missing coverage.)');
197
+ }
198
+ /**
199
+ * Probes whether any `components.conf` changed since the last FULL
200
+ * `fireforge build` — i.e. since the compiled StaticComponents table was
201
+ * last regenerated. `components.conf` entries bake into compiled code; a
202
+ * scoped `test --build` packages the file but the child process resolves
203
+ * the OLD table and fails with `NS_ERROR_MALFORMED_URI` that reads as a
204
+ * test bug.
205
+ *
206
+ * The diff anchors to the baseline's `staticComponentsBaseline` (the last
207
+ * full build's engine HEAD SHA), NOT the baseline's own `engineHeadSha`
208
+ * which a scoped `test --build` advances. Dirty candidates are hash-checked
209
+ * against the anchor's fingerprints so only genuine content divergence
210
+ * counts. No baseline / no anchor (pre-0.38.0 marker) → fresh. Never
211
+ * throws — the probes it composes degrade to verbose lines and empty
212
+ * results on git failure, matching {@link checkStaleBuildForTest}.
213
+ */
214
+ export async function checkStaticComponentsStale(engineDir, baseline) {
215
+ const anchor = baseline?.staticComponentsBaseline;
216
+ if (baseline === undefined || anchor === undefined) {
217
+ return { stale: false, changedManifests: [] };
218
+ }
219
+ const changed = await collectChangedEnginePaths(engineDir, { ...baseline, engineHeadSha: anchor.engineHeadSha }, 'Static-components preflight');
220
+ const changedManifests = [];
221
+ for (const path of changed.filter((p) => isXpcomManifestPath(p))) {
222
+ const recorded = anchor.fingerprints[path];
223
+ const live = await hashEngineFile(engineDir, path);
224
+ if (recorded === undefined || live === undefined || recorded !== live) {
225
+ changedManifests.push(path);
226
+ }
227
+ }
228
+ return { stale: changedManifests.length > 0, changedManifests };
229
+ }
230
+ /**
231
+ * Formats the refusal shown when a run would dispatch against a stale
232
+ * compiled StaticComponents table. Same probe/copy split as
233
+ * {@link formatStaleBuildWarning} so tests can pin structure and wording
234
+ * independently.
235
+ */
236
+ export function formatStaticComponentsRefusal(changedManifests) {
237
+ const head = changedManifests.slice(0, STALE_PATHS_LIMIT);
238
+ const truncated = changedManifests.length - head.length;
239
+ const list = head.join(', ') + (truncated > 0 ? `, … (+${truncated} more)` : '');
240
+ return (`The compiled StaticComponents table is stale: ${list} changed since the last full "fireforge build".\n` +
241
+ 'components.conf registrations are baked into compiled code that only a FULL build ' +
242
+ 'regenerates — a scoped "fireforge test --build" repackages files but the child process ' +
243
+ 'resolves the old component table and fails with NS_ERROR_MALFORMED_URI that reads as a ' +
244
+ 'test bug. Run "fireforge build" first. (--allow-stale-build does not bypass this check — ' +
245
+ 'it accepts stale packaged content, not a stale compiled registration. Pass ' +
246
+ '--allow-stale-components only if you rebuilt out-of-band and accept the risk.)');
247
+ }
@@ -2,5 +2,5 @@
2
2
  * Re-exports all command-related types from focused sub-modules.
3
3
  */
4
4
  export type { BuildOptions, DiscardOptions, DoctorOptions, DownloadOptions, ExportOptions, FurnaceApplyOptions, FurnaceCreateOptions, FurnaceDeployOptions, FurnaceOverrideOptions, FurnacePreviewOptions, FurnaceRefreshOptions, FurnaceRemoveOptions, FurnaceSyncOptions, FurnaceValidateOptions, GlobalOptions, ImportOptions, LintCommandOptions, PackageOptions, PatchCompactOptions, PatchDeleteOptions, PatchLintIgnoreOptions, PatchMoveFilesOptions, PatchRenameOptions, PatchReorderOptions, PatchSplitOptions, PatchStagedDependencyOptions, PatchTierOptions, RebaseOptions, ReExportOptions, RegisterOptions, ResetOptions, RunOptions, SetupOptions, SourceSetOptions, StatusOptions, TestOptions, TokenAddOptions, WireOptions, } from './options.js';
5
- export type { ImportSummary, PatchCategory, PatchesManifest, PatchInfo, PatchLintIssue, PatchMetadata, PatchResult, PatchStagedDependencies, PatchStagedForwardImport, } from './patches.js';
5
+ export type { ImportSummary, PatchCategory, PatchesManifest, PatchInfo, PatchLintIssue, PatchMetadata, PatchResult, PatchStagedDependencies, PatchStagedForwardImport, PatchStagedRegistration, } from './patches.js';
6
6
  export type { DoctorCheck, ProjectStatus, TokenCoverageFileEntry, TokenCoverageReport, } from './project.js';
@@ -43,6 +43,10 @@ export interface SourceSetOptions {
43
43
  sha256?: string;
44
44
  /** Clear any existing pinned SHA-256 */
45
45
  clearSha256?: boolean;
46
+ /** Optional release-candidate build directory (e.g. "build2") */
47
+ candidate?: string;
48
+ /** Clear any existing release-candidate build directory */
49
+ clearCandidate?: boolean;
46
50
  }
47
51
  /**
48
52
  * Options for the build command.
@@ -62,6 +66,12 @@ export interface BuildOptions {
62
66
  * for any mismatch the rewriter cannot prove safe.
63
67
  */
64
68
  rewriteMozinfo?: boolean;
69
+ /**
70
+ * Parsed `--wait-lock [seconds]` value (`true` for the bare flag, meaning
71
+ * 60). Consumed at the CLI layer to bound the engine-session-lock wait;
72
+ * the command implementation ignores it.
73
+ */
74
+ waitLock?: number | boolean;
65
75
  }
66
76
  /**
67
77
  * Options for the export command.
@@ -99,6 +109,12 @@ export interface ExportOptions {
99
109
  forceUnsafe?: boolean;
100
110
  /** Exclude furnace-managed file paths from the export. */
101
111
  excludeFurnace?: boolean;
112
+ /**
113
+ * Export the deployed engine copy even when the `components/` source
114
+ * changed since the last furnace apply. Without this flag the export is
115
+ * refused so a stale deployed copy cannot silently land in the patch.
116
+ */
117
+ allowStaleFurnace?: boolean;
102
118
  /**
103
119
  * Acknowledge that the export will create cross-patch ownership overlap
104
120
  * with existing non-superseded patches. Without this flag, `export`
@@ -106,6 +122,12 @@ export interface ExportOptions {
106
122
  * another patch, because the resulting queue fails `verify` immediately.
107
123
  */
108
124
  allowOverlap?: boolean;
125
+ /**
126
+ * Parsed `--wait-lock [seconds]` value (`true` for the bare flag, meaning
127
+ * 60). Consumed at the CLI layer to bound the engine-session-lock wait;
128
+ * the command implementation ignores it.
129
+ */
130
+ waitLock?: number | boolean;
109
131
  /**
110
132
  * Force a tier override on the new patch's `PatchMetadata.tier`. Only
111
133
  * `"branding"` is currently recognised — Commander rejects other values
@@ -210,6 +232,12 @@ export interface ReExportOptions {
210
232
  * before the interactive/`--yes` confirmation path.
211
233
  */
212
234
  allowShrink?: boolean;
235
+ /**
236
+ * Export the deployed engine copy even when the `components/` source
237
+ * changed since the last furnace apply. Without this flag the re-export
238
+ * is refused so a stale deployed copy cannot silently land in the patch.
239
+ */
240
+ allowStaleFurnace?: boolean;
213
241
  /** Bypass cross-patch lint refusal on projected shrink state */
214
242
  forceUnsafe?: boolean;
215
243
  /**
@@ -239,6 +267,12 @@ export interface ReExportOptions {
239
267
  * patch to also suppress X").
240
268
  */
241
269
  lintIgnore?: string[];
270
+ /**
271
+ * Parsed `--wait-lock [seconds]` value (`true` for the bare flag, meaning
272
+ * 60). Consumed at the CLI layer to bound the engine-session-lock wait;
273
+ * the command implementation ignores it.
274
+ */
275
+ waitLock?: number | boolean;
242
276
  }
243
277
  /**
244
278
  * Options for the `fireforge patch tier` subcommand. Sets or clears the
@@ -278,16 +312,27 @@ export interface PatchLintIgnoreOptions {
278
312
  * declarations, or clear all staged dependencies from the patch.
279
313
  */
280
314
  export interface PatchStagedDependencyOptions {
281
- /** Add a forward-import staged dependency. */
315
+ /** Add a staged dependency declaration. */
282
316
  add?: boolean;
283
- /** Remove matching forward-import staged dependency declarations. */
317
+ /** Remove matching staged dependency declarations. */
284
318
  remove?: boolean;
285
319
  /** Drop the stagedDependencies field entirely. */
286
320
  clear?: boolean;
287
- /** Importing file path relative to engine/. */
321
+ /**
322
+ * Declaration shape: `import` (forward import, the default when unset) or
323
+ * `registration` (jar.mn packaging line, customElements or actor
324
+ * registration). Registration entries use `line` instead of `specifier`.
325
+ */
326
+ kind?: string;
327
+ /** Declaring file path relative to engine/. */
288
328
  file?: string;
289
- /** Exact import specifier as it appears in source. */
329
+ /** Exact import specifier as it appears in source (`kind: import`). */
290
330
  specifier?: string;
331
+ /**
332
+ * Registration/packaging line as the patch adds it, compared
333
+ * whitespace-trimmed (`kind: registration`).
334
+ */
335
+ line?: string;
291
336
  /** Later-created file path relative to engine/. */
292
337
  creates?: string;
293
338
  /** Optional exact patch filename expected to create `creates`. */
@@ -327,13 +372,31 @@ export interface PatchSplitOptions {
327
372
  skipLint?: boolean;
328
373
  }
329
374
  /**
330
- * Options for the preview-only `fireforge patch move-files` subcommand.
331
- * It validates an ownership transfer and prints the explicit
332
- * `re-export --files` commands needed to perform it.
375
+ * Options for the `fireforge patch move-files` subcommand. Without
376
+ * `--create` it is preview-only: it validates an ownership transfer and
377
+ * prints the explicit `re-export --files` commands needed to perform it.
378
+ * With `--create --order <n>` the target patch is created at that order
379
+ * and the files move into it as one split-style transaction.
333
380
  */
334
381
  export interface PatchMoveFilesOptions {
335
382
  /** File paths relative to engine/ to move from the source patch to the target patch. */
336
383
  file?: string[];
384
+ /** Create the target patch (transactional bootstrap of a split). Requires `order`. */
385
+ create?: boolean;
386
+ /** Exact sparse order for the created patch. Only valid with `create`. */
387
+ order?: number;
388
+ /** Category for the created patch; defaults to the source patch's. */
389
+ category?: string;
390
+ /** Description for the created patch. */
391
+ description?: string;
392
+ /** Preview the create+move without writing. */
393
+ dryRun?: boolean;
394
+ /** Skip interactive confirmation (required for non-TTY). */
395
+ yes?: boolean;
396
+ /** Bypass projected-lint refusals. */
397
+ forceUnsafe?: boolean;
398
+ /** Skip per-patch lint of the projected bodies. */
399
+ skipLint?: boolean;
337
400
  }
338
401
  /**
339
402
  * Options for the rebase command.
@@ -434,6 +497,14 @@ export interface TestOptions {
434
497
  killStaleMarionette?: boolean;
435
498
  /** Permit tests against packageable engine edits newer than the last successful build. */
436
499
  allowStaleBuild?: boolean;
500
+ /**
501
+ * Permit tests despite `components.conf` changes that only a full
502
+ * `fireforge build` compiles into the StaticComponents table — the
503
+ * packaged child process will resolve the OLD table. For operators who
504
+ * rebuilt out-of-band and accept the risk; distinct from
505
+ * `allowStaleBuild`, which only accepts stale packaged content.
506
+ */
507
+ allowStaleComponents?: boolean;
437
508
  /** Run the configured short harness canary. `true` means use `fireforge.json`'s test.canaryPath. */
438
509
  canary?: string | boolean;
439
510
  /**
@@ -464,6 +535,12 @@ export interface TestOptions {
464
535
  * checkers that consume a sample artifact after the run.
465
536
  */
466
537
  perfSamples?: string;
538
+ /**
539
+ * Parsed `--wait-lock [seconds]` value (`true` for the bare flag, meaning
540
+ * 60). Consumed at the CLI layer to bound the engine-session-lock wait;
541
+ * the command implementation ignores it.
542
+ */
543
+ waitLock?: number | boolean;
467
544
  }
468
545
  /**
469
546
  * Options for the furnace apply command.
@@ -127,6 +127,15 @@ export interface PatchMetadata {
127
127
  export interface PatchStagedDependencies {
128
128
  /** Exact forward-import declarations allowed for this patch. */
129
129
  forwardImports?: PatchStagedForwardImport[];
130
+ /**
131
+ * Registration-shaped forward dependencies: packaging or registration
132
+ * LINES (a jar.mn entry, a customElements registration, an actor
133
+ * registration) this patch adds that reference a file a later patch
134
+ * creates. Unlike `forwardImports`, these are validated by matching the
135
+ * declared line against the patch's added content, not by finding an
136
+ * import specifier — packaging lines have no import to match.
137
+ */
138
+ registrations?: PatchStagedRegistration[];
130
139
  }
131
140
  /** A single intentional forward import to a later-created file. */
132
141
  export interface PatchStagedForwardImport {
@@ -141,6 +150,26 @@ export interface PatchStagedForwardImport {
141
150
  /** Optional human-readable rationale for the staged dependency. */
142
151
  reason?: string;
143
152
  }
153
+ /**
154
+ * A single intentional registration/packaging line referencing a
155
+ * later-created file.
156
+ */
157
+ export interface PatchStagedRegistration {
158
+ /** Declaring file path relative to engine/ (e.g. `toolkit/content/jar.mn`). */
159
+ file: string;
160
+ /**
161
+ * The registration/packaging line exactly as the patch adds it. Compared
162
+ * whitespace-trimmed against the patch's added lines in `file`, so
163
+ * indentation differences do not break the match.
164
+ */
165
+ line: string;
166
+ /** Later-created file path relative to engine/. */
167
+ creates: string;
168
+ /** Optional exact patch filename expected to create `creates`. */
169
+ owner?: string;
170
+ /** Optional human-readable rationale for the staged dependency. */
171
+ reason?: string;
172
+ }
144
173
  /**
145
174
  * Schema for patches/patches.json file.
146
175
  */
@@ -12,6 +12,13 @@ export interface FirefoxConfig {
12
12
  product: FirefoxProduct;
13
13
  /** Optional pinned SHA-256 for the resolved source archive */
14
14
  sha256?: string;
15
+ /**
16
+ * Optional release-candidate build directory (e.g. "build2"). When set,
17
+ * the source archive resolves under
18
+ * `pub/<product>/candidates/<version>-candidates/<candidate>/` instead of
19
+ * `pub/<product>/releases/<version>/`, for pre-release verification.
20
+ */
21
+ candidate?: string;
15
22
  }
16
23
  /**
17
24
  * Supported project license SPDX identifiers.
@@ -46,6 +46,19 @@ export interface CustomComponentConfig {
46
46
  targetPath: string;
47
47
  /** Whether to register in customElements.js */
48
48
  register: boolean;
49
+ /**
50
+ * What the component IS. `'element'` (the default when unset) is a custom
51
+ * element with its own tag. `'library'` is a base class + helpers module
52
+ * that defines no element of its own (e.g. a shared MozLitElement
53
+ * subclass other components extend): `furnace validate` skips the
54
+ * `no-custom-element-define` / `not-moz-lit-element` compatibility checks
55
+ * and the `missing-css` structural advisory for it, while headers, module
56
+ * shape, `relative-import`, and (when a `.css` exists) the CSS
57
+ * compatibility rules still apply. A library exports no element, so
58
+ * `kind: "library"` requires `register: false` — the combination with
59
+ * `register: true` is rejected at config parse time.
60
+ */
61
+ kind?: 'element' | 'library';
49
62
  /** Whether this component uses Fluent l10n */
50
63
  localized: boolean;
51
64
  /** Stock component tag names composed internally by this component */
@@ -6,6 +6,7 @@
6
6
  export declare function pickDefined<T extends Record<string, unknown>>(obj: T): {
7
7
  [K in keyof T]+?: Exclude<T[K], undefined>;
8
8
  };
9
+ import type { Command } from 'commander';
9
10
  /**
10
11
  * Wraps an option-argument parser so its failures surface through
11
12
  * commander's own invalid-argument channel.
@@ -21,3 +22,22 @@ export declare function pickDefined<T extends Record<string, unknown>>(obj: T):
21
22
  * helper (or throw commander's InvalidArgumentError directly).
22
23
  */
23
24
  export declare function commanderArgParser<T>(parse: (raw: string) => T): (raw: string) => T;
25
+ /**
26
+ * Resolves the parsed `--wait-lock [seconds]` option value into a wait budget
27
+ * in seconds for `withEngineSessionLock`:
28
+ * - absent (`undefined`) → `undefined` (legacy ~1 s fail-fast),
29
+ * - bare flag (`true`) → {@link DEFAULT_WAIT_LOCK_SECONDS},
30
+ * - explicit value → integer validated into 1..3600.
31
+ *
32
+ * Accepts the raw string too, so it doubles as the option's arg parser (wrap
33
+ * with {@link commanderArgParser} so failures surface through commander's
34
+ * invalid-argument channel).
35
+ */
36
+ export declare function resolveWaitLockSeconds(value: string | number | boolean | undefined): number | undefined;
37
+ /**
38
+ * Registers the shared `--wait-lock [seconds]` flag on an engine-mutating
39
+ * command. The parsed option value is `true` for the bare flag or a validated
40
+ * integer; feed it through {@link resolveWaitLockSeconds} at the call site to
41
+ * obtain the `waitLockSeconds` for `withEngineSessionLock`.
42
+ */
43
+ export declare function addWaitLockOption(command: Command): Command;
@@ -14,6 +14,7 @@ export function pickDefined(obj) {
14
14
  return result;
15
15
  }
16
16
  import { InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
17
+ import { GeneralError } from '../errors/base.js';
17
18
  /**
18
19
  * Wraps an option-argument parser so its failures surface through
19
20
  * commander's own invalid-argument channel.
@@ -38,3 +39,41 @@ export function commanderArgParser(parse) {
38
39
  }
39
40
  };
40
41
  }
42
+ /** Wait budget applied when `--wait-lock` is passed without a value. */
43
+ const DEFAULT_WAIT_LOCK_SECONDS = 60;
44
+ /**
45
+ * Resolves the parsed `--wait-lock [seconds]` option value into a wait budget
46
+ * in seconds for `withEngineSessionLock`:
47
+ * - absent (`undefined`) → `undefined` (legacy ~1 s fail-fast),
48
+ * - bare flag (`true`) → {@link DEFAULT_WAIT_LOCK_SECONDS},
49
+ * - explicit value → integer validated into 1..3600.
50
+ *
51
+ * Accepts the raw string too, so it doubles as the option's arg parser (wrap
52
+ * with {@link commanderArgParser} so failures surface through commander's
53
+ * invalid-argument channel).
54
+ */
55
+ export function resolveWaitLockSeconds(value) {
56
+ if (value === undefined || value === false) {
57
+ return undefined;
58
+ }
59
+ if (value === true) {
60
+ return DEFAULT_WAIT_LOCK_SECONDS;
61
+ }
62
+ if (typeof value === 'number') {
63
+ return value;
64
+ }
65
+ const n = Number.parseInt(value, 10);
66
+ if (!Number.isFinite(n) || n < 1 || n > 3600) {
67
+ throw new GeneralError(`--wait-lock must be an integer in 1..3600 (got "${value}")`);
68
+ }
69
+ return n;
70
+ }
71
+ /**
72
+ * Registers the shared `--wait-lock [seconds]` flag on an engine-mutating
73
+ * command. The parsed option value is `true` for the bare flag or a validated
74
+ * integer; feed it through {@link resolveWaitLockSeconds} at the call site to
75
+ * obtain the `waitLockSeconds` for `withEngineSessionLock`.
76
+ */
77
+ export function addWaitLockOption(command) {
78
+ return command.option('--wait-lock [seconds]', 'Wait up to this many seconds (default 60) for another FireForge engine-mutating command to release the engine lock, instead of failing immediately', commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
79
+ }