@hominis/fireforge 0.30.0 → 0.31.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 +26 -1
- package/README.md +22 -5
- package/dist/src/commands/export-all.js +5 -15
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +36 -0
- package/dist/src/commands/export.js +47 -112
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +1 -1
- package/dist/src/commands/lint-per-patch.js +119 -78
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +96 -84
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +58 -0
- package/dist/src/commands/test-run.js +88 -0
- package/dist/src/commands/test.js +169 -257
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +48 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +171 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -2
- package/dist/src/core/git-file-ops.d.ts +0 -12
- package/dist/src/core/git-file-ops.js +2 -2
- package/dist/src/core/lint-cache.d.ts +3 -13
- package/dist/src/core/lint-cache.js +11 -5
- package/dist/src/core/mach.d.ts +5 -1
- package/dist/src/core/mach.js +6 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.js +53 -7
- package/dist/src/core/patch-lint-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.js +132 -125
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +1 -1
- package/dist/src/core/test-xpcshell-retry.js +4 -2
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +0 -21
- package/dist/src/core/typecheck-shim.js +26 -4
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +105 -0
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { loadConfig } from '../core/config.js';
|
|
4
4
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
5
|
-
import { buildPerPatchLintCacheKey, getCachedPerPatchLintIssues, loadPerPatchLintCache, savePerPatchLintCache, setCachedPerPatchLintIssues, } from '../core/lint-cache.js';
|
|
5
|
+
import { buildPerPatchLintCacheKey, getCachedPerPatchLintIssues, getPerPatchLintCacheHeadSha, loadPerPatchLintCache, savePerPatchLintCache, setCachedPerPatchLintIssues, } from '../core/lint-cache.js';
|
|
6
6
|
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
7
7
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
8
8
|
import { evaluatePatchPolicy } from '../core/patch-policy.js';
|
|
@@ -13,91 +13,75 @@ function buildPerPatchMaxWarningsMessage(count, maxWarnings, linted) {
|
|
|
13
13
|
return (`Patch lint found ${count} warning(s) across ${linted} patch(es), exceeding --max-warnings ${maxWarnings}.` +
|
|
14
14
|
' If this is a release gate, run with --per-patch to identify the owning patch. For intentional staged imports, use patch staged-dependency; for ownership repairs, preview patch move-files, patch reorder --dry-run, or re-export --files --dry-run; add scoped lintIgnore only after review.');
|
|
15
15
|
}
|
|
16
|
+
function emitTierNotice(filename, files, tier) {
|
|
17
|
+
const decision = resolvePatchSizeTier(files, tier);
|
|
18
|
+
if (decision.tier !== 'branding')
|
|
19
|
+
return;
|
|
20
|
+
info(decision.source === 'explicit'
|
|
21
|
+
? `${filename}: branding threshold tier applied via patches.json \`tier: "branding"\` opt-in.`
|
|
22
|
+
: `${filename}: branding threshold tier applied (all files under browser/branding/ plus registration siblings).`);
|
|
23
|
+
}
|
|
16
24
|
/**
|
|
17
|
-
* Lints
|
|
18
|
-
*
|
|
19
|
-
*
|
|
25
|
+
* Lints one queued patch against its own isolated diff, reusing the cache
|
|
26
|
+
* entry when the cache key matches. Pushes the patch's issues (prefixed
|
|
27
|
+
* with its filename) onto `ctx.issues`. Returns whether the patch was
|
|
28
|
+
* skipped (no files present / empty diff), served from cache, or linted
|
|
29
|
+
* fresh — and whether a fresh result was written to the cache.
|
|
20
30
|
*/
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
async function lintQueuedPatch(patch, lintCtx) {
|
|
32
|
+
const { projectRoot, paths, config, ctx, cache, engineHeadSha, issues } = lintCtx;
|
|
33
|
+
const existing = [];
|
|
34
|
+
for (const f of patch.filesAffected) {
|
|
35
|
+
if (await pathExists(join(paths.engine, f)))
|
|
36
|
+
existing.push(f);
|
|
27
37
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const cache = options.noCache === true ? undefined : await loadPerPatchLintCache(projectRoot);
|
|
31
|
-
let cacheDirty = false;
|
|
32
|
-
let reusedCacheEntries = 0;
|
|
33
|
-
const issues = [];
|
|
34
|
-
for (const issue of evaluatePatchPolicy(config, manifest)) {
|
|
35
|
-
issues.push({
|
|
36
|
-
file: issue.filename,
|
|
37
|
-
check: `patch-policy/${issue.code}`,
|
|
38
|
-
message: issue.message,
|
|
39
|
-
severity: issue.severity,
|
|
40
|
-
});
|
|
38
|
+
if (existing.length === 0) {
|
|
39
|
+
return { status: 'skipped', wroteCache: false };
|
|
41
40
|
}
|
|
42
|
-
|
|
43
|
-
let
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const decision = resolvePatchSizeTier(existing, patch.tier);
|
|
61
|
-
if (decision.tier === 'branding') {
|
|
62
|
-
info(decision.source === 'explicit'
|
|
63
|
-
? `${patch.filename}: branding threshold tier applied via patches.json \`tier: "branding"\` opt-in.`
|
|
64
|
-
: `${patch.filename}: branding threshold tier applied (all files under browser/branding/ plus registration siblings).`);
|
|
65
|
-
}
|
|
66
|
-
let patchIssues;
|
|
67
|
-
if (cache) {
|
|
68
|
-
const cacheKey = await buildPerPatchLintCacheKey({
|
|
69
|
-
projectRoot,
|
|
70
|
-
engineDir: paths.engine,
|
|
71
|
-
patchesDir: paths.patches,
|
|
72
|
-
patch,
|
|
73
|
-
existingFiles: existing,
|
|
74
|
-
config,
|
|
75
|
-
queueContext: ctx,
|
|
76
|
-
});
|
|
77
|
-
patchIssues = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey);
|
|
78
|
-
if (patchIssues) {
|
|
79
|
-
reusedCacheEntries++;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, ctx, ignore, patch.tier);
|
|
83
|
-
setCachedPerPatchLintIssues(cache, patch.filename, cacheKey, patchIssues);
|
|
84
|
-
cacheDirty = true;
|
|
41
|
+
const ignore = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
42
|
+
let cacheKey;
|
|
43
|
+
if (cache) {
|
|
44
|
+
cacheKey = await buildPerPatchLintCacheKey({
|
|
45
|
+
projectRoot,
|
|
46
|
+
engineDir: paths.engine,
|
|
47
|
+
patchesDir: paths.patches,
|
|
48
|
+
patch,
|
|
49
|
+
existingFiles: existing,
|
|
50
|
+
config,
|
|
51
|
+
queueContext: ctx,
|
|
52
|
+
...(engineHeadSha === undefined ? {} : { engineHeadSha }),
|
|
53
|
+
});
|
|
54
|
+
const cached = getCachedPerPatchLintIssues(cache, patch.filename, cacheKey);
|
|
55
|
+
if (cached) {
|
|
56
|
+
emitTierNotice(patch.filename, existing, patch.tier);
|
|
57
|
+
for (const issue of cached) {
|
|
58
|
+
issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
|
|
85
59
|
}
|
|
60
|
+
return { status: 'cached', wroteCache: false };
|
|
86
61
|
}
|
|
87
|
-
else {
|
|
88
|
-
patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, ctx, ignore, patch.tier);
|
|
89
|
-
}
|
|
90
|
-
for (const issue of patchIssues) {
|
|
91
|
-
issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
|
|
92
|
-
}
|
|
93
|
-
linted++;
|
|
94
62
|
}
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
63
|
+
const diff = await getDiffForFilesAgainstHead(paths.engine, existing);
|
|
64
|
+
if (!diff.trim()) {
|
|
65
|
+
return { status: 'skipped', wroteCache: false };
|
|
66
|
+
}
|
|
67
|
+
emitTierNotice(patch.filename, existing, patch.tier);
|
|
68
|
+
const patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, ctx, ignore, patch.tier);
|
|
69
|
+
let wroteCache = false;
|
|
70
|
+
if (cache && cacheKey) {
|
|
71
|
+
setCachedPerPatchLintIssues(cache, patch.filename, cacheKey, patchIssues);
|
|
72
|
+
wroteCache = true;
|
|
100
73
|
}
|
|
74
|
+
for (const issue of patchIssues) {
|
|
75
|
+
issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
|
|
76
|
+
}
|
|
77
|
+
return { status: 'linted', wroteCache };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reporting + exit phase of per-patch lint: renders every issue row,
|
|
81
|
+
* prints the per-patch summary, and applies the failure criteria
|
|
82
|
+
* (errors, `--max-warnings`) by throwing GeneralError.
|
|
83
|
+
*/
|
|
84
|
+
function reportPerPatchOutcome(issues, linted, skipped, options) {
|
|
101
85
|
if (issues.length === 0) {
|
|
102
86
|
if (linted === 0 && skipped > 0) {
|
|
103
87
|
info(`No patches in the queue have been applied to engine/. Run "fireforge import" first if you want lint findings against the staged hunks; otherwise this is expected.`);
|
|
@@ -137,4 +121,61 @@ export async function lintPerPatch(projectRoot, paths, options = {}) {
|
|
|
137
121
|
outro('Lint passed');
|
|
138
122
|
}
|
|
139
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Lints each patch in the queue as its own isolated diff, honouring
|
|
126
|
+
* per-patch `lintIgnore` entries. Cross-patch rules still run once over
|
|
127
|
+
* the whole queue so queue-level findings are not lost by the rescoping.
|
|
128
|
+
*/
|
|
129
|
+
export async function lintPerPatch(projectRoot, paths, options = {}) {
|
|
130
|
+
const manifest = await loadPatchesManifest(paths.patches);
|
|
131
|
+
if (!manifest || manifest.patches.length === 0) {
|
|
132
|
+
info('No patches in manifest — nothing to lint per-patch.');
|
|
133
|
+
outro('Nothing to lint');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const config = await loadConfig(projectRoot);
|
|
137
|
+
const ctx = await buildPatchQueueContext(paths.patches);
|
|
138
|
+
const cache = options.noCache === true ? undefined : await loadPerPatchLintCache(projectRoot);
|
|
139
|
+
const engineHeadSha = cache ? await getPerPatchLintCacheHeadSha(paths.engine) : undefined;
|
|
140
|
+
const issues = [];
|
|
141
|
+
for (const issue of evaluatePatchPolicy(config, manifest)) {
|
|
142
|
+
issues.push({
|
|
143
|
+
file: issue.filename,
|
|
144
|
+
check: `patch-policy/${issue.code}`,
|
|
145
|
+
message: issue.message,
|
|
146
|
+
severity: issue.severity,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
let linted = 0;
|
|
150
|
+
let skipped = 0;
|
|
151
|
+
let cacheDirty = false;
|
|
152
|
+
let reusedCacheEntries = 0;
|
|
153
|
+
for (const patch of manifest.patches) {
|
|
154
|
+
const result = await lintQueuedPatch(patch, {
|
|
155
|
+
projectRoot,
|
|
156
|
+
paths,
|
|
157
|
+
config,
|
|
158
|
+
ctx,
|
|
159
|
+
cache,
|
|
160
|
+
engineHeadSha,
|
|
161
|
+
issues,
|
|
162
|
+
});
|
|
163
|
+
if (result.status === 'skipped') {
|
|
164
|
+
skipped++;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (result.status === 'cached')
|
|
168
|
+
reusedCacheEntries++;
|
|
169
|
+
if (result.wroteCache)
|
|
170
|
+
cacheDirty = true;
|
|
171
|
+
linted++;
|
|
172
|
+
}
|
|
173
|
+
issues.push(...lintPatchQueue(ctx));
|
|
174
|
+
if (cache && cacheDirty)
|
|
175
|
+
await savePerPatchLintCache(projectRoot, cache);
|
|
176
|
+
if (reusedCacheEntries > 0) {
|
|
177
|
+
info(`Reused lint cache for ${reusedCacheEntries} patch${reusedCacheEntries === 1 ? '' : 'es'}.`);
|
|
178
|
+
}
|
|
179
|
+
reportPerPatchOutcome(issues, linted, skipped, options);
|
|
180
|
+
}
|
|
140
181
|
//# sourceMappingURL=lint-per-patch.js.map
|
|
@@ -1,63 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { CommandContext } from '../types/cli.js';
|
|
3
|
-
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
4
|
-
/** Options controlling how the lint command filters and tags its output. */
|
|
5
|
-
export interface LintCommandOptions {
|
|
6
|
-
/**
|
|
7
|
-
* When set, tag each issue as `introduced` or `cumulative` based on
|
|
8
|
-
* whether its file changed since this git revision (e.g. `HEAD`, a
|
|
9
|
-
* branch name, or a SHA). Issues are not filtered — the full set still
|
|
10
|
-
* prints — but a diff-scoped summary makes it trivial to see which
|
|
11
|
-
* errors the current task introduced.
|
|
12
|
-
*/
|
|
13
|
-
since?: string;
|
|
14
|
-
/**
|
|
15
|
-
* When set together with {@link since}, scope the exit code to issues
|
|
16
|
-
* tagged `introduced`. Cumulative pre-existing errors still print (so
|
|
17
|
-
* the operator can still see the full queue state) but do not fail
|
|
18
|
-
* lint. Motivating case: a branch whose diff is clean but whose repo
|
|
19
|
-
* already carries unrelated `raw-color` / license-header errors from
|
|
20
|
-
* older patches. Without this flag, CI treats the clean branch as
|
|
21
|
-
* failing; with it, a branch "breaks the build" only when its own diff
|
|
22
|
-
* introduced a new error.
|
|
23
|
-
*
|
|
24
|
-
* Requires {@link since}: without a revision to diff against there is
|
|
25
|
-
* no distinction between introduced and cumulative, so the flag is
|
|
26
|
-
* rejected up-front rather than silently ignored.
|
|
27
|
-
*/
|
|
28
|
-
onlyIntroduced?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Lint each patch in the queue as its own isolated diff, rather than
|
|
31
|
-
* the aggregate `git diff HEAD` across all applied patches.
|
|
32
|
-
*
|
|
33
|
-
* Motivating case: running `fireforge lint` (no args) on a repo where
|
|
34
|
-
* `fireforge import` or `fireforge rebase` has just applied the full
|
|
35
|
-
* patch queue produces an aggregate diff (every patch's changes
|
|
36
|
-
* summed). The patch-size advisory rules (`large-patch-lines`,
|
|
37
|
-
* `large-patch-files`) then fire against the sum — e.g. "Patch is
|
|
38
|
-
* 37529 lines" on a queue of 22 individually-fine patches — which
|
|
39
|
-
* reads as a task-specific regression when it is really an artefact
|
|
40
|
-
* of the aggregation. `--per-patch` rescopes the diff to each patch's
|
|
41
|
-
* own `filesAffected`, honours the patch's own `lintIgnore`, and runs
|
|
42
|
-
* the cross-patch rules once over the whole queue so queue-level
|
|
43
|
-
* findings (duplicate creations, forward imports) still surface.
|
|
44
|
-
*
|
|
45
|
-
* Mutually exclusive with passing explicit file paths — the two
|
|
46
|
-
* scope contracts are different.
|
|
47
|
-
*/
|
|
48
|
-
perPatch?: boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Maximum warning count tolerated before lint exits non-zero. Mirrors
|
|
51
|
-
* ESLint's `--max-warnings` shape for release gates that want advisory
|
|
52
|
-
* findings to become blocking without changing default CLI behavior.
|
|
53
|
-
*/
|
|
54
|
-
maxWarnings?: number;
|
|
55
|
-
/**
|
|
56
|
-
* Bypass per-patch lint cache reads and writes. Accepted in aggregate mode
|
|
57
|
-
* for CLI consistency, but only `--per-patch` currently uses the cache.
|
|
58
|
-
*/
|
|
59
|
-
noCache?: boolean;
|
|
60
|
-
}
|
|
3
|
+
import type { LintCommandOptions, PatchLintIssue } from '../types/commands/index.js';
|
|
61
4
|
/**
|
|
62
5
|
* Result of {@link applyAggregateLintIgnoreSuppression}.
|
|
63
6
|
*/
|
|
@@ -215,13 +215,12 @@ export function applyAggregateLintIgnoreSuppression(issues, ctx) {
|
|
|
215
215
|
return { issues: filtered, dropped: issues.length - filtered.length };
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
218
|
+
* Up-front flag validation for `lintCommand`: rejects `--only-introduced`
|
|
219
|
+
* without `--since`, non-integer `--max-warnings`, and `--per-patch`
|
|
220
|
+
* combined with explicit file paths — each a misconfiguration that should
|
|
221
|
+
* fail loud rather than silently narrow the result.
|
|
222
222
|
*/
|
|
223
|
-
|
|
224
|
-
intro('FireForge Lint');
|
|
223
|
+
function validateLintFlags(options, files) {
|
|
225
224
|
// `--only-introduced` scopes the exit code to `--since`-tagged issues, so
|
|
226
225
|
// without a revision to anchor the diff there is no "introduced" subset
|
|
227
226
|
// to scope to — reject the combination up-front so a misconfigured CI
|
|
@@ -242,78 +241,16 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
242
241
|
if (options.perPatch && files.length > 0) {
|
|
243
242
|
throw new GeneralError('--per-patch cannot be combined with explicit file paths. Pass either --per-patch or a file list, not both.');
|
|
244
243
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
// Load the config before resolving the diff so we can pass
|
|
257
|
-
// `binaryName` into the aggregate-mode branding exclusion in
|
|
258
|
-
// `resolveLintDiff`. The config was previously loaded only after
|
|
259
|
-
// the diff was resolved; hoisting it is cheap and keeps the two
|
|
260
|
-
// call sites close together.
|
|
261
|
-
const config = await loadConfig(projectRoot);
|
|
262
|
-
// Pull the Furnace-managed prefix set up-front so aggregate lint can
|
|
263
|
-
// mirror the branding exclusion for Furnace material — without it,
|
|
264
|
-
// preview-generated stories under `browser/components/storybook/
|
|
265
|
-
// stories/furnace/` show up as license-header errors on every
|
|
266
|
-
// post-preview lint run.
|
|
267
|
-
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
268
|
-
const diff = await resolveLintDiff(paths.engine, files, config.binaryName, furnacePrefixes);
|
|
269
|
-
if (diff === null)
|
|
270
|
-
return;
|
|
271
|
-
const filesAffected = extractAffectedFiles(diff);
|
|
272
|
-
// Build patch queue context once so it can be shared between the
|
|
273
|
-
// per-patch ownership resolver and the cross-patch rules.
|
|
274
|
-
let ctx;
|
|
275
|
-
if (await pathExists(paths.patches)) {
|
|
276
|
-
ctx = await buildPatchQueueContext(paths.patches);
|
|
277
|
-
}
|
|
278
|
-
let issues = [
|
|
279
|
-
...(await lintExportedPatch(paths.engine, filesAffected, diff, config, ctx)),
|
|
280
|
-
];
|
|
281
|
-
// Cross-patch rules operate over the whole queue, so run them whenever a
|
|
282
|
-
// patches directory exists — they surface duplicate /dev/null creations
|
|
283
|
-
// and forward-import chains that the per-patch orchestrator cannot see.
|
|
284
|
-
if (ctx) {
|
|
285
|
-
issues.push(...lintPatchQueue(ctx));
|
|
286
|
-
}
|
|
287
|
-
// Honor per-patch `lintIgnore` in aggregate mode by attributing each
|
|
288
|
-
// issue's file to its owning patches via the manifest's
|
|
289
|
-
// `filesAffected`. Per-patch mode threads `lintIgnore` directly into
|
|
290
|
-
// `lintExportedPatch`; aggregate mode previously had no patch-level
|
|
291
|
-
// scope to consult, so a check an operator had explicitly waived in
|
|
292
|
-
// `patches.json` re-surfaced on every `--since` run (CI default).
|
|
293
|
-
if (ctx) {
|
|
294
|
-
const result = applyAggregateLintIgnoreSuppression(issues, ctx);
|
|
295
|
-
issues = result.issues;
|
|
296
|
-
if (result.dropped > 0) {
|
|
297
|
-
info(`Suppressed ${result.dropped} issue(s) via per-patch lintIgnore (aggregate mode).`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
// When a queue manifest exists AND files were NOT scoped explicitly, the
|
|
301
|
-
// "diff" we just linted is every applied patch summed together. Patch-
|
|
302
|
-
// size rules (`large-patch-lines`, `large-patch-files`) then fire against
|
|
303
|
-
// the aggregate rather than any individual patch, producing counts like
|
|
304
|
-
// "Patch is 37529 lines" that read as a task-specific regression but are
|
|
305
|
-
// really an artefact of aggregation. Surface a one-line note pointing at
|
|
306
|
-
// `--per-patch` so the operator knows the per-patch scope exists before
|
|
307
|
-
// they read the error message as "my queue is broken".
|
|
308
|
-
//
|
|
309
|
-
// In aggregate mode over a multi-patch queue we also downgrade the two
|
|
310
|
-
// size rules from `error` to `warning`. Before this downgrade, a
|
|
311
|
-
// fresh-imported patch stack of 20+ patches hard-failed `fireforge lint`
|
|
312
|
-
// on lines-per-aggregate counts that are mathematically impossible to
|
|
313
|
-
// satisfy without splitting patches that were already split — the
|
|
314
|
-
// actionable unit is the individual patch, and `--per-patch` is the
|
|
315
|
-
// mode that matches. Per-patch mode keeps errors as errors (see
|
|
316
|
-
// `lintPerPatch` below).
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Aggregate-mode patch-size softening: when the linted diff is every
|
|
247
|
+
* applied patch summed (no explicit file scope, multi-patch queue), the
|
|
248
|
+
* `large-patch-lines` / `large-patch-files` counts are an artefact of
|
|
249
|
+
* aggregation rather than a property of any one patch. Surface the
|
|
250
|
+
* `--per-patch` hint and downgrade those two rules to warnings; per-patch
|
|
251
|
+
* mode keeps them as errors.
|
|
252
|
+
*/
|
|
253
|
+
function downgradeAggregateSizeRules(issues, files, ctx) {
|
|
317
254
|
const aggregateHintApplicable = files.length === 0 && ctx !== undefined && ctx.entries.length > 1;
|
|
318
255
|
if (aggregateHintApplicable &&
|
|
319
256
|
issues.some((i) => i.check === 'large-patch-lines' || i.check === 'large-patch-files')) {
|
|
@@ -325,18 +262,21 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
325
262
|
}
|
|
326
263
|
}
|
|
327
264
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Reporting + exit phase of `lintCommand`: tags issues against `--since`,
|
|
268
|
+
* renders every notice/warning/error row, prints the summary, and applies
|
|
269
|
+
* the failure criteria (`--only-introduced` scoping, `--max-warnings`)
|
|
270
|
+
* by throwing GeneralError. Issues must be non-empty.
|
|
271
|
+
*/
|
|
272
|
+
async function reportLintOutcome(engineDir, issues, options) {
|
|
333
273
|
// Diff-scoping: tag each issue as introduced-in-current-task vs
|
|
334
274
|
// cumulative-pre-existing-drift. Never filters — full set still prints
|
|
335
275
|
// and exit code semantics are unchanged — but the per-line prefix and
|
|
336
276
|
// summary make triage trivial on a large patch series.
|
|
337
277
|
const sinceActive = Boolean(options.since);
|
|
338
278
|
if (options.since) {
|
|
339
|
-
const diffFiles = await collectDiffFilePaths(
|
|
279
|
+
const diffFiles = await collectDiffFilePaths(engineDir, options.since);
|
|
340
280
|
tagLintIssues(issues, diffFiles);
|
|
341
281
|
}
|
|
342
282
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
@@ -399,6 +339,78 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
399
339
|
outro('Lint passed');
|
|
400
340
|
}
|
|
401
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Runs the lint command to check engine changes against patch quality rules.
|
|
344
|
+
* @param projectRoot - Root directory of the project
|
|
345
|
+
* @param files - Optional file/directory paths to lint (relative to engine/)
|
|
346
|
+
* @param options - Additional lint options such as `--since` diff-scoping
|
|
347
|
+
*/
|
|
348
|
+
export async function lintCommand(projectRoot, files, options = {}) {
|
|
349
|
+
intro('FireForge Lint');
|
|
350
|
+
validateLintFlags(options, files);
|
|
351
|
+
const paths = getProjectPaths(projectRoot);
|
|
352
|
+
if (!(await pathExists(paths.engine))) {
|
|
353
|
+
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
354
|
+
}
|
|
355
|
+
if (!(await isGitRepository(paths.engine))) {
|
|
356
|
+
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
357
|
+
}
|
|
358
|
+
if (options.perPatch) {
|
|
359
|
+
await lintPerPatch(projectRoot, paths, options);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Load the config before resolving the diff so we can pass
|
|
363
|
+
// `binaryName` into the aggregate-mode branding exclusion in
|
|
364
|
+
// `resolveLintDiff`. The config was previously loaded only after
|
|
365
|
+
// the diff was resolved; hoisting it is cheap and keeps the two
|
|
366
|
+
// call sites close together.
|
|
367
|
+
const config = await loadConfig(projectRoot);
|
|
368
|
+
// Pull the Furnace-managed prefix set up-front so aggregate lint can
|
|
369
|
+
// mirror the branding exclusion for Furnace material — without it,
|
|
370
|
+
// preview-generated stories under `browser/components/storybook/
|
|
371
|
+
// stories/furnace/` show up as license-header errors on every
|
|
372
|
+
// post-preview lint run.
|
|
373
|
+
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
374
|
+
const diff = await resolveLintDiff(paths.engine, files, config.binaryName, furnacePrefixes);
|
|
375
|
+
if (diff === null)
|
|
376
|
+
return;
|
|
377
|
+
const filesAffected = extractAffectedFiles(diff);
|
|
378
|
+
// Build patch queue context once so it can be shared between the
|
|
379
|
+
// per-patch ownership resolver and the cross-patch rules.
|
|
380
|
+
let ctx;
|
|
381
|
+
if (await pathExists(paths.patches)) {
|
|
382
|
+
ctx = await buildPatchQueueContext(paths.patches);
|
|
383
|
+
}
|
|
384
|
+
let issues = [
|
|
385
|
+
...(await lintExportedPatch(paths.engine, filesAffected, diff, config, ctx)),
|
|
386
|
+
];
|
|
387
|
+
// Cross-patch rules operate over the whole queue, so run them whenever a
|
|
388
|
+
// patches directory exists — they surface duplicate /dev/null creations
|
|
389
|
+
// and forward-import chains that the per-patch orchestrator cannot see.
|
|
390
|
+
if (ctx) {
|
|
391
|
+
issues.push(...lintPatchQueue(ctx));
|
|
392
|
+
}
|
|
393
|
+
// Honor per-patch `lintIgnore` in aggregate mode by attributing each
|
|
394
|
+
// issue's file to its owning patches via the manifest's
|
|
395
|
+
// `filesAffected`. Per-patch mode threads `lintIgnore` directly into
|
|
396
|
+
// `lintExportedPatch`; aggregate mode previously had no patch-level
|
|
397
|
+
// scope to consult, so a check an operator had explicitly waived in
|
|
398
|
+
// `patches.json` re-surfaced on every `--since` run (CI default).
|
|
399
|
+
if (ctx) {
|
|
400
|
+
const result = applyAggregateLintIgnoreSuppression(issues, ctx);
|
|
401
|
+
issues = result.issues;
|
|
402
|
+
if (result.dropped > 0) {
|
|
403
|
+
info(`Suppressed ${result.dropped} issue(s) via per-patch lintIgnore (aggregate mode).`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
downgradeAggregateSizeRules(issues, files, ctx);
|
|
407
|
+
if (issues.length === 0) {
|
|
408
|
+
success('No lint issues found.');
|
|
409
|
+
outro('Lint passed');
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
await reportLintOutcome(paths.engine, issues, options);
|
|
413
|
+
}
|
|
402
414
|
/** Registers the lint command on the CLI program. */
|
|
403
415
|
export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
404
416
|
const lint = program
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* `fireforge patch compact` — closes ordinal gaps in the patch queue.
|
|
3
3
|
*
|
|
4
4
|
* After deletes or splits, patch ordinals may have gaps (e.g. 1, 3, 7).
|
|
5
|
-
* This command renumbers
|
|
6
|
-
*
|
|
5
|
+
* This command renumbers patches to close those gaps in a single atomic
|
|
6
|
+
* operation, preserving relative order. Without a patch policy the whole
|
|
7
|
+
* queue is renumbered from 1; with `patchPolicy.ranges` configured the
|
|
8
|
+
* compaction is range-aware (each category range compacts independently,
|
|
9
|
+
* reserved ranges and out-of-range strays are left untouched).
|
|
7
10
|
*/
|
|
8
11
|
import { Command } from 'commander';
|
|
9
12
|
import type { CommandContext } from '../../types/cli.js';
|