@hominis/fireforge 0.47.1 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +158 -0
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +4 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -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 +14 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- 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-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +8 -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-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -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 +7 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -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/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +151 -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 +10 -2
- 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/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- 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/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +68 -121
- package/dist/src/core/token-inventory.js +12 -7
- 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 +86 -12
- 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 +29 -3
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +46 -0
- package/dist/src/utils/process.d.ts +5 -2
- package/dist/src/utils/process.js +21 -8
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +82 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +13 -8
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +50 -1
- package/docs/testing.md +77 -12
- package/package.json +10 -5
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { lstat, readdir, readFile, readlink, realpath, rename, rm, symlink, } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
4
|
+
import { GeneralError } from '../errors/base.js';
|
|
5
|
+
import { getNodeErrorCode } from '../utils/errors.js';
|
|
6
|
+
import { writeText } from '../utils/fs.js';
|
|
7
|
+
/** Visits objdir entries without following directory symlinks into another tree. */
|
|
8
|
+
async function walk(dir, visit) {
|
|
9
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
10
|
+
const path = join(dir, entry.name);
|
|
11
|
+
if (entry.isDirectory())
|
|
12
|
+
await walk(path, visit);
|
|
13
|
+
else
|
|
14
|
+
await visit(path, entry.isSymbolicLink());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Refuses an objdir that writes through a directory alias into a different checkout. */
|
|
18
|
+
export async function assertLocalObjdir(engineDir, objDir) {
|
|
19
|
+
if (!/^[^/\\:]+$/.test(objDir) || objDir === '.' || objDir === '..') {
|
|
20
|
+
throw new GeneralError(`Objdir ${objDir} must be a local directory name.`);
|
|
21
|
+
}
|
|
22
|
+
const expected = resolve(engineDir, objDir);
|
|
23
|
+
if ((await realpath(expected)) !== resolve(await realpath(engineDir), objDir) ||
|
|
24
|
+
!(await lstat(expected)).isDirectory()) {
|
|
25
|
+
throw new GeneralError(`Objdir ${expected} resolves outside its local directory. Refusing to configure or rewrite it.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function replacePrefix(value, oldRoot, newRoot) {
|
|
29
|
+
return value === oldRoot || value.startsWith(oldRoot + sep)
|
|
30
|
+
? newRoot + value.slice(oldRoot.length)
|
|
31
|
+
: value;
|
|
32
|
+
}
|
|
33
|
+
function isDependencyMetadata(path) {
|
|
34
|
+
return (/\.(?:pp|d)$/.test(path) ||
|
|
35
|
+
/[/\\]faster[/\\]/.test(path) ||
|
|
36
|
+
/[/\\]config\.statusd[/\\].*[/\\]config\.track$/.test(path) ||
|
|
37
|
+
/[/\\]\.fingerprint[/\\].*\.json$/.test(path));
|
|
38
|
+
}
|
|
39
|
+
/** Repairs copied links and dependency metadata BEFORE configure can consume config.track. */
|
|
40
|
+
export async function relocateObjdirProducts(engineDir, objDir, oldRoot) {
|
|
41
|
+
await assertLocalObjdir(engineDir, objDir);
|
|
42
|
+
const root = resolve(engineDir, objDir);
|
|
43
|
+
const newRoot = resolve(engineDir);
|
|
44
|
+
const canonicalRoot = await realpath(root);
|
|
45
|
+
// Configure writes these groups. Even a local objdir can contain a copied
|
|
46
|
+
// directory alias into a peer, so validate each parent before touching it.
|
|
47
|
+
for (const directory of ['config.statusd', 'config.statusd/substs', 'config.statusd/defines']) {
|
|
48
|
+
const path = join(root, directory);
|
|
49
|
+
try {
|
|
50
|
+
if (!(await lstat(path)).isDirectory() ||
|
|
51
|
+
(await realpath(path)) !== join(canonicalRoot, directory))
|
|
52
|
+
throw new GeneralError(`Partial-config directory ${path} is aliased. Refusing relocation before configure.`);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (getNodeErrorCode(error) !== 'ENOENT')
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
await walk(root, async (path, link) => {
|
|
60
|
+
// Remove tracking symlinks too, without following their write targets.
|
|
61
|
+
if (/[/\\]config\.statusd[/\\].*[/\\]config\.track$/.test(path)) {
|
|
62
|
+
await rm(path);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (link) {
|
|
66
|
+
const target = await readlink(path);
|
|
67
|
+
const absolute = resolve(dirname(path), target);
|
|
68
|
+
const relocated = replacePrefix(absolute, oldRoot, newRoot);
|
|
69
|
+
if (relocated !== absolute) {
|
|
70
|
+
const temp = `${path}.relocate-${process.pid}`;
|
|
71
|
+
await symlink(relocated, temp);
|
|
72
|
+
try {
|
|
73
|
+
await rename(temp, path);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
await rm(temp, { force: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else if (isDependencyMetadata(path)) {
|
|
81
|
+
const content = await readFile(path, 'utf8');
|
|
82
|
+
const rewritten = content.replaceAll(oldRoot + sep, newRoot + sep);
|
|
83
|
+
if (content !== rewritten)
|
|
84
|
+
await writeText(path, rewritten);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Finds a link still consulting the previous source tree after relocation/configure. */
|
|
89
|
+
export async function findStaleObjdirLink(engineDir, objDir, oldRoot) {
|
|
90
|
+
let violation;
|
|
91
|
+
await walk(resolve(engineDir, objDir), async (path, link) => {
|
|
92
|
+
if (!link || violation)
|
|
93
|
+
return;
|
|
94
|
+
const target = resolve(dirname(path), await readlink(path));
|
|
95
|
+
const canonical = await realpath(path).catch(() => target);
|
|
96
|
+
if ([target, canonical].some((value) => value === oldRoot || value.startsWith(oldRoot + sep))) {
|
|
97
|
+
violation = `${path} still links to the previous engine ${oldRoot}`;
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return violation;
|
|
101
|
+
}
|
|
102
|
+
/** Prunes only dangling _tests symlinks; never descends through a symlink. */
|
|
103
|
+
export async function pruneDanglingTestLinks(engineDir, objDir) {
|
|
104
|
+
let removed = 0;
|
|
105
|
+
const root = join(engineDir, objDir, '_tests');
|
|
106
|
+
try {
|
|
107
|
+
if (!(await lstat(root)).isDirectory())
|
|
108
|
+
return 0;
|
|
109
|
+
await assertLocalObjdir(engineDir, objDir);
|
|
110
|
+
await walk(root, async (path, link) => {
|
|
111
|
+
if (!link)
|
|
112
|
+
return;
|
|
113
|
+
try {
|
|
114
|
+
await realpath(path);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (getNodeErrorCode(error) !== 'ENOENT')
|
|
118
|
+
throw error;
|
|
119
|
+
await rm(path);
|
|
120
|
+
removed++;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (getNodeErrorCode(error) !== 'ENOENT')
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
return removed;
|
|
129
|
+
}
|
|
130
|
+
/** Names a partial-config group whose entire persisted value set was nulled. */
|
|
131
|
+
export async function assertHealthyPartialConfig(engineDir, objDir) {
|
|
132
|
+
for (const group of ['substs', 'defines']) {
|
|
133
|
+
const dir = join(engineDir, objDir, 'config.statusd', group);
|
|
134
|
+
let entries;
|
|
135
|
+
try {
|
|
136
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
140
|
+
continue;
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
const files = entries.filter((entry) => entry.isFile() && entry.name !== 'config.track');
|
|
144
|
+
if (files.length === 0)
|
|
145
|
+
continue;
|
|
146
|
+
const values = await Promise.all(files.map((entry) => readFile(join(dir, entry.name), 'utf8')));
|
|
147
|
+
if (values.every((value) => value.trim() === 'null')) {
|
|
148
|
+
throw new GeneralError(`All ${files.length} partial-config ${group} values in ${dir} are null. Repair with "cd engine && python3 ./mach configure", then retry the full build.`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -26,6 +26,18 @@ export interface FuzzyApplyResult {
|
|
|
26
26
|
/** List of `.rej` files created (when falling through to --reject). */
|
|
27
27
|
rejectFiles?: string[];
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* The context-reduction ladder shared by the real apply and the dry-run
|
|
31
|
+
* replay: step 0 is an exact apply, step k passes `-C(3-k)`.
|
|
32
|
+
* @param maxFuzz - Maximum context-reduction steps (validated by callers)
|
|
33
|
+
* @returns One `git apply` argument list per step, in order
|
|
34
|
+
*/
|
|
35
|
+
export declare function contextReductionSteps(maxFuzz: number): string[][];
|
|
36
|
+
/**
|
|
37
|
+
* Validates a `maxFuzz` value.
|
|
38
|
+
* @throws InvalidArgumentError when `maxFuzz` is not a non-negative integer
|
|
39
|
+
*/
|
|
40
|
+
export declare function assertValidMaxFuzz(maxFuzz: number): void;
|
|
29
41
|
/**
|
|
30
42
|
* Attempts to apply a patch with escalating context reduction.
|
|
31
43
|
*
|
|
@@ -23,6 +23,29 @@ import { exec } from '../utils/process.js';
|
|
|
23
23
|
import { ensureGit } from './git-base.js';
|
|
24
24
|
/** Git's default diff context width, the ceiling for reduction steps. */
|
|
25
25
|
const GIT_DEFAULT_CONTEXT = 3;
|
|
26
|
+
/**
|
|
27
|
+
* The context-reduction ladder shared by the real apply and the dry-run
|
|
28
|
+
* replay: step 0 is an exact apply, step k passes `-C(3-k)`.
|
|
29
|
+
* @param maxFuzz - Maximum context-reduction steps (validated by callers)
|
|
30
|
+
* @returns One `git apply` argument list per step, in order
|
|
31
|
+
*/
|
|
32
|
+
export function contextReductionSteps(maxFuzz) {
|
|
33
|
+
const maxSteps = Math.min(maxFuzz, GIT_DEFAULT_CONTEXT);
|
|
34
|
+
const steps = [];
|
|
35
|
+
for (let step = 0; step <= maxSteps; step++) {
|
|
36
|
+
steps.push(step > 0 ? [`-C${GIT_DEFAULT_CONTEXT - step}`] : []);
|
|
37
|
+
}
|
|
38
|
+
return steps;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Validates a `maxFuzz` value.
|
|
42
|
+
* @throws InvalidArgumentError when `maxFuzz` is not a non-negative integer
|
|
43
|
+
*/
|
|
44
|
+
export function assertValidMaxFuzz(maxFuzz) {
|
|
45
|
+
if (!Number.isInteger(maxFuzz) || maxFuzz < 0) {
|
|
46
|
+
throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${maxFuzz}.`, 'maxFuzz');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
26
49
|
// ── Implementation ──
|
|
27
50
|
/**
|
|
28
51
|
* Attempts to apply a patch with escalating context reduction.
|
|
@@ -41,14 +64,12 @@ const GIT_DEFAULT_CONTEXT = 3;
|
|
|
41
64
|
* (including the exact-match one) and fall straight through to `--reject`
|
|
42
65
|
*/
|
|
43
66
|
export async function applyPatchWithFuzz(patchPath, engineDir, maxFuzz = 3) {
|
|
44
|
-
|
|
45
|
-
throw new InvalidArgumentError(`maxFuzz must be a non-negative integer, got ${maxFuzz}.`, 'maxFuzz');
|
|
46
|
-
}
|
|
67
|
+
assertValidMaxFuzz(maxFuzz);
|
|
47
68
|
await ensureGit();
|
|
48
69
|
// Try exact match first, then escalate context reduction.
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
const ladder = contextReductionSteps(maxFuzz);
|
|
71
|
+
const maxSteps = ladder.length - 1;
|
|
72
|
+
for (const [step, contextArgs] of ladder.entries()) {
|
|
52
73
|
const check = await exec('git', ['apply', '--check', ...contextArgs, '--', patchPath], {
|
|
53
74
|
cwd: engineDir,
|
|
54
75
|
});
|
|
@@ -63,6 +63,19 @@ export interface PatchedContentContext {
|
|
|
63
63
|
getAffectingPatches: (filePath: string) => readonly PatchInfo[];
|
|
64
64
|
/** Patch body text, memoized per patch path across the whole batch. */
|
|
65
65
|
readPatchBody: (patch: PatchInfo) => Promise<string>;
|
|
66
|
+
/**
|
|
67
|
+
* Reads the HEAD content of every path in one `git cat-file --batch`
|
|
68
|
+
* and holds it for the next {@link computePatched} of each path, which
|
|
69
|
+
* otherwise spawns one `git show` per file. Call it before a loop.
|
|
70
|
+
*/
|
|
71
|
+
prefetchBase: (filePaths: readonly string[]) => Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Hashes every live file in one `git hash-object` per ARG_MAX chunk and
|
|
74
|
+
* holds the hashes for {@link liveBlobHash}.
|
|
75
|
+
*/
|
|
76
|
+
prefetchLiveBlobHashes: (fullPaths: readonly string[]) => Promise<void>;
|
|
77
|
+
/** The live blob hash of a file, from the prefetch or a single hash-object. */
|
|
78
|
+
liveBlobHash: (fullPath: string) => Promise<string | undefined>;
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
68
81
|
* Builds a batched patched-content context that loads the manifest and
|
|
@@ -11,7 +11,7 @@ import { pathExists, readText, writeFileAtomic, writeText } from '../utils/fs.js
|
|
|
11
11
|
import { verbose } from '../utils/logger.js';
|
|
12
12
|
import { isContainedRelativePath } from '../utils/paths.js';
|
|
13
13
|
import { applyPatchIdempotent, reversePatch } from './git.js';
|
|
14
|
-
import { getFileContentAtRef } from './git-file-ops.js';
|
|
14
|
+
import { getFileContentAtRef, getFilesContentAtRef, hashObjectBatch } from './git-file-ops.js';
|
|
15
15
|
import { discoverPatches } from './patch-files.js';
|
|
16
16
|
import { loadPatchesManifest } from './patch-manifest.js';
|
|
17
17
|
import { extractAffectedFiles, extractConflictingFiles, isNewFileInPatch, parseDiffSections, } from './patch-parse.js';
|
|
@@ -395,12 +395,37 @@ export async function createPatchedContentContext(patchesDir, engineDir) {
|
|
|
395
395
|
return body;
|
|
396
396
|
};
|
|
397
397
|
const getAffectingPatches = (filePath) => affectingByFile.get(filePath) ?? [];
|
|
398
|
+
// Prefetched HEAD content, consumed on first use so a large scan does not
|
|
399
|
+
// hold every base file for the life of the context.
|
|
400
|
+
const baseContent = new Map();
|
|
401
|
+
const takeBase = async (filePath) => {
|
|
402
|
+
if (baseContent.has(filePath)) {
|
|
403
|
+
const content = baseContent.get(filePath) ?? null;
|
|
404
|
+
baseContent.delete(filePath);
|
|
405
|
+
return content;
|
|
406
|
+
}
|
|
407
|
+
return getFileContentAtRef(engineDir, filePath);
|
|
408
|
+
};
|
|
409
|
+
const liveHashes = new Map();
|
|
398
410
|
return {
|
|
399
411
|
manifestPatches: manifest?.patches ?? [],
|
|
400
412
|
getAffectingPatches,
|
|
401
413
|
readPatchBody,
|
|
414
|
+
prefetchBase: async (filePaths) => {
|
|
415
|
+
const wanted = filePaths.filter((file) => !baseContent.has(file));
|
|
416
|
+
for (const [file, content] of await getFilesContentAtRef(engineDir, wanted)) {
|
|
417
|
+
baseContent.set(file, content);
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
prefetchLiveBlobHashes: async (fullPaths) => {
|
|
421
|
+
const wanted = fullPaths.filter((path) => !liveHashes.has(path));
|
|
422
|
+
for (const [path, hash] of await hashObjectBatch(engineDir, wanted)) {
|
|
423
|
+
liveHashes.set(path, hash);
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
liveBlobHash: async (fullPath) => liveHashes.get(fullPath) ?? (await hashObjectBatch(engineDir, [fullPath])).get(fullPath),
|
|
402
427
|
computePatched: async (filePath) => {
|
|
403
|
-
let content = await
|
|
428
|
+
let content = await takeBase(filePath);
|
|
404
429
|
for (const patch of getAffectingPatches(filePath)) {
|
|
405
430
|
content = applyPatchTextToContent(content, await readPatchBody(patch), filePath);
|
|
406
431
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed `SourceFile`s reused across programs, keyed by file name and the
|
|
3
|
+
* parse options TypeScript asked for. The language service shares files
|
|
4
|
+
* across programs the same way (its document registry): a `SourceFile` is
|
|
5
|
+
* parsed and bound once, and every program that includes it reuses both.
|
|
6
|
+
* Only valid while the files cannot change and every program uses the same
|
|
7
|
+
* compiler options, which holds within one lint run.
|
|
8
|
+
*/
|
|
9
|
+
export type CheckJsSourceFileCache = Map<string, import('typescript').SourceFile>;
|
|
10
|
+
/**
|
|
11
|
+
* Returns `host` with a shared-parse `getSourceFile` when a cache is given,
|
|
12
|
+
* or `host` itself when not.
|
|
13
|
+
* @param host - The program's compiler host
|
|
14
|
+
* @param cache - Shared parse cache, or undefined for an unshared program
|
|
15
|
+
* @param shimPath - Path the host serves the composed shim at
|
|
16
|
+
* @param shimSource - The composed shim text
|
|
17
|
+
*/
|
|
18
|
+
export declare function withSharedSourceFiles(host: import('typescript').CompilerHost, cache: CheckJsSourceFileCache | undefined, shimPath: string, shimSource: string): import('typescript').CompilerHost;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Parsed-source sharing across checkJs programs.
|
|
4
|
+
*
|
|
5
|
+
* Its own module rather than part of `patch-lint-checkjs.ts` only because
|
|
6
|
+
* that file sits on a 500-line budget.
|
|
7
|
+
*/
|
|
8
|
+
import { sha256Hex } from '../utils/hash.js';
|
|
9
|
+
/**
|
|
10
|
+
* Wraps a host's `getSourceFile` so each file is parsed at most once per
|
|
11
|
+
* cache. The shim is keyed by its content as well, since the shim text is
|
|
12
|
+
* composed per call rather than read from disk.
|
|
13
|
+
*/
|
|
14
|
+
function cachingGetSourceFile(cache, shimPath, shimSource, getSourceFile) {
|
|
15
|
+
const shimDigest = sha256Hex(shimSource);
|
|
16
|
+
return (fileName, languageVersionOrOptions, onError, shouldCreate) => {
|
|
17
|
+
const parse = typeof languageVersionOrOptions === 'object'
|
|
18
|
+
? `${languageVersionOrOptions.languageVersion}|${String(languageVersionOrOptions.impliedNodeFormat)}`
|
|
19
|
+
: String(languageVersionOrOptions);
|
|
20
|
+
const key = fileName === shimPath ? `${fileName}\0${parse}\0${shimDigest}` : `${fileName}\0${parse}`;
|
|
21
|
+
const cached = cache.get(key);
|
|
22
|
+
if (cached !== undefined)
|
|
23
|
+
return cached;
|
|
24
|
+
const created = getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreate);
|
|
25
|
+
if (created !== undefined)
|
|
26
|
+
cache.set(key, created);
|
|
27
|
+
return created;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Returns `host` with a shared-parse `getSourceFile` when a cache is given,
|
|
32
|
+
* or `host` itself when not.
|
|
33
|
+
* @param host - The program's compiler host
|
|
34
|
+
* @param cache - Shared parse cache, or undefined for an unshared program
|
|
35
|
+
* @param shimPath - Path the host serves the composed shim at
|
|
36
|
+
* @param shimSource - The composed shim text
|
|
37
|
+
*/
|
|
38
|
+
export function withSharedSourceFiles(host, cache, shimPath, shimSource) {
|
|
39
|
+
if (cache === undefined)
|
|
40
|
+
return host;
|
|
41
|
+
return {
|
|
42
|
+
...host,
|
|
43
|
+
getSourceFile: cachingGetSourceFile(cache, shimPath, shimSource, host.getSourceFile.bind(host)),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
28
28
|
import type { PatchLintCheckJsCompilerOptions, PatchLintConfig, PatchLintSeverityGate } from '../types/config.js';
|
|
29
|
+
import { type CheckJsSourceFileCache } from './patch-lint-checkjs-cache.js';
|
|
29
30
|
/** How a checkJs run controls reporting and resolution. See module docs. */
|
|
30
31
|
export interface CheckJsMode {
|
|
31
32
|
strict: boolean;
|
|
@@ -76,6 +77,11 @@ export interface RunCheckJsGroupedInput {
|
|
|
76
77
|
* targets.
|
|
77
78
|
*/
|
|
78
79
|
rootScope?: ReadonlySet<string>;
|
|
80
|
+
/**
|
|
81
|
+
* Parsed source files shared across several programs built with the same
|
|
82
|
+
* compiler options and shim in one run. See {@link runCheckJsTestFilesGrouped}.
|
|
83
|
+
*/
|
|
84
|
+
sourceFileCache?: CheckJsSourceFileCache;
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
87
|
* Builds the checkJs program once over `resolutionOwned` and returns its
|
|
@@ -30,6 +30,8 @@ import { toError } from '../utils/errors.js';
|
|
|
30
30
|
import { pathExists } from '../utils/fs.js';
|
|
31
31
|
import { verbose } from '../utils/logger.js';
|
|
32
32
|
import { normalizePathSlashes } from '../utils/paths.js';
|
|
33
|
+
import { withGeckoDomTypes } from './gecko-dom-types.js';
|
|
34
|
+
import { withSharedSourceFiles } from './patch-lint-checkjs-cache.js';
|
|
33
35
|
import { collectUnmanagedCompanions, retargetUnmanagedCompanionHints, } from './patch-lint-unmanaged-companion.js';
|
|
34
36
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
35
37
|
// ---------------------------------------------------------------------------
|
|
@@ -294,7 +296,7 @@ export async function runCheckJsGrouped(input) {
|
|
|
294
296
|
// Custom compiler host: reads patch-owned files from disk, returns
|
|
295
297
|
// the shim for the shim path, and returns empty content for
|
|
296
298
|
// anything else to avoid reading the full Firefox tree.
|
|
297
|
-
const defaultHost = ts.createCompilerHost(options);
|
|
299
|
+
const defaultHost = withGeckoDomTypes(ts, ts.createCompilerHost(options));
|
|
298
300
|
// Files pulled in via a reviewed `paths` mapping: outside the owned set
|
|
299
301
|
// but read from disk so the resolver's targets actually type-check.
|
|
300
302
|
const pathsResolved = new Set();
|
|
@@ -340,7 +342,7 @@ export async function runCheckJsGrouped(input) {
|
|
|
340
342
|
});
|
|
341
343
|
},
|
|
342
344
|
};
|
|
343
|
-
const program = ts.createProgram(rootFiles, options, host);
|
|
345
|
+
const program = ts.createProgram(rootFiles, options, withSharedSourceFiles(host, input.sourceFileCache, shimPath, shimSource));
|
|
344
346
|
const byFile = groupOwnedDiagnostics(ts, [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()], relByAbsolute, input.mode?.undefinedIdentifiers ?? 'warning');
|
|
345
347
|
verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s) across ${byFile.size} owning file(s)`);
|
|
346
348
|
return { byFile, global: [] };
|
|
@@ -461,6 +463,11 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
|
|
|
461
463
|
const mode = modeFromPatchLintConfig(patchLint);
|
|
462
464
|
const seenGlobal = new Set();
|
|
463
465
|
const files = [...testFiles].sort((a, b) => a.localeCompare(b));
|
|
466
|
+
// Every test program below shares options and shim, and mostly the same
|
|
467
|
+
// files: TypeScript's lib declarations, the shim, and the directory's
|
|
468
|
+
// head.js helpers. Parsing them once instead of once per test file took
|
|
469
|
+
// a 467-test queue's pass from 66 s to a fraction of that.
|
|
470
|
+
const sourceFileCache = new Map();
|
|
464
471
|
const companions = await collectUnmanagedCompanions(repoDir, files, testFiles);
|
|
465
472
|
for (const file of files) {
|
|
466
473
|
// Under a rootScope only the scoped files get their own program, but
|
|
@@ -487,6 +494,7 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
|
|
|
487
494
|
projectRoot,
|
|
488
495
|
mode,
|
|
489
496
|
builtinShimSuffix: TEST_HARNESS_SHIM,
|
|
497
|
+
sourceFileCache,
|
|
490
498
|
});
|
|
491
499
|
const own = result.byFile.get(file);
|
|
492
500
|
if (own && own.length > 0) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
13
13
|
import type { FireForgeConfig, PatchPolicyConfig } from '../types/config.js';
|
|
14
|
+
import type { ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
|
|
14
15
|
/**
|
|
15
16
|
* One patch's contribution to {@link PatchQueueContext}.
|
|
16
17
|
*
|
|
@@ -39,7 +40,7 @@ export interface PatchQueueEntry {
|
|
|
39
40
|
* reconstructable `GIT binary patch`, or a payload-less
|
|
40
41
|
* `Binary files … differ` stub. `newFiles` is text-only by design (a
|
|
41
42
|
* binary authors no imports, so the import rules gain nothing from it and
|
|
42
|
-
* `
|
|
43
|
+
* `extractNewFileContentFromDiff` correctly refuses to decode one), but "does a
|
|
43
44
|
* later patch create this path" is a question about existence after apply,
|
|
44
45
|
* not about content, and answering it from `newFiles` made every binary
|
|
45
46
|
* creation invisible to the staged-registration rules.
|
|
@@ -76,6 +77,11 @@ export interface PatchQueueContext {
|
|
|
76
77
|
* Absent = current behavior (hint always printed).
|
|
77
78
|
*/
|
|
78
79
|
patchPolicy?: PatchPolicyConfig;
|
|
80
|
+
/**
|
|
81
|
+
* Which carriers the forward-registration rule checks, from
|
|
82
|
+
* `patchLint.forwardRegistration`. Absent = `support-files` only.
|
|
83
|
+
*/
|
|
84
|
+
forwardRegistration?: ForwardRegistrationScope;
|
|
79
85
|
}
|
|
80
86
|
/**
|
|
81
87
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
* module.
|
|
12
12
|
*/
|
|
13
13
|
import { basename } from 'node:path';
|
|
14
|
-
import { toError } from '../utils/errors.js';
|
|
15
14
|
import { readText } from '../utils/fs.js';
|
|
16
15
|
import { verbose } from '../utils/logger.js';
|
|
17
16
|
import { stripJsComments } from '../utils/regex.js';
|
|
@@ -24,7 +23,7 @@ import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registrat
|
|
|
24
23
|
import { describeRegistrationFailure, findStagedRegistrationFailure, isLaterOwner, quoteRegistrationLine, } from './patch-lint-staged-registration.js';
|
|
25
24
|
import { loadPatchesManifest } from './patch-manifest-io.js';
|
|
26
25
|
import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
|
|
27
|
-
import {
|
|
26
|
+
import { extractNewFileContentsFromDiff } from './patch-transform.js';
|
|
28
27
|
/**
|
|
29
28
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
30
29
|
* directory and extracting new-file content for each creation.
|
|
@@ -50,15 +49,11 @@ export async function buildPatchQueueContext(patchesDir, config) {
|
|
|
50
49
|
for (const patch of patches) {
|
|
51
50
|
const diff = await readText(patch.path);
|
|
52
51
|
const newFilePaths = detectNewFilesInDiff(diff);
|
|
53
|
-
|
|
54
|
-
for
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
catch (error) {
|
|
60
|
-
verbose(`Skipping forward-import scan for ${newFile} in ${patch.filename}: ${toError(error).message}`);
|
|
61
|
-
}
|
|
52
|
+
// One parse of the body already in hand. Extracting per file re-read
|
|
53
|
+
// the whole patch from disk for every file it creates.
|
|
54
|
+
const { contents: newFiles, refused } = extractNewFileContentsFromDiff(diff, newFilePaths);
|
|
55
|
+
for (const newFile of refused) {
|
|
56
|
+
verbose(`Skipping forward-import scan for ${newFile} in ${patch.filename}: binary section cannot be extracted as text`);
|
|
62
57
|
}
|
|
63
58
|
// Added-line content for every file the patch modifies but does not
|
|
64
59
|
// create. Fed to the forward-import rule so imports introduced into
|
|
@@ -87,7 +82,13 @@ export async function buildPatchQueueContext(patchesDir, config) {
|
|
|
87
82
|
}
|
|
88
83
|
// Sort by order so rules can rely on entries being in apply order.
|
|
89
84
|
entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
90
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
entries,
|
|
87
|
+
...(config?.patchPolicy ? { patchPolicy: config.patchPolicy } : {}),
|
|
88
|
+
...(config?.patchLint?.forwardRegistration !== undefined
|
|
89
|
+
? { forwardRegistration: config.patchLint.forwardRegistration }
|
|
90
|
+
: {}),
|
|
91
|
+
};
|
|
91
92
|
}
|
|
92
93
|
// The creators map (split out of `lintPatchQueueDuplicateCreations` so
|
|
93
94
|
// `status --ownership` can consume the structured data) lives in
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PatchQueueForwardRegistrationEntry, PatchQueueView } from './patch-lint-queue-types.js';
|
|
2
|
+
/** Which carriers `forward-registration` checks. */
|
|
3
|
+
export type ForwardRegistrationScope = 'support-files' | 'extended';
|
|
4
|
+
/** The carrier kind a forward registration was found in. */
|
|
5
|
+
export type ForwardRegistrationKind = 'support-files' | 'jar-mn' | 'moz-build' | 'test-manifest' | 'custom-elements';
|
|
6
|
+
/** One undeclared forward registration: a line naming a file a later patch creates. */
|
|
7
|
+
export interface ForwardRegistration {
|
|
8
|
+
/** The patch that adds the registration line. */
|
|
9
|
+
patch: string;
|
|
10
|
+
/** Carrier kind. */
|
|
11
|
+
kind: ForwardRegistrationKind;
|
|
12
|
+
/** Engine-relative path of the carrier file (the manifest, jar.mn, …). */
|
|
13
|
+
file: string;
|
|
14
|
+
/** The added line, whitespace-trimmed, that names the target. */
|
|
15
|
+
line: string;
|
|
16
|
+
/** The token on that line that names the target, as written. */
|
|
17
|
+
token: string;
|
|
18
|
+
/** Engine-relative path of the file the later patch creates. */
|
|
19
|
+
creates: string;
|
|
20
|
+
/** The later patch that creates it. */
|
|
21
|
+
owner: string;
|
|
22
|
+
/** How many distinct added lines in the carrier name the same target. */
|
|
23
|
+
occurrences: number;
|
|
24
|
+
/** Paste-and-run declaration that records the stage as intentional. */
|
|
25
|
+
command: string;
|
|
26
|
+
}
|
|
27
|
+
/** The discharge command, in the exact shape the support-files arm prints. */
|
|
28
|
+
export declare function formatRegistrationDeclaration(patch: string, file: string, line: string, creates: string, owner: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Finds every undeclared forward registration in the extended carriers:
|
|
31
|
+
* a registration line a patch adds that names a file only a later-ordered
|
|
32
|
+
* patch creates. Creators come from `createdFiles` (the `new file mode`
|
|
33
|
+
* sections), never `filesAffected`.
|
|
34
|
+
*
|
|
35
|
+
* @param ctx - Queue in application order
|
|
36
|
+
* @returns One record per (patch, carrier, created file), first line quoted
|
|
37
|
+
*/
|
|
38
|
+
export declare function findExtendedForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>): ForwardRegistration[];
|