@hominis/fireforge 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/dist/src/commands/build.js +13 -8
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/download.js +5 -2
- package/dist/src/commands/export-flow.d.ts +6 -2
- package/dist/src/commands/export-flow.js +17 -11
- package/dist/src/commands/export-placement-gate.js +4 -6
- package/dist/src/commands/export-placement-policy.d.ts +11 -3
- package/dist/src/commands/export-placement-policy.js +32 -29
- package/dist/src/commands/export-shared.js +17 -2
- package/dist/src/commands/export.js +5 -5
- package/dist/src/commands/furnace/index.js +23 -18
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +15 -5
- package/dist/src/commands/lint.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- package/dist/src/commands/patch/move-files-create.d.ts +19 -0
- package/dist/src/commands/patch/move-files-create.js +116 -0
- package/dist/src/commands/patch/move-files-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -1
- package/dist/src/commands/patch/move-files.js +74 -15
- package/dist/src/commands/patch/rename.js +4 -1
- package/dist/src/commands/patch/split.d.ts +11 -1
- package/dist/src/commands/patch/split.js +6 -5
- package/dist/src/commands/re-export-scan.d.ts +17 -0
- package/dist/src/commands/re-export-scan.js +51 -0
- package/dist/src/commands/re-export.js +17 -6
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/source.js +28 -3
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +17 -3
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-register.js +6 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +8 -2
- package/dist/src/commands/test-stale-gate.d.ts +44 -0
- package/dist/src/commands/test-stale-gate.js +81 -0
- package/dist/src/commands/test.js +52 -81
- package/dist/src/core/build-audit.d.ts +12 -0
- package/dist/src/core/build-audit.js +14 -0
- package/dist/src/core/build-baseline-types.d.ts +41 -0
- package/dist/src/core/build-baseline.d.ts +6 -1
- package/dist/src/core/build-baseline.js +48 -7
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +14 -1
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -2
- package/dist/src/core/file-lock.d.ts +33 -0
- package/dist/src/core/file-lock.js +32 -6
- package/dist/src/core/firefox-archive.d.ts +2 -1
- package/dist/src/core/firefox-archive.js +32 -8
- package/dist/src/core/firefox.d.ts +8 -3
- package/dist/src/core/firefox.js +11 -6
- package/dist/src/core/furnace-validate-helpers.js +28 -1
- package/dist/src/core/license-headers.d.ts +24 -0
- package/dist/src/core/license-headers.js +103 -1
- package/dist/src/core/patch-export.d.ts +10 -1
- package/dist/src/core/patch-export.js +12 -2
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -1
- package/dist/src/core/patch-lint-checkjs.js +30 -14
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint.d.ts +3 -0
- package/dist/src/core/patch-lint.js +29 -8
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/status-classify.d.ts +11 -2
- package/dist/src/core/status-classify.js +49 -25
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +56 -1
- package/dist/src/core/test-stale-check.js +114 -4
- package/dist/src/core/token-manager.js +39 -2
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/typecheck-shim.d.ts +13 -0
- package/dist/src/core/typecheck-shim.js +17 -2
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/types/commands/options.d.ts +69 -3
- package/dist/src/types/config.d.ts +22 -0
- package/dist/src/utils/options.d.ts +20 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/validation.d.ts +5 -0
- package/dist/src/utils/validation.js +7 -0
- package/package.json +3 -3
|
@@ -10,17 +10,16 @@ import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionette
|
|
|
10
10
|
import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
|
|
11
11
|
import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
|
|
12
12
|
import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
|
|
13
|
-
import { checkStaleBuildForTest, findUncoveredRequestPaths, formatStaleBuildWarning, formatTestCoverageRefusal, } from '../core/test-stale-check.js';
|
|
14
|
-
import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
|
|
15
13
|
import { GeneralError } from '../errors/base.js';
|
|
16
14
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
17
15
|
import { toError } from '../utils/errors.js';
|
|
18
16
|
import { pathExists } from '../utils/fs.js';
|
|
19
|
-
import { info, intro, outro, spinner, success, verbose
|
|
17
|
+
import { info, intro, outro, spinner, success, verbose } from '../utils/logger.js';
|
|
20
18
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
21
19
|
import { diagnoseShardOutcome, finalizeSingleRunOutcome } from './test-diagnose.js';
|
|
22
|
-
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
20
|
+
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, reportCanaryOutcome, resolveCanaryPath, } from './test-modes.js';
|
|
23
21
|
import { DEFAULT_HARNESS_RETRIES, runShardedTests, runTestsWithRetries, } from './test-run.js';
|
|
22
|
+
import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
|
|
24
23
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
25
24
|
const missingPaths = [];
|
|
26
25
|
for (const testPath of testPaths) {
|
|
@@ -34,19 +33,6 @@ async function assertTestPathsExist(engineDir, testPaths) {
|
|
|
34
33
|
throw new GeneralError(`Test path${missingPaths.length === 1 ? '' : 's'} not found under engine/: ${missingPaths.join(', ')}\n\n` +
|
|
35
34
|
'If you expected these files to come from your patch stack, run "fireforge import" first.');
|
|
36
35
|
}
|
|
37
|
-
async function classifyTestHarnesses(engineDir, normalizedPaths) {
|
|
38
|
-
const result = { xpcshell: [], nonXpcshell: [] };
|
|
39
|
-
for (const testPath of normalizedPaths) {
|
|
40
|
-
const manifest = await findNearestXpcshellManifest(engineDir, testPath);
|
|
41
|
-
if (manifest) {
|
|
42
|
-
result.xpcshell.push(testPath);
|
|
43
|
-
}
|
|
44
|
-
else {
|
|
45
|
-
result.nonXpcshell.push(testPath);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return result;
|
|
49
|
-
}
|
|
50
36
|
/**
|
|
51
37
|
* Picks the mach dispatch target for a (non-mixed) run. A single-suite run
|
|
52
38
|
* auto-routes to the suite-specific command (`mach xpcshell-test` /
|
|
@@ -66,12 +52,6 @@ function resolveTestSuite(classification, forceGeneric) {
|
|
|
66
52
|
}
|
|
67
53
|
return 'generic';
|
|
68
54
|
}
|
|
69
|
-
function buildMixedHarnessMessage(classification) {
|
|
70
|
-
return ('FireForge cannot run xpcshell and browser/mochitest paths in the same mach invocation.\n\n' +
|
|
71
|
-
'Split this into separate `fireforge test` commands so each manifest selects its own harness:\n' +
|
|
72
|
-
` - xpcshell: ${classification.xpcshell.join(', ')}\n` +
|
|
73
|
-
` - browser/mochitest: ${classification.nonXpcshell.join(', ')}`);
|
|
74
|
-
}
|
|
75
55
|
function filterRedundantXpcshellFlavorArgs(machArgs, classification) {
|
|
76
56
|
if (classification.xpcshell.length === 0 || classification.nonXpcshell.length > 0) {
|
|
77
57
|
return [...machArgs];
|
|
@@ -135,9 +115,14 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
135
115
|
// same files — in any invocation shape — is not refused. A failed
|
|
136
116
|
// write never fails the run. The coverage claim is scoped to the
|
|
137
117
|
// requested test paths, since a file-scoped `test --build` only
|
|
138
|
-
// guarantees packaging for those manifests.
|
|
118
|
+
// guarantees packaging for those manifests. The previous baseline is
|
|
119
|
+
// passed through so a scoped write carries the static-components
|
|
120
|
+
// anchor forward (a `mach build faster` does not rebake
|
|
121
|
+
// components.conf into the compiled table).
|
|
139
122
|
try {
|
|
140
|
-
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage
|
|
123
|
+
await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, testPackagingCoverage, previousBaseline, testPackagingCoverage === 'full'
|
|
124
|
+
? 'fireforge test --build'
|
|
125
|
+
: `fireforge test --build ${testPackagingCoverage.join(' ')}`);
|
|
141
126
|
}
|
|
142
127
|
catch (baselineError) {
|
|
143
128
|
verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
|
|
@@ -150,51 +135,6 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
|
|
|
150
135
|
: 'Pre-test build failed', 'mach build faster');
|
|
151
136
|
});
|
|
152
137
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Stale-build preflight — when `--build` was NOT requested, detect
|
|
155
|
-
* packageable engine edits since the last successful build and fail
|
|
156
|
-
* UP-FRONT unless the operator explicitly accepts the stale package risk.
|
|
157
|
-
*
|
|
158
|
-
* Packaging COVERAGE is checked first, on EVERY non-`--build` run and
|
|
159
|
-
* regardless of staleness or `--allow-stale-build`: a runtime packaged by
|
|
160
|
-
* a file-scoped `test --build` can lack support fixtures for OTHER
|
|
161
|
-
* manifests even when nothing changed since — dispatching such a run
|
|
162
|
-
* hangs on missing fixtures rather than failing, so the flag (which only
|
|
163
|
-
* accepts stale CONTENT) must not be the trigger. Field incident: a
|
|
164
|
-
* three-file scoped rebuild left `file_tiles_audio.html` unpackaged and a
|
|
165
|
-
* later run over different files timed out twice at 45s waiting on
|
|
166
|
-
* `DOMAudioPlaybackStarted`.
|
|
167
|
-
*
|
|
168
|
-
* Exception: a path-less `test --doctor` stops at the Marionette health
|
|
169
|
-
* check (`runDoctorPreflight` returns 'stop' when no test paths were
|
|
170
|
-
* given) and never dispatches a test, so it needs no packaging coverage —
|
|
171
|
-
* treating it as a full-suite request would refuse a probe that touches
|
|
172
|
-
* no fixtures. The stale-content refusal still applies to it unchanged.
|
|
173
|
-
*/
|
|
174
|
-
async function enforceStaleBuildGate(projectRoot, engineDir, options, normalizedPaths) {
|
|
175
|
-
const stale = await checkStaleBuildForTest(projectRoot, engineDir);
|
|
176
|
-
const dispatchesNoTests = options.doctor === true && normalizedPaths.length === 0;
|
|
177
|
-
if (!dispatchesNoTests) {
|
|
178
|
-
const recordedCoverage = stale.baseline?.testPackagingCoverage;
|
|
179
|
-
const uncovered = findUncoveredRequestPaths(recordedCoverage, normalizedPaths);
|
|
180
|
-
if (uncovered.length > 0) {
|
|
181
|
-
throw new GeneralError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : []));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const staleMessage = stale.stale
|
|
185
|
-
? `${formatStaleBuildWarning(stale)}\n\n` +
|
|
186
|
-
'Run `fireforge test --build` to refresh the packaged runtime first, or pass ' +
|
|
187
|
-
'`--allow-stale-build` if you intentionally rebuilt out-of-band and accept the risk.'
|
|
188
|
-
: undefined;
|
|
189
|
-
if (staleMessage !== undefined) {
|
|
190
|
-
if (options.allowStaleBuild === true) {
|
|
191
|
-
warn(staleMessage);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
throw new GeneralError(staleMessage);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
138
|
function logTestSelection(scopes) {
|
|
199
139
|
if (scopes.length > 0) {
|
|
200
140
|
const labels = scopes.map((scope) => scope.isDirectory && scope.testFileCount > 0
|
|
@@ -281,7 +221,7 @@ async function runDoctorPreflight(args) {
|
|
|
281
221
|
* `--mach-arg` and the xpcshell flavor that ignores the pref entirely.
|
|
282
222
|
* Mutates `extraArgs` in place.
|
|
283
223
|
*/
|
|
284
|
-
function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
224
|
+
function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly = false) {
|
|
285
225
|
// Auto-forward the Marionette port to mach when `--marionette-port` is
|
|
286
226
|
// set. `--setpref=marionette.port=<n>` configures where the browser
|
|
287
227
|
// listener binds; `--marionette=127.0.0.1:<n>` tells the mochitest harness
|
|
@@ -300,6 +240,14 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
|
300
240
|
// `--marionette=...` (or two-token `--marionette host:port`).
|
|
301
241
|
if (options.marionettePort === undefined)
|
|
302
242
|
return;
|
|
243
|
+
if (xpcshellOnly) {
|
|
244
|
+
// Manifest classification says every requested path is xpcshell —
|
|
245
|
+
// xpcshell ignores the browser Marionette path entirely, and the
|
|
246
|
+
// mochitest client flags previously forwarded here made mach reject
|
|
247
|
+
// the dispatch (FORGE F10).
|
|
248
|
+
info(`--marionette-port=${options.marionettePort} applied to the preflight probe only: the requested paths are xpcshell-only, and xpcshell ignores the browser Marionette port. Not forwarding --setpref=marionette.port or --marionette to mach.`);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
303
251
|
{
|
|
304
252
|
const operatorAlreadyForwarded = forwardedPort !== undefined;
|
|
305
253
|
const machArgs = options.machArg ?? [];
|
|
@@ -318,8 +266,23 @@ function appendMarionetteForwardingArgs(extraArgs, options, forwardedPort) {
|
|
|
318
266
|
}
|
|
319
267
|
}
|
|
320
268
|
}
|
|
321
|
-
async function ensureTestMarionettePortAvailable(port, binaryName,
|
|
322
|
-
if (
|
|
269
|
+
async function ensureTestMarionettePortAvailable(port, binaryName, options, skip) {
|
|
270
|
+
if (skip.xpcshellOnly && !skip.doctor) {
|
|
271
|
+
// xpcshell does not bind the browser Marionette port, so a developer's
|
|
272
|
+
// interactive browser holding 2828 must not kill an xpcshell run
|
|
273
|
+
// (FORGE F10). --doctor keeps the preflight: its probe launches a
|
|
274
|
+
// Marionette browser regardless of the requested harness.
|
|
275
|
+
const message = 'Skipping the Marionette stale-port preflight: all requested paths are xpcshell ' +
|
|
276
|
+
'(xpcshell does not bind the browser Marionette port).';
|
|
277
|
+
if (options.marionettePort !== undefined || options.killStaleMarionette === true) {
|
|
278
|
+
info(message);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
verbose(message);
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (options.killStaleMarionette === true) {
|
|
323
286
|
await ensureMarionettePortAvailable(port, { binaryName, killStaleBrowser: true });
|
|
324
287
|
return;
|
|
325
288
|
}
|
|
@@ -363,11 +326,19 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
363
326
|
// stale/coverage refusals keep precedence over missing-path errors.)
|
|
364
327
|
const requestedPaths = canaryPath !== undefined ? [canaryPath] : testPaths;
|
|
365
328
|
const normalizedPaths = requestedPaths.map((p) => stripEnginePrefix(p).trim());
|
|
329
|
+
const { classification, xpcshellOnly } = await classifyBeforeDispatch(paths.engine, normalizedPaths);
|
|
366
330
|
// Run incremental build if requested
|
|
367
331
|
if (options.build) {
|
|
368
332
|
// A path-less `test --build` runs (and packages for) the full suite;
|
|
369
|
-
// a scoped invocation only vouches for the requested paths.
|
|
333
|
+
// a scoped invocation only vouches for the requested paths. A SCOPED
|
|
334
|
+
// rebuild also cannot regenerate the compiled StaticComponents table,
|
|
335
|
+
// so it runs the components.conf gate up-front (the path-less shape
|
|
336
|
+
// refreshes the anchor itself and skips it).
|
|
370
337
|
const coverage = normalizedPaths.length === 0 ? 'full' : normalizedPaths;
|
|
338
|
+
if (normalizedPaths.length > 0) {
|
|
339
|
+
const previousBaseline = await readBuildBaseline(projectRoot);
|
|
340
|
+
await enforceStaticComponentsGate(paths.engine, previousBaseline, options);
|
|
341
|
+
}
|
|
371
342
|
await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, coverage);
|
|
372
343
|
info('');
|
|
373
344
|
}
|
|
@@ -396,7 +367,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
396
367
|
// generic bind failure. 2026-04-21 eval (Finding #20): a stale
|
|
397
368
|
// `-marionette` process from `fresh/` poisoned a later test run in
|
|
398
369
|
// the sibling `mybrowser/` workspace.
|
|
399
|
-
await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options
|
|
370
|
+
await ensureTestMarionettePortAvailable(effectivePort, projectConfig.binaryName, options, {
|
|
371
|
+
xpcshellOnly,
|
|
372
|
+
doctor: options.doctor === true,
|
|
373
|
+
});
|
|
400
374
|
if (options.doctor) {
|
|
401
375
|
const doctorOutcome = await runDoctorPreflight({
|
|
402
376
|
engineDir: paths.engine,
|
|
@@ -410,10 +384,6 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
410
384
|
return;
|
|
411
385
|
}
|
|
412
386
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
413
|
-
const classification = await classifyTestHarnesses(paths.engine, normalizedPaths);
|
|
414
|
-
if (classification.xpcshell.length > 0 && classification.nonXpcshell.length > 0) {
|
|
415
|
-
throw new GeneralError(buildMixedHarnessMessage(classification));
|
|
416
|
-
}
|
|
417
387
|
const suite = resolveTestSuite(classification, options.genericMachTest === true);
|
|
418
388
|
const forwardedMachArgs = options.machArg && options.machArg.length > 0
|
|
419
389
|
? filterRedundantXpcshellFlavorArgs(options.machArg, classification)
|
|
@@ -436,7 +406,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
436
406
|
if (forwardedMachArgs.length > 0) {
|
|
437
407
|
extraArgs.push(...forwardedMachArgs);
|
|
438
408
|
}
|
|
439
|
-
appendMarionetteForwardingArgs(extraArgs, options, forwardedPort);
|
|
409
|
+
appendMarionetteForwardingArgs(extraArgs, options, forwardedPort, xpcshellOnly);
|
|
440
410
|
// Directory arguments mean EXACTLY that directory: mozbuild's test
|
|
441
411
|
// resolver matches paths by string prefix, so a bare directory arg
|
|
442
412
|
// silently swept in prefix-named siblings (152.0b7 → 153.0b8 drill:
|
|
@@ -472,6 +442,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
472
442
|
suite,
|
|
473
443
|
baseExtraArgs: extraArgs,
|
|
474
444
|
harnessRetries,
|
|
445
|
+
headless: options.headless === true,
|
|
475
446
|
...(perfSampleEnv ? { env: perfSampleEnv } : {}),
|
|
476
447
|
};
|
|
477
448
|
const postRebuildContext = options.build
|
|
@@ -517,7 +488,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
517
488
|
reportCanaryOutcome(outcome);
|
|
518
489
|
return;
|
|
519
490
|
}
|
|
520
|
-
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext);
|
|
491
|
+
finalizeSingleRunOutcome(outcome, normalizedPaths, projectConfig.binaryName, postRebuildContext, options.headless === true);
|
|
521
492
|
}
|
|
522
493
|
/**
|
|
523
494
|
* Builds the perf-sample env contract for the harness run (field report
|
|
@@ -36,6 +36,18 @@ export interface AuditSummary {
|
|
|
36
36
|
* @returns True when the path implies packaging.
|
|
37
37
|
*/
|
|
38
38
|
export declare function isPackageablePath(sourcePath: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Decides whether a source path is an XPCOM static-component manifest —
|
|
41
|
+
* i.e. a file whose registrations are baked into the compiled
|
|
42
|
+
* StaticComponents table at FULL-build time rather than packaged into
|
|
43
|
+
* `dist/`. This basename check is the single extension point for that
|
|
44
|
+
* classification; parsing `moz.build` `XPCOM_MANIFESTS` entries to follow
|
|
45
|
+
* renamed manifests is out of scope.
|
|
46
|
+
*
|
|
47
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
48
|
+
* @returns True when the path is a `components.conf` manifest.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isXpcomManifestPath(sourcePath: string): boolean;
|
|
39
51
|
/**
|
|
40
52
|
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
41
53
|
* stale artifacts and a summary info line at the end. Always returns
|
|
@@ -111,6 +111,20 @@ export function isPackageablePath(sourcePath) {
|
|
|
111
111
|
}
|
|
112
112
|
return false;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Decides whether a source path is an XPCOM static-component manifest —
|
|
116
|
+
* i.e. a file whose registrations are baked into the compiled
|
|
117
|
+
* StaticComponents table at FULL-build time rather than packaged into
|
|
118
|
+
* `dist/`. This basename check is the single extension point for that
|
|
119
|
+
* classification; parsing `moz.build` `XPCOM_MANIFESTS` entries to follow
|
|
120
|
+
* renamed manifests is out of scope.
|
|
121
|
+
*
|
|
122
|
+
* @param sourcePath Engine-relative POSIX path.
|
|
123
|
+
* @returns True when the path is a `components.conf` manifest.
|
|
124
|
+
*/
|
|
125
|
+
export function isXpcomManifestPath(sourcePath) {
|
|
126
|
+
return basename(sourcePath.replace(/\\/g, '/')) === 'components.conf';
|
|
127
|
+
}
|
|
114
128
|
/*
|
|
115
129
|
* Finds the unique obj-star directory with a dist subtree, or undefined
|
|
116
130
|
* when zero or multiple match. The ambiguous case is already rejected
|
|
@@ -55,8 +55,49 @@ export interface BuildBaseline {
|
|
|
55
55
|
* The record is project-scoped, which is also per-obj-dir: multi-objdir
|
|
56
56
|
* checkouts are refused up-front (`AmbiguousBuildArtifactsError`), so at
|
|
57
57
|
* most one obj dir exists per project.
|
|
58
|
+
*
|
|
59
|
+
* Union/"shared coverage" across successive scoped builds was evaluated
|
|
60
|
+
* and rejected as unsound — every baseline write refreshes
|
|
61
|
+
* `packageableFingerprints` for ALL dirty packageable paths, so a union
|
|
62
|
+
* would whitewash an earlier scope's edited fixtures while
|
|
63
|
+
* `obj-*`/`_tests/` still holds its stale staging; coverage therefore
|
|
64
|
+
* REPLACES.
|
|
58
65
|
*/
|
|
59
66
|
testPackagingCoverage?: TestPackagingCoverage;
|
|
67
|
+
/**
|
|
68
|
+
* Anchor for the compiled StaticComponents table: the engine HEAD SHA of
|
|
69
|
+
* the last FULL-coverage build plus content fingerprints of the
|
|
70
|
+
* `components.conf` manifests that were dirty at that moment. Written
|
|
71
|
+
* fresh only on FULL-coverage baseline writes (`fireforge build`,
|
|
72
|
+
* `build --ui`, path-less `test --build`); a scoped `test --build`
|
|
73
|
+
* carries the previous record forward verbatim, because `mach build
|
|
74
|
+
* faster` does not rebake `components.conf` registrations into the
|
|
75
|
+
* compiled table. Missing on pre-0.38.0 baselines — the
|
|
76
|
+
* static-components stale check degrades to "fresh" in that case.
|
|
77
|
+
*/
|
|
78
|
+
staticComponentsBaseline?: StaticComponentsBaseline;
|
|
79
|
+
/**
|
|
80
|
+
* Invocation shape that recorded this baseline (`'fireforge build'`,
|
|
81
|
+
* `'fireforge build --ui'`, `'fireforge test --build [paths]'`).
|
|
82
|
+
* Informational — surfaced by `fireforge status --test-coverage`.
|
|
83
|
+
* Missing on pre-0.39.0 baselines; render as "unknown".
|
|
84
|
+
*/
|
|
85
|
+
recordedBy?: string;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* State of the engine at the last FULL build, as far as compiled-in XPCOM
|
|
89
|
+
* component registration is concerned. See
|
|
90
|
+
* {@link BuildBaseline.staticComponentsBaseline}.
|
|
91
|
+
*/
|
|
92
|
+
export interface StaticComponentsBaseline {
|
|
93
|
+
/** Engine HEAD SHA at the time of the last full build. */
|
|
94
|
+
engineHeadSha: string;
|
|
95
|
+
/**
|
|
96
|
+
* Hex-encoded SHA-256 per `components.conf` path that was dirty
|
|
97
|
+
* (modified-against-HEAD or untracked) when the full build ran. Keys are
|
|
98
|
+
* engine-relative POSIX paths.
|
|
99
|
+
*/
|
|
100
|
+
fingerprints: Record<string, string>;
|
|
60
101
|
}
|
|
61
102
|
/** Coverage claim of the packaged test runtime. See {@link BuildBaseline.testPackagingCoverage}. */
|
|
62
103
|
export type TestPackagingCoverage = 'full' | string[];
|
|
@@ -42,5 +42,10 @@ export declare function readBuildBaseline(projectRoot: string): Promise<BuildBas
|
|
|
42
42
|
* @param testPackagingCoverage - Coverage claim of the packaged test
|
|
43
43
|
* runtime this build produced (`'full'`, or the scoped request paths of
|
|
44
44
|
* a `test --build` invocation). Omitted → field left off the marker.
|
|
45
|
+
* @param previousBaseline - The baseline this write replaces, when the
|
|
46
|
+
* caller has it. A scoped (non-`'full'`) write carries its
|
|
47
|
+
* `staticComponentsBaseline` forward verbatim — `mach build faster`
|
|
48
|
+
* does not rebake `components.conf` registrations, so the last FULL
|
|
49
|
+
* build stays the honest anchor for the compiled StaticComponents table.
|
|
45
50
|
*/
|
|
46
|
-
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage): Promise<void>;
|
|
51
|
+
export declare function writeBuildBaseline(projectRoot: string, engineDir: string, binaryName: string, testPackagingCoverage?: TestPackagingCoverage, previousBaseline?: BuildBaseline, recordedBy?: string): Promise<void>;
|
|
@@ -22,7 +22,7 @@ import { join } from 'node:path';
|
|
|
22
22
|
import { toError } from '../utils/errors.js';
|
|
23
23
|
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
24
24
|
import { verbose } from '../utils/logger.js';
|
|
25
|
-
import { isPackageablePath } from './build-audit.js';
|
|
25
|
+
import { isPackageablePath, isXpcomManifestPath } from './build-audit.js';
|
|
26
26
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
27
27
|
import { getHead, hasChanges, isMissingHeadError } from './git.js';
|
|
28
28
|
import { git } from './git-base.js';
|
|
@@ -68,8 +68,13 @@ export async function readBuildBaseline(projectRoot) {
|
|
|
68
68
|
* @param testPackagingCoverage - Coverage claim of the packaged test
|
|
69
69
|
* runtime this build produced (`'full'`, or the scoped request paths of
|
|
70
70
|
* a `test --build` invocation). Omitted → field left off the marker.
|
|
71
|
+
* @param previousBaseline - The baseline this write replaces, when the
|
|
72
|
+
* caller has it. A scoped (non-`'full'`) write carries its
|
|
73
|
+
* `staticComponentsBaseline` forward verbatim — `mach build faster`
|
|
74
|
+
* does not rebake `components.conf` registrations, so the last FULL
|
|
75
|
+
* build stays the honest anchor for the compiled StaticComponents table.
|
|
71
76
|
*/
|
|
72
|
-
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage) {
|
|
77
|
+
export async function writeBuildBaseline(projectRoot, engineDir, binaryName, testPackagingCoverage, previousBaseline, recordedBy) {
|
|
73
78
|
let engineHeadSha = '';
|
|
74
79
|
try {
|
|
75
80
|
engineHeadSha = await getHead(engineDir);
|
|
@@ -84,12 +89,21 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
const packageableFingerprints = await collectPackageableFingerprints(engineDir);
|
|
92
|
+
// A full-coverage write (and the legacy no-claim `fireforge build` shape)
|
|
93
|
+
// just recompiled the StaticComponents table, so it anchors the table to
|
|
94
|
+
// the current engine state. A scoped write did not — carry the previous
|
|
95
|
+
// anchor forward verbatim.
|
|
96
|
+
const staticComponentsBaseline = testPackagingCoverage === undefined || testPackagingCoverage === 'full'
|
|
97
|
+
? await collectStaticComponentsBaseline(engineDir, engineHeadSha)
|
|
98
|
+
: previousBaseline?.staticComponentsBaseline;
|
|
87
99
|
const baseline = {
|
|
88
100
|
engineHeadSha,
|
|
89
101
|
builtAt: new Date().toISOString(),
|
|
90
102
|
binaryName,
|
|
91
103
|
...(packageableFingerprints !== undefined ? { packageableFingerprints } : {}),
|
|
92
104
|
...(testPackagingCoverage !== undefined ? { testPackagingCoverage } : {}),
|
|
105
|
+
...(staticComponentsBaseline !== undefined ? { staticComponentsBaseline } : {}),
|
|
106
|
+
...(recordedBy !== undefined ? { recordedBy } : {}),
|
|
93
107
|
};
|
|
94
108
|
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
95
109
|
}
|
|
@@ -107,6 +121,33 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName, tes
|
|
|
107
121
|
* back to the pre-0.16.0 "path-only" behavior on the next test run.
|
|
108
122
|
*/
|
|
109
123
|
async function collectPackageableFingerprints(engineDir) {
|
|
124
|
+
return collectDirtyFingerprints(engineDir, isPackageablePath, 'packageable fingerprint');
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Anchor for the compiled StaticComponents table: the engine HEAD SHA of
|
|
128
|
+
* this (full) build plus content fingerprints of the `components.conf`
|
|
129
|
+
* manifests dirty right now — the same dirty-path probe family as
|
|
130
|
+
* {@link collectPackageableFingerprints}, filtered to XPCOM manifests.
|
|
131
|
+
* Returns `undefined` on probe failure so a broken probe omits the field
|
|
132
|
+
* (the static-components stale check then degrades to "fresh") rather than
|
|
133
|
+
* anchoring to a garbage record.
|
|
134
|
+
*/
|
|
135
|
+
async function collectStaticComponentsBaseline(engineDir, engineHeadSha) {
|
|
136
|
+
const fingerprints = await collectDirtyFingerprints(engineDir, isXpcomManifestPath, 'static-components fingerprint');
|
|
137
|
+
if (fingerprints === undefined) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
return { engineHeadSha, fingerprints };
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Shared dirty-path fingerprint probe backing
|
|
144
|
+
* {@link collectPackageableFingerprints} and
|
|
145
|
+
* {@link collectStaticComponentsBaseline}: enumerates engine workdir
|
|
146
|
+
* modifications (tracked and untracked), keeps the paths `includePath`
|
|
147
|
+
* accepts, and hashes each. Returns `undefined` on any git failure so a
|
|
148
|
+
* broken probe never corrupts the on-disk baseline with `{}`.
|
|
149
|
+
*/
|
|
150
|
+
async function collectDirtyFingerprints(engineDir, includePath, contextLabel) {
|
|
110
151
|
try {
|
|
111
152
|
const dirtyPaths = new Set();
|
|
112
153
|
if (await hasChanges(engineDir)) {
|
|
@@ -120,12 +161,12 @@ async function collectPackageableFingerprints(engineDir) {
|
|
|
120
161
|
dirtyPaths.add(untracked);
|
|
121
162
|
}
|
|
122
163
|
}
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
164
|
+
const included = [...dirtyPaths].filter(includePath);
|
|
165
|
+
if (included.length === 0) {
|
|
125
166
|
return {};
|
|
126
167
|
}
|
|
127
168
|
const fingerprints = {};
|
|
128
|
-
for (const relPath of
|
|
169
|
+
for (const relPath of included) {
|
|
129
170
|
try {
|
|
130
171
|
const buffer = await readFile(join(engineDir, relPath));
|
|
131
172
|
fingerprints[relPath] = createHash('sha256').update(buffer).digest('hex');
|
|
@@ -134,13 +175,13 @@ async function collectPackageableFingerprints(engineDir) {
|
|
|
134
175
|
// A file that disappeared between status probe and hash is
|
|
135
176
|
// expected in concurrent scenarios; skip it without failing the
|
|
136
177
|
// whole baseline write.
|
|
137
|
-
verbose(`Build baseline: skipping
|
|
178
|
+
verbose(`Build baseline: skipping ${contextLabel} for ${relPath} — ${toError(fileError).message}`);
|
|
138
179
|
}
|
|
139
180
|
}
|
|
140
181
|
return fingerprints;
|
|
141
182
|
}
|
|
142
183
|
catch (error) {
|
|
143
|
-
verbose(`Build baseline:
|
|
184
|
+
verbose(`Build baseline: ${contextLabel} probe failed — ${toError(error).message}`);
|
|
144
185
|
return undefined;
|
|
145
186
|
}
|
|
146
187
|
}
|
|
@@ -24,6 +24,15 @@ export interface PrepareBuildOptions {
|
|
|
24
24
|
*/
|
|
25
25
|
previousBaseline?: BuildBaseline | undefined;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
29
|
+
* configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
|
|
30
|
+
* on macOS) are environmental interruptions, not compiler failures — naming
|
|
31
|
+
* that in the failure text saves the operator a fruitless log hunt
|
|
32
|
+
* (FORGE F16). Returns undefined for codes <= 128 (regular failures) and
|
|
33
|
+
* for codes past the conventional signal range, so callers append nothing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function describeSignalShapedExit(exitCode: number): string | undefined;
|
|
27
36
|
/**
|
|
28
37
|
* Returns true when the file path matches a pattern that forces
|
|
29
38
|
* `mach configure` to regenerate the backend. Exported for testing.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Shared pre-flight logic for build and package commands:
|
|
4
4
|
* story cleanup, branding setup, Furnace component application, and mozconfig generation.
|
|
5
5
|
*/
|
|
6
|
+
import { constants as osConstants } from 'node:os';
|
|
6
7
|
import { BuildError } from '../errors/build.js';
|
|
7
8
|
import { FurnaceError } from '../errors/furnace.js';
|
|
8
9
|
import { toError } from '../utils/errors.js';
|
|
@@ -33,6 +34,27 @@ function extractMachConfigureError(result) {
|
|
|
33
34
|
return '';
|
|
34
35
|
return combined.split('\n').slice(-40).join('\n').trim();
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Describes an exit code in the shell's 128+N signal convention. Truncated
|
|
39
|
+
* configure/build logs with a signal-shaped exit (e.g. 144 = 128+16, SIGURG
|
|
40
|
+
* on macOS) are environmental interruptions, not compiler failures — naming
|
|
41
|
+
* that in the failure text saves the operator a fruitless log hunt
|
|
42
|
+
* (FORGE F16). Returns undefined for codes <= 128 (regular failures) and
|
|
43
|
+
* for codes past the conventional signal range, so callers append nothing.
|
|
44
|
+
*/
|
|
45
|
+
export function describeSignalShapedExit(exitCode) {
|
|
46
|
+
if (exitCode <= 128 || exitCode > 192)
|
|
47
|
+
return undefined;
|
|
48
|
+
const signalNumber = exitCode - 128;
|
|
49
|
+
const names = Object.entries(osConstants.signals)
|
|
50
|
+
.filter(([, value]) => value === signalNumber)
|
|
51
|
+
.map(([name]) => name);
|
|
52
|
+
const label = names.length > 0 ? names.join('/') : `signal ${signalNumber}`;
|
|
53
|
+
return (`Exit ${exitCode} is signal-shaped (${exitCode} - 128 = ${signalNumber}, ${label} on this host): ` +
|
|
54
|
+
'the process was likely interrupted externally (OOM-killer, terminal disconnect, ' +
|
|
55
|
+
'display/session teardown) rather than failing on its own, and the log may be ' +
|
|
56
|
+
'truncated mid-step.');
|
|
57
|
+
}
|
|
36
58
|
/**
|
|
37
59
|
* Builds the `BuildError` for a non-zero auto-configure exit: the output
|
|
38
60
|
* tail (so the underlying mozbuild failure survives), any matched
|
|
@@ -43,8 +65,10 @@ function extractMachConfigureError(result) {
|
|
|
43
65
|
function buildConfigureFailureError(captured) {
|
|
44
66
|
const detail = extractMachConfigureError(captured);
|
|
45
67
|
const hints = explainMachError(`${captured.stderr}\n${captured.stdout}`);
|
|
68
|
+
const signalNote = describeSignalShapedExit(captured.exitCode);
|
|
46
69
|
return new BuildError(`Backend regeneration failed: mach configure exited with code ${captured.exitCode}.` +
|
|
47
70
|
(detail ? `\n\nmach configure output (tail):\n${detail}` : '') +
|
|
71
|
+
(signalNote ? `\n\n${signalNote}` : '') +
|
|
48
72
|
(hints.length > 0 ? `\n\n${hints.map((hint) => `Hint: ${hint}`).join('\n')}` : '') +
|
|
49
73
|
'\n\nBuild stopped because continuing would hide the real configure failure.', 'mach configure');
|
|
50
74
|
}
|
|
@@ -11,7 +11,7 @@ export declare const STATE_FILENAME = "state.json";
|
|
|
11
11
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
12
12
|
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "test", "externalToolchains", "license", "wire", "patchLint", "patchPolicy", "typecheck", "markerComment"];
|
|
13
13
|
/** Supported config paths that can be read or set without --force. */
|
|
14
|
-
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
14
|
+
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "firefox.candidate", "build", "build.jobs", "test", "test.canaryPath", "test.canaryTimeoutSeconds", "externalToolchains", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "patchPolicy", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
15
15
|
/**
|
|
16
16
|
* Gets all project paths based on a root directory.
|
|
17
17
|
* @param root - Root directory of the project
|
|
@@ -6,7 +6,7 @@ import { ConfigError } from '../errors/config.js';
|
|
|
6
6
|
import { verbose } from '../utils/logger.js';
|
|
7
7
|
import { parseObject } from '../utils/parse.js';
|
|
8
8
|
import { isContainedRelativePath, isExplicitAbsolutePath } from '../utils/paths.js';
|
|
9
|
-
import { isValidAppId, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
|
|
9
|
+
import { isValidAppId, isValidFirefoxCandidate, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
|
|
10
10
|
import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
|
|
11
11
|
import { parsePatchPolicyBlock } from './config-validate-patch-policy.js';
|
|
12
12
|
import { parseExternalToolchainsBlock, parseTestBlock } from './config-validate-test-toolchains.js';
|
|
@@ -81,10 +81,15 @@ function parseFirefoxBlock(rec) {
|
|
|
81
81
|
if (firefoxSha256 !== undefined && !/^[a-f0-9]{64}$/i.test(firefoxSha256)) {
|
|
82
82
|
throw new ConfigError('Config field "firefox.sha256" must be a 64-character SHA-256 hex digest');
|
|
83
83
|
}
|
|
84
|
+
const firefoxCandidate = optionalConfigString(firefoxRec, 'candidate', 'firefox.candidate');
|
|
85
|
+
if (firefoxCandidate !== undefined && !isValidFirefoxCandidate(firefoxCandidate)) {
|
|
86
|
+
throw new ConfigError('Config field "firefox.candidate" must look like "buildN" (e.g. "build2")');
|
|
87
|
+
}
|
|
84
88
|
return {
|
|
85
89
|
version: firefoxVersion,
|
|
86
90
|
product: firefoxProduct,
|
|
87
91
|
...(firefoxSha256 !== undefined ? { sha256: firefoxSha256.toLowerCase() } : {}),
|
|
92
|
+
...(firefoxCandidate !== undefined ? { candidate: firefoxCandidate } : {}),
|
|
88
93
|
};
|
|
89
94
|
}
|
|
90
95
|
/** Parses the optional `build` block (currently just `build.jobs`). */
|
|
@@ -343,6 +348,10 @@ function parsePatchLintBlock(rec) {
|
|
|
343
348
|
if (chromeScriptJsDoc !== undefined) {
|
|
344
349
|
out.chromeScriptJsDoc = chromeScriptJsDoc;
|
|
345
350
|
}
|
|
351
|
+
const undefinedIdentifiers = parseSeverityGate(rec.raw('undefinedIdentifiers'), 'patchLint.undefinedIdentifiers');
|
|
352
|
+
if (undefinedIdentifiers !== undefined) {
|
|
353
|
+
out.undefinedIdentifiers = undefinedIdentifiers;
|
|
354
|
+
}
|
|
346
355
|
if (out.checkJsStrict === true && out.checkJs !== true) {
|
|
347
356
|
throw new ConfigError('Config field "patchLint.checkJsStrict" requires "patchLint.checkJs": true');
|
|
348
357
|
}
|
|
@@ -408,6 +417,10 @@ function parseTypecheckBlock(rec) {
|
|
|
408
417
|
if (overrides) {
|
|
409
418
|
out.projectOverrides = overrides;
|
|
410
419
|
}
|
|
420
|
+
const undefinedIdentifiers = parseSeverityGate(rec.raw('undefinedIdentifiers'), 'typecheck.undefinedIdentifiers');
|
|
421
|
+
if (undefinedIdentifiers !== undefined) {
|
|
422
|
+
out.undefinedIdentifiers = undefinedIdentifiers;
|
|
423
|
+
}
|
|
411
424
|
return out;
|
|
412
425
|
}
|
|
413
426
|
/**
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch-aware restore planning for `fireforge discard` (FORGE F1, P0).
|
|
3
|
+
*
|
|
4
|
+
* The engine convention is: HEAD = pristine upstream Firefox, and the
|
|
5
|
+
* applied patch queue lives as UNCOMMITTED worktree changes on top of it
|
|
6
|
+
* (`git apply` without `--index` → patch-edited tracked files are unstaged
|
|
7
|
+
* modifications, patch-created files are untracked). The pre-0.39.0
|
|
8
|
+
* `discard` was purely git-mechanical — it restored to HEAD and deleted
|
|
9
|
+
* untracked files, silently reverting patch-backed files PAST their owning
|
|
10
|
+
* patch and deleting patch-created files outright.
|
|
11
|
+
*
|
|
12
|
+
* This module classifies each status entry against the patch manifest's
|
|
13
|
+
* ownership claims and plans the correct restore target: the PATCH-APPLIED
|
|
14
|
+
* baseline (HEAD content folded through every affecting patch in order) for
|
|
15
|
+
* claimed files, and the legacy git mechanics for unmanaged ones.
|
|
16
|
+
*/
|
|
17
|
+
import type { GitStatusEntry } from './git-base.js';
|
|
18
|
+
/** How one status entry's canonical path will be restored. */
|
|
19
|
+
export type DiscardKind = 'unmanaged' | 'patch-backed' | 'patch-created' | 'patch-deleted';
|
|
20
|
+
/** Restore plan for one git status entry. */
|
|
21
|
+
export interface DiscardBaselinePlan {
|
|
22
|
+
entry: GitStatusEntry;
|
|
23
|
+
kind: DiscardKind;
|
|
24
|
+
/** Claiming patch filenames for `entry.file` ∪ `entry.originalPath`. */
|
|
25
|
+
owners: string[];
|
|
26
|
+
/** True when more than one patch claims the path (ownership conflict). */
|
|
27
|
+
conflicted: boolean;
|
|
28
|
+
/** Baseline content for `entry.file`; null = absent at baseline. Unset for 'unmanaged'. */
|
|
29
|
+
expectedContent?: string | null;
|
|
30
|
+
/** Rename/copy only: baseline content for `originalPath` when that side is claimed. */
|
|
31
|
+
expectedOriginalContent?: string | null;
|
|
32
|
+
}
|
|
33
|
+
/** Builds an all-unmanaged plan set — the `--to-upstream` (legacy) shape. */
|
|
34
|
+
export declare function planUpstreamDiscards(entries: readonly GitStatusEntry[]): DiscardBaselinePlan[];
|
|
35
|
+
/**
|
|
36
|
+
* Plans the restore target for each status entry. Entries claimed by the
|
|
37
|
+
* patch manifest restore to the patch-applied baseline; unclaimed entries
|
|
38
|
+
* keep the legacy git mechanics. A missing patches dir or empty manifest
|
|
39
|
+
* degrades every plan to `unmanaged` (identical to pre-0.39.0 behavior).
|
|
40
|
+
*
|
|
41
|
+
* A corrupt manifest or an unreconstructible patch baseline REFUSES with a
|
|
42
|
+
* GeneralError naming `--to-upstream` — silently falling back to pristine
|
|
43
|
+
* HEAD is exactly the data-loss path this module exists to close.
|
|
44
|
+
*/
|
|
45
|
+
export declare function planDiscardBaselines(patchesDir: string, engineDir: string, entries: readonly GitStatusEntry[]): Promise<DiscardBaselinePlan[]>;
|
|
46
|
+
/**
|
|
47
|
+
* Applies one restore plan. Patch-claimed paths end up exactly as `import`
|
|
48
|
+
* leaves them: patched content as an unstaged worktree state (tracked files
|
|
49
|
+
* read ` M`, created files `??`), nothing staged. Unmanaged plans delegate
|
|
50
|
+
* to the legacy {@link discardStatusEntry} mechanics.
|
|
51
|
+
*/
|
|
52
|
+
export declare function applyDiscardBaseline(engineDir: string, plan: DiscardBaselinePlan): Promise<void>;
|
|
53
|
+
/** One-line annotation for dry-run listings and confirmation summaries. */
|
|
54
|
+
export declare function describeDiscardBaseline(plan: DiscardBaselinePlan): string;
|
|
55
|
+
/** Single-file outro naming the baseline that was restored. */
|
|
56
|
+
export declare function describeDiscardOutcome(plan: DiscardBaselinePlan, toUpstream: boolean): string;
|
|
57
|
+
/** Batch outro summarising how many files restored to which baseline. */
|
|
58
|
+
export declare function summarizeDiscardBaselines(plans: readonly DiscardBaselinePlan[], succeeded: number): string;
|
|
59
|
+
/** Warn text for a multi-owner (conflicted) restore. */
|
|
60
|
+
export declare function describeConflictWarning(plan: DiscardBaselinePlan): string;
|