@hominis/fireforge 0.17.0 → 0.18.1
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 +53 -0
- package/README.md +60 -33
- package/dist/src/commands/build.js +18 -4
- package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +18 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +159 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/doctor-working-tree.d.ts +29 -0
- package/dist/src/commands/doctor-working-tree.js +93 -0
- package/dist/src/commands/doctor.js +22 -12
- package/dist/src/commands/export-all.js +74 -4
- package/dist/src/commands/export-shared.d.ts +7 -1
- package/dist/src/commands/export-shared.js +21 -3
- package/dist/src/commands/furnace/create-xpcshell.js +4 -2
- package/dist/src/commands/furnace/override.js +23 -13
- package/dist/src/commands/furnace/preview.js +38 -0
- package/dist/src/commands/furnace/remove.js +75 -1
- package/dist/src/commands/furnace/rename-xpcshell.d.ts +35 -0
- package/dist/src/commands/furnace/rename-xpcshell.js +97 -0
- package/dist/src/commands/furnace/rename.js +32 -4
- package/dist/src/commands/lint.js +19 -6
- package/dist/src/commands/patch/delete.js +4 -1
- package/dist/src/commands/patch/reorder.js +4 -1
- package/dist/src/commands/re-export-files.js +3 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/rebase/index.js +19 -1
- package/dist/src/commands/register.js +11 -0
- package/dist/src/commands/status.js +44 -5
- package/dist/src/commands/test.js +68 -16
- package/dist/src/commands/token-coverage.js +10 -3
- package/dist/src/commands/verify.js +81 -6
- package/dist/src/commands/watch.js +43 -7
- package/dist/src/commands/wire.js +16 -0
- package/dist/src/core/browser-wire.js +21 -4
- package/dist/src/core/build-audit.js +10 -0
- package/dist/src/core/furnace-constants.d.ts +14 -0
- package/dist/src/core/furnace-constants.js +16 -0
- package/dist/src/core/furnace-validate.js +67 -1
- package/dist/src/core/git-base.d.ts +27 -2
- package/dist/src/core/git-base.js +41 -3
- package/dist/src/core/git-diff.js +21 -2
- package/dist/src/core/git.js +53 -14
- package/dist/src/core/mach.d.ts +26 -8
- package/dist/src/core/mach.js +24 -8
- package/dist/src/core/manifest-rules.js +10 -1
- package/dist/src/core/manifest-tokenizers.d.ts +6 -0
- package/dist/src/core/manifest-tokenizers.js +28 -0
- package/dist/src/core/marionette-preflight.d.ts +16 -0
- package/dist/src/core/marionette-preflight.js +19 -0
- package/dist/src/core/patch-lint-diff-tag.d.ts +20 -0
- package/dist/src/core/patch-lint-diff-tag.js +25 -0
- package/dist/src/core/patch-lint.d.ts +47 -2
- package/dist/src/core/patch-lint.js +94 -18
- package/dist/src/core/patch-manifest-consistency.js +15 -2
- package/dist/src/core/patch-manifest-io.js +10 -0
- package/dist/src/core/patch-manifest-resolve.d.ts +20 -1
- package/dist/src/core/patch-manifest-resolve.js +29 -2
- package/dist/src/core/patch-manifest-validate.js +25 -1
- package/dist/src/core/patch-registration-refs.d.ts +42 -0
- package/dist/src/core/patch-registration-refs.js +117 -0
- package/dist/src/core/token-coverage.js +24 -0
- package/dist/src/core/wire-destroy.d.ts +7 -3
- package/dist/src/core/wire-destroy.js +11 -6
- package/dist/src/core/wire-init.d.ts +9 -3
- package/dist/src/core/wire-init.js +18 -6
- package/dist/src/core/wire-subscript.d.ts +7 -3
- package/dist/src/core/wire-subscript.js +11 -4
- package/dist/src/core/xpcshell-appdir.d.ts +19 -5
- package/dist/src/core/xpcshell-appdir.js +46 -20
- package/dist/src/errors/git.d.ts +20 -0
- package/dist/src/errors/git.js +39 -0
- package/dist/src/types/commands/patches.d.ts +23 -0
- package/dist/src/types/furnace.d.ts +9 -0
- package/dist/src/utils/parse.d.ts +7 -0
- package/dist/src/utils/parse.js +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Ownership-aware working-tree check for `fireforge doctor`.
|
|
4
|
+
*
|
|
5
|
+
* Partitions engine-tree dirtiness into `branding`, `patch-backed`,
|
|
6
|
+
* `furnace`, `conflict`, and `unmanaged` buckets, and only warns on the
|
|
7
|
+
* last two — everything else is tool-managed state that the operator
|
|
8
|
+
* did not author directly.
|
|
9
|
+
*
|
|
10
|
+
* Split out of `doctor.ts` so that file stays under the per-file LOC
|
|
11
|
+
* budget; see the call site in `runEngineGitChecks`.
|
|
12
|
+
*/
|
|
13
|
+
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
14
|
+
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
15
|
+
import { classifyFiles } from '../core/status-classify.js';
|
|
16
|
+
import { ok, warning } from './doctor.js';
|
|
17
|
+
function summarizeWorkingTreeChangeCount(changeCount) {
|
|
18
|
+
return `Engine working tree has ${changeCount} local change${changeCount === 1 ? '' : 's'}. Some FireForge commands assume a clean baseline and may behave differently until these are exported, discarded, or committed.`;
|
|
19
|
+
}
|
|
20
|
+
function formatManagedDetail(counts) {
|
|
21
|
+
return [
|
|
22
|
+
counts.patchBacked > 0 ? `${counts.patchBacked} patch-backed` : null,
|
|
23
|
+
counts.branding > 0 ? `${counts.branding} branding` : null,
|
|
24
|
+
counts.furnace > 0 ? `${counts.furnace} furnace` : null,
|
|
25
|
+
]
|
|
26
|
+
.filter((part) => part !== null)
|
|
27
|
+
.join(', ');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Inspects the engine working tree and returns a single
|
|
31
|
+
* `DoctorCheck`. Ownership-aware: patch-backed / branding / furnace
|
|
32
|
+
* rows are reported as OK with an ownership summary; unmanaged drift
|
|
33
|
+
* warns; cross-patch conflicts warn loudly with a pointer at
|
|
34
|
+
* `fireforge status --ownership` + `fireforge verify`.
|
|
35
|
+
*
|
|
36
|
+
* Before 0.16.1 this check warned on every dirty row regardless of
|
|
37
|
+
* ownership and told the operator to export/discard/reset — advice
|
|
38
|
+
* that was actively destructive on a patch-backed import (eval
|
|
39
|
+
* Finding: a correctly imported 126-file patch stack was reported as
|
|
40
|
+
* unhealthy and the suggested fix would have dropped the entire
|
|
41
|
+
* import). Returns `undefined` when the worktree is clean so the
|
|
42
|
+
* caller can emit its own ok() row.
|
|
43
|
+
*/
|
|
44
|
+
export async function inspectEngineWorkingTree(ctx) {
|
|
45
|
+
const { paths } = ctx;
|
|
46
|
+
const rawStatus = await getWorkingTreeStatus(paths.engine);
|
|
47
|
+
const workingTreeStatus = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
|
|
48
|
+
if (workingTreeStatus.length === 0) {
|
|
49
|
+
return ok('Engine working tree');
|
|
50
|
+
}
|
|
51
|
+
if (!ctx.config) {
|
|
52
|
+
return warning('Engine working tree', summarizeWorkingTreeChangeCount(workingTreeStatus.length), 'Use "fireforge status" to review changes, then export, discard, or reset them as appropriate.');
|
|
53
|
+
}
|
|
54
|
+
const furnacePrefixes = await collectFurnaceManagedPrefixes(ctx.projectRoot);
|
|
55
|
+
const classified = await classifyFiles(workingTreeStatus.map((entry) => ({ status: entry.status, file: entry.file })), paths.engine, paths.patches, ctx.config.binaryName, furnacePrefixes);
|
|
56
|
+
const counts = {
|
|
57
|
+
branding: 0,
|
|
58
|
+
furnace: 0,
|
|
59
|
+
patchBacked: 0,
|
|
60
|
+
conflict: 0,
|
|
61
|
+
unmanaged: 0,
|
|
62
|
+
};
|
|
63
|
+
for (const entry of classified) {
|
|
64
|
+
if (entry.classification === 'branding')
|
|
65
|
+
counts.branding++;
|
|
66
|
+
else if (entry.classification === 'furnace')
|
|
67
|
+
counts.furnace++;
|
|
68
|
+
else if (entry.classification === 'patch-backed')
|
|
69
|
+
counts.patchBacked++;
|
|
70
|
+
else if (entry.classification === 'conflict')
|
|
71
|
+
counts.conflict++;
|
|
72
|
+
else
|
|
73
|
+
counts.unmanaged++;
|
|
74
|
+
}
|
|
75
|
+
if (counts.conflict > 0) {
|
|
76
|
+
return warning('Engine working tree', `Engine working tree has ${counts.conflict} cross-patch ownership conflict${counts.conflict === 1 ? '' : 's'}. Multiple patches in patches.json claim the same file.`, 'Run "fireforge status --ownership" to see the conflicting patches, then run "fireforge verify" and resolve the overlap.');
|
|
77
|
+
}
|
|
78
|
+
const managedTotal = counts.branding + counts.furnace + counts.patchBacked;
|
|
79
|
+
if (counts.unmanaged === 0) {
|
|
80
|
+
const managedDetail = formatManagedDetail(counts);
|
|
81
|
+
return {
|
|
82
|
+
name: 'Engine working tree',
|
|
83
|
+
passed: true,
|
|
84
|
+
severity: 'ok',
|
|
85
|
+
message: `${managedTotal} tool-managed change${managedTotal === 1 ? '' : 's'} (${managedDetail}), 0 unmanaged. Use "fireforge status --ownership" for details.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const managedTail = managedTotal > 0
|
|
89
|
+
? ` (${managedTotal} other change${managedTotal === 1 ? '' : 's'} are tool-managed: ${formatManagedDetail(counts)}).`
|
|
90
|
+
: '';
|
|
91
|
+
return warning('Engine working tree', `Engine working tree has ${counts.unmanaged} unmanaged change${counts.unmanaged === 1 ? '' : 's'}.${managedTail}`, 'Use "fireforge status --ownership" to separate patch-backed from unmanaged files, then export, discard, or reset only the unmanaged set.');
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=doctor-working-tree.js.map
|
|
@@ -2,7 +2,6 @@ import { configExists, getProjectPaths, loadConfig, loadState } from '../core/co
|
|
|
2
2
|
import { furnaceConfigExists as checkFurnaceConfigExists } from '../core/furnace-config.js';
|
|
3
3
|
import { getCurrentBranch, getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
|
|
4
4
|
import { ensureGit } from '../core/git-base.js';
|
|
5
|
-
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
6
5
|
import { ensureMach, ensurePython } from '../core/mach.js';
|
|
7
6
|
import { countPatches } from '../core/patch-apply.js';
|
|
8
7
|
import { rebuildPatchesManifest, validatePatchesManifestConsistency, validatePatchIntegrity, } from '../core/patch-manifest.js';
|
|
@@ -12,6 +11,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
12
11
|
import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
13
12
|
import { executableExists } from '../utils/process.js';
|
|
14
13
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
14
|
+
import { inspectEngineWorkingTree } from './doctor-working-tree.js';
|
|
15
15
|
/**
|
|
16
16
|
* Builds a DoctorCheck object representing a successful "OK" check.
|
|
17
17
|
* Exported for sibling check modules that declare `DoctorCheckDefinition`
|
|
@@ -58,9 +58,6 @@ async function executeCheck(definition, ctx) {
|
|
|
58
58
|
return [failure(definition.name, toError(err).message, definition.fix)];
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
function summarizeWorkingTreeChangeCount(changeCount) {
|
|
62
|
-
return `Engine working tree has ${changeCount} local change${changeCount === 1 ? '' : 's'}. Some FireForge commands assume a clean baseline and may behave differently until these are exported, discarded, or committed.`;
|
|
63
|
-
}
|
|
64
61
|
/**
|
|
65
62
|
* Runs the subset of engine checks that depend on a healthy git repository
|
|
66
63
|
* and HEAD. This group shares mutable state (currentHead, canValidateBranch),
|
|
@@ -89,13 +86,9 @@ async function runEngineGitChecks(ctx) {
|
|
|
89
86
|
rows.push(ok('Engine state consistency'));
|
|
90
87
|
}
|
|
91
88
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
rows.push(warning('Engine working tree', summarizeWorkingTreeChangeCount(workingTreeStatus.length), 'Use "fireforge status" to review changes, then export, discard, or reset them as appropriate.'));
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
rows.push(ok('Engine working tree'));
|
|
89
|
+
const workingTreeRow = await inspectEngineWorkingTree(ctx);
|
|
90
|
+
if (workingTreeRow) {
|
|
91
|
+
rows.push(workingTreeRow);
|
|
99
92
|
}
|
|
100
93
|
let branch;
|
|
101
94
|
if (canValidateBranch) {
|
|
@@ -227,6 +220,11 @@ const DOCTOR_CHECKS = [
|
|
|
227
220
|
{
|
|
228
221
|
name: 'Engine is git repository',
|
|
229
222
|
skipIf: (ctx) => !ctx.engineExists,
|
|
223
|
+
// runEngineGitChecks consults ctx.config for ownership-aware
|
|
224
|
+
// working-tree classification; declare the dependency so a future
|
|
225
|
+
// reorder doesn't silently regress the doctor back to the
|
|
226
|
+
// count-only fallback.
|
|
227
|
+
dependsOn: ['fireforge.json is valid'],
|
|
230
228
|
run: async (ctx) => {
|
|
231
229
|
const isRepo = await isGitRepository(ctx.paths.engine);
|
|
232
230
|
if (!isRepo) {
|
|
@@ -324,7 +322,19 @@ const DOCTOR_CHECKS = [
|
|
|
324
322
|
// (only `filesAffected` / ordering drifted) are not flagged.
|
|
325
323
|
if (repaired.recoveredFilenames.length > 0) {
|
|
326
324
|
for (const filename of repaired.recoveredFilenames) {
|
|
327
|
-
|
|
325
|
+
// 2026-04-24 eval Finding 6: the repair path used to tell the
|
|
326
|
+
// operator to hand-edit patches.json, which contradicts the
|
|
327
|
+
// README + Hominis docs that treat the manifest as
|
|
328
|
+
// FireForge-owned. Point at the existing `re-export` /
|
|
329
|
+
// `export` workflow instead so the fix stays inside the tool:
|
|
330
|
+
// re-exporting the same files with an explicit `--description`
|
|
331
|
+
// overwrites the recovered entry with operator-supplied
|
|
332
|
+
// metadata and supersedes the mtime-based createdAt stamp.
|
|
333
|
+
warn(`Recovered manifest entry for ${filename} with generic description and mtime-based createdAt. ` +
|
|
334
|
+
'Re-export the affected files with `fireforge re-export <filename> --description "<your description>"` ' +
|
|
335
|
+
'(or `fireforge export <paths...> --name <name> --category <category> --description "<your description>"`) ' +
|
|
336
|
+
'to overwrite the reconstructed metadata, or accept the generic description if the original text is not recoverable. ' +
|
|
337
|
+
'Avoid hand-editing patches.json — FireForge owns that file and will regenerate it on the next manifest consistency pass.');
|
|
328
338
|
}
|
|
329
339
|
}
|
|
330
340
|
return warning('Patch manifest consistency', `Rebuilt patches.json from ${repaired.manifest.patches.length} patch${repaired.manifest.patches.length === 1 ? '' : 'es'}${repaired.recoveredFilenames.length > 0 ? ` (${repaired.recoveredFilenames.length} with reconstructed metadata — see warnings above)` : ''}. Review recovered metadata before release.`);
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import { Option } from 'commander';
|
|
3
3
|
import { isBrandingManagedPath } from '../core/branding.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
-
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
5
|
+
import { collectFurnaceManagedPrefixes, furnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
|
|
6
6
|
import { hasChanges, isGitRepository } from '../core/git.js';
|
|
7
7
|
import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
8
|
-
import { getWorkingTreeStatus } from '../core/git-status.js';
|
|
8
|
+
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
9
9
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
10
10
|
import { commitExportedPatch, findAllPatchesForFiles } from '../core/patch-export.js';
|
|
11
11
|
import { buildPatchQueueContext, collectNewFileCreatorsByPath, detectNewFilesInDiff, } from '../core/patch-lint.js';
|
|
12
|
+
import { collectPatchRegistrationReferences } from '../core/patch-registration-refs.js';
|
|
12
13
|
import { GeneralError } from '../errors/base.js';
|
|
13
14
|
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
14
15
|
import { info, intro, outro, spinner } from '../utils/logger.js';
|
|
@@ -41,7 +42,14 @@ async function resolveFurnaceExclusionPolicy(paths, projectRoot, excludeFurnace)
|
|
|
41
42
|
const prefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
42
43
|
if (prefixes.size === 0)
|
|
43
44
|
return new Set();
|
|
44
|
-
|
|
45
|
+
// Expand collapsed `?? dir/` entries before matching against Furnace
|
|
46
|
+
// prefixes — otherwise a Furnace-introduced directory slips past the
|
|
47
|
+
// filter and later lands in the non-Furnace path list that feeds the
|
|
48
|
+
// aggregate diff, where `getDiffForFilesAgainstHead` crashes with
|
|
49
|
+
// EISDIR (eval finding: export-all unusable on a fresh project with
|
|
50
|
+
// Furnace scaffolding).
|
|
51
|
+
const rawStatus = await getWorkingTreeStatus(paths.engine);
|
|
52
|
+
const changedFiles = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
|
|
45
53
|
const furnaceManagedFiles = changedFiles
|
|
46
54
|
.flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
|
|
47
55
|
.filter((file) => [...prefixes].some((prefix) => file.startsWith(prefix)));
|
|
@@ -55,6 +63,62 @@ async function resolveFurnaceExclusionPolicy(paths, projectRoot, excludeFurnace)
|
|
|
55
63
|
'Review them with "fireforge status" or "fireforge furnace status", ' +
|
|
56
64
|
'or pass --exclude-furnace to export the non-Furnace subset of the diff.');
|
|
57
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Refuses the export when the resulting patch would register furnace
|
|
68
|
+
* component source files it does not itself carry. 2026-04-24 eval
|
|
69
|
+
* Finding 1: operators running `export-all --exclude-furnace` after
|
|
70
|
+
* `furnace create --localized --with-tests` ended up with patches that
|
|
71
|
+
* added `toolkit/content/widgets/moz-qa-panel/*` via jar.mn /
|
|
72
|
+
* customElements.js / locale jar.mn but excluded the component source
|
|
73
|
+
* files themselves. The resulting patch queue was structurally broken
|
|
74
|
+
* and `fireforge verify` stayed silent. We now detect the condition
|
|
75
|
+
* pre-write and ask the operator to either include the component
|
|
76
|
+
* sources (skip `--exclude-furnace`) or revert the furnace changes
|
|
77
|
+
* before exporting.
|
|
78
|
+
*
|
|
79
|
+
* The check runs against the synthesised patch body before
|
|
80
|
+
* `commitExportedPatch` writes anything, so no broken patch is left on
|
|
81
|
+
* disk when the refusal fires.
|
|
82
|
+
*/
|
|
83
|
+
async function checkDanglingFurnaceRegistrations(projectRoot, diff, furnaceExcluded) {
|
|
84
|
+
if (furnaceExcluded.size === 0)
|
|
85
|
+
return;
|
|
86
|
+
if (!(await furnaceConfigExists(projectRoot)))
|
|
87
|
+
return;
|
|
88
|
+
const refs = collectPatchRegistrationReferences(diff);
|
|
89
|
+
if (refs.length === 0)
|
|
90
|
+
return;
|
|
91
|
+
const config = await loadFurnaceConfig(projectRoot);
|
|
92
|
+
// Build the set of furnace-managed component names so we can tell
|
|
93
|
+
// "registers moz-qa-panel (furnace-managed)" apart from "registers
|
|
94
|
+
// moz-button (an upstream widget this patch legitimately touches)".
|
|
95
|
+
const furnaceComponentNames = new Set([
|
|
96
|
+
...Object.keys(config.custom),
|
|
97
|
+
...Object.keys(config.overrides),
|
|
98
|
+
...config.stock,
|
|
99
|
+
]);
|
|
100
|
+
const dangling = [];
|
|
101
|
+
for (const ref of refs) {
|
|
102
|
+
if (!furnaceExcluded.has(ref.targetPath))
|
|
103
|
+
continue;
|
|
104
|
+
const tagMatch = /toolkit\/content\/widgets\/([a-z][a-z0-9-]*)\//.exec(ref.targetPath);
|
|
105
|
+
const ftlMatch = /toolkit\/locales\/en-US\/toolkit\/global\/([a-z][a-z0-9-]*)\.ftl$/.exec(ref.targetPath);
|
|
106
|
+
const component = tagMatch?.[1] ?? ftlMatch?.[1];
|
|
107
|
+
if (!component || !furnaceComponentNames.has(component))
|
|
108
|
+
continue;
|
|
109
|
+
dangling.push({ component, targetPath: ref.targetPath, source: ref.source });
|
|
110
|
+
}
|
|
111
|
+
if (dangling.length === 0)
|
|
112
|
+
return;
|
|
113
|
+
const summary = dangling
|
|
114
|
+
.map((d) => ` • ${d.component} — registered via ${d.source} → ${d.targetPath}`)
|
|
115
|
+
.join('\n');
|
|
116
|
+
throw new GeneralError('Export-all --exclude-furnace would produce a patch that registers furnace-managed components without including their source files.\n\n' +
|
|
117
|
+
`Dangling registrations:\n${summary}\n\n` +
|
|
118
|
+
'To proceed, either:\n' +
|
|
119
|
+
' 1. Drop the --exclude-furnace flag so the source files are captured alongside the registration edits.\n' +
|
|
120
|
+
' 2. Revert the registration hunks (or the whole furnace workflow) before re-running export-all — registrations belong with their components, and splitting them across separate patches is what "verify" catches post-hoc as a dangling-registration error.');
|
|
121
|
+
}
|
|
58
122
|
/**
|
|
59
123
|
* Refuses the export when the aggregate diff would create (new-file-mode) a
|
|
60
124
|
* path that some existing patch in the queue already creates. `verify`
|
|
@@ -129,7 +193,8 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
129
193
|
// output shape aligned with the single-file `export` command.
|
|
130
194
|
let diff;
|
|
131
195
|
if (furnaceExcluded.size > 0) {
|
|
132
|
-
const
|
|
196
|
+
const rawChanged = await getWorkingTreeStatus(paths.engine);
|
|
197
|
+
const allChanged = await expandUntrackedDirectoryEntries(paths.engine, rawChanged);
|
|
133
198
|
const nonFurnacePaths = [
|
|
134
199
|
...new Set(allChanged
|
|
135
200
|
.flatMap((entry) => [entry.file, entry.originalPath].filter((value) => !!value))
|
|
@@ -155,6 +220,11 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
155
220
|
// the aggregate would newly create, so it runs here instead of alongside
|
|
156
221
|
// the branding / furnace guards that operate on the raw status list.
|
|
157
222
|
await checkDuplicateNewFileCreations(paths, diff);
|
|
223
|
+
// Dangling-furnace-registration preflight (Finding 1). Runs after the
|
|
224
|
+
// diff is assembled so we can inspect the exact hunks the operator is
|
|
225
|
+
// about to land; runs BEFORE any write so a refusal leaves the
|
|
226
|
+
// patches directory untouched.
|
|
227
|
+
await checkDanglingFurnaceRegistrations(projectRoot, diff, furnaceExcluded);
|
|
158
228
|
// Check for non-interactive mode
|
|
159
229
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
160
230
|
// Auto-fix missing license headers on new files (interactive only)
|
|
@@ -17,8 +17,14 @@ import type { SpinnerHandle } from '../utils/logger.js';
|
|
|
17
17
|
* `--skip-lint` when exactly one advisory rule does not apply to a
|
|
18
18
|
* specific patch — e.g. `large-patch-lines` on a cohesive branding
|
|
19
19
|
* bundle that genuinely cannot be split.
|
|
20
|
+
* @param patchTier - Optional explicit tier override (threaded from
|
|
21
|
+
* `PatchMetadata.tier`). Forces the branding-tier thresholds when
|
|
22
|
+
* set, independent of the auto-detect allowlist. When the branding
|
|
23
|
+
* tier is applied (either via this opt-in or the auto-detect), a
|
|
24
|
+
* single `info()` line surfaces the choice so the tier decision is
|
|
25
|
+
* visible rather than silent.
|
|
20
26
|
*/
|
|
21
|
-
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string
|
|
27
|
+
export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext, ignoreChecks?: ReadonlySet<string>, patchTier?: 'branding'): Promise<void>;
|
|
22
28
|
/**
|
|
23
29
|
* Resolves patch metadata interactively or from flags, with shared validation.
|
|
24
30
|
* @param options - Export command options
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { confirm, select, text } from '@clack/prompts';
|
|
4
4
|
import { addLicenseHeaderToFile, getLicenseHeader } from '../core/license-headers.js';
|
|
5
5
|
import { findAllPatchesForFiles } from '../core/patch-export.js';
|
|
6
|
-
import { commentStyleForFile, detectNewFilesInDiff, lintExportedPatch, } from '../core/patch-lint.js';
|
|
6
|
+
import { commentStyleForFile, detectNewFilesInDiff, lintExportedPatch, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
7
7
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
8
8
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
9
9
|
import { pathExists, readText } from '../utils/fs.js';
|
|
@@ -24,9 +24,27 @@ import { isValidPatchCategory, PATCH_CATEGORIES, validatePatchName } from '../ut
|
|
|
24
24
|
* `--skip-lint` when exactly one advisory rule does not apply to a
|
|
25
25
|
* specific patch — e.g. `large-patch-lines` on a cohesive branding
|
|
26
26
|
* bundle that genuinely cannot be split.
|
|
27
|
+
* @param patchTier - Optional explicit tier override (threaded from
|
|
28
|
+
* `PatchMetadata.tier`). Forces the branding-tier thresholds when
|
|
29
|
+
* set, independent of the auto-detect allowlist. When the branding
|
|
30
|
+
* tier is applied (either via this opt-in or the auto-detect), a
|
|
31
|
+
* single `info()` line surfaces the choice so the tier decision is
|
|
32
|
+
* visible rather than silent.
|
|
27
33
|
*/
|
|
28
|
-
export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx, ignoreChecks) {
|
|
29
|
-
|
|
34
|
+
export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx, ignoreChecks, patchTier) {
|
|
35
|
+
// Compute the tier decision independently of the lint pipeline so the
|
|
36
|
+
// decision can be surfaced even when the rule body emitted no issues
|
|
37
|
+
// (e.g. a branding patch under the soft threshold still benefits from
|
|
38
|
+
// operators knowing which tier governed the run). The same helper is
|
|
39
|
+
// reused inside `lintPatchSize`, so the surfaced tier and the tier
|
|
40
|
+
// that actually drove the thresholds never drift.
|
|
41
|
+
const tierDecision = resolvePatchSizeTier(filesAffected, patchTier);
|
|
42
|
+
if (tierDecision.tier === 'branding') {
|
|
43
|
+
info(tierDecision.source === 'explicit'
|
|
44
|
+
? 'Lint: branding threshold tier applied via patches.json `tier: "branding"` opt-in.'
|
|
45
|
+
: 'Lint: branding threshold tier applied (patch is all under browser/branding/ plus registration siblings).');
|
|
46
|
+
}
|
|
47
|
+
const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config, patchQueueCtx, ignoreChecks, patchTier);
|
|
30
48
|
if (issues.length === 0)
|
|
31
49
|
return;
|
|
32
50
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* per-file LOC budget and the scaffolder is unit-testable in isolation.
|
|
6
6
|
*/
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
+
import { xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
8
9
|
import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
9
10
|
import { getLicenseHeader } from '../../core/license-headers.js';
|
|
10
11
|
import { ensureDir, pathExists, writeText } from '../../utils/fs.js';
|
|
@@ -26,8 +27,9 @@ import { generateXpcshellManifestContent, generateXpcshellTestContent, xpcshellT
|
|
|
26
27
|
* auto-insertion that guessed wrong would be worse than a note.
|
|
27
28
|
*/
|
|
28
29
|
export async function scaffoldXpcshellTestFiles(componentName, license, forgeConfig, paths, journal) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
30
|
+
const parentRelDir = xpcshellTestParentDir(forgeConfig.binaryName);
|
|
31
|
+
const parentDirName = parentRelDir.split('/').slice(-1)[0] ?? `${forgeConfig.binaryName}-xpcshell`;
|
|
32
|
+
const testDir = join(paths.engine, parentRelDir, componentName);
|
|
31
33
|
if (journal && !(await pathExists(testDir))) {
|
|
32
34
|
recordCreatedDir(journal, testDir);
|
|
33
35
|
}
|
|
@@ -79,17 +79,14 @@ async function copyOverrideFiles(engineDir, srcDir, destDir, componentName, hasF
|
|
|
79
79
|
return copiedFiles;
|
|
80
80
|
}
|
|
81
81
|
/**
|
|
82
|
-
* Writes override metadata to disk and updates furnace.json with the new
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* @param details - Source component metadata from the engine scan
|
|
89
|
-
* @param firefoxVersion - Firefox version recorded in the workspace config
|
|
90
|
-
* @param config - Mutable Furnace config object to update
|
|
82
|
+
* Writes override metadata to disk and updates furnace.json with the new
|
|
83
|
+
* override entry. Re-reads the current on-disk furnace.json inside the
|
|
84
|
+
* operation lock and splices the new entry onto the fresh state so two
|
|
85
|
+
* concurrent `furnace override` commands cannot race their read-modify
|
|
86
|
+
* -write cycles into a single surviving entry (eval 2: parallel overrides
|
|
87
|
+
* both reported success but furnace.json kept only the second writer).
|
|
91
88
|
*/
|
|
92
|
-
async function saveOverrideConfig(projectRoot, destDir, componentName, overrideType, description, details, firefoxVersion,
|
|
89
|
+
async function saveOverrideConfig(projectRoot, destDir, componentName, overrideType, description, details, firefoxVersion, journal, baseCommit) {
|
|
93
90
|
const overrideJson = {
|
|
94
91
|
type: overrideType,
|
|
95
92
|
description,
|
|
@@ -100,14 +97,27 @@ async function saveOverrideConfig(projectRoot, destDir, componentName, overrideT
|
|
|
100
97
|
const overrideJsonPath = join(destDir, 'override.json');
|
|
101
98
|
await snapshotFile(journal, overrideJsonPath);
|
|
102
99
|
await writeJson(overrideJsonPath, overrideJson);
|
|
103
|
-
|
|
100
|
+
// Re-read the current furnace.json inside the lock. The outer caller
|
|
101
|
+
// loaded a snapshot before entering `runFurnaceMutation`, but another
|
|
102
|
+
// furnace mutation (override / init / sync) may have landed in between
|
|
103
|
+
// — writing back the stale snapshot would drop that concurrent write.
|
|
104
|
+
const freshConfig = await loadAuthoringFurnaceConfig(projectRoot);
|
|
105
|
+
freshConfig.overrides[componentName] = {
|
|
104
106
|
type: overrideType,
|
|
105
107
|
description,
|
|
106
108
|
basePath: details.sourcePath,
|
|
107
109
|
baseVersion: firefoxVersion,
|
|
108
110
|
...(baseCommit ? { baseCommit } : {}),
|
|
109
111
|
};
|
|
110
|
-
|
|
112
|
+
// Promote from the stock bucket here, against the fresh state, so the
|
|
113
|
+
// stock→override transition survives even when another concurrent
|
|
114
|
+
// override already rewrote furnace.json between the outer read and
|
|
115
|
+
// this write.
|
|
116
|
+
const stockIndex = freshConfig.stock.indexOf(componentName);
|
|
117
|
+
if (stockIndex !== -1) {
|
|
118
|
+
freshConfig.stock.splice(stockIndex, 1);
|
|
119
|
+
}
|
|
120
|
+
await writeFurnaceConfig(projectRoot, freshConfig);
|
|
111
121
|
}
|
|
112
122
|
/**
|
|
113
123
|
* Performs the transactional mutation phase of furnace override under the
|
|
@@ -123,7 +133,7 @@ async function performOverrideMutations(args) {
|
|
|
123
133
|
try {
|
|
124
134
|
const filesCopied = await copyOverrideFiles(args.engineDir, args.srcDir, args.destDir, args.componentName, args.details.hasFTL, args.overrideType, args.ftlDir, journal);
|
|
125
135
|
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
126
|
-
await saveOverrideConfig(args.projectRoot, args.destDir, args.componentName, args.overrideType, args.description, args.details, args.firefoxVersion,
|
|
136
|
+
await saveOverrideConfig(args.projectRoot, args.destDir, args.componentName, args.overrideType, args.description, args.details, args.firefoxVersion, journal, args.baseCommit);
|
|
127
137
|
return filesCopied;
|
|
128
138
|
}
|
|
129
139
|
catch (error) {
|
|
@@ -163,6 +163,38 @@ async function assertPreviewPrerequisites(engineDir) {
|
|
|
163
163
|
'Run "fireforge bootstrap" (or the underlying `mach bootstrap` in the engine) to populate the toolchain config, then rerun "fireforge furnace preview".');
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Emits a framing banner when the Storybook workspace has not yet had
|
|
168
|
+
* its npm dependencies installed. `mach storybook` will drive the
|
|
169
|
+
* install internally and print ELSPROBLEMS / UNMET DEPENDENCY lines
|
|
170
|
+
* verbatim; without this banner operators reliably read the npm output
|
|
171
|
+
* as a failure (2026-04-24 eval Finding 13).
|
|
172
|
+
*
|
|
173
|
+
* Skipped when `--install` was explicitly requested — that path already
|
|
174
|
+
* runs `mach storybook upgrade` before the preview launches, so the npm
|
|
175
|
+
* output for the subsequent `mach storybook` invocation is a no-op.
|
|
176
|
+
*/
|
|
177
|
+
async function announceStorybookFirstRunIfNeeded(engineDir, installRequested) {
|
|
178
|
+
if (installRequested)
|
|
179
|
+
return;
|
|
180
|
+
const storybookNodeModules = join(engineDir, 'browser', 'components', 'storybook', 'node_modules');
|
|
181
|
+
const storybookDepsMissing = !(await pathExists(storybookNodeModules));
|
|
182
|
+
if (!storybookDepsMissing)
|
|
183
|
+
return;
|
|
184
|
+
info('Storybook workspace dependencies are not yet installed. The next step will install ~1000 npm packages via `mach storybook`; expect npm error-style output below. This is a one-time first-run cost — Storybook will start once the install finishes.');
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Surfaces an explicit success banner after a clean mach-storybook
|
|
188
|
+
* exit so the operator's scrollback visually terminates the npm noise
|
|
189
|
+
* from the first-run install. Only fires on expected exit codes — non-
|
|
190
|
+
* zero cases fall through to the existing
|
|
191
|
+
* `buildStorybookFailureMessage` classification.
|
|
192
|
+
*/
|
|
193
|
+
function announceStorybookCleanExitIfApplicable(exitCode) {
|
|
194
|
+
if (exitCode === 0 || exitCode === 130 || exitCode === 143) {
|
|
195
|
+
info('Storybook stopped cleanly.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
166
198
|
/**
|
|
167
199
|
* Runs the furnace preview command to start Storybook for component preview.
|
|
168
200
|
* @param projectRoot - Root directory of the project
|
|
@@ -269,10 +301,16 @@ export async function furnacePreviewCommand(projectRoot, options = {}) {
|
|
|
269
301
|
}
|
|
270
302
|
installSpinner.stop('Storybook dependencies reinstalled');
|
|
271
303
|
}
|
|
304
|
+
// 2026-04-24 eval Finding 13: frame the npm noise that `mach
|
|
305
|
+
// storybook` emits on first-run as expected progress rather than a
|
|
306
|
+
// failure. The banner-before / banner-after helpers are extracted
|
|
307
|
+
// so the command body stays under the per-function LOC budget.
|
|
308
|
+
await announceStorybookFirstRunIfNeeded(paths.engine, options.install ?? false);
|
|
272
309
|
// Start Storybook
|
|
273
310
|
info('Starting Storybook...');
|
|
274
311
|
info('Press Ctrl+C to stop\n');
|
|
275
312
|
previewResult = await runMachCapture(['storybook'], paths.engine);
|
|
313
|
+
announceStorybookCleanExitIfApplicable(previewResult.exitCode);
|
|
276
314
|
}
|
|
277
315
|
catch (error) {
|
|
278
316
|
primaryError = error;
|
|
@@ -3,9 +3,10 @@ import { readdir, unlink } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { confirm } from '@clack/prompts';
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
6
|
+
import { removeCustomFtlJarMnEntry } from '../../core/furnace-apply-ftl.js';
|
|
6
7
|
import { extractComponentChecksums, getOverrideEngineTargetPath, isOverrideCopyCandidate, restoreOverrideFileToBaseline, } from '../../core/furnace-apply-helpers.js';
|
|
7
8
|
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
8
|
-
import { resolveFtlDir } from '../../core/furnace-constants.js';
|
|
9
|
+
import { resolveFtlDir, xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
9
10
|
import { recordFurnaceRollbackFailure, runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
10
11
|
import { removeCustomElementRegistration, removeJarMnEntries, } from '../../core/furnace-registration.js';
|
|
11
12
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
@@ -211,6 +212,64 @@ async function cleanupCustomTestFiles(name, projectRoot, journal) {
|
|
|
211
212
|
}
|
|
212
213
|
return { partialFailures };
|
|
213
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Removes generated xpcshell test scaffolds associated with a custom
|
|
217
|
+
* component. 2026-04-24 eval Finding 5: `furnace remove` handled
|
|
218
|
+
* browser mochitests via `cleanupCustomTestFiles` but never touched the
|
|
219
|
+
* xpcshell scaffold tree, so an operator who ran
|
|
220
|
+
* `furnace create --with-tests --xpcshell` followed by `furnace remove`
|
|
221
|
+
* was left with orphan `xpcshell.toml` + `test_<name>_packaged.js`
|
|
222
|
+
* files still referencing the removed component. This cleanup pass
|
|
223
|
+
* mirrors the mochitest one — snapshot before removal, warn-and-
|
|
224
|
+
* continue semantics, explicit summary when partial failures occur.
|
|
225
|
+
*/
|
|
226
|
+
async function cleanupCustomXpcshellTestFiles(name, projectRoot, journal) {
|
|
227
|
+
const partialFailures = [];
|
|
228
|
+
let forgeConfig;
|
|
229
|
+
try {
|
|
230
|
+
forgeConfig = await loadConfig(projectRoot);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const msg = `Could not load config for xpcshell test cleanup — ${toError(error).message}. Remove xpcshell test files manually if needed.`;
|
|
234
|
+
warn(msg);
|
|
235
|
+
partialFailures.push(msg);
|
|
236
|
+
return { partialFailures };
|
|
237
|
+
}
|
|
238
|
+
const paths = getProjectPaths(projectRoot);
|
|
239
|
+
const xpcshellRoot = join(paths.engine, xpcshellTestParentDir(forgeConfig.binaryName));
|
|
240
|
+
const componentXpcshellDir = join(xpcshellRoot, name);
|
|
241
|
+
if (!(await pathExists(componentXpcshellDir)))
|
|
242
|
+
return { partialFailures };
|
|
243
|
+
try {
|
|
244
|
+
await snapshotDir(journal, componentXpcshellDir);
|
|
245
|
+
await removeDir(componentXpcshellDir);
|
|
246
|
+
info(`Deleted xpcshell test scaffold directory: ${componentXpcshellDir.replace(paths.engine + '/', 'engine/')}`);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
const msg = `Could not delete xpcshell test scaffold — ${toError(error).message}. Remove it manually if needed.`;
|
|
250
|
+
warn(msg);
|
|
251
|
+
partialFailures.push(msg);
|
|
252
|
+
}
|
|
253
|
+
// If the xpcshell parent directory is now empty (no other components
|
|
254
|
+
// had scaffolds), drop it too so `furnace validate` stays quiet about
|
|
255
|
+
// the empty per-binary tree. Warn-and-continue on any failure.
|
|
256
|
+
try {
|
|
257
|
+
if (await pathExists(xpcshellRoot)) {
|
|
258
|
+
const remaining = await readdir(xpcshellRoot);
|
|
259
|
+
if (remaining.length === 0) {
|
|
260
|
+
await snapshotDir(journal, xpcshellRoot);
|
|
261
|
+
await removeDir(xpcshellRoot);
|
|
262
|
+
info(`Deleted empty xpcshell parent directory: ${xpcshellRoot.replace(paths.engine + '/', 'engine/')}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
267
|
+
const msg = `Could not clean up xpcshell parent directory — ${toError(error).message}. Remove it manually if needed.`;
|
|
268
|
+
warn(msg);
|
|
269
|
+
partialFailures.push(msg);
|
|
270
|
+
}
|
|
271
|
+
return { partialFailures };
|
|
272
|
+
}
|
|
214
273
|
function dropChecksumsByPrefix(state, prefix) {
|
|
215
274
|
const result = { ...state };
|
|
216
275
|
if (state.appliedChecksums) {
|
|
@@ -353,12 +412,27 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
353
412
|
await removeFile(ftlPath);
|
|
354
413
|
info(`Deleted localized file engine/${ftlRel}`);
|
|
355
414
|
}
|
|
415
|
+
// Drop the locale jar.mn chrome registration that `applyCustomFtlFile`
|
|
416
|
+
// wrote during deploy — otherwise the engine is left with a
|
|
417
|
+
// `locale/.../${name}.ftl` entry pointing at a file we just
|
|
418
|
+
// deleted. 2026-04-21 eval (Finding #1): `furnace remove` left
|
|
419
|
+
// `browser/locales/jar.mn` referencing the missing FTL, which
|
|
420
|
+
// would break the next package-manifest validation.
|
|
421
|
+
await removeCustomFtlJarMnEntry(paths.engine, `${name}.ftl`, ftlDir, customConfig, journal);
|
|
356
422
|
}
|
|
357
423
|
}
|
|
358
424
|
let testCleanupFailures = [];
|
|
359
425
|
if (type === 'custom') {
|
|
360
426
|
const result = await cleanupCustomTestFiles(name, projectRoot, journal);
|
|
361
427
|
testCleanupFailures = result.partialFailures;
|
|
428
|
+
// 2026-04-24 eval Finding 5: also clean up xpcshell scaffolds
|
|
429
|
+
// generated by `furnace create --with-tests --xpcshell`. The
|
|
430
|
+
// mochitest cleanup above covers `browser/base/content/test/
|
|
431
|
+
// <binary>/`, but xpcshell scaffolds live in the sibling
|
|
432
|
+
// `<binary>-xpcshell/` directory and were orphaned by prior
|
|
433
|
+
// versions.
|
|
434
|
+
const xpcshellResult = await cleanupCustomXpcshellTestFiles(name, projectRoot, journal);
|
|
435
|
+
testCleanupFailures.push(...xpcshellResult.partialFailures);
|
|
362
436
|
}
|
|
363
437
|
// Remove entry from furnace.json
|
|
364
438
|
if (type === 'stock') {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xpcshell scaffold rename helper extracted from `rename.ts`.
|
|
3
|
+
*
|
|
4
|
+
* 2026-04-24 eval Finding 5: `furnace create --with-tests --xpcshell`
|
|
5
|
+
* writes a scaffold at `browser/base/content/test/<binary>-xpcshell/
|
|
6
|
+
* <name>/` and `furnace rename` did not update it. The helper below
|
|
7
|
+
* renames the directory, updates the test filename, rewrites the
|
|
8
|
+
* `xpcshell.toml` section header, and re-writes the test body so word-
|
|
9
|
+
* boundary occurrences of the old tag / underscored name map to the new
|
|
10
|
+
* ones.
|
|
11
|
+
*
|
|
12
|
+
* Extracted to keep `rename.ts` under the per-file LOC budget —
|
|
13
|
+
* `rename.ts` already carries mochikit + browser-mochitest + FTL
|
|
14
|
+
* handling, and tacking xpcshell onto that tree pushed the file past
|
|
15
|
+
* the limit.
|
|
16
|
+
*/
|
|
17
|
+
import { type RollbackJournal } from '../../core/furnace-rollback.js';
|
|
18
|
+
/**
|
|
19
|
+
* Renames an xpcshell test scaffold in place. Moves the directory,
|
|
20
|
+
* rewrites the test filename, updates the `[test_name]` section header
|
|
21
|
+
* in `xpcshell.toml`, and word-boundary-rewrites occurrences of the
|
|
22
|
+
* old tag / old underscored name inside the test body.
|
|
23
|
+
*
|
|
24
|
+
* Best-effort: any failure logs a warning through the shared logger
|
|
25
|
+
* but never throws — the component rename itself has already succeeded
|
|
26
|
+
* at this point, and blocking on a test rewrite would leave the
|
|
27
|
+
* operator with a half-renamed component.
|
|
28
|
+
*
|
|
29
|
+
* @param engineDir - Absolute path to the engine directory under the project.
|
|
30
|
+
* @param projectRoot - Absolute path to the project root, used to load the binary name.
|
|
31
|
+
* @param oldName - Pre-rename component tag name.
|
|
32
|
+
* @param newName - Post-rename component tag name.
|
|
33
|
+
* @param journal - Rollback journal that the rename mutation writes to before touching files.
|
|
34
|
+
*/
|
|
35
|
+
export declare function renameXpcshellTestFiles(engineDir: string, projectRoot: string, oldName: string, newName: string, journal: RollbackJournal): Promise<void>;
|