@hominis/fireforge 0.50.0 → 0.51.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 +47 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.js +24 -7
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +211 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +9 -0
- package/dist/src/commands/test-orchestrate.js +22 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +4 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +65 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- 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-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +10 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +39 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +4 -2
- package/dist/src/core/furnace-validate-accessibility.js +61 -27
- package/dist/src/core/furnace-validate-exceptions.d.ts +12 -4
- package/dist/src/core/furnace-validate-exceptions.js +41 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +27 -0
- package/dist/src/core/negative-control-compiled.js +73 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +36 -3
- package/dist/src/core/test-run-completeness.js +57 -17
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +5 -1
- package/dist/src/core/typecheck-shim.js +115 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +20 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +94 -9
- package/docs/furnace.md +43 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +64 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +73 -4
- package/package.json +3 -3
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Storage and merge logic behind `fireforge park`.
|
|
4
|
+
*
|
|
5
|
+
* Several sessions edit the same Hominis files on one engine, and a peer's
|
|
6
|
+
* `re-export --refuse-foreign-drift` refuses while another session's hunks
|
|
7
|
+
* sit in its patch's files. Setting those hunks aside meant reversing each
|
|
8
|
+
* one by hand. A park records, for every file of one patch that drifted:
|
|
9
|
+
*
|
|
10
|
+
* - `before`: the file as the session left it (what the park takes away),
|
|
11
|
+
* - `after`: what the park writes in its place (the queue-applied
|
|
12
|
+
* baseline, or the working tree minus the named hunks).
|
|
13
|
+
*
|
|
14
|
+
* Restoring is a three-way merge of the file as it is now (ours) with
|
|
15
|
+
* `before` (theirs) over `after` (base), so the owner's commit in between
|
|
16
|
+
* is kept and the parked hunks come back on top of it.
|
|
17
|
+
*
|
|
18
|
+
* A park lives under `.fireforge/park/<label>/`: `park.json` plus one copy
|
|
19
|
+
* of each side per file. Absent files are recorded as absent, never as
|
|
20
|
+
* empty.
|
|
21
|
+
*/
|
|
22
|
+
import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
|
|
23
|
+
import { tmpdir } from 'node:os';
|
|
24
|
+
import { dirname, join } from 'node:path';
|
|
25
|
+
import { InvalidArgumentError } from '../errors/base.js';
|
|
26
|
+
import { getNodeErrorCode } from '../utils/errors.js';
|
|
27
|
+
import { ensureDir, pathExists, readJson, readText, removeFile, writeJson, writeText, } from '../utils/fs.js';
|
|
28
|
+
import { exec } from '../utils/process.js';
|
|
29
|
+
import { FIREFORGE_DIR } from './config-paths.js';
|
|
30
|
+
const LABEL_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
31
|
+
/** The directory holding every park of a project. */
|
|
32
|
+
function parkRoot(projectRoot) {
|
|
33
|
+
return join(projectRoot, FIREFORGE_DIR, 'park');
|
|
34
|
+
}
|
|
35
|
+
/** Refuses a label that is not a plain file-name segment. */
|
|
36
|
+
function assertParkLabel(label) {
|
|
37
|
+
if (!LABEL_PATTERN.test(label)) {
|
|
38
|
+
throw new InvalidArgumentError(`Park label "${label}" must be letters, digits, ".", "_" or "-", starting with a letter or digit.`, '--label');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function sideDir(dir, side) {
|
|
42
|
+
return join(dir, side);
|
|
43
|
+
}
|
|
44
|
+
/** Writes a park; refuses to overwrite an existing label. */
|
|
45
|
+
export async function writePark(projectRoot, record) {
|
|
46
|
+
assertParkLabel(record.label);
|
|
47
|
+
const dir = join(parkRoot(projectRoot), record.label);
|
|
48
|
+
if (await pathExists(dir)) {
|
|
49
|
+
throw new InvalidArgumentError(`A park named "${record.label}" already exists. Restore or drop it first, or pass another --label.`, '--label');
|
|
50
|
+
}
|
|
51
|
+
for (const file of record.files) {
|
|
52
|
+
for (const side of ['before', 'after']) {
|
|
53
|
+
const content = file[side];
|
|
54
|
+
if (content === null)
|
|
55
|
+
continue;
|
|
56
|
+
const target = join(sideDir(dir, side), file.path);
|
|
57
|
+
await ensureDir(dirname(target));
|
|
58
|
+
await writeText(target, content);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const index = {
|
|
62
|
+
label: record.label,
|
|
63
|
+
patch: record.patch,
|
|
64
|
+
createdAt: record.createdAt,
|
|
65
|
+
mode: record.mode,
|
|
66
|
+
files: record.files.map((file) => ({
|
|
67
|
+
path: file.path,
|
|
68
|
+
before: file.before !== null,
|
|
69
|
+
after: file.after !== null,
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
await writeJson(join(dir, 'park.json'), index);
|
|
73
|
+
return dir;
|
|
74
|
+
}
|
|
75
|
+
/** Reads a park back, contents included. */
|
|
76
|
+
export async function readPark(projectRoot, label) {
|
|
77
|
+
assertParkLabel(label);
|
|
78
|
+
const dir = join(parkRoot(projectRoot), label);
|
|
79
|
+
if (!(await pathExists(join(dir, 'park.json')))) {
|
|
80
|
+
throw new InvalidArgumentError(`No park named "${label}". List them with "fireforge park --list".`, label);
|
|
81
|
+
}
|
|
82
|
+
const index = await readJson(join(dir, 'park.json'));
|
|
83
|
+
const files = [];
|
|
84
|
+
for (const entry of index.files) {
|
|
85
|
+
files.push({
|
|
86
|
+
path: entry.path,
|
|
87
|
+
before: entry.before ? await readText(join(sideDir(dir, 'before'), entry.path)) : null,
|
|
88
|
+
after: entry.after ? await readText(join(sideDir(dir, 'after'), entry.path)) : null,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return { ...index, files };
|
|
92
|
+
}
|
|
93
|
+
/** Every park of the project, oldest first. */
|
|
94
|
+
export async function listParks(projectRoot) {
|
|
95
|
+
let names;
|
|
96
|
+
try {
|
|
97
|
+
names = (await readdir(parkRoot(projectRoot), { withFileTypes: true }))
|
|
98
|
+
.filter((entry) => entry.isDirectory())
|
|
99
|
+
.map((entry) => entry.name);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
103
|
+
return [];
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
const parks = [];
|
|
107
|
+
for (const name of names) {
|
|
108
|
+
const path = join(parkRoot(projectRoot), name, 'park.json');
|
|
109
|
+
if (await pathExists(path))
|
|
110
|
+
parks.push(await readJson(path));
|
|
111
|
+
}
|
|
112
|
+
return parks.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
113
|
+
}
|
|
114
|
+
/** Deletes a park. */
|
|
115
|
+
export async function dropPark(projectRoot, label) {
|
|
116
|
+
assertParkLabel(label);
|
|
117
|
+
const dir = join(parkRoot(projectRoot), label);
|
|
118
|
+
if (!(await pathExists(dir))) {
|
|
119
|
+
throw new InvalidArgumentError(`No park named "${label}".`, label);
|
|
120
|
+
}
|
|
121
|
+
await rm(dir, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
/** Reads an engine file, null when absent. */
|
|
124
|
+
export async function readEngineFile(engineDir, path) {
|
|
125
|
+
const full = join(engineDir, path);
|
|
126
|
+
return (await pathExists(full)) ? await readText(full) : null;
|
|
127
|
+
}
|
|
128
|
+
/** Writes an engine file, removing it for null. */
|
|
129
|
+
export async function writeEngineFile(engineDir, path, content) {
|
|
130
|
+
const full = join(engineDir, path);
|
|
131
|
+
if (content === null) {
|
|
132
|
+
if (await pathExists(full))
|
|
133
|
+
await removeFile(full);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
await ensureDir(dirname(full));
|
|
137
|
+
await writeText(full, content);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Three-way merges `ours` and `theirs` over `base` with `git merge-file`.
|
|
141
|
+
* Conflicts come back as marker blocks.
|
|
142
|
+
*/
|
|
143
|
+
export async function mergeThreeWay(ours, base, theirs, path) {
|
|
144
|
+
const scratch = await mkdtemp(join(tmpdir(), 'fireforge-park-'));
|
|
145
|
+
try {
|
|
146
|
+
const files = {
|
|
147
|
+
ours: join(scratch, 'ours'),
|
|
148
|
+
base: join(scratch, 'base'),
|
|
149
|
+
theirs: join(scratch, 'theirs'),
|
|
150
|
+
};
|
|
151
|
+
await writeFile(files.ours, ours);
|
|
152
|
+
await writeFile(files.base, base);
|
|
153
|
+
await writeFile(files.theirs, theirs);
|
|
154
|
+
const result = await exec('git', [
|
|
155
|
+
'merge-file',
|
|
156
|
+
'-p',
|
|
157
|
+
'-L',
|
|
158
|
+
`${path} (now)`,
|
|
159
|
+
'-L',
|
|
160
|
+
`${path} (parked from)`,
|
|
161
|
+
'-L',
|
|
162
|
+
`${path} (parked)`,
|
|
163
|
+
files.ours,
|
|
164
|
+
files.base,
|
|
165
|
+
files.theirs,
|
|
166
|
+
]);
|
|
167
|
+
if (result.exitCode < 0 || result.exitCode > 127) {
|
|
168
|
+
throw new Error(`git merge-file failed for ${path}: ${result.stderr.trim()}`);
|
|
169
|
+
}
|
|
170
|
+
return { content: result.stdout, conflicts: result.exitCode };
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
await rm(scratch, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* What restoring one parked file writes: `before` when the file is still
|
|
178
|
+
* as the park left it, nothing when it already matches `before`, and a
|
|
179
|
+
* three-way merge otherwise. Absence only merges with absence.
|
|
180
|
+
*/
|
|
181
|
+
export async function planRestore(file, now) {
|
|
182
|
+
if (now === file.after)
|
|
183
|
+
return { outcome: 'restored', content: file.before };
|
|
184
|
+
if (now === file.before)
|
|
185
|
+
return { outcome: 'unchanged', content: now };
|
|
186
|
+
if (now === null || file.before === null || file.after === null) {
|
|
187
|
+
return { outcome: 'conflict', content: now };
|
|
188
|
+
}
|
|
189
|
+
const merged = await mergeThreeWay(now, file.after, file.before, file.path);
|
|
190
|
+
return { outcome: merged.conflicts > 0 ? 'conflict' : 'merged', content: merged.content };
|
|
191
|
+
}
|
|
192
|
+
/** Message ids a Fluent file's lines declare (`id = …`, `-term = …`). */
|
|
193
|
+
export function fluentIds(lines) {
|
|
194
|
+
const ids = [];
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
const match = /^(-?[A-Za-z][\w-]*)\s*=/.exec(line);
|
|
197
|
+
if (match?.[1] !== undefined)
|
|
198
|
+
ids.push(match[1]);
|
|
199
|
+
}
|
|
200
|
+
return ids;
|
|
201
|
+
}
|
|
202
|
+
/** Lines `before` has that `after` lacks, as a multiset difference. */
|
|
203
|
+
export function removedLines(before, after) {
|
|
204
|
+
const remaining = new Map();
|
|
205
|
+
for (const line of (after ?? '').split('\n'))
|
|
206
|
+
remaining.set(line, (remaining.get(line) ?? 0) + 1);
|
|
207
|
+
const removed = [];
|
|
208
|
+
for (const line of (before ?? '').split('\n')) {
|
|
209
|
+
const count = remaining.get(line) ?? 0;
|
|
210
|
+
if (count > 0)
|
|
211
|
+
remaining.set(line, count - 1);
|
|
212
|
+
else
|
|
213
|
+
removed.push(line);
|
|
214
|
+
}
|
|
215
|
+
return removed;
|
|
216
|
+
}
|
|
@@ -24,6 +24,8 @@ export interface UpdatePatchAndMetadataOptions {
|
|
|
24
24
|
newContent: string;
|
|
25
25
|
/** Manifest-row fields to merge into the patch's metadata. */
|
|
26
26
|
updates: Partial<PatchMetadata>;
|
|
27
|
+
/** Manifest-row fields to delete, e.g. a staged block left empty. */
|
|
28
|
+
unset?: readonly (keyof PatchMetadata)[] | undefined;
|
|
27
29
|
/** Hook run under the lock after the mutation succeeds. */
|
|
28
30
|
onCommitted?: UpdatePatchCommittedHook | undefined;
|
|
29
31
|
/** Policy gate run against the under-lock projected manifest. */
|
|
@@ -49,3 +51,5 @@ export interface UpdatePatchAndMetadataOptions {
|
|
|
49
51
|
* already byte-identical to the new content.
|
|
50
52
|
*/
|
|
51
53
|
export declare function updatePatchAndMetadata(options: UpdatePatchAndMetadataOptions): Promise<boolean>;
|
|
54
|
+
/** A copy of `row` without `fields`. */
|
|
55
|
+
export declare function withoutFields(row: PatchMetadata, fields: readonly (keyof PatchMetadata)[]): PatchMetadata;
|
|
@@ -25,7 +25,7 @@ import { buildProjectedManifest, enforcePatchPolicy } from './patch-policy.js';
|
|
|
25
25
|
* already byte-identical to the new content.
|
|
26
26
|
*/
|
|
27
27
|
export async function updatePatchAndMetadata(options) {
|
|
28
|
-
const { patchesDir, filename, newContent, updates, onCommitted, policyGate, lockOptions = {}, } = options;
|
|
28
|
+
const { patchesDir, filename, newContent, updates, unset = [], onCommitted, policyGate, lockOptions = {}, } = options;
|
|
29
29
|
return withPatchDirectoryLock(patchesDir, async () => {
|
|
30
30
|
const manifest = await loadPatchesManifestForWrite(patchesDir);
|
|
31
31
|
if (!manifest) {
|
|
@@ -41,7 +41,7 @@ export async function updatePatchAndMetadata(options) {
|
|
|
41
41
|
}
|
|
42
42
|
const originalContent = await readText(patchPath);
|
|
43
43
|
const existingPatch = manifest.patches[patchIndex];
|
|
44
|
-
manifest.patches[patchIndex] = { ...existingPatch, ...updates };
|
|
44
|
+
manifest.patches[patchIndex] = withoutFields({ ...existingPatch, ...updates }, unset);
|
|
45
45
|
if (policyGate !== undefined) {
|
|
46
46
|
enforcePatchPolicy({
|
|
47
47
|
config: policyGate.config,
|
|
@@ -59,7 +59,7 @@ export async function updatePatchAndMetadata(options) {
|
|
|
59
59
|
await writeText(patchPath, normalized);
|
|
60
60
|
patchWritten = true;
|
|
61
61
|
}
|
|
62
|
-
await mutatePatchRowsInManifest(patchesDir, [filename], () => ({ set: updates }));
|
|
62
|
+
await mutatePatchRowsInManifest(patchesDir, [filename], () => ({ set: updates, unset }));
|
|
63
63
|
}
|
|
64
64
|
catch (error) {
|
|
65
65
|
if (patchWritten) {
|
|
@@ -84,3 +84,12 @@ export async function updatePatchAndMetadata(options) {
|
|
|
84
84
|
return bodyChanged;
|
|
85
85
|
}, lockOptions);
|
|
86
86
|
}
|
|
87
|
+
/** A copy of `row` without `fields`. */
|
|
88
|
+
export function withoutFields(row, fields) {
|
|
89
|
+
if (fields.length === 0)
|
|
90
|
+
return row;
|
|
91
|
+
const copy = { ...row };
|
|
92
|
+
for (const field of fields)
|
|
93
|
+
Reflect.deleteProperty(copy, field);
|
|
94
|
+
return copy;
|
|
95
|
+
}
|
|
@@ -18,9 +18,10 @@ export declare function buildPlanExportInput(input: PlanExportInput): PlanExport
|
|
|
18
18
|
/**
|
|
19
19
|
* Gets the next patch number for a new patch.
|
|
20
20
|
* @param patchesDir - Path to the patches directory
|
|
21
|
+
* @param reserved - Orders held by `patch reserve`, skipped
|
|
21
22
|
* @returns Next patch number (e.g., "005" for 4 existing patches)
|
|
22
23
|
*/
|
|
23
|
-
export declare function getNextPatchNumber(patchesDir: string): Promise<string>;
|
|
24
|
+
export declare function getNextPatchNumber(patchesDir: string, reserved?: ReadonlySet<number>): Promise<string>;
|
|
24
25
|
/**
|
|
25
26
|
* Sanitizes a human-readable name into a filename slug.
|
|
26
27
|
*
|
|
@@ -59,7 +60,7 @@ export declare function patchNameSlug(name: string, category: string): string;
|
|
|
59
60
|
* @param name - Human-readable name
|
|
60
61
|
* @returns Filename like "001-ui-sidebar.patch"
|
|
61
62
|
*/
|
|
62
|
-
export declare function getNextPatchFilename(patchesDir: string, category: PatchCategory, name: string): Promise<string>;
|
|
63
|
+
export declare function getNextPatchFilename(patchesDir: string, category: PatchCategory, name: string, reserved?: ReadonlySet<number>): Promise<string>;
|
|
63
64
|
export interface CommitExportedPatchInput {
|
|
64
65
|
patchesDir: string;
|
|
65
66
|
category: PatchCategory;
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { toError } from '../utils/errors.js';
|
|
4
4
|
import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
|
|
5
|
-
import { warn } from '../utils/logger.js';
|
|
5
|
+
import { info, warn } from '../utils/logger.js';
|
|
6
6
|
import { discoverPatches, withPatchDirectoryLock } from './patch-apply.js';
|
|
7
7
|
import { findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
|
|
8
8
|
import { addPatchToManifest, loadPatchesManifestForWrite, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest.js';
|
|
9
9
|
import { formatPatchOrder, requirePatchOrder } from './patch-parse.js';
|
|
10
10
|
import { allocatePolicyOrder, enforcePatchPolicy } from './patch-policy.js';
|
|
11
|
-
import { manifestWithRetiredSlots } from './patch-retired.js';
|
|
11
|
+
import { manifestWithRetiredSlots, reservationAt, reservedOrderSet } from './patch-retired.js';
|
|
12
12
|
export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
|
|
13
13
|
import { escapeRegex } from '../utils/regex.js';
|
|
14
14
|
export { mutatePatchMetadata, updatePatchMetadata } from './patch-export-metadata.js';
|
|
@@ -40,18 +40,16 @@ export function buildPlanExportInput(input) {
|
|
|
40
40
|
/**
|
|
41
41
|
* Gets the next patch number for a new patch.
|
|
42
42
|
* @param patchesDir - Path to the patches directory
|
|
43
|
+
* @param reserved - Orders held by `patch reserve`, skipped
|
|
43
44
|
* @returns Next patch number (e.g., "005" for 4 existing patches)
|
|
44
45
|
*/
|
|
45
|
-
export async function getNextPatchNumber(patchesDir) {
|
|
46
|
+
export async function getNextPatchNumber(patchesDir, reserved = new Set()) {
|
|
46
47
|
const patches = await discoverPatches(patchesDir);
|
|
47
|
-
if (patches.length === 0) {
|
|
48
|
-
return '001';
|
|
49
|
-
}
|
|
50
48
|
const finitePatches = patches.filter((p) => Number.isFinite(p.order));
|
|
51
|
-
if (finitePatches.length === 0)
|
|
52
|
-
return '001';
|
|
53
49
|
const maxOrder = finitePatches.reduce((max, p) => Math.max(max, p.order), 0);
|
|
54
|
-
|
|
50
|
+
let nextNumber = maxOrder + 1;
|
|
51
|
+
while (reserved.has(nextNumber))
|
|
52
|
+
nextNumber++;
|
|
55
53
|
return String(nextNumber).padStart(Math.max(3, String(nextNumber).length), '0');
|
|
56
54
|
}
|
|
57
55
|
/**
|
|
@@ -118,8 +116,8 @@ export function patchNameSlug(name, category) {
|
|
|
118
116
|
* @param name - Human-readable name
|
|
119
117
|
* @returns Filename like "001-ui-sidebar.patch"
|
|
120
118
|
*/
|
|
121
|
-
export async function getNextPatchFilename(patchesDir, category, name) {
|
|
122
|
-
const patchNumber = await getNextPatchNumber(patchesDir);
|
|
119
|
+
export async function getNextPatchFilename(patchesDir, category, name, reserved = new Set()) {
|
|
120
|
+
const patchNumber = await getNextPatchNumber(patchesDir, reserved);
|
|
123
121
|
const sanitizedName = patchNameSlug(name, category);
|
|
124
122
|
return `${patchNumber}-${category}-${sanitizedName}.patch`;
|
|
125
123
|
}
|
|
@@ -234,12 +232,15 @@ async function computeExportPlanUnderLock(input) {
|
|
|
234
232
|
// wipes the queue metadata, and the rollback path would then delete
|
|
235
233
|
// patches.json entirely because manifestBefore looked absent.
|
|
236
234
|
const manifestBefore = await loadPatchesManifestForWrite(input.patchesDir);
|
|
235
|
+
// A default allocation never lands on a reservation; only an explicit
|
|
236
|
+
// `--order` placement fills one.
|
|
237
|
+
const reserved = reservedOrderSet(manifestBefore ?? {});
|
|
237
238
|
const policyOrder = input.config !== undefined
|
|
238
|
-
? allocatePolicyOrder(input.config, manifestBefore?.patches ?? [], input.category)
|
|
239
|
+
? allocatePolicyOrder(input.config, manifestBefore?.patches ?? [], input.category, reserved)
|
|
239
240
|
: null;
|
|
240
241
|
const patchFilename = policyOrder !== null
|
|
241
242
|
? `${formatPatchOrder(policyOrder)}-${input.category}-${patchNameSlug(input.name, input.category)}.patch`
|
|
242
|
-
: await getNextPatchFilename(input.patchesDir, input.category, input.name);
|
|
243
|
+
: await getNextPatchFilename(input.patchesDir, input.category, input.name, reserved);
|
|
243
244
|
const patchPath = join(input.patchesDir, patchFilename);
|
|
244
245
|
const metadata = {
|
|
245
246
|
filename: patchFilename,
|
|
@@ -267,6 +268,12 @@ async function computeExportPlanUnderLock(input) {
|
|
|
267
268
|
const afterPatches = (manifestBefore?.patches ?? []).filter((p) => !supersededSet.has(p.filename) && p.filename !== patchFilename);
|
|
268
269
|
afterPatches.push(metadata);
|
|
269
270
|
afterPatches.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
271
|
+
const reservation = reservationAt(manifestBefore, metadata.order);
|
|
272
|
+
if (reservation !== undefined) {
|
|
273
|
+
info(`Order ${formatPatchOrder(metadata.order)} was reserved by ${reservation.owner}` +
|
|
274
|
+
`${reservation.reason !== undefined ? ` (${reservation.reason})` : ''}; ` +
|
|
275
|
+
'this export fills the reservation.');
|
|
276
|
+
}
|
|
270
277
|
return {
|
|
271
278
|
patchFilename,
|
|
272
279
|
patchPath,
|
|
@@ -274,7 +281,7 @@ async function computeExportPlanUnderLock(input) {
|
|
|
274
281
|
supersededDetails,
|
|
275
282
|
supersededPatches,
|
|
276
283
|
manifestBefore: manifestBefore ?? null,
|
|
277
|
-
// A patch landing on a retired order fills that slot.
|
|
284
|
+
// A patch landing on a retired or reserved order fills that slot.
|
|
278
285
|
manifestAfter: manifestWithRetiredSlots(manifestBefore, afterPatches),
|
|
279
286
|
};
|
|
280
287
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { extname, join } from 'node:path';
|
|
4
4
|
import { pathExists, readText } from '../utils/fs.js';
|
|
5
5
|
import { extractAffectedFiles, extractOrder } from './patch-parse.js';
|
|
@@ -9,16 +9,34 @@ export async function discoverPatches(patchesDir) {
|
|
|
9
9
|
return [];
|
|
10
10
|
}
|
|
11
11
|
const entries = await readdir(patchesDir, { withFileTypes: true });
|
|
12
|
-
const patches =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
const patches = [];
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (extname(entry.name) !== '.patch')
|
|
15
|
+
continue;
|
|
16
|
+
// A symlinked patch (a scratch queue built from links to the real
|
|
17
|
+
// patches) counts when it resolves to a regular file; a dangling link or
|
|
18
|
+
// a link to a directory does not.
|
|
19
|
+
if (!entry.isFile() &&
|
|
20
|
+
!(entry.isSymbolicLink() && (await isLinkToFile(patchesDir, entry.name)))) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
patches.push({
|
|
24
|
+
path: join(patchesDir, entry.name),
|
|
25
|
+
filename: entry.name,
|
|
26
|
+
order: extractOrder(entry.name),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
19
29
|
patches.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
20
30
|
return patches;
|
|
21
31
|
}
|
|
32
|
+
async function isLinkToFile(dir, name) {
|
|
33
|
+
try {
|
|
34
|
+
return (await stat(join(dir, name))).isFile();
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
22
40
|
/** Counts the patch files currently present in a patch directory. */
|
|
23
41
|
export async function countPatches(patchesDir) {
|
|
24
42
|
const patches = await discoverPatches(patchesDir);
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Explains an implicit-`any` checkJs finding that comes from a URL import
|
|
3
3
|
* the per-patch program cannot resolve.
|
|
4
4
|
*
|
|
5
|
-
* The program
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* The program resolves URL imports only to modules the queue creates (for
|
|
6
|
+
* a test, those created up to its own patch, plus its directory's
|
|
7
|
+
* head.js), so a `resource:///` or `chrome://` import of a module a later
|
|
8
|
+
* patch creates, an upstream module, or a basename two patches create
|
|
9
|
+
* falls back to the shim's `declare module 'resource:*'` wildcard and
|
|
10
|
+
* types as `any`. A callback
|
|
9
11
|
* over its values then has implicitly-`any` parameters under
|
|
10
12
|
* `checkJsStrict`, while the whole-tree `fireforge typecheck`, which sees
|
|
11
13
|
* the module, is clean. Without the note the two disagree with no
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* Explains an implicit-`any` checkJs finding that comes from a URL import
|
|
4
4
|
* the per-patch program cannot resolve.
|
|
5
5
|
*
|
|
6
|
-
* The program
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* The program resolves URL imports only to modules the queue creates (for
|
|
7
|
+
* a test, those created up to its own patch, plus its directory's
|
|
8
|
+
* head.js), so a `resource:///` or `chrome://` import of a module a later
|
|
9
|
+
* patch creates, an upstream module, or a basename two patches create
|
|
10
|
+
* falls back to the shim's `declare module 'resource:*'` wildcard and
|
|
11
|
+
* types as `any`. A callback
|
|
10
12
|
* over its values then has implicitly-`any` parameters under
|
|
11
13
|
* `checkJsStrict`, while the whole-tree `fireforge typecheck`, which sees
|
|
12
14
|
* the module, is clean. Without the note the two disagree with no
|
|
@@ -35,9 +37,9 @@ export function anyTypedImportHint(code, specifiers) {
|
|
|
35
37
|
return '';
|
|
36
38
|
const list = [...specifiers].sort().join(', ');
|
|
37
39
|
return (` (This file imports ${list}, which this per-patch checkJs program cannot resolve — ` +
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'parameter explicitly; the whole-tree "fireforge typecheck" resolves the real type.)');
|
|
40
|
+
'no patch up to this one creates the module under that name: a later patch creates it, ' +
|
|
41
|
+
'it is upstream, or two patches create the same basename — so the import is typed `any`. ' +
|
|
42
|
+
'Annotate the parameter explicitly; the whole-tree "fireforge typecheck" resolves the real type.)');
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* The host's `resolveModuleNameLiterals`: owned specifiers first, then a
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collects a checkJs program's diagnostics, for every file or for a report
|
|
3
|
+
* scope only. A per-test program resolves the modules its imports reach
|
|
4
|
+
* (see `runCheckJsTestFilesGrouped`), and asking for the whole program's
|
|
5
|
+
* diagnostics would type-check each of those modules once per test, so a
|
|
6
|
+
* scoped run asks only for the files it reports. Split out of
|
|
7
|
+
* patch-lint-checkjs.ts for the line budget.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* The program's semantic and syntactic diagnostics, limited to the files
|
|
11
|
+
* `reportOnly` names when it is set.
|
|
12
|
+
*
|
|
13
|
+
* @param program - The built program
|
|
14
|
+
* @param relByAbsolute - Owned files: TS path → repo-relative path
|
|
15
|
+
* @param reportOnly - Repo-relative files to collect for, or all when unset
|
|
16
|
+
*/
|
|
17
|
+
export declare function collectCheckJsDiagnostics(program: import('typescript').Program, relByAbsolute: ReadonlyMap<string, string>, reportOnly: ReadonlySet<string> | undefined): import('typescript').Diagnostic[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Collects a checkJs program's diagnostics, for every file or for a report
|
|
4
|
+
* scope only. A per-test program resolves the modules its imports reach
|
|
5
|
+
* (see `runCheckJsTestFilesGrouped`), and asking for the whole program's
|
|
6
|
+
* diagnostics would type-check each of those modules once per test, so a
|
|
7
|
+
* scoped run asks only for the files it reports. Split out of
|
|
8
|
+
* patch-lint-checkjs.ts for the line budget.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The program's semantic and syntactic diagnostics, limited to the files
|
|
12
|
+
* `reportOnly` names when it is set.
|
|
13
|
+
*
|
|
14
|
+
* @param program - The built program
|
|
15
|
+
* @param relByAbsolute - Owned files: TS path → repo-relative path
|
|
16
|
+
* @param reportOnly - Repo-relative files to collect for, or all when unset
|
|
17
|
+
*/
|
|
18
|
+
export function collectCheckJsDiagnostics(program, relByAbsolute, reportOnly) {
|
|
19
|
+
if (reportOnly === undefined) {
|
|
20
|
+
return [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()];
|
|
21
|
+
}
|
|
22
|
+
return program
|
|
23
|
+
.getSourceFiles()
|
|
24
|
+
.filter((sourceFile) => {
|
|
25
|
+
const rel = relByAbsolute.get(sourceFile.fileName);
|
|
26
|
+
return rel !== undefined && reportOnly.has(rel);
|
|
27
|
+
})
|
|
28
|
+
.flatMap((sourceFile) => [
|
|
29
|
+
...program.getSemanticDiagnostics(sourceFile),
|
|
30
|
+
...program.getSyntacticDiagnostics(sourceFile),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
@@ -82,6 +82,8 @@ export interface RunCheckJsGroupedInput {
|
|
|
82
82
|
* compiler options and shim in one run. See {@link runCheckJsTestFilesGrouped}.
|
|
83
83
|
*/
|
|
84
84
|
sourceFileCache?: CheckJsSourceFileCache;
|
|
85
|
+
/** When set, diagnostics are collected only for these repo-relative files. */
|
|
86
|
+
reportOnly?: ReadonlySet<string>;
|
|
85
87
|
}
|
|
86
88
|
/**
|
|
87
89
|
* Builds the checkJs program once over `resolutionOwned` and returns its
|
|
@@ -136,12 +138,19 @@ export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles:
|
|
|
136
138
|
* reported per program (head helpers report from their own program), and
|
|
137
139
|
* run-level errors are de-duplicated across programs.
|
|
138
140
|
*
|
|
141
|
+
* With `resolvableModules`, a test's `resource:///` and `chrome://`
|
|
142
|
+
* imports also resolve to the modules it returns for that test (the
|
|
143
|
+
* modules the queue creates up to the test's own patch), so a test that
|
|
144
|
+
* imports an earlier patch's module is typed against its real source
|
|
145
|
+
* instead of the shim's `any` wildcard. Those modules are resolution
|
|
146
|
+
* targets only: never roots, and never reported on.
|
|
147
|
+
*
|
|
139
148
|
* @param repoDir - Absolute engine (repository) directory
|
|
140
149
|
* @param testFiles - Patch-owned test-script paths (repo-relative)
|
|
141
150
|
* @param patchLint - The resolved `patchLint` config block
|
|
142
151
|
* @param projectRoot - FireForge project root for shim resolution
|
|
143
152
|
*/
|
|
144
|
-
export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>): Promise<GroupedCheckJsResult>;
|
|
153
|
+
export declare function runCheckJsTestFilesGrouped(repoDir: string, testFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, rootScope?: ReadonlySet<string>, resolvableModules?: (testFile: string) => ReadonlySet<string>): Promise<GroupedCheckJsResult>;
|
|
145
154
|
/**
|
|
146
155
|
* Grouped variant of {@link invokePatchLintCheckJs}: builds one queue-wide
|
|
147
156
|
* checkJs program over `patchOwnedFiles` and returns its findings grouped by
|
|
@@ -33,6 +33,7 @@ import { normalizePathSlashes } from '../utils/paths.js';
|
|
|
33
33
|
import { withGeckoDomTypes } from './gecko-dom-types.js';
|
|
34
34
|
import { anyTypedImportHint, createModuleLiteralResolver, } from './patch-lint-checkjs-any-import.js';
|
|
35
35
|
import { withSharedSourceFiles } from './patch-lint-checkjs-cache.js';
|
|
36
|
+
import { collectCheckJsDiagnostics } from './patch-lint-checkjs-diagnostics.js';
|
|
36
37
|
import { collectUnmanagedCompanions, retargetUnmanagedCompanionHints, } from './patch-lint-unmanaged-companion.js';
|
|
37
38
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
38
39
|
// ---------------------------------------------------------------------------
|
|
@@ -340,7 +341,7 @@ export async function runCheckJsGrouped(input) {
|
|
|
340
341
|
resolveModuleNameLiterals: createModuleLiteralResolver(resolveOwnedSpecifier, resolveViaPaths, anyTypedImports),
|
|
341
342
|
};
|
|
342
343
|
const program = ts.createProgram(rootFiles, options, withSharedSourceFiles(host, input.sourceFileCache, shimPath, shimSource));
|
|
343
|
-
const byFile = groupOwnedDiagnostics(ts,
|
|
344
|
+
const byFile = groupOwnedDiagnostics(ts, collectCheckJsDiagnostics(program, relByAbsolute, input.reportOnly), relByAbsolute, input.mode?.undefinedIdentifiers ?? 'warning', anyTypedImports);
|
|
344
345
|
verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s) across ${byFile.size} owning file(s)`);
|
|
345
346
|
return { byFile, global: [] };
|
|
346
347
|
}
|
|
@@ -448,12 +449,19 @@ export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint
|
|
|
448
449
|
* reported per program (head helpers report from their own program), and
|
|
449
450
|
* run-level errors are de-duplicated across programs.
|
|
450
451
|
*
|
|
452
|
+
* With `resolvableModules`, a test's `resource:///` and `chrome://`
|
|
453
|
+
* imports also resolve to the modules it returns for that test (the
|
|
454
|
+
* modules the queue creates up to the test's own patch), so a test that
|
|
455
|
+
* imports an earlier patch's module is typed against its real source
|
|
456
|
+
* instead of the shim's `any` wildcard. Those modules are resolution
|
|
457
|
+
* targets only: never roots, and never reported on.
|
|
458
|
+
*
|
|
451
459
|
* @param repoDir - Absolute engine (repository) directory
|
|
452
460
|
* @param testFiles - Patch-owned test-script paths (repo-relative)
|
|
453
461
|
* @param patchLint - The resolved `patchLint` config block
|
|
454
462
|
* @param projectRoot - FireForge project root for shim resolution
|
|
455
463
|
*/
|
|
456
|
-
export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint, projectRoot, rootScope) {
|
|
464
|
+
export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint, projectRoot, rootScope, resolvableModules) {
|
|
457
465
|
const merged = { byFile: new Map(), global: [] };
|
|
458
466
|
if (testFiles.size === 0)
|
|
459
467
|
return merged;
|
|
@@ -484,7 +492,9 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
|
|
|
484
492
|
}
|
|
485
493
|
const result = await runCheckJsGrouped({
|
|
486
494
|
repoDir,
|
|
487
|
-
resolutionOwned: roots,
|
|
495
|
+
resolutionOwned: new Set([...roots, ...(resolvableModules?.(file) ?? [])]),
|
|
496
|
+
rootScope: roots,
|
|
497
|
+
reportOnly: new Set([file]),
|
|
488
498
|
...(patchLint.checkJsTestShim !== undefined
|
|
489
499
|
? { extraShimPath: patchLint.checkJsTestShim }
|
|
490
500
|
: {}),
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* module.
|
|
11
11
|
*/
|
|
12
12
|
import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
|
|
13
|
-
import type { FireForgeConfig, PatchPolicyConfig } from '../types/config.js';
|
|
13
|
+
import type { FireForgeConfig, PatchLintTokenScale, PatchPolicyConfig } from '../types/config.js';
|
|
14
14
|
import type { ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
|
|
15
15
|
/**
|
|
16
16
|
* One patch's contribution to {@link PatchQueueContext}.
|
|
@@ -82,6 +82,8 @@ export interface PatchQueueContext {
|
|
|
82
82
|
* `patchLint.forwardRegistration`. Absent = `support-files` only.
|
|
83
83
|
*/
|
|
84
84
|
forwardRegistration?: ForwardRegistrationScope;
|
|
85
|
+
/** `patchLint.tokenScales`, for the scales' `minFiles` floors. */
|
|
86
|
+
tokenScales?: PatchLintTokenScale[];
|
|
85
87
|
}
|
|
86
88
|
/**
|
|
87
89
|
* Builds a {@link PatchQueueContext} by reading every .patch file in the
|
|
@@ -21,6 +21,7 @@ import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-dif
|
|
|
21
21
|
import { lintPatchQueueForwardRegistrations } from './patch-lint-forward-registration.js';
|
|
22
22
|
import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registration.js';
|
|
23
23
|
import { describeRegistrationFailure, findStagedRegistrationFailure, isLaterOwner, quoteRegistrationLine, STAGED_DEPENDENCY_REASON_PLACEHOLDER, stagedDependencyUnusedFingerprint, } from './patch-lint-staged-registration.js';
|
|
24
|
+
import { lintTokenScaleFloors } from './patch-lint-token-scale.js';
|
|
24
25
|
import { loadPatchesManifest } from './patch-manifest-io.js';
|
|
25
26
|
import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
|
|
26
27
|
import { extractNewFileContentsFromDiff } from './patch-transform.js';
|
|
@@ -88,6 +89,9 @@ export async function buildPatchQueueContext(patchesDir, config) {
|
|
|
88
89
|
...(config?.patchLint?.forwardRegistration !== undefined
|
|
89
90
|
? { forwardRegistration: config.patchLint.forwardRegistration }
|
|
90
91
|
: {}),
|
|
92
|
+
...(config?.patchLint?.tokenScales !== undefined
|
|
93
|
+
? { tokenScales: config.patchLint.tokenScales }
|
|
94
|
+
: {}),
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
97
|
// The creators map (split out of `lintPatchQueueDuplicateCreations` so
|
|
@@ -581,5 +585,6 @@ export function lintPatchQueue(ctx) {
|
|
|
581
585
|
...lintPatchQueueModuleRegistrations(ctx),
|
|
582
586
|
...lintPatchQueueForwardRegistrations(ctx),
|
|
583
587
|
...lintPatchQueueBinaryBodies(ctx),
|
|
588
|
+
...lintTokenScaleFloors(ctx.entries.flatMap((entry) => [...entry.newFiles]), ctx.tokenScales ?? []),
|
|
584
589
|
];
|
|
585
590
|
}
|