@hominis/fireforge 0.47.0 → 0.48.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 +65 -0
- package/dist/bin/fireforge.js +11 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +156 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +10 -0
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export.js +4 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-harness-teardown.d.ts +62 -0
- package/dist/src/commands/test-harness-teardown.js +128 -0
- package/dist/src/commands/test-register.js +2 -1
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +36 -7
- package/dist/src/commands/test-verdict.d.ts +19 -4
- package/dist/src/commands/test-verdict.js +22 -4
- package/dist/src/commands/test.js +18 -10
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +7 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/file-lock.js +16 -9
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/harness-helper-pids.d.ts +98 -0
- package/dist/src/core/harness-helper-pids.js +240 -0
- package/dist/src/core/harness-orphans.d.ts +53 -8
- package/dist/src/core/harness-orphans.js +125 -44
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach.d.ts +16 -0
- package/dist/src/core/mach.js +6 -1
- package/dist/src/core/marionette-preflight.js +9 -3
- package/dist/src/core/parent-exit-watchdog.d.ts +62 -0
- package/dist/src/core/parent-exit-watchdog.js +69 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +8 -1
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +85 -11
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +26 -1
- package/dist/src/types/config.d.ts +17 -0
- package/dist/src/utils/process-group.d.ts +5 -1
- package/dist/src/utils/process-group.js +7 -3
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +49 -0
- package/dist/src/utils/process.d.ts +20 -0
- package/dist/src/utils/process.js +16 -3
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +88 -1
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +21 -4
- package/docs/machine-output.md +22 -9
- package/docs/patch-workflow.md +35 -1
- package/docs/testing.md +68 -4
- package/package.json +7 -3
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Profile selection for `fireforge run`.
|
|
4
|
+
*
|
|
5
|
+
* `mach run` on desktop has no `--profile` option of its own: its parser's
|
|
6
|
+
* `--profile/-P` belongs to the Android run parser. On desktop a profile
|
|
7
|
+
* reaches Firefox as a program parameter (`-profile <dir>`), and mach skips
|
|
8
|
+
* creating the objdir's `tmp/profile-default` when it sees one among the
|
|
9
|
+
* params.
|
|
10
|
+
* Mach's `--temp-profile` does mint a fresh directory, but inside the objdir
|
|
11
|
+
* and never removes it, so FireForge owns the temporary profile instead:
|
|
12
|
+
* it creates the directory, seeds the two prefs mach would have written,
|
|
13
|
+
* passes it as `-profile`, and deletes it when the run ends.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join, resolve } from 'node:path';
|
|
18
|
+
import { InvalidArgumentError } from '../errors/base.js';
|
|
19
|
+
import { isProcessAlive, toError } from '../utils/errors.js';
|
|
20
|
+
import { verbose, warn } from '../utils/logger.js';
|
|
21
|
+
/** Directory-name prefix of every FireForge-owned temporary run profile. */
|
|
22
|
+
export const RUN_PROFILE_PREFIX = 'fireforge-run-profile-';
|
|
23
|
+
/** Marker file naming the pid of the FireForge process that owns the profile. */
|
|
24
|
+
const OWNER_MARKER = '.fireforge-run-owner';
|
|
25
|
+
/**
|
|
26
|
+
* The prefs `mach run` writes into the profiles it creates itself. A profile
|
|
27
|
+
* passed with `-profile` gets none of them, and a first-run default-browser
|
|
28
|
+
* prompt would sit in a headed smoke window.
|
|
29
|
+
*/
|
|
30
|
+
const SEEDED_USER_JS = 'user_pref("browser.shell.checkDefaultBrowser", false);\n' +
|
|
31
|
+
'user_pref("browser.aboutConfig.showWarning", false);\n';
|
|
32
|
+
/**
|
|
33
|
+
* Temporary profiles this process created and has not yet removed. The
|
|
34
|
+
* command's own `finally` loses the race to the entry point's signal
|
|
35
|
+
* handler, whose `process.exit` never lets it run, so the handler removes
|
|
36
|
+
* these itself once the browser tree has shut down.
|
|
37
|
+
*/
|
|
38
|
+
const activeTemporaryProfiles = new Set();
|
|
39
|
+
async function removeTemporaryProfile(dir) {
|
|
40
|
+
try {
|
|
41
|
+
await rm(dir, { recursive: true, force: true });
|
|
42
|
+
activeTemporaryProfiles.delete(dir);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
warn(`Could not remove temporary run profile ${dir}: ${toError(error).message}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Removes every temporary run profile this process still holds. Called by
|
|
50
|
+
* the CLI entry point's signal handler after child shutdown. Never throws.
|
|
51
|
+
*/
|
|
52
|
+
export async function removeActiveRunProfiles() {
|
|
53
|
+
await Promise.all([...activeTemporaryProfiles].map((dir) => removeTemporaryProfile(dir)));
|
|
54
|
+
}
|
|
55
|
+
const NO_PROFILE = {
|
|
56
|
+
args: [],
|
|
57
|
+
temporary: false,
|
|
58
|
+
dispose: () => Promise.resolve(),
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Chooses the profile for a run.
|
|
62
|
+
* @param request - The run's profile flags
|
|
63
|
+
* @returns The profile arguments and their release handle
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveRunProfile(request) {
|
|
66
|
+
if (request.profile !== undefined && request.tempProfile === true) {
|
|
67
|
+
throw new InvalidArgumentError('--profile and --temp-profile are mutually exclusive: name a profile or ask for a fresh one.', 'profile');
|
|
68
|
+
}
|
|
69
|
+
if (request.profile !== undefined) {
|
|
70
|
+
const dir = resolve(request.profile);
|
|
71
|
+
return { args: ['-profile', dir], dir, temporary: false, dispose: NO_PROFILE.dispose };
|
|
72
|
+
}
|
|
73
|
+
if (request.tempProfile !== true && !request.smoke) {
|
|
74
|
+
return NO_PROFILE;
|
|
75
|
+
}
|
|
76
|
+
const dir = await mkdtemp(join(request.tempRoot ?? tmpdir(), RUN_PROFILE_PREFIX));
|
|
77
|
+
activeTemporaryProfiles.add(dir);
|
|
78
|
+
// The owner marker goes in first, so a FireForge killed past this point
|
|
79
|
+
// leaves a directory the next run can prove abandoned.
|
|
80
|
+
await writeFile(join(dir, OWNER_MARKER), `${process.pid}\n`);
|
|
81
|
+
await writeFile(join(dir, 'user.js'), SEEDED_USER_JS);
|
|
82
|
+
verbose(`Temporary run profile: ${dir}`);
|
|
83
|
+
return {
|
|
84
|
+
args: ['-profile', dir],
|
|
85
|
+
dir,
|
|
86
|
+
temporary: true,
|
|
87
|
+
dispose: () => removeTemporaryProfile(dir),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Removes temporary run profiles whose owning FireForge process is gone.
|
|
92
|
+
* The `finally` that releases a profile covers a clean exit, a failed run
|
|
93
|
+
* and a forwarded SIGINT/SIGTERM, but not a SIGKILL of FireForge itself;
|
|
94
|
+
* this sweep is the backstop for that case. A directory whose marker is
|
|
95
|
+
* missing or unreadable is left alone: nothing proves it is abandoned.
|
|
96
|
+
* @param tempRoot - Base directory to sweep (defaults to the OS tmpdir)
|
|
97
|
+
* @returns The number of directories removed
|
|
98
|
+
*/
|
|
99
|
+
export async function sweepAbandonedRunProfiles(tempRoot = tmpdir()) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = await readdir(tempRoot);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
let removed = 0;
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
if (!entry.startsWith(RUN_PROFILE_PREFIX))
|
|
110
|
+
continue;
|
|
111
|
+
const dir = join(tempRoot, entry);
|
|
112
|
+
let pid;
|
|
113
|
+
try {
|
|
114
|
+
pid = Number.parseInt((await readFile(join(dir, OWNER_MARKER), 'utf8')).trim(), 10);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid || isProcessAlive(pid)) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
await rm(dir, { recursive: true, force: true });
|
|
124
|
+
removed += 1;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
verbose(`Could not remove abandoned run profile ${dir}: ${toError(error).message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return removed;
|
|
131
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { PatchedContentContext } from './patch-apply.js';
|
|
28
28
|
/** Batched patch lookup, injected by callers that classify many files. */
|
|
29
|
-
export type AffectingPatchLookup = Pick<PatchedContentContext, 'getAffectingPatches' | 'readPatchBody'
|
|
29
|
+
export type AffectingPatchLookup = Pick<PatchedContentContext, 'getAffectingPatches' | 'readPatchBody'> & Partial<Pick<PatchedContentContext, 'liveBlobHash'>>;
|
|
30
30
|
/**
|
|
31
31
|
* Classification result for a binary comparison. Structurally assignable
|
|
32
32
|
* to `ClassifiedFile` without importing it (which would create a cycle
|
|
@@ -111,7 +111,9 @@ export async function classifyBinaryOwnedFile(args) {
|
|
|
111
111
|
}
|
|
112
112
|
if (section.hasBinaryDelta && section.indexNewHash !== undefined) {
|
|
113
113
|
const fullPath = join(engineDir, entry.file);
|
|
114
|
-
const live =
|
|
114
|
+
const live = args.lookup?.liveBlobHash !== undefined
|
|
115
|
+
? await args.lookup.liveBlobHash(fullPath)
|
|
116
|
+
: (await hashObjectBatch(engineDir, [fullPath])).get(fullPath);
|
|
115
117
|
if (live !== undefined) {
|
|
116
118
|
// Prefix-compare: recorded hashes may be abbreviated (≥7 chars).
|
|
117
119
|
const matches = live.startsWith(section.indexNewHash) || section.indexNewHash.startsWith(live);
|
|
@@ -13,6 +13,7 @@ import { readText } from '../utils/fs.js';
|
|
|
13
13
|
import { verbose } from '../utils/logger.js';
|
|
14
14
|
import { normalizePathSlashes } from '../utils/paths.js';
|
|
15
15
|
import { isBrandingManagedPath } from './branding.js';
|
|
16
|
+
import { isBinaryFile } from './git-file-ops.js';
|
|
16
17
|
import { createPatchedContentContext } from './patch-apply.js';
|
|
17
18
|
import { classifyBinaryOwnedFile } from './status-binary.js';
|
|
18
19
|
/**
|
|
@@ -141,6 +142,7 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
|
|
|
141
142
|
// expected-vs-actual content comparison then routes them into `unmanaged`
|
|
142
143
|
// when the content matches neither owner's expectation.
|
|
143
144
|
const patchClaims = buildPatchClaims(ctx.manifestPatches);
|
|
145
|
+
await prefetchSingleOwnerContent(files, engineDir, patchClaims, ctx);
|
|
144
146
|
const deps = {
|
|
145
147
|
engineDir,
|
|
146
148
|
patchesDir,
|
|
@@ -149,12 +151,37 @@ export async function classifyFiles(files, engineDir, patchesDir, binaryName, fu
|
|
|
149
151
|
patchClaims,
|
|
150
152
|
ctx,
|
|
151
153
|
};
|
|
152
|
-
// Bounded pool over per-file classification
|
|
153
|
-
// spawns git). Order preserved by the mapper. Per-file failures settle
|
|
154
|
+
// Bounded pool over per-file classification. Order preserved by the mapper. Per-file failures settle
|
|
154
155
|
// inside classifySingleOwnerFile's catch, so one bad file never rejects
|
|
155
156
|
// the batch.
|
|
156
157
|
return mapWithConcurrency(files, CLASSIFY_CONCURRENCY, (entry) => classifyEntry(entry, deps));
|
|
157
158
|
}
|
|
159
|
+
/**
|
|
160
|
+
* Fetches, in two git processes, what the per-file comparison would
|
|
161
|
+
* otherwise spawn git for file by file: the HEAD content of every
|
|
162
|
+
* single-owner path (one `cat-file --batch`) and the live blob hash of every
|
|
163
|
+
* one that is binary on disk (one `hash-object` per ARG_MAX chunk). On a
|
|
164
|
+
* 1,600-file Firefox scan that was ~1,550 `git show` and `hash-object`
|
|
165
|
+
* spawns. Fetching for a path the classifier ends up routing elsewhere
|
|
166
|
+
* (a branding path, say) costs a few bytes, never a wrong answer.
|
|
167
|
+
*/
|
|
168
|
+
async function prefetchSingleOwnerContent(files, engineDir, patchClaims, ctx) {
|
|
169
|
+
const singleOwner = files.filter((entry) => patchClaims.get(entry.file)?.length === 1);
|
|
170
|
+
if (singleOwner.length === 0)
|
|
171
|
+
return;
|
|
172
|
+
await ctx.prefetchBase(singleOwner.map((entry) => entry.file));
|
|
173
|
+
const present = singleOwner.filter((entry) => getPrimaryStatusCode(entry.status) !== 'D');
|
|
174
|
+
const binary = await mapWithConcurrency(present, CLASSIFY_CONCURRENCY, async (entry) => {
|
|
175
|
+
try {
|
|
176
|
+
return (await isBinaryFile(engineDir, entry.file)) ? join(engineDir, entry.file) : undefined;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// The per-file path probes again and settles the failure itself.
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
await ctx.prefetchLiveBlobHashes(binary.filter((path) => path !== undefined));
|
|
184
|
+
}
|
|
158
185
|
/** Concurrency bound for per-file classification (matches import's guard). */
|
|
159
186
|
const CLASSIFY_CONCURRENCY = 8;
|
|
160
187
|
/** Classifies one status entry. Extracted so the pool worker stays flat. */
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
*/
|
|
28
28
|
/** Tools the preflight knows how to probe. */
|
|
29
29
|
export type ToolchainTool = 'cbindgen' | 'rustc';
|
|
30
|
+
/**
|
|
31
|
+
* Resolves the mach state directory the same way mach itself does:
|
|
32
|
+
* `$MOZBUILD_STATE_PATH`, else `~/.mozbuild` (same resolution as
|
|
33
|
+
* `mach-resource-shim.ts`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function mozbuildStateDir(): string;
|
|
30
36
|
/** One probed candidate binary for a tool. */
|
|
31
37
|
export interface ToolchainCandidate {
|
|
32
38
|
/** Binary path (or bare name for the PATH candidate). */
|
|
@@ -37,7 +37,7 @@ import { verbose } from '../utils/logger.js';
|
|
|
37
37
|
* `$MOZBUILD_STATE_PATH`, else `~/.mozbuild` (same resolution as
|
|
38
38
|
* `mach-resource-shim.ts`).
|
|
39
39
|
*/
|
|
40
|
-
function mozbuildStateDir() {
|
|
40
|
+
export function mozbuildStateDir() {
|
|
41
41
|
return process.env['MOZBUILD_STATE_PATH'] ?? join(homedir(), '.mozbuild');
|
|
42
42
|
}
|
|
43
43
|
/** Where each tool's minimum is declared inside the Firefox tree. */
|
|
@@ -26,6 +26,16 @@ import type { TypecheckProjectResult } from '../types/typecheck.js';
|
|
|
26
26
|
* in CI logs.
|
|
27
27
|
*/
|
|
28
28
|
export declare const CHECK_JS_DISABLED_NOTICE = "Project sets \"checkJs: false\" \u2014 skipping (override the jsconfig to enable typecheck).";
|
|
29
|
+
/** Run-level options for {@link runTypecheck}. */
|
|
30
|
+
export interface RunTypecheckOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Directory for the per-project incremental build info. Defaults to
|
|
33
|
+
* `<projectRoot>/.fireforge/typecheck`.
|
|
34
|
+
*/
|
|
35
|
+
cacheDir?: string;
|
|
36
|
+
/** Build every project from scratch and write no build info. */
|
|
37
|
+
noCache?: boolean;
|
|
38
|
+
}
|
|
29
39
|
/**
|
|
30
40
|
* Runs `fireforge typecheck` against every project listed in `cfg.projects`.
|
|
31
41
|
*
|
|
@@ -55,7 +65,7 @@ export declare const CHECK_JS_DISABLED_NOTICE = "Project sets \"checkJs: false\"
|
|
|
55
65
|
* in declared order. The CLI is responsible for printing the issues
|
|
56
66
|
* and computing the exit code.
|
|
57
67
|
*/
|
|
58
|
-
export declare function runTypecheck(projectRoot: string, cfg: TypecheckConfig): Promise<TypecheckProjectResult[]>;
|
|
68
|
+
export declare function runTypecheck(projectRoot: string, cfg: TypecheckConfig, runOptions?: RunTypecheckOptions): Promise<TypecheckProjectResult[]>;
|
|
59
69
|
/**
|
|
60
70
|
* Converts an absolute path to a path relative to the project root,
|
|
61
71
|
* for display in CLI output. Falls back to the absolute path when
|
|
@@ -18,10 +18,13 @@
|
|
|
18
18
|
* without installing it, the function returns a clear error pointing
|
|
19
19
|
* at `npm install typescript`.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
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';
|
|
26
29
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
27
30
|
/**
|
|
@@ -60,7 +63,7 @@ export const CHECK_JS_DISABLED_NOTICE = 'Project sets "checkJs: false" — skipp
|
|
|
60
63
|
* in declared order. The CLI is responsible for printing the issues
|
|
61
64
|
* and computing the exit code.
|
|
62
65
|
*/
|
|
63
|
-
export async function runTypecheck(projectRoot, cfg) {
|
|
66
|
+
export async function runTypecheck(projectRoot, cfg, runOptions = {}) {
|
|
64
67
|
// Dynamic import, so typescript stays a dev dependency. Same pattern
|
|
65
68
|
// as `patch-lint-checkjs.ts`. The empty-projects-array case is
|
|
66
69
|
// already rejected by config-validate, so we don't gate the import
|
|
@@ -133,7 +136,9 @@ export async function runTypecheck(projectRoot, cfg) {
|
|
|
133
136
|
});
|
|
134
137
|
continue;
|
|
135
138
|
}
|
|
136
|
-
results.push(await runTypecheckForProject(ts, projectRoot, projectPath, shimSource, cfg.undefinedIdentifiers ?? 'warning'
|
|
139
|
+
results.push(await runTypecheckForProject(ts, projectRoot, projectPath, shimSource, cfg.undefinedIdentifiers ?? 'warning', runOptions.noCache === true
|
|
140
|
+
? undefined
|
|
141
|
+
: (runOptions.cacheDir ?? join(projectRoot, '.fireforge', 'typecheck'))));
|
|
137
142
|
}
|
|
138
143
|
return results;
|
|
139
144
|
}
|
|
@@ -151,7 +156,7 @@ function resolveProjectExtraShim(cfg, projectPath) {
|
|
|
151
156
|
return cfg.extraShim;
|
|
152
157
|
}
|
|
153
158
|
/** Runs typecheck for a single jsconfig path, isolating its failures. */
|
|
154
|
-
async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource, undefinedIdentifiers = 'warning') {
|
|
159
|
+
async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource, undefinedIdentifiers = 'warning', cacheDir) {
|
|
155
160
|
const absConfig = resolve(projectRoot, projectPath);
|
|
156
161
|
if (!(await pathExists(absConfig))) {
|
|
157
162
|
return {
|
|
@@ -223,12 +228,12 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
|
|
|
223
228
|
noEmit: true,
|
|
224
229
|
allowJs: parsed.options.allowJs ?? true,
|
|
225
230
|
checkJs: parsed.options.checkJs ?? true,
|
|
226
|
-
//
|
|
227
|
-
// `incremental` (or names a `tsBuildInfoFile`) would
|
|
228
|
-
// drop a `.tsbuildinfo` inside the primary engine
|
|
229
|
-
// writer that invalidates a concurrent
|
|
230
|
-
//
|
|
231
|
-
//
|
|
231
|
+
// Never the jsconfig's own incremental sidecar: a user jsconfig under
|
|
232
|
+
// `engine/` that sets `incremental` (or names a `tsBuildInfoFile`) would
|
|
233
|
+
// have this command drop a `.tsbuildinfo` inside the primary engine
|
|
234
|
+
// checkout, a second writer that invalidates a concurrent
|
|
235
|
+
// `fireforge test`'s engine fingerprint. FireForge's own build info
|
|
236
|
+
// lives under `.fireforge/typecheck/` instead (see below).
|
|
232
237
|
incremental: false,
|
|
233
238
|
// skipLibCheck is not forced. The user owns it via their jsconfig.
|
|
234
239
|
};
|
|
@@ -267,7 +272,38 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
|
|
|
267
272
|
return defaultHost.readFile(fileName);
|
|
268
273
|
},
|
|
269
274
|
};
|
|
270
|
-
|
|
275
|
+
// Incremental build info, keyed by everything outside the program's own
|
|
276
|
+
// files that can change a verdict: the composed shim, the TypeScript
|
|
277
|
+
// version, the compiler options, the undefined-identifier gate and the
|
|
278
|
+
// FireForge version. A change to any of them selects a different file,
|
|
279
|
+
// so a stale verdict is never replayed. Files inside the program are
|
|
280
|
+
// tracked by TypeScript's own per-file hashes in the build info.
|
|
281
|
+
const buildInfoFile = cacheDir === undefined
|
|
282
|
+
? undefined
|
|
283
|
+
: await prepareBuildInfoFile(cacheDir, projectPath, {
|
|
284
|
+
shim: sha256Hex(shimSource),
|
|
285
|
+
typescript: ts.version,
|
|
286
|
+
fireforge: getPackageVersion(),
|
|
287
|
+
undefinedIdentifiers,
|
|
288
|
+
options,
|
|
289
|
+
});
|
|
290
|
+
let program;
|
|
291
|
+
let writeBuildInfo = () => undefined;
|
|
292
|
+
if (buildInfoFile === undefined) {
|
|
293
|
+
program = ts.createProgram(rootFiles, options, host);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
const builder = ts.createIncrementalProgram({
|
|
297
|
+
rootNames: rootFiles,
|
|
298
|
+
options: { ...options, incremental: true, tsBuildInfoFile: buildInfoFile },
|
|
299
|
+
host: withSourceFileVersions(host),
|
|
300
|
+
});
|
|
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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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.
|
|
@@ -497,6 +499,13 @@ export interface RebaseOptions {
|
|
|
497
499
|
export interface RunOptions {
|
|
498
500
|
/** Additional arguments to pass to the browser */
|
|
499
501
|
args?: string[];
|
|
502
|
+
/** Launch with this profile directory, passed to Firefox as `-profile`. */
|
|
503
|
+
profile?: string;
|
|
504
|
+
/**
|
|
505
|
+
* Launch with a fresh temporary profile, removed when the run ends.
|
|
506
|
+
* Implied by `smokeExit` unless `profile` is given.
|
|
507
|
+
*/
|
|
508
|
+
tempProfile?: boolean;
|
|
500
509
|
/**
|
|
501
510
|
* Enable smoke-run mode. Launches the browser, streams the console,
|
|
502
511
|
* sends SIGTERM to the whole process group after `smokeExit` seconds,
|
|
@@ -612,9 +621,19 @@ export interface TestOptions extends EngineLockWaitable {
|
|
|
612
621
|
* project's objdir. The census itself runs on every test dispatch and is
|
|
613
622
|
* report-only. This flag opts into the kill. Survivors slow later runs
|
|
614
623
|
* without appearing in their output, so the visibility is the default and
|
|
615
|
-
* the termination is the choice.
|
|
624
|
+
* the termination is the choice. `test.reapOrphans: "reap"` in
|
|
625
|
+
* `fireforge.json` makes it the repo's default.
|
|
616
626
|
*/
|
|
617
627
|
reapOrphans?: boolean;
|
|
628
|
+
/**
|
|
629
|
+
* Path to write the harness process-group id to, rewritten on every mach
|
|
630
|
+
* spawn (each retry attempt and shard) and removed when the run ends under
|
|
631
|
+
* FireForge's control. mach runs as its own process-group leader, so a
|
|
632
|
+
* supervisor's kill of its own group never reaches it; a supervisor that
|
|
633
|
+
* had to SIGKILL FireForge takes the whole harness tree afterwards with
|
|
634
|
+
* `kill -- -<pgid>`. The file survives exactly that case, by design.
|
|
635
|
+
*/
|
|
636
|
+
pgidFile?: string;
|
|
618
637
|
/** Permit tests against packageable engine edits newer than the last successful build. */
|
|
619
638
|
allowStaleBuild?: boolean;
|
|
620
639
|
/**
|
|
@@ -1105,4 +1124,10 @@ export interface LintCommandOptions {
|
|
|
1105
1124
|
* {@link perPatch}.
|
|
1106
1125
|
*/
|
|
1107
1126
|
report?: string;
|
|
1127
|
+
/**
|
|
1128
|
+
* With `--per-patch`: `'full'` (default) prints every NOTICE line;
|
|
1129
|
+
* `'summary'` prints one line per check with its count and patch count.
|
|
1130
|
+
* Errors and warnings always print in full.
|
|
1131
|
+
*/
|
|
1132
|
+
notices?: 'summary' | 'full';
|
|
1108
1133
|
}
|
|
@@ -47,6 +47,13 @@ export interface TestConfig {
|
|
|
47
47
|
canaryPath?: string;
|
|
48
48
|
/** Short no-output ceiling for `fireforge test --canary`, in seconds. */
|
|
49
49
|
canaryTimeoutSeconds?: number;
|
|
50
|
+
/**
|
|
51
|
+
* What the preflight orphan census does with a harness helper that
|
|
52
|
+
* survived an earlier run in this objdir: `report` (default) names it and
|
|
53
|
+
* proceeds; `reap` terminates it and stamps `orphans-reaped=<n>` on the
|
|
54
|
+
* verdict line. `--reap-orphans` is the per-invocation equivalent.
|
|
55
|
+
*/
|
|
56
|
+
reapOrphans?: 'report' | 'reap';
|
|
50
57
|
}
|
|
51
58
|
/** A single external executable required by a project-specific toolchain. */
|
|
52
59
|
export interface ExternalToolRequirement {
|
|
@@ -348,6 +355,16 @@ export interface PatchLintConfig {
|
|
|
348
355
|
* scope for the per-patch tier.
|
|
349
356
|
*/
|
|
350
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';
|
|
351
368
|
/** Enforce JSDoc on class-method exports in patch-owned .sys.mjs files. Default: 'off'. */
|
|
352
369
|
jsdocClassMethods?: PatchLintSeverityGate;
|
|
353
370
|
/** Require ≥1 assertion in any patch-touched browser_*.js test file (new or modified). Default: 'off'. */
|
|
@@ -32,8 +32,12 @@ interface ProcessGroupSurvivor {
|
|
|
32
32
|
* about anything that still refuses to die. POSIX only (no-op on win32).
|
|
33
33
|
* The only kill target is `-pgid`, never anything outside the group.
|
|
34
34
|
* A healthy run costs exactly one `pgrep`.
|
|
35
|
+
*
|
|
36
|
+
* `context` names why the group is being reaped in the warning; the default
|
|
37
|
+
* is the post-close sweep. The parent-exit watchdog passes its own, because
|
|
38
|
+
* there the group is still running when the sweep begins.
|
|
35
39
|
*/
|
|
36
|
-
export declare function sweepProcessGroup(pgid: number, graceMs?: number): Promise<{
|
|
40
|
+
export declare function sweepProcessGroup(pgid: number, graceMs?: number, context?: string): Promise<{
|
|
37
41
|
survivors: ProcessGroupSurvivor[];
|
|
38
42
|
}>;
|
|
39
43
|
export {};
|
|
@@ -130,15 +130,19 @@ function describeSurvivors(list) {
|
|
|
130
130
|
* about anything that still refuses to die. POSIX only (no-op on win32).
|
|
131
131
|
* The only kill target is `-pgid`, never anything outside the group.
|
|
132
132
|
* A healthy run costs exactly one `pgrep`.
|
|
133
|
+
*
|
|
134
|
+
* `context` names why the group is being reaped in the warning; the default
|
|
135
|
+
* is the post-close sweep. The parent-exit watchdog passes its own, because
|
|
136
|
+
* there the group is still running when the sweep begins.
|
|
133
137
|
*/
|
|
134
|
-
export async function sweepProcessGroup(pgid, graceMs = SWEEP_GRACE_MS) {
|
|
138
|
+
export async function sweepProcessGroup(pgid, graceMs = SWEEP_GRACE_MS, context = 'after exit') {
|
|
135
139
|
if (process.platform === 'win32')
|
|
136
140
|
return { survivors: [] };
|
|
137
141
|
const survivors = await listGroupSurvivors(pgid);
|
|
138
142
|
if (survivors.length === 0)
|
|
139
143
|
return { survivors };
|
|
140
|
-
warn(`Harness process group ${pgid}
|
|
141
|
-
`process(es)
|
|
144
|
+
warn(`Harness process group ${pgid} has ${survivors.length} live ` +
|
|
145
|
+
`process(es) ${context} — reaping the group. ${describeSurvivors(survivors)}`);
|
|
142
146
|
try {
|
|
143
147
|
process.kill(-pgid, 'SIGTERM');
|
|
144
148
|
}
|
|
@@ -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>;
|