@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,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* xpcshell scaffold rename helper extracted from `rename.ts`.
|
|
4
|
+
*
|
|
5
|
+
* 2026-04-24 eval Finding 5: `furnace create --with-tests --xpcshell`
|
|
6
|
+
* writes a scaffold at `browser/base/content/test/<binary>-xpcshell/
|
|
7
|
+
* <name>/` and `furnace rename` did not update it. The helper below
|
|
8
|
+
* renames the directory, updates the test filename, rewrites the
|
|
9
|
+
* `xpcshell.toml` section header, and re-writes the test body so word-
|
|
10
|
+
* boundary occurrences of the old tag / underscored name map to the new
|
|
11
|
+
* ones.
|
|
12
|
+
*
|
|
13
|
+
* Extracted to keep `rename.ts` under the per-file LOC budget —
|
|
14
|
+
* `rename.ts` already carries mochikit + browser-mochitest + FTL
|
|
15
|
+
* handling, and tacking xpcshell onto that tree pushed the file past
|
|
16
|
+
* the limit.
|
|
17
|
+
*/
|
|
18
|
+
import { readdir } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { loadConfig } from '../../core/config.js';
|
|
21
|
+
import { xpcshellTestParentDir } from '../../core/furnace-constants.js';
|
|
22
|
+
import { snapshotFile } from '../../core/furnace-rollback.js';
|
|
23
|
+
import { toError } from '../../utils/errors.js';
|
|
24
|
+
import { ensureDir, pathExists, readText, removeDir, removeFile, writeText, } from '../../utils/fs.js';
|
|
25
|
+
import { info, warn } from '../../utils/logger.js';
|
|
26
|
+
/** Escapes regex metacharacters so a user-supplied name stays literal. */
|
|
27
|
+
function escapeRegex(input) {
|
|
28
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Renames an xpcshell test scaffold in place. Moves the directory,
|
|
32
|
+
* rewrites the test filename, updates the `[test_name]` section header
|
|
33
|
+
* in `xpcshell.toml`, and word-boundary-rewrites occurrences of the
|
|
34
|
+
* old tag / old underscored name inside the test body.
|
|
35
|
+
*
|
|
36
|
+
* Best-effort: any failure logs a warning through the shared logger
|
|
37
|
+
* but never throws — the component rename itself has already succeeded
|
|
38
|
+
* at this point, and blocking on a test rewrite would leave the
|
|
39
|
+
* operator with a half-renamed component.
|
|
40
|
+
*
|
|
41
|
+
* @param engineDir - Absolute path to the engine directory under the project.
|
|
42
|
+
* @param projectRoot - Absolute path to the project root, used to load the binary name.
|
|
43
|
+
* @param oldName - Pre-rename component tag name.
|
|
44
|
+
* @param newName - Post-rename component tag name.
|
|
45
|
+
* @param journal - Rollback journal that the rename mutation writes to before touching files.
|
|
46
|
+
*/
|
|
47
|
+
export async function renameXpcshellTestFiles(engineDir, projectRoot, oldName, newName, journal) {
|
|
48
|
+
let forgeConfig;
|
|
49
|
+
try {
|
|
50
|
+
forgeConfig = await loadConfig(projectRoot);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return; // Cannot determine scaffold path without config.
|
|
54
|
+
}
|
|
55
|
+
const parentDir = join(engineDir, xpcshellTestParentDir(forgeConfig.binaryName));
|
|
56
|
+
if (!(await pathExists(parentDir)))
|
|
57
|
+
return;
|
|
58
|
+
const oldScaffoldDir = join(parentDir, oldName);
|
|
59
|
+
const newScaffoldDir = join(parentDir, newName);
|
|
60
|
+
if (!(await pathExists(oldScaffoldDir)))
|
|
61
|
+
return;
|
|
62
|
+
const oldUnderscored = oldName.replace(/-/g, '_');
|
|
63
|
+
const newUnderscored = newName.replace(/-/g, '_');
|
|
64
|
+
const oldTestFileName = `test_${oldUnderscored}_packaged.js`;
|
|
65
|
+
const newTestFileName = `test_${newUnderscored}_packaged.js`;
|
|
66
|
+
try {
|
|
67
|
+
await ensureDir(newScaffoldDir);
|
|
68
|
+
const entries = await readdir(oldScaffoldDir, { withFileTypes: true });
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!entry.isFile())
|
|
71
|
+
continue;
|
|
72
|
+
const oldFilePath = join(oldScaffoldDir, entry.name);
|
|
73
|
+
const renamedFileName = entry.name === oldTestFileName ? newTestFileName : entry.name;
|
|
74
|
+
const newFilePath = join(newScaffoldDir, renamedFileName);
|
|
75
|
+
await snapshotFile(journal, oldFilePath);
|
|
76
|
+
const body = await readText(oldFilePath);
|
|
77
|
+
let updated = body;
|
|
78
|
+
if (entry.name === 'xpcshell.toml') {
|
|
79
|
+
updated = updated.replace(new RegExp(`\\[${escapeRegex(`"${oldTestFileName}"`)}\\]`, 'g'), `["${newTestFileName}"]`);
|
|
80
|
+
}
|
|
81
|
+
else if (entry.name === oldTestFileName) {
|
|
82
|
+
const oldTagPattern = new RegExp(`(?<![\\w-])${escapeRegex(oldName)}(?![\\w-])`, 'g');
|
|
83
|
+
updated = updated.replace(oldTagPattern, newName);
|
|
84
|
+
const oldUnderscoredPattern = new RegExp(`(?<![\\w])${escapeRegex(oldUnderscored)}(?![\\w])`, 'g');
|
|
85
|
+
updated = updated.replace(oldUnderscoredPattern, newUnderscored);
|
|
86
|
+
}
|
|
87
|
+
await writeText(newFilePath, updated);
|
|
88
|
+
await removeFile(oldFilePath);
|
|
89
|
+
}
|
|
90
|
+
await removeDir(oldScaffoldDir);
|
|
91
|
+
info(`Renamed xpcshell scaffold directory: ${xpcshellTestParentDir(forgeConfig.binaryName)}/${oldName} → ${xpcshellTestParentDir(forgeConfig.binaryName)}/${newName}`);
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
warn(`Could not rename xpcshell scaffold — ${toError(error).message}. Rename the scaffold files manually if needed.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=rename-xpcshell.js.map
|
|
@@ -3,9 +3,9 @@ import { readdir } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
5
5
|
import { getFurnacePaths, loadFurnaceConfig, updateFurnaceState, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
|
-
import { isComponentSourceFile, resolveFtlDir, tagNameToClassName, } from '../../core/furnace-constants.js';
|
|
6
|
+
import { isComponentSourceFile, resolveFtlChromeSubPath, resolveFtlDir, resolveFtlLocaleJarMnPath, tagNameToClassName, } from '../../core/furnace-constants.js';
|
|
7
7
|
import { recordFurnaceRollbackFailure, runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
8
|
-
import { addCustomElementRegistration, addJarMnEntries, removeCustomElementRegistration, removeJarMnEntries, } from '../../core/furnace-registration.js';
|
|
8
|
+
import { addCustomElementRegistration, addJarMnEntries, addLocaleFtlJarMnEntry, removeCustomElementRegistration, removeJarMnEntries, removeLocaleFtlJarMnEntry, } from '../../core/furnace-registration.js';
|
|
9
9
|
import { CUSTOM_ELEMENT_TAG_PATTERN, CUSTOM_ELEMENT_TAG_RULES, } from '../../core/furnace-registration-validate.js';
|
|
10
10
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
11
11
|
import { getStoriesDir } from '../../core/furnace-stories.js';
|
|
@@ -14,6 +14,7 @@ import { FurnaceError } from '../../errors/furnace.js';
|
|
|
14
14
|
import { toError } from '../../utils/errors.js';
|
|
15
15
|
import { copyFile, ensureDir, pathExists, readText, removeDir, removeFile, writeText, } from '../../utils/fs.js';
|
|
16
16
|
import { info, intro, note, outro, warn } from '../../utils/logger.js';
|
|
17
|
+
import { renameXpcshellTestFiles } from './rename-xpcshell.js';
|
|
17
18
|
/** Escapes regex metacharacters so a user-supplied name is literal inside a RegExp. */
|
|
18
19
|
function escapeRegex(input) {
|
|
19
20
|
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
@@ -272,7 +273,8 @@ async function performRenameMutations(args) {
|
|
|
272
273
|
// 3. Update engine registrations (custom components only)
|
|
273
274
|
if (isCustom && config.custom[newName]?.register && (await pathExists(args.engineDir))) {
|
|
274
275
|
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
275
|
-
|
|
276
|
+
const isLocalized = config.custom[newName].localized;
|
|
277
|
+
await updateEngineRegistrations(args.engineDir, oldName, newName, newDir, ftlDir, isLocalized, journal);
|
|
276
278
|
}
|
|
277
279
|
// 4. Re-key furnace-state.json checksums from old name to new name
|
|
278
280
|
await rekeyStateChecksums(args.projectRoot, componentType, oldName, newName);
|
|
@@ -298,6 +300,14 @@ async function performRenameMutations(args) {
|
|
|
298
300
|
// either failed the test run outright or (worse) passed for the
|
|
299
301
|
// wrong component.
|
|
300
302
|
await renameMochikitTestFiles(args.engineDir, oldName, newName, journal);
|
|
303
|
+
// 2026-04-24 eval Finding 5: xpcshell scaffolds live in yet
|
|
304
|
+
// another tree (`browser/base/content/test/<binary>-xpcshell/
|
|
305
|
+
// <name>/`). Before this call, renaming a component scaffolded
|
|
306
|
+
// with `--with-tests --xpcshell` left a directory whose name
|
|
307
|
+
// still referenced the pre-rename component, plus a test file
|
|
308
|
+
// whose underscored name referenced the old tag — both of
|
|
309
|
+
// which then failed to match the new component.
|
|
310
|
+
await renameXpcshellTestFiles(args.engineDir, projectRoot, oldName, newName, journal);
|
|
301
311
|
// Clear the stale deployed component directory so the next
|
|
302
312
|
// `furnace apply` is the single writer of the new name's
|
|
303
313
|
// deployment. Without this, eval runs showed the old widget
|
|
@@ -358,7 +368,7 @@ async function rekeyStateChecksums(projectRoot, componentType, oldName, newName)
|
|
|
358
368
|
return result;
|
|
359
369
|
});
|
|
360
370
|
}
|
|
361
|
-
async function updateEngineRegistrations(engineDir, oldName, newName, newDir, ftlDir, journal) {
|
|
371
|
+
async function updateEngineRegistrations(engineDir, oldName, newName, newDir, ftlDir, isLocalized, journal) {
|
|
362
372
|
const customElementsPath = join(engineDir, 'toolkit/content/customElements.js');
|
|
363
373
|
const jarMnPath = join(engineDir, 'toolkit/content/jar.mn');
|
|
364
374
|
if (await pathExists(customElementsPath)) {
|
|
@@ -386,6 +396,24 @@ async function updateEngineRegistrations(engineDir, oldName, newName, newDir, ft
|
|
|
386
396
|
await writeText(newFtlPath, ftlContent);
|
|
387
397
|
await removeFile(oldFtlPath);
|
|
388
398
|
}
|
|
399
|
+
// Re-wire the locale jar.mn chrome registration when the component is
|
|
400
|
+
// localized. Before this, `updateEngineRegistrations` renamed the .ftl
|
|
401
|
+
// file on disk but left the locale jar.mn pointing at
|
|
402
|
+
// `locale/.../${oldName}.ftl`, so `furnace validate` passed while the
|
|
403
|
+
// engine still carried a stale registration for the now-missing file
|
|
404
|
+
// (eval finding: stale old-name registration after rename).
|
|
405
|
+
if (isLocalized) {
|
|
406
|
+
const chromeSubPath = resolveFtlChromeSubPath(ftlDir);
|
|
407
|
+
const localeJarRel = resolveFtlLocaleJarMnPath(ftlDir);
|
|
408
|
+
if (chromeSubPath !== undefined && localeJarRel !== undefined) {
|
|
409
|
+
const localeJarAbs = join(engineDir, localeJarRel);
|
|
410
|
+
if (await pathExists(localeJarAbs)) {
|
|
411
|
+
await snapshotFile(journal, localeJarAbs);
|
|
412
|
+
await removeLocaleFtlJarMnEntry(engineDir, localeJarRel, oldName, chromeSubPath);
|
|
413
|
+
await addLocaleFtlJarMnEntry(engineDir, localeJarRel, newName, chromeSubPath);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
389
417
|
}
|
|
390
418
|
/**
|
|
391
419
|
* Renames a custom or override component atomically: updates directory name,
|
|
@@ -5,9 +5,9 @@ import { isBrandingManagedPath } from '../core/branding.js';
|
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
6
|
import { getStatusWithCodes, hasChanges, isGitRepository } from '../core/git.js';
|
|
7
7
|
import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
8
|
-
import {
|
|
8
|
+
import { expandUntrackedDirectoryEntries, getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, getWorkingTreeStatus, } from '../core/git-status.js';
|
|
9
9
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
10
|
-
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue } from '../core/patch-lint.js';
|
|
10
|
+
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue, resolvePatchSizeTier, } from '../core/patch-lint.js';
|
|
11
11
|
import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
|
|
12
12
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
13
13
|
import { GeneralError } from '../errors/base.js';
|
|
@@ -103,10 +103,17 @@ async function resolveLintDiff(engineDir, files, binaryName) {
|
|
|
103
103
|
// survive in the patch queue as-is. The exclusion mirrors the
|
|
104
104
|
// `branding` bucket in `fireforge status` so the two views stay
|
|
105
105
|
// consistent.
|
|
106
|
+
//
|
|
107
|
+
// `expandUntrackedDirectoryEntries` promotes collapsed `?? dir/`
|
|
108
|
+
// status rows to individual file entries before the diff pass.
|
|
109
|
+
// Without it, a patch that introduces a new directory shows up as
|
|
110
|
+
// `?? browser/modules/<fork>/` and `getDiffForFilesAgainstHead`
|
|
111
|
+
// crashed with EISDIR reading the directory as if it were a file
|
|
112
|
+
// (eval finding: aggregate lint unusable on a real imported queue).
|
|
106
113
|
if (binaryName) {
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
const allPaths = [...new Set(
|
|
114
|
+
const rawStatus = await getWorkingTreeStatus(engineDir);
|
|
115
|
+
const expanded = await expandUntrackedDirectoryEntries(engineDir, rawStatus);
|
|
116
|
+
const allPaths = [...new Set(expanded.map((entry) => entry.file))];
|
|
110
117
|
const nonBrandingPaths = allPaths.filter((path) => !isBrandingManagedPath(path, binaryName));
|
|
111
118
|
const excludedCount = allPaths.length - nonBrandingPaths.length;
|
|
112
119
|
if (excludedCount > 0) {
|
|
@@ -315,7 +322,13 @@ async function lintPerPatch(projectRoot, paths) {
|
|
|
315
322
|
if (!diff.trim())
|
|
316
323
|
continue;
|
|
317
324
|
const ignore = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
318
|
-
const
|
|
325
|
+
const decision = resolvePatchSizeTier(existing, patch.tier);
|
|
326
|
+
if (decision.tier === 'branding') {
|
|
327
|
+
info(decision.source === 'explicit'
|
|
328
|
+
? `${patch.filename}: branding threshold tier applied via patches.json \`tier: "branding"\` opt-in.`
|
|
329
|
+
: `${patch.filename}: branding threshold tier applied (all files under browser/branding/ plus registration siblings).`);
|
|
330
|
+
}
|
|
331
|
+
const patchIssues = await lintExportedPatch(paths.engine, existing, diff, config, ctx, ignore, patch.tier);
|
|
319
332
|
for (const issue of patchIssues) {
|
|
320
333
|
issues.push({ ...issue, file: `${patch.filename} :: ${issue.file}` });
|
|
321
334
|
}
|
|
@@ -39,7 +39,10 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
|
|
|
39
39
|
}
|
|
40
40
|
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
41
41
|
if (!target) {
|
|
42
|
-
|
|
42
|
+
const available = manifest.patches
|
|
43
|
+
.map((p) => p.name && p.name !== p.filename ? `${p.filename} (name: ${p.name})` : p.filename)
|
|
44
|
+
.join(', ');
|
|
45
|
+
throw new InvalidArgumentError(`Patch "${identifier}" not found. Accepted identifiers: ordinal (e.g. 2), filename (e.g. 002-ui-foo.patch), or manifest name (e.g. ui-foo). Available: ${available}`, identifier);
|
|
43
46
|
}
|
|
44
47
|
// Build the full queue context once so we can scan each patch's newFiles
|
|
45
48
|
// without re-parsing for the dependency check below.
|
|
@@ -270,7 +270,10 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
|
|
|
270
270
|
}
|
|
271
271
|
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
272
272
|
if (!target) {
|
|
273
|
-
|
|
273
|
+
const available = manifest.patches
|
|
274
|
+
.map((p) => p.name && p.name !== p.filename ? `${p.filename} (name: ${p.name})` : p.filename)
|
|
275
|
+
.join(', ');
|
|
276
|
+
throw new InvalidArgumentError(`Patch "${identifier}" not found. Accepted identifiers: ordinal (e.g. 2), filename (e.g. 002-ui-foo.patch), or manifest name (e.g. ui-foo). Available: ${available}`, identifier);
|
|
274
277
|
}
|
|
275
278
|
const { destinationOrder, anchorFilename } = resolveDestination(target, manifest.patches, options);
|
|
276
279
|
const renameMap = computeRenameMap(manifest.patches, target, destinationOrder);
|
|
@@ -75,8 +75,10 @@ export async function reExportFilesInPlace(paths, selectedPatches, options, conf
|
|
|
75
75
|
// `lintIgnore` threads through so a shrink of an advisory-noisy-but-
|
|
76
76
|
// intentional patch (branding bundle, localised-resource pack) does not
|
|
77
77
|
// have to choose between `--skip-lint` (blunt) and the full rebase path.
|
|
78
|
+
// `target.tier` threads the explicit branding-threshold opt-in for
|
|
79
|
+
// the branding patch that also touches a non-allowlisted sibling.
|
|
78
80
|
const ignoreChecks = target.lintIgnore?.length ? new Set(target.lintIgnore) : undefined;
|
|
79
|
-
await runPatchLint(paths.engine, actualProjectedFiles, projectedDiff, config, options.skipLint, undefined, ignoreChecks);
|
|
81
|
+
await runPatchLint(paths.engine, actualProjectedFiles, projectedDiff, config, options.skipLint, undefined, ignoreChecks, target.tier);
|
|
80
82
|
// Project the cross-patch context: replace the target entry with its
|
|
81
83
|
// would-be shrunken self (new diff + new newFiles + new
|
|
82
84
|
// modifiedFileAdditions). The projected entry must repopulate both
|
|
@@ -201,8 +201,11 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
201
201
|
// intentional patch (a cohesive branding bundle, a localised-resource
|
|
202
202
|
// pack) without either `--skip-lint` (too blunt) or falling through to
|
|
203
203
|
// the full `rebase` flow (which internally skips the lint pipeline).
|
|
204
|
+
// The paired `patch.tier` threads the explicit branding-threshold
|
|
205
|
+
// opt-in the same way, for the branding patch that also touches a
|
|
206
|
+
// non-allowlisted registration sibling.
|
|
204
207
|
const ignoreChecks = patch.lintIgnore?.length ? new Set(patch.lintIgnore) : undefined;
|
|
205
|
-
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, undefined, ignoreChecks);
|
|
208
|
+
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, undefined, ignoreChecks, patch.tier);
|
|
206
209
|
if (isDryRun) {
|
|
207
210
|
info(`[dry-run] ${patch.filename}: ${existingFiles.length} file(s)`);
|
|
208
211
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
14
14
|
import { getFurnacePaths, updateFurnaceState } from '../../core/furnace-config.js';
|
|
15
|
-
import { getHead, isGitRepository, resetChanges } from '../../core/git.js';
|
|
15
|
+
import { getHead, isGitRepository, isMissingHeadError, resetChanges } from '../../core/git.js';
|
|
16
16
|
import { discoverPatches } from '../../core/patch-files.js';
|
|
17
17
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
18
18
|
import { hasActiveRebaseSession, saveRebaseSession } from '../../core/rebase-session.js';
|
|
@@ -40,6 +40,24 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
40
40
|
if (!(await isGitRepository(paths.engine))) {
|
|
41
41
|
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
42
42
|
}
|
|
43
|
+
// 2026-04-24 eval Finding 11: `rebase --dry-run` used to print
|
|
44
|
+
// "Dry run complete" without validating that the engine had a valid
|
|
45
|
+
// HEAD. A previous `download --force` abort could leave `.git/`
|
|
46
|
+
// initialized but unborn (no baseline commit); the real rebase then
|
|
47
|
+
// failed immediately with `fatal: ambiguous argument 'HEAD'` on the
|
|
48
|
+
// first `git rev-parse HEAD` call. Replicate the same baseline check
|
|
49
|
+
// here so dry-run mirrors the real-run preconditions and operators
|
|
50
|
+
// cannot mistake a broken baseline for a ready-to-rebase tree.
|
|
51
|
+
try {
|
|
52
|
+
await getHead(paths.engine);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (isMissingHeadError(err)) {
|
|
56
|
+
throw new GeneralError('Engine repository has no baseline commit yet — a previous "fireforge download" was interrupted before git created the initial Firefox source commit. ' +
|
|
57
|
+
'Re-run "fireforge download --force" to recreate the baseline repository cleanly, then retry the rebase.');
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
43
61
|
const config = await loadConfig(projectRoot);
|
|
44
62
|
const currentVersion = config.firefox.version;
|
|
45
63
|
const manifest = await loadPatchesManifest(paths.patches);
|
|
@@ -45,6 +45,17 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
|
|
|
45
45
|
}
|
|
46
46
|
const result = await registerFile(projectRoot, engineRelativePath, options.dryRun, options.after);
|
|
47
47
|
if (options.dryRun) {
|
|
48
|
+
// 2026-04-21 eval (Finding #8): dry-run always said "Would
|
|
49
|
+
// register" even when the rule's idempotency check already knew
|
|
50
|
+
// the entry was present, so automation read the plan as "work to
|
|
51
|
+
// do" and the following real run then reported "Already
|
|
52
|
+
// registered". Surface the idempotency decision in dry-run too so
|
|
53
|
+
// the plan mirrors the real command's outcome.
|
|
54
|
+
if (result.skipped) {
|
|
55
|
+
info(`[dry-run] Already registered: ${engineRelativePath} in ${result.manifest}`);
|
|
56
|
+
outro('Dry run complete');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
48
59
|
info(`[dry-run] Would register ${engineRelativePath}`);
|
|
49
60
|
info(` manifest: ${result.manifest}`);
|
|
50
61
|
info(` entry: ${result.entry}`);
|
|
@@ -70,11 +70,39 @@ async function printUnregisteredWarnings(files, projectRoot, binaryName) {
|
|
|
70
70
|
if (newFiles.length === 0)
|
|
71
71
|
return;
|
|
72
72
|
const registrableFiles = newFiles.filter((f) => matchesRegistrablePattern(f.file, binaryName));
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
// `isFileRegistered` throws `GeneralError("Manifest not found: ...")` when a
|
|
74
|
+
// rule sees a file whose parent manifest does not yet exist on disk — e.g.
|
|
75
|
+
// a brand-new `browser/modules/<binary>/` directory with no `moz.build`.
|
|
76
|
+
// `status` is a read-only reporter; before 0.18.1 the rejected promise
|
|
77
|
+
// bubbled through `Promise.all` and exited status with code 1, breaking the
|
|
78
|
+
// "use status --unmanaged to discover new files before running register"
|
|
79
|
+
// workflow. We now bucket missing-manifest cases into a distinct warning
|
|
80
|
+
// list while still surfacing the same actionable signal. Other error
|
|
81
|
+
// shapes continue to propagate (permission denied, corrupt file, etc.) so
|
|
82
|
+
// we do not silently hide anything surprising.
|
|
83
|
+
const registrationChecks = await Promise.all(registrableFiles.map(async (f) => {
|
|
84
|
+
try {
|
|
85
|
+
return {
|
|
86
|
+
file: f.file,
|
|
87
|
+
registered: await isFileRegistered(projectRoot, f.file),
|
|
88
|
+
manifestMissing: false,
|
|
89
|
+
manifestMissingMessage: undefined,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err instanceof GeneralError && /^Manifest not found:/i.test(err.message)) {
|
|
94
|
+
return {
|
|
95
|
+
file: f.file,
|
|
96
|
+
registered: false,
|
|
97
|
+
manifestMissing: true,
|
|
98
|
+
manifestMissingMessage: err.message,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}));
|
|
104
|
+
const unregistered = registrationChecks.filter((f) => !f.registered && !f.manifestMissing);
|
|
105
|
+
const manifestMissing = registrationChecks.filter((f) => f.manifestMissing);
|
|
78
106
|
if (unregistered.length > 0) {
|
|
79
107
|
info('');
|
|
80
108
|
warn('Potentially unregistered files:');
|
|
@@ -82,6 +110,17 @@ async function printUnregisteredWarnings(files, projectRoot, binaryName) {
|
|
|
82
110
|
info(` ${f.file} — run 'fireforge register ${f.file}'`);
|
|
83
111
|
}
|
|
84
112
|
}
|
|
113
|
+
if (manifestMissing.length > 0) {
|
|
114
|
+
info('');
|
|
115
|
+
warn('Files whose registration manifest does not exist yet:');
|
|
116
|
+
for (const f of manifestMissing) {
|
|
117
|
+
// `manifestMissingMessage` is always the specific
|
|
118
|
+
// "Manifest not found: <path>" string when manifestMissing is
|
|
119
|
+
// true (see the catch branch above that sets them together).
|
|
120
|
+
info(` ${f.file} — ${f.manifestMissingMessage}`);
|
|
121
|
+
info(` Create the parent manifest, then run 'fireforge register ${f.file}'.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
85
124
|
}
|
|
86
125
|
/**
|
|
87
126
|
* Renders raw worktree status as machine-parseable porcelain-style output.
|
|
@@ -4,13 +4,13 @@ import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
5
|
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, testWithOutput, } from '../core/mach.js';
|
|
6
6
|
import { assertMarionettePortAvailable } from '../core/marionette-port.js';
|
|
7
|
-
import { reportMarionettePreflight, runMarionettePreflight } from '../core/marionette-preflight.js';
|
|
7
|
+
import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
|
|
8
8
|
import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
|
|
9
9
|
import { operatorAlreadySetAppPath, resolveXpcshellAppdirArg, } from '../core/xpcshell-appdir.js';
|
|
10
10
|
import { GeneralError } from '../errors/base.js';
|
|
11
11
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
12
12
|
import { pathExists } from '../utils/fs.js';
|
|
13
|
-
import { info, intro, outro, spinner, warn } from '../utils/logger.js';
|
|
13
|
+
import { info, intro, outro, spinner, success, warn } from '../utils/logger.js';
|
|
14
14
|
import { pickDefined } from '../utils/options.js';
|
|
15
15
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
16
16
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
@@ -47,6 +47,32 @@ function hasStaleBuildArtifactsSignal(output) {
|
|
|
47
47
|
return (/chrome:\/\/branding\/locale\/brand\.properties/i.test(output) ||
|
|
48
48
|
/browser\/branding\/[^/\s]+\/moz\.build/i.test(output));
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Fork-module-not-registered signal. 2026-04-21 eval Finding #14:
|
|
52
|
+
* a hominis test failed with `Failed to load resource:///modules/hominis/
|
|
53
|
+
* HominisStore.sys.mjs`. The branding pattern happened to also match
|
|
54
|
+
* because the test harness printed a branding warning during its
|
|
55
|
+
* teardown, and the stale-build branch won by precedence — telling the
|
|
56
|
+
* operator to rebuild when the real fix is to register the module in
|
|
57
|
+
* the fork's `browser/modules/<binary>/moz.build`. Match a
|
|
58
|
+
* `resource:///modules/<binaryName>/` pattern so fork-owned module
|
|
59
|
+
* failures surface the right diagnosis.
|
|
60
|
+
*/
|
|
61
|
+
function hasForkModuleSignal(output, binaryName) {
|
|
62
|
+
const pattern = new RegExp(`Failed to load resource:\\/\\/\\/modules\\/${binaryName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\/`, 'i');
|
|
63
|
+
return pattern.test(output);
|
|
64
|
+
}
|
|
65
|
+
function buildForkModuleMessage(binaryName) {
|
|
66
|
+
return (`Test failed to load a fork-owned module at resource:///modules/${binaryName}/*.sys.mjs.\n\n` +
|
|
67
|
+
'This is almost always a module-registration issue, not a stale build. The fork module directory is missing an entry that maps its file into the resource URI tree, so `ChromeUtils.importESModule` cannot resolve it.\n\n' +
|
|
68
|
+
'Check that:\n' +
|
|
69
|
+
` - browser/modules/${binaryName}/moz.build lists the missing module in EXTRA_JS_MODULES.\n` +
|
|
70
|
+
` - browser/modules/moz.build references the ${binaryName}/ subdirectory (DIRS += [...]).\n` +
|
|
71
|
+
' - The last `fireforge build` (or `fireforge build --ui`) completed successfully against the current manifests. If the registration is new, the UI-faster build path may not pick it up — a full build may be required.\n\n' +
|
|
72
|
+
'Use `fireforge register browser/modules/' +
|
|
73
|
+
binaryName +
|
|
74
|
+
'/<file>.sys.mjs` to add the EXTRA_JS_MODULES entry if it is missing.');
|
|
75
|
+
}
|
|
50
76
|
// Detects the broader xpcshell symptom where every `resource:///modules/...`
|
|
51
77
|
// import fails — the signature of xpcshell running with the wrong app-dir on
|
|
52
78
|
// a manifest that sets `firefox-appdir = "browser"`. Checked AFTER the
|
|
@@ -56,17 +82,22 @@ function hasXpcshellAppdirSignal(output) {
|
|
|
56
82
|
return /Failed to load resource:\/\/\/modules\//i.test(output);
|
|
57
83
|
}
|
|
58
84
|
function buildXpcshellAppdirMessage(injectionAttempted) {
|
|
85
|
+
const isMacos = process.platform === 'darwin';
|
|
86
|
+
const macosNote = isMacos
|
|
87
|
+
? 'Detected: macOS host. On macOS the xpcshell harness binds `-a` to `<obj>/dist/<App>.app/Contents/Resources` by default and frequently ignores `--app-path` overrides when the `.app` bundle is present — the surest fix is the `<appname>-appdir` migration below rather than trying to force a different path.\n\n'
|
|
88
|
+
: '';
|
|
59
89
|
const triggerLines = injectionAttempted
|
|
60
|
-
? 'FireForge auto-injected `--app-path=<absolute>` against the resolved obj-dir before mach test ran, but the failure persists. The injected path either does not match the appdir layout your harness expects, or the harness
|
|
90
|
+
? 'FireForge auto-injected `--app-path=<absolute>` against the resolved obj-dir before mach test ran, but the failure persists. The injected path either does not match the appdir layout your harness expects, or (on macOS) the harness bound `-a` to the `.app/Contents/Resources` default and ignored the override.\n\n'
|
|
61
91
|
: 'Likely triggers:\n' +
|
|
62
92
|
' - The nearest xpcshell.toml sets `firefox-appdir = "browser"` but the harness reads `<appname>-appdir` instead — the literal `firefox-appdir` directive is silently ignored on rebranded forks (appname != "firefox").\n' +
|
|
63
93
|
' - FireForge could not find an xpcshell.toml above the test path, so the auto-injection never ran.\n\n';
|
|
64
94
|
return ('xpcshell failed to load core resource:///modules/*.sys.mjs imports.\n\n' +
|
|
65
95
|
'This is the canonical symptom of xpcshell running with the wrong app directory: the runtime resolves `resource:///modules/` against the parent of the expected app root, so every `ChromeUtils.importESModule("resource:///modules/…")` throws.\n\n' +
|
|
96
|
+
macosNote +
|
|
66
97
|
triggerLines +
|
|
67
98
|
'Options:\n' +
|
|
68
|
-
' - Add `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the xpcshell.toml [DEFAULT] so the harness reads the appname-keyed value directly.\n' +
|
|
69
|
-
' - Pass overrides through `fireforge test <path> --mach-arg="--app-path=<absolute>"` to inject the path verbatim (operator overrides always win over auto-injection).\n' +
|
|
99
|
+
' - Add `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the xpcshell.toml [DEFAULT] so the harness reads the appname-keyed value directly. This is the most reliable fix on rebranded macOS builds.\n' +
|
|
100
|
+
' - Pass overrides through `fireforge test <path> --mach-arg="--app-path=<absolute>"` to inject the path verbatim (operator overrides always win over auto-injection, but see the macOS caveat above).\n' +
|
|
70
101
|
' - Remove `firefox-appdir = "browser"` from the xpcshell.toml [DEFAULT] and move browser-chrome dependencies into a browser-chrome mochitest (see `fireforge furnace create --test-style=browser-chrome`).\n' +
|
|
71
102
|
' - If the test only touches toolkit chrome (chrome://global/*), drop the `firefox-appdir` setting entirely — toolkit chrome is registered without it.');
|
|
72
103
|
}
|
|
@@ -87,13 +118,22 @@ function buildMochitestHttp3ServerMessage() {
|
|
|
87
118
|
" - The `BROWSER_CHROME_MANIFESTS` entry for your fork's chrome.manifest is registered.\n\n" +
|
|
88
119
|
'This is an upstream Firefox harness interaction; FireForge can only diagnose it.');
|
|
89
120
|
}
|
|
90
|
-
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted) {
|
|
121
|
+
function handleNonZeroTestExit(result, normalizedPaths, appdirInjectionAttempted, binaryName) {
|
|
91
122
|
if (result.exitCode === 0 || result.exitCode === 130)
|
|
92
123
|
return;
|
|
93
124
|
const combinedOutput = `${result.stdout}\n${result.stderr}`;
|
|
94
125
|
if (/UNKNOWN TEST\b/i.test(combinedOutput)) {
|
|
95
126
|
throw new GeneralError(buildUnknownTestMessage(normalizedPaths));
|
|
96
127
|
}
|
|
128
|
+
// Fork-owned module load failures must beat the branding stale-build
|
|
129
|
+
// branch: 2026-04-21 eval (Finding #14) saw a hominis test fail with
|
|
130
|
+
// `Failed to load resource:///modules/hominis/HominisStore.sys.mjs`
|
|
131
|
+
// while the harness teardown printed a branding warning that the old
|
|
132
|
+
// stale-build pattern matched, so the operator was told to rebuild
|
|
133
|
+
// when the real fix is to register the missing module.
|
|
134
|
+
if (hasForkModuleSignal(combinedOutput, binaryName)) {
|
|
135
|
+
throw new GeneralError(buildForkModuleMessage(binaryName));
|
|
136
|
+
}
|
|
97
137
|
// Branding-specific stale-build signals keep priority over the broader
|
|
98
138
|
// xpcshell-appdir hint: when `chrome://branding/locale/brand.properties`
|
|
99
139
|
// fails to resolve, the fix really is "rebuild", not "pass --app-path".
|
|
@@ -157,8 +197,8 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
157
197
|
if (options.build) {
|
|
158
198
|
await prepareBuildEnvironment(projectRoot, paths, projectConfig);
|
|
159
199
|
const s = spinner('Running incremental build...');
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
200
|
+
const buildResult = await buildUI(paths.engine);
|
|
201
|
+
if (buildResult.exitCode !== 0) {
|
|
162
202
|
s.error('Pre-test build failed');
|
|
163
203
|
throw new BuildError('Pre-test build failed', 'mach build faster');
|
|
164
204
|
}
|
|
@@ -193,20 +233,32 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
193
233
|
// no paths are supplied this is the only step — it's the fastest way to tell
|
|
194
234
|
// marionette-wedged apart from test-discovery-failure.
|
|
195
235
|
if (options.doctor) {
|
|
236
|
+
// Write the "Running marionette preflight..." banner via
|
|
237
|
+
// `process.stdout.write` directly before `info()` so non-TTY captures
|
|
238
|
+
// always see the banner even if clack's renderer defers output in
|
|
239
|
+
// pipe mode. `info()` is still called so TTY users keep the normal
|
|
240
|
+
// clack box-drawing framing.
|
|
241
|
+
process.stdout.write('Running marionette preflight...\n');
|
|
196
242
|
info('Running marionette preflight...');
|
|
197
243
|
const preflight = await runMarionettePreflight(paths.engine);
|
|
244
|
+
// 2026-04-24 eval Finding 7: the pre-0.18.1 code used
|
|
245
|
+
// `success()` + `outro()` + a direct `process.stdout.write` as a
|
|
246
|
+
// belt-and-suspenders but still reproducibly dropped the PASS summary
|
|
247
|
+
// under non-TTY capture (observed: `tee`-wrapped eval output saw only
|
|
248
|
+
// the intro). The fix writes the authoritative PASS/FAIL line via
|
|
249
|
+
// `process.stdout.write` as the very first output after the probe
|
|
250
|
+
// returns, so the captured stream has an unambiguous summary no
|
|
251
|
+
// matter what clack does on top. The clack-rendered banner
|
|
252
|
+
// (`info`/`warn`) is retained so TTY users keep the visual framing.
|
|
253
|
+
const directLine = formatMarionettePreflightLine(preflight);
|
|
254
|
+
process.stdout.write(`${directLine}\n`);
|
|
198
255
|
reportMarionettePreflight(preflight);
|
|
199
256
|
if (testPaths.length === 0) {
|
|
200
257
|
if (!preflight.ok) {
|
|
201
258
|
throw new GeneralError('Marionette preflight reported FAIL — see output above.');
|
|
202
259
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
// unclosed tree — in the eval's non-TTY capture the info line
|
|
206
|
-
// itself failed to render, so `test --doctor` looked like it had
|
|
207
|
-
// exited silently after the spinner start line. The outro also
|
|
208
|
-
// gives scripts a deterministic "done" marker to parse.
|
|
209
|
-
outro(`Marionette preflight: PASS (${preflight.durationMs}ms)`);
|
|
260
|
+
success(directLine);
|
|
261
|
+
outro('Test completed');
|
|
210
262
|
return;
|
|
211
263
|
}
|
|
212
264
|
if (!preflight.ok) {
|
|
@@ -256,7 +308,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
256
308
|
catch (error) {
|
|
257
309
|
throw new BuildError('Test process failed to start', 'mach test', error instanceof Error ? error : undefined);
|
|
258
310
|
}
|
|
259
|
-
handleNonZeroTestExit(result, normalizedPaths, appdirInjection);
|
|
311
|
+
handleNonZeroTestExit(result, normalizedPaths, appdirInjection, projectConfig.binaryName);
|
|
260
312
|
}
|
|
261
313
|
/**
|
|
262
314
|
* Resolves and (when applicable) appends an `--app-path=<abs>` arg to
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
4
4
|
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
5
|
-
import {
|
|
5
|
+
import { isGitRepository } from '../core/git.js';
|
|
6
|
+
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
6
7
|
import { measureTokenCoverage } from '../core/token-coverage.js';
|
|
7
8
|
import { getTokensCssPath } from '../core/token-manager.js';
|
|
8
9
|
import { GeneralError } from '../errors/base.js';
|
|
@@ -23,8 +24,14 @@ export async function tokenCoverageCommand(projectRoot) {
|
|
|
23
24
|
}
|
|
24
25
|
const config = await loadConfig(projectRoot);
|
|
25
26
|
const tokensCssPath = getTokensCssPath(config.binaryName);
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// Expand collapsed `?? dir/` untracked entries so untracked CSS files
|
|
28
|
+
// inside a new patch-added directory are included in coverage. Before
|
|
29
|
+
// this, an imported fork that added a new CSS tree saw "No modified
|
|
30
|
+
// CSS files" because `git status --porcelain` collapsed the directory
|
|
31
|
+
// and the file-extension filter could not see the .css inside.
|
|
32
|
+
const rawStatus = await getWorkingTreeStatus(paths.engine);
|
|
33
|
+
const expandedStatus = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
|
|
34
|
+
const statusCssFiles = expandedStatus
|
|
28
35
|
.filter((f) => f.file.endsWith('.css') && f.file !== tokensCssPath)
|
|
29
36
|
.map((f) => f.file);
|
|
30
37
|
// Also scan CSS files deployed by Furnace custom components. Deployed
|