@hominis/fireforge 0.15.7 → 0.15.9
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 +44 -0
- package/README.md +103 -12
- package/dist/src/commands/export-shared.d.ts +6 -1
- package/dist/src/commands/export-shared.js +7 -2
- package/dist/src/commands/furnace/create-dry-run.d.ts +7 -0
- package/dist/src/commands/furnace/create-dry-run.js +7 -2
- package/dist/src/commands/furnace/create-features.d.ts +24 -0
- package/dist/src/commands/furnace/create-features.js +56 -0
- package/dist/src/commands/furnace/create-templates.d.ts +9 -5
- package/dist/src/commands/furnace/create-templates.js +14 -6
- package/dist/src/commands/furnace/create.js +34 -39
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/lint.d.ts +20 -0
- package/dist/src/commands/lint.js +157 -44
- package/dist/src/commands/re-export-files.js +6 -2
- package/dist/src/commands/re-export.js +37 -4
- package/dist/src/commands/run.d.ts +15 -1
- package/dist/src/commands/run.js +202 -7
- package/dist/src/commands/test.js +97 -2
- package/dist/src/core/furnace-apply-ftl.d.ts +5 -3
- package/dist/src/core/furnace-apply-ftl.js +6 -2
- package/dist/src/core/furnace-apply-helpers.js +14 -4
- package/dist/src/core/furnace-config-custom.d.ts +14 -0
- package/dist/src/core/furnace-config-custom.js +64 -0
- package/dist/src/core/furnace-config.js +2 -39
- package/dist/src/core/furnace-validate-accessibility.d.ts +9 -2
- package/dist/src/core/furnace-validate-accessibility.js +17 -3
- package/dist/src/core/furnace-validate-helpers.d.ts +13 -1
- package/dist/src/core/furnace-validate-helpers.js +19 -0
- package/dist/src/core/furnace-validate-structure.js +6 -2
- package/dist/src/core/furnace-validate.js +6 -3
- package/dist/src/core/mach.d.ts +26 -0
- package/dist/src/core/mach.js +25 -1
- package/dist/src/core/patch-lint.d.ts +6 -1
- package/dist/src/core/patch-lint.js +14 -1
- package/dist/src/core/shared-ftl.d.ts +28 -0
- package/dist/src/core/shared-ftl.js +42 -0
- package/dist/src/core/smoke-patterns.d.ts +45 -0
- package/dist/src/core/smoke-patterns.js +100 -0
- package/dist/src/core/xpcshell-appdir.d.ts +143 -0
- package/dist/src/core/xpcshell-appdir.js +273 -0
- package/dist/src/errors/codes.d.ts +13 -0
- package/dist/src/errors/codes.js +13 -0
- package/dist/src/errors/run.d.ts +16 -0
- package/dist/src/errors/run.js +22 -0
- package/dist/src/types/commands/options.d.ts +58 -0
- package/dist/src/types/commands/patches.d.ts +22 -0
- package/dist/src/types/furnace.d.ts +39 -0
- package/dist/src/utils/process.d.ts +63 -0
- package/dist/src/utils/process.js +122 -0
- package/package.json +1 -1
|
@@ -25,6 +25,26 @@ export interface LintCommandOptions {
|
|
|
25
25
|
* rejected up-front rather than silently ignored.
|
|
26
26
|
*/
|
|
27
27
|
onlyIntroduced?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Lint each patch in the queue as its own isolated diff, rather than
|
|
30
|
+
* the aggregate `git diff HEAD` across all applied patches.
|
|
31
|
+
*
|
|
32
|
+
* Motivating case: running `fireforge lint` (no args) on a repo where
|
|
33
|
+
* `fireforge import` or `fireforge rebase` has just applied the full
|
|
34
|
+
* patch queue produces an aggregate diff (every patch's changes
|
|
35
|
+
* summed). The patch-size advisory rules (`large-patch-lines`,
|
|
36
|
+
* `large-patch-files`) then fire against the sum — e.g. "Patch is
|
|
37
|
+
* 37529 lines" on a queue of 22 individually-fine patches — which
|
|
38
|
+
* reads as a task-specific regression when it is really an artefact
|
|
39
|
+
* of the aggregation. `--per-patch` rescopes the diff to each patch's
|
|
40
|
+
* own `filesAffected`, honours the patch's own `lintIgnore`, and runs
|
|
41
|
+
* the cross-patch rules once over the whole queue so queue-level
|
|
42
|
+
* findings (duplicate creations, forward imports) still surface.
|
|
43
|
+
*
|
|
44
|
+
* Mutually exclusive with passing explicit file paths — the two
|
|
45
|
+
* scope contracts are different.
|
|
46
|
+
*/
|
|
47
|
+
perPatch?: boolean;
|
|
28
48
|
}
|
|
29
49
|
/**
|
|
30
50
|
* Runs the lint command to check engine changes against patch quality rules.
|
|
@@ -8,40 +8,27 @@ import { getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, } fro
|
|
|
8
8
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
9
9
|
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue } from '../core/patch-lint.js';
|
|
10
10
|
import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
|
|
11
|
+
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
11
12
|
import { GeneralError } from '../errors/base.js';
|
|
12
13
|
import { pathExists } from '../utils/fs.js';
|
|
13
14
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
14
15
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Resolves the diff the lint command should run against. Returns `null` when
|
|
17
|
+
* there is nothing to lint (e.g. no matching files, clean tree, or empty
|
|
18
|
+
* diff content) — callers treat that as the early-exit signal and stop.
|
|
19
|
+
*
|
|
20
|
+
* Extracted from {@link lintCommand} so that function stays under the
|
|
21
|
+
* per-function LOC budget as the command grows; the two file-mode and
|
|
22
|
+
* aggregate-mode branches share no state with the post-lint reporting
|
|
23
|
+
* pipeline, so the split is a pure rename rather than a refactor.
|
|
19
24
|
*/
|
|
20
|
-
|
|
21
|
-
intro('FireForge Lint');
|
|
22
|
-
// `--only-introduced` scopes the exit code to `--since`-tagged issues, so
|
|
23
|
-
// without a revision to anchor the diff there is no "introduced" subset
|
|
24
|
-
// to scope to — reject the combination up-front so a misconfigured CI
|
|
25
|
-
// invocation fails loud instead of silently treating every error as
|
|
26
|
-
// cumulative and passing.
|
|
27
|
-
if (options.onlyIntroduced && !options.since) {
|
|
28
|
-
throw new GeneralError('--only-introduced requires --since <git-rev> so introduced-vs-cumulative can be distinguished.');
|
|
29
|
-
}
|
|
30
|
-
const paths = getProjectPaths(projectRoot);
|
|
31
|
-
if (!(await pathExists(paths.engine))) {
|
|
32
|
-
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
33
|
-
}
|
|
34
|
-
if (!(await isGitRepository(paths.engine))) {
|
|
35
|
-
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
36
|
-
}
|
|
37
|
-
let diff;
|
|
25
|
+
async function resolveLintDiff(engineDir, files) {
|
|
38
26
|
if (files.length > 0) {
|
|
39
|
-
// Collect specific files/directories
|
|
40
27
|
const collectedFiles = new Set();
|
|
41
28
|
let fileStatuses;
|
|
42
29
|
let untrackedFiles;
|
|
43
30
|
for (const inputPath of files) {
|
|
44
|
-
const fullInputPath = join(
|
|
31
|
+
const fullInputPath = join(engineDir, inputPath);
|
|
45
32
|
let isDirectory = false;
|
|
46
33
|
try {
|
|
47
34
|
const fileStat = await stat(fullInputPath);
|
|
@@ -52,47 +39,87 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
52
39
|
}
|
|
53
40
|
if (isDirectory) {
|
|
54
41
|
const dirPath = inputPath.endsWith('/') ? inputPath.slice(0, -1) : inputPath;
|
|
55
|
-
const modifiedFiles = await getModifiedFilesInDir(
|
|
56
|
-
const dirUntrackedFiles = await getUntrackedFilesInDir(
|
|
42
|
+
const modifiedFiles = await getModifiedFilesInDir(engineDir, dirPath);
|
|
43
|
+
const dirUntrackedFiles = await getUntrackedFilesInDir(engineDir, dirPath);
|
|
57
44
|
for (const f of modifiedFiles)
|
|
58
45
|
collectedFiles.add(f);
|
|
59
46
|
for (const f of dirUntrackedFiles)
|
|
60
47
|
collectedFiles.add(f);
|
|
61
48
|
}
|
|
62
49
|
else {
|
|
63
|
-
if (!fileStatuses)
|
|
64
|
-
fileStatuses = await getStatusWithCodes(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
untrackedFiles = await getUntrackedFiles(paths.engine);
|
|
68
|
-
}
|
|
50
|
+
if (!fileStatuses)
|
|
51
|
+
fileStatuses = await getStatusWithCodes(engineDir);
|
|
52
|
+
if (!untrackedFiles)
|
|
53
|
+
untrackedFiles = await getUntrackedFiles(engineDir);
|
|
69
54
|
const hasStatus = fileStatuses.some((s) => s.file === inputPath) || untrackedFiles.includes(inputPath);
|
|
70
|
-
if (hasStatus)
|
|
55
|
+
if (hasStatus)
|
|
71
56
|
collectedFiles.add(inputPath);
|
|
72
|
-
}
|
|
73
57
|
}
|
|
74
58
|
}
|
|
75
59
|
if (collectedFiles.size === 0) {
|
|
76
60
|
info('No modified files found in the specified paths.');
|
|
77
61
|
outro('Nothing to lint');
|
|
78
|
-
return;
|
|
62
|
+
return null;
|
|
79
63
|
}
|
|
80
|
-
diff = await getDiffForFilesAgainstHead(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Lint all changes
|
|
84
|
-
if (!(await hasChanges(paths.engine))) {
|
|
85
|
-
info('No changes to lint.');
|
|
64
|
+
const diff = await getDiffForFilesAgainstHead(engineDir, [...collectedFiles].sort());
|
|
65
|
+
if (!diff.trim()) {
|
|
66
|
+
info('No diff content to lint.');
|
|
86
67
|
outro('Nothing to lint');
|
|
87
|
-
return;
|
|
68
|
+
return null;
|
|
88
69
|
}
|
|
89
|
-
diff
|
|
70
|
+
return diff;
|
|
90
71
|
}
|
|
72
|
+
if (!(await hasChanges(engineDir))) {
|
|
73
|
+
info('No changes to lint.');
|
|
74
|
+
outro('Nothing to lint');
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const diff = await getAllDiff(engineDir);
|
|
91
78
|
if (!diff.trim()) {
|
|
92
79
|
info('No diff content to lint.');
|
|
93
80
|
outro('Nothing to lint');
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return diff;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Runs the lint command to check engine changes against patch quality rules.
|
|
87
|
+
* @param projectRoot - Root directory of the project
|
|
88
|
+
* @param files - Optional file/directory paths to lint (relative to engine/)
|
|
89
|
+
* @param options - Additional lint options such as `--since` diff-scoping
|
|
90
|
+
*/
|
|
91
|
+
export async function lintCommand(projectRoot, files, options = {}) {
|
|
92
|
+
intro('FireForge Lint');
|
|
93
|
+
// `--only-introduced` scopes the exit code to `--since`-tagged issues, so
|
|
94
|
+
// without a revision to anchor the diff there is no "introduced" subset
|
|
95
|
+
// to scope to — reject the combination up-front so a misconfigured CI
|
|
96
|
+
// invocation fails loud instead of silently treating every error as
|
|
97
|
+
// cumulative and passing.
|
|
98
|
+
if (options.onlyIntroduced && !options.since) {
|
|
99
|
+
throw new GeneralError('--only-introduced requires --since <git-rev> so introduced-vs-cumulative can be distinguished.');
|
|
100
|
+
}
|
|
101
|
+
// `--per-patch` rescopes the diff from "aggregate engine state" to "each
|
|
102
|
+
// patch's own filesAffected". Mixing in explicit file paths would produce
|
|
103
|
+
// an ambiguous set — is the file list an additional filter, or does it
|
|
104
|
+
// replace the per-patch scope? Reject up-front so the operator gets a
|
|
105
|
+
// clear error rather than a silently-narrowed result.
|
|
106
|
+
if (options.perPatch && files.length > 0) {
|
|
107
|
+
throw new GeneralError('--per-patch cannot be combined with explicit file paths. Pass either --per-patch or a file list, not both.');
|
|
108
|
+
}
|
|
109
|
+
const paths = getProjectPaths(projectRoot);
|
|
110
|
+
if (!(await pathExists(paths.engine))) {
|
|
111
|
+
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
112
|
+
}
|
|
113
|
+
if (!(await isGitRepository(paths.engine))) {
|
|
114
|
+
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
115
|
+
}
|
|
116
|
+
if (options.perPatch) {
|
|
117
|
+
await lintPerPatch(projectRoot, paths);
|
|
94
118
|
return;
|
|
95
119
|
}
|
|
120
|
+
const diff = await resolveLintDiff(paths.engine, files);
|
|
121
|
+
if (diff === null)
|
|
122
|
+
return;
|
|
96
123
|
const config = await loadConfig(projectRoot);
|
|
97
124
|
const filesAffected = extractAffectedFiles(diff);
|
|
98
125
|
// Build patch queue context once so it can be shared between the
|
|
@@ -110,6 +137,19 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
110
137
|
if (ctx) {
|
|
111
138
|
issues.push(...lintPatchQueue(ctx));
|
|
112
139
|
}
|
|
140
|
+
// When a queue manifest exists AND files were NOT scoped explicitly, the
|
|
141
|
+
// "diff" we just linted is every applied patch summed together. Patch-
|
|
142
|
+
// size rules (`large-patch-lines`, `large-patch-files`) then fire against
|
|
143
|
+
// the aggregate rather than any individual patch, producing counts like
|
|
144
|
+
// "Patch is 37529 lines" that read as a task-specific regression but are
|
|
145
|
+
// really an artefact of aggregation. Surface a one-line note pointing at
|
|
146
|
+
// `--per-patch` so the operator knows the per-patch scope exists before
|
|
147
|
+
// they read the error message as "my queue is broken".
|
|
148
|
+
const aggregateHintApplicable = files.length === 0 && ctx !== undefined && ctx.entries.length > 1;
|
|
149
|
+
if (aggregateHintApplicable &&
|
|
150
|
+
issues.some((i) => i.check === 'large-patch-lines' || i.check === 'large-patch-files')) {
|
|
151
|
+
info('NOTE: aggregate diff across all applied patches. Use `fireforge lint --per-patch` to lint each patch individually; patch-size rules fire against the sum in aggregate mode.');
|
|
152
|
+
}
|
|
113
153
|
if (issues.length === 0) {
|
|
114
154
|
success('No lint issues found.');
|
|
115
155
|
outro('Lint passed');
|
|
@@ -164,13 +204,83 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
164
204
|
}
|
|
165
205
|
outro('Lint passed with warnings');
|
|
166
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Lints each patch in the queue as its own isolated diff, honouring
|
|
209
|
+
* per-patch `lintIgnore` entries. Cross-patch rules still run once over
|
|
210
|
+
* the whole queue so queue-level findings (duplicate creations, forward
|
|
211
|
+
* imports) are not lost by the rescoping.
|
|
212
|
+
*
|
|
213
|
+
* Kept separate from {@link lintCommand}'s aggregate path because the
|
|
214
|
+
* two scopes have genuinely different contracts — the aggregate path
|
|
215
|
+
* reports what `git diff HEAD` looks like right now, the per-patch
|
|
216
|
+
* path reports what each patch's own slice of that diff looks like.
|
|
217
|
+
* Sharing a loop would hide the distinction and force the caller to
|
|
218
|
+
* decide semantics mid-function.
|
|
219
|
+
*/
|
|
220
|
+
async function lintPerPatch(projectRoot, paths) {
|
|
221
|
+
const manifest = await loadPatchesManifest(paths.patches);
|
|
222
|
+
if (!manifest || manifest.patches.length === 0) {
|
|
223
|
+
info('No patches in manifest — nothing to lint per-patch.');
|
|
224
|
+
outro('Nothing to lint');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const config = await loadConfig(projectRoot);
|
|
228
|
+
const ctx = await buildPatchQueueContext(paths.patches);
|
|
229
|
+
const issues = [];
|
|
230
|
+
let linted = 0;
|
|
231
|
+
for (const patch of manifest.patches) {
|
|
232
|
+
const existing = [];
|
|
233
|
+
for (const f of patch.filesAffected) {
|
|
234
|
+
if (await pathExists(join(paths.engine, f)))
|
|
235
|
+
existing.push(f);
|
|
236
|
+
}
|
|
237
|
+
if (existing.length === 0)
|
|
238
|
+
continue;
|
|
239
|
+
const diff = await getDiffForFilesAgainstHead(paths.engine, existing);
|
|
240
|
+
if (!diff.trim())
|
|
241
|
+
continue;
|
|
242
|
+
const ignore = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
243
|
+
const patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, ctx, ignore);
|
|
244
|
+
for (const issue of patchIssues) {
|
|
245
|
+
issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
|
|
246
|
+
}
|
|
247
|
+
linted++;
|
|
248
|
+
}
|
|
249
|
+
// Cross-patch rules over the whole queue — rescoping per-patch would
|
|
250
|
+
// lose these findings, so they run exactly once against the full
|
|
251
|
+
// context.
|
|
252
|
+
issues.push(...lintPatchQueue(ctx));
|
|
253
|
+
if (issues.length === 0) {
|
|
254
|
+
success(`No lint issues found across ${linted} patch(es).`);
|
|
255
|
+
outro('Lint passed');
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const errors = issues.filter((i) => i.severity === 'error');
|
|
259
|
+
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
260
|
+
const notices = issues.filter((i) => i.severity === 'notice');
|
|
261
|
+
for (const issue of notices)
|
|
262
|
+
info(`NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
263
|
+
for (const issue of warnings)
|
|
264
|
+
warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
|
|
265
|
+
for (const issue of errors)
|
|
266
|
+
warn(`ERROR [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
267
|
+
info(`\nLint (per-patch over ${linted} patch(es)): ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
268
|
+
if (errors.length > 0) {
|
|
269
|
+
outro('Lint failed');
|
|
270
|
+
throw new GeneralError(`Patch lint found ${errors.length} error(s) across ${linted} patch(es). Fix these before exporting.`);
|
|
271
|
+
}
|
|
272
|
+
outro('Lint passed with warnings');
|
|
273
|
+
}
|
|
167
274
|
/** Registers the lint command on the CLI program. */
|
|
168
275
|
export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
169
276
|
program
|
|
170
277
|
.command('lint [paths...]')
|
|
171
|
-
.description('Lint engine changes against patch quality rules'
|
|
278
|
+
.description('Lint engine changes against patch quality rules. Default: aggregate diff against HEAD ' +
|
|
279
|
+
'(every applied patch summed). Use --per-patch for per-patch scope, or pass explicit ' +
|
|
280
|
+
'file paths to narrow to those.')
|
|
172
281
|
.option('--since <git-rev>', 'Tag issues as [introduced] or [cumulative] based on whether the file changed since <git-rev> (e.g. HEAD, a branch, a SHA)')
|
|
173
282
|
.option('--only-introduced', 'Fail only on issues tagged [introduced] (requires --since). Cumulative errors still print but do not set a non-zero exit.')
|
|
283
|
+
.option('--per-patch', "Lint each patch in the queue as its own isolated diff. Rescopes patch-size rules so they fire against individual patches rather than the aggregate. Honours each patch's `lintIgnore` entries.")
|
|
174
284
|
.action(withErrorHandling(async (paths, options) => {
|
|
175
285
|
const lintOptions = {};
|
|
176
286
|
if (options.since !== undefined) {
|
|
@@ -179,6 +289,9 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
179
289
|
if (options.onlyIntroduced !== undefined) {
|
|
180
290
|
lintOptions.onlyIntroduced = options.onlyIntroduced;
|
|
181
291
|
}
|
|
292
|
+
if (options.perPatch !== undefined) {
|
|
293
|
+
lintOptions.perPatch = options.perPatch;
|
|
294
|
+
}
|
|
182
295
|
await lintCommand(getProjectRoot(), paths, lintOptions);
|
|
183
296
|
}));
|
|
184
297
|
}
|
|
@@ -71,8 +71,12 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
|
|
|
71
71
|
'Remove those paths from --files or modify them before retrying.', '--files');
|
|
72
72
|
}
|
|
73
73
|
// Run the per-patch lint against the projected diff. This mirrors what
|
|
74
|
-
// runPatchLint does in the standard re-export path.
|
|
75
|
-
|
|
74
|
+
// runPatchLint does in the standard re-export path. The target patch's
|
|
75
|
+
// `lintIgnore` threads through so a shrink of an advisory-noisy-but-
|
|
76
|
+
// intentional patch (branding bundle, localised-resource pack) does not
|
|
77
|
+
// have to choose between `--skip-lint` (blunt) and the full rebase path.
|
|
78
|
+
const ignoreChecks = target.lintIgnore?.length ? new Set(target.lintIgnore) : undefined;
|
|
79
|
+
await runPatchLint(paths.engine, actualProjectedFiles, projectedDiff, config, options.skipLint, undefined, ignoreChecks);
|
|
76
80
|
// Project the cross-patch context: replace the target entry with its
|
|
77
81
|
// would-be shrunken self (new diff + new newFiles + new
|
|
78
82
|
// modifiedFileAdditions). The projected entry must repopulate both
|
|
@@ -6,7 +6,7 @@ import { isGitRepository } from '../core/git.js';
|
|
|
6
6
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
7
7
|
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
8
8
|
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
9
|
-
import { getClaimedFiles, loadPatchesManifest, resolvePatchIdentifier, } from '../core/patch-manifest.js';
|
|
9
|
+
import { getClaimedFiles, loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
10
10
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
11
11
|
import { toError } from '../utils/errors.js';
|
|
12
12
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -97,7 +97,14 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
97
97
|
warn(`Skipped ${patch.filename}: no changes (files unchanged from HEAD)`);
|
|
98
98
|
return false;
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
// Thread the patch's own `lintIgnore` list through so the per-patch
|
|
101
|
+
// suppression honoured by export/export-all is also honoured here.
|
|
102
|
+
// Without this, `re-export` could not refresh an advisory-noisy but
|
|
103
|
+
// intentional patch (a cohesive branding bundle, a localised-resource
|
|
104
|
+
// pack) without either `--skip-lint` (too blunt) or falling through to
|
|
105
|
+
// the full `rebase` flow (which internally skips the lint pipeline).
|
|
106
|
+
const ignoreChecks = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
107
|
+
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, undefined, ignoreChecks);
|
|
101
108
|
if (isDryRun) {
|
|
102
109
|
info(`[dry-run] ${patch.filename}: ${existingFiles.length} file(s)`);
|
|
103
110
|
}
|
|
@@ -219,13 +226,16 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
219
226
|
}
|
|
220
227
|
const config = await loadConfig(projectRoot);
|
|
221
228
|
let reExported = 0;
|
|
229
|
+
const reExportedFilenames = [];
|
|
222
230
|
const progress = spinner('Preparing re-export...');
|
|
223
231
|
for (const patch of selectedPatches) {
|
|
224
232
|
progress.message(`Re-exporting ${patch.filename}...`);
|
|
225
233
|
try {
|
|
226
234
|
const exported = await reExportSinglePatch(patch, paths, manifest, options, isDryRun, config);
|
|
227
|
-
if (exported)
|
|
235
|
+
if (exported) {
|
|
228
236
|
reExported++;
|
|
237
|
+
reExportedFilenames.push(patch.filename);
|
|
238
|
+
}
|
|
229
239
|
}
|
|
230
240
|
catch (error) {
|
|
231
241
|
warn(`Failed to re-export ${patch.filename}`);
|
|
@@ -236,14 +246,34 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
236
246
|
progress.error('Re-export failed');
|
|
237
247
|
throw new GeneralError('All selected patches failed to re-export. Check the errors above.');
|
|
238
248
|
}
|
|
249
|
+
// `--stamp` only fires on a run where every selected patch refreshed
|
|
250
|
+
// cleanly. A partial success would leave some patches with a stale body
|
|
251
|
+
// but a new version — the opposite of the "what I tested, what the
|
|
252
|
+
// manifest says" invariant `sourceEsrVersion` exists to record. A
|
|
253
|
+
// non-empty `reExportedFilenames` with fewer entries than `selectedPatches`
|
|
254
|
+
// means a lint failure or missing-file skip landed somewhere in the loop,
|
|
255
|
+
// which we refuse to version-stamp through.
|
|
256
|
+
const shouldStamp = options.stamp === true && !isDryRun && reExported > 0 && reExported === selectedPatches.length;
|
|
257
|
+
if (shouldStamp) {
|
|
258
|
+
await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version);
|
|
259
|
+
}
|
|
239
260
|
if (isDryRun) {
|
|
240
261
|
progress.stop('Dry run complete');
|
|
241
262
|
success(`[dry-run] Would re-export ${reExported} of ${selectedPatches.length} patch(es)`);
|
|
263
|
+
if (options.stamp === true) {
|
|
264
|
+
info(`[dry-run] Would stamp sourceEsrVersion=${config.firefox.version} on ${reExported} patch(es)`);
|
|
265
|
+
}
|
|
242
266
|
outro('Dry run complete');
|
|
243
267
|
}
|
|
244
268
|
else {
|
|
245
269
|
progress.stop('Re-export complete');
|
|
246
270
|
success(`Re-exported ${reExported} of ${selectedPatches.length} patch(es)`);
|
|
271
|
+
if (shouldStamp) {
|
|
272
|
+
success(`Stamped sourceEsrVersion=${config.firefox.version} on ${reExportedFilenames.length} patch(es)`);
|
|
273
|
+
}
|
|
274
|
+
else if (options.stamp === true && reExported !== selectedPatches.length) {
|
|
275
|
+
warn('--stamp was requested but some patches failed or were skipped; refusing to stamp a partial set.');
|
|
276
|
+
}
|
|
247
277
|
outro('Re-export complete');
|
|
248
278
|
}
|
|
249
279
|
}
|
|
@@ -251,7 +281,9 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
251
281
|
export function registerReExport(program, { getProjectRoot, withErrorHandling }) {
|
|
252
282
|
program
|
|
253
283
|
.command('re-export [patches...]')
|
|
254
|
-
.description('
|
|
284
|
+
.description('Refresh existing patch bodies (and filesAffected with --scan) from the current engine ' +
|
|
285
|
+
'state. Does NOT change sourceEsrVersion by default — use --stamp or run rebase for ' +
|
|
286
|
+
'version stamping.')
|
|
255
287
|
.option('-a, --all', 'Re-export all patches')
|
|
256
288
|
.option('-s, --scan', 'Scan directories for new/removed files and update filesAffected')
|
|
257
289
|
.option('--files <paths>', 'Restrict the re-exported filesAffected to this comma-separated list (single target patch only)', (value) => value
|
|
@@ -262,6 +294,7 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
262
294
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
263
295
|
.option('-y, --yes', 'Skip confirmation when --files shrinks a patch (required for non-TTY)')
|
|
264
296
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
297
|
+
.option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's sourceEsrVersion in patches.json to firefox.version from fireforge.json. No effect on a partial run.")
|
|
265
298
|
.action(withErrorHandling(async (patches, options) => {
|
|
266
299
|
await reExportCommand(getProjectRoot(), patches, pickDefined(options));
|
|
267
300
|
}));
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { CommandContext } from '../types/cli.js';
|
|
3
|
+
import type { RunOptions } from '../types/commands/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Exit code returned by smoke-run mode when the captured console stream
|
|
6
|
+
* produced one or more error lines that did NOT match the operator's
|
|
7
|
+
* allowlist.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SMOKE_EXIT_FAILURE: 12;
|
|
10
|
+
/**
|
|
11
|
+
* Exit code returned by smoke-run mode when the browser itself exited
|
|
12
|
+
* with a non-clean status before the smoke window elapsed — i.e. a
|
|
13
|
+
* launch-side failure we could NOT observe as a console error line
|
|
14
|
+
* (crash before console wiring, missing profile, etc.).
|
|
15
|
+
*/
|
|
16
|
+
export declare const SMOKE_LAUNCH_FAILURE: 13;
|
|
3
17
|
/**
|
|
4
18
|
* Runs the run command to launch the built browser.
|
|
5
19
|
* @param projectRoot - Root directory of the project
|
|
6
20
|
*/
|
|
7
|
-
export declare function runCommand(projectRoot: string): Promise<void>;
|
|
21
|
+
export declare function runCommand(projectRoot: string, options?: RunOptions): Promise<void>;
|
|
8
22
|
/** Registers the run command on the CLI program. */
|
|
9
23
|
export declare function registerRun(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
|