@hominis/fireforge 0.19.6 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +22 -4
- package/dist/src/commands/build.js +19 -1
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/download.js +188 -185
- package/dist/src/commands/export-flow.js +2 -13
- package/dist/src/commands/furnace/chrome-doc-remove.d.ts +13 -0
- package/dist/src/commands/furnace/chrome-doc-remove.js +142 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +32 -0
- package/dist/src/commands/furnace/chrome-doc.js +113 -1
- package/dist/src/commands/furnace/create-validation.d.ts +6 -0
- package/dist/src/commands/furnace/create-validation.js +59 -0
- package/dist/src/commands/furnace/create.js +13 -88
- package/dist/src/commands/furnace/index.js +14 -0
- package/dist/src/commands/furnace/refresh.js +11 -2
- package/dist/src/commands/furnace/remove-state.d.ts +5 -0
- package/dist/src/commands/furnace/remove-state.js +14 -0
- package/dist/src/commands/furnace/remove.js +33 -45
- package/dist/src/commands/furnace/rename-browser-test.d.ts +2 -0
- package/dist/src/commands/furnace/rename-browser-test.js +28 -0
- package/dist/src/commands/furnace/rename-helpers.d.ts +13 -0
- package/dist/src/commands/furnace/rename-helpers.js +42 -0
- package/dist/src/commands/furnace/rename.js +29 -48
- package/dist/src/commands/status.js +22 -3
- package/dist/src/commands/test.js +3 -0
- package/dist/src/commands/watch.js +9 -2
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +5 -0
- package/dist/src/core/config.js +11 -7
- package/dist/src/core/file-lock.js +2 -2
- package/dist/src/core/firefox-cache.d.ts +1 -1
- package/dist/src/core/firefox-cache.js +43 -17
- package/dist/src/core/firefox-download.js +12 -4
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +2 -2
- package/dist/src/core/furnace-config.js +4 -0
- package/dist/src/core/furnace-refresh.js +16 -5
- package/dist/src/core/patch-lint-imports.d.ts +5 -0
- package/dist/src/core/patch-lint-imports.js +68 -0
- package/dist/src/core/patch-lint.js +2 -3
- package/dist/src/types/config.d.ts +2 -0
- package/dist/src/utils/fs.d.ts +5 -0
- package/dist/src/utils/fs.js +54 -1
- package/dist/src/utils/process.js +4 -1
- package/package.json +2 -2
|
@@ -16,6 +16,7 @@ import { FurnaceError } from '../../errors/furnace.js';
|
|
|
16
16
|
import { toError } from '../../utils/errors.js';
|
|
17
17
|
import { pathExists, readText, removeDir, removeFile, writeText } from '../../utils/fs.js';
|
|
18
18
|
import { cancel, info, intro, isCancel, outro, warn } from '../../utils/logger.js';
|
|
19
|
+
import { dropChecksumsByPrefix } from './remove-state.js';
|
|
19
20
|
/**
|
|
20
21
|
* Removes an entire TOML section (header + body lines) for a given test file.
|
|
21
22
|
* Matches from `["filename"]` up to the next section header `[` or end-of-file,
|
|
@@ -328,15 +329,22 @@ async function cleanupCustomXpcshellTestFiles(name, projectRoot, journal) {
|
|
|
328
329
|
}
|
|
329
330
|
return { partialFailures };
|
|
330
331
|
}
|
|
331
|
-
function
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
result.engineChecksums = Object.fromEntries(Object.entries(state.engineChecksums).filter(([k]) => !k.startsWith(prefix)));
|
|
332
|
+
async function loadFreshRemoveTarget(projectRoot, name, engineDir) {
|
|
333
|
+
const config = await loadFurnaceConfig(projectRoot);
|
|
334
|
+
const state = await loadFurnaceState(projectRoot);
|
|
335
|
+
const type = findComponentType(config, name);
|
|
336
|
+
if (!type) {
|
|
337
|
+
throw new FurnaceError(`Component "${name}" not found in furnace.json. Run "fireforge furnace list" to see registered components.`, name);
|
|
338
338
|
}
|
|
339
|
-
|
|
339
|
+
await requireGitEngineForRemove(type, name, engineDir);
|
|
340
|
+
return { config, ftlDir: resolveFtlDir(config.ftlBasePath), state, type };
|
|
341
|
+
}
|
|
342
|
+
async function cleanupAllCustomTestFiles(name, projectRoot, journal) {
|
|
343
|
+
const result = await cleanupCustomTestFiles(name, projectRoot, journal);
|
|
344
|
+
const failures = [...result.partialFailures];
|
|
345
|
+
failures.push(...(await cleanupCustomXpcshellTestFiles(name, projectRoot, journal)).partialFailures);
|
|
346
|
+
failures.push(...(await cleanupCustomMochikitTestFiles(name, projectRoot, journal)).partialFailures);
|
|
347
|
+
return failures;
|
|
340
348
|
}
|
|
341
349
|
/**
|
|
342
350
|
* Confirms the remove operation interactively when TTY is available, or
|
|
@@ -380,9 +388,7 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
380
388
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
381
389
|
intro('Furnace Remove');
|
|
382
390
|
const config = await loadFurnaceConfig(projectRoot);
|
|
383
|
-
const state = await loadFurnaceState(projectRoot);
|
|
384
391
|
const furnacePaths = getFurnacePaths(projectRoot);
|
|
385
|
-
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
386
392
|
// Find which section the component belongs to
|
|
387
393
|
const type = findComponentType(config, name);
|
|
388
394
|
if (!type) {
|
|
@@ -402,8 +408,9 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
402
408
|
const journal = createRollbackJournal();
|
|
403
409
|
ctx.registerJournal(journal);
|
|
404
410
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
411
|
+
const { config: freshConfig, ftlDir, state: freshState, type: freshType, } = await loadFreshRemoveTarget(projectRoot, name, paths.engine);
|
|
412
|
+
if (freshType === 'override') {
|
|
413
|
+
const overrideConfig = freshConfig.overrides[name];
|
|
407
414
|
const dir = join(furnacePaths.overridesDir, name);
|
|
408
415
|
// Restore deployed engine files BEFORE removing the workspace
|
|
409
416
|
// directory. The restore set is the union of (a) files currently in
|
|
@@ -411,7 +418,7 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
411
418
|
// — without (b), source-side deletions would orphan engine copies
|
|
412
419
|
// that this command can never see again.
|
|
413
420
|
if (overrideConfig?.basePath) {
|
|
414
|
-
const previousKeys = Object.keys(extractComponentChecksums(
|
|
421
|
+
const previousKeys = Object.keys(extractComponentChecksums(freshState.appliedChecksums, 'override', name));
|
|
415
422
|
const { restored, removed } = await restoreOverrideEngineFiles(paths.engine, dir, overrideConfig, previousKeys, ftlDir, journal);
|
|
416
423
|
if (restored > 0) {
|
|
417
424
|
info(`Restored ${restored} file${restored === 1 ? '' : 's'} in engine/${overrideConfig.basePath} to Firefox baseline`);
|
|
@@ -426,8 +433,8 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
426
433
|
info(`Deleted components/overrides/${name}/`);
|
|
427
434
|
}
|
|
428
435
|
}
|
|
429
|
-
else if (
|
|
430
|
-
const customConfig =
|
|
436
|
+
else if (freshType === 'custom') {
|
|
437
|
+
const customConfig = freshConfig.custom[name];
|
|
431
438
|
// Custom-component removal mutates engine files (jar.mn,
|
|
432
439
|
// customElements.js, deployed widgets, optional .ftl) and the
|
|
433
440
|
// rollback journal is the only safety net for those edits while
|
|
@@ -479,46 +486,27 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
479
486
|
await removeCustomFtlJarMnEntry(paths.engine, `${name}.ftl`, ftlDir, customConfig, journal);
|
|
480
487
|
}
|
|
481
488
|
}
|
|
482
|
-
|
|
483
|
-
if (type === 'custom') {
|
|
484
|
-
const result = await cleanupCustomTestFiles(name, projectRoot, journal);
|
|
485
|
-
testCleanupFailures = result.partialFailures;
|
|
486
|
-
// 2026-04-24 eval Finding 5: also clean up xpcshell scaffolds
|
|
487
|
-
// generated by `furnace create --with-tests --xpcshell`. The
|
|
488
|
-
// mochitest cleanup above covers `browser/base/content/test/
|
|
489
|
-
// <binary>/`, but xpcshell scaffolds live in the sibling
|
|
490
|
-
// `<binary>-xpcshell/` directory and were orphaned by prior
|
|
491
|
-
// versions.
|
|
492
|
-
const xpcshellResult = await cleanupCustomXpcshellTestFiles(name, projectRoot, journal);
|
|
493
|
-
testCleanupFailures.push(...xpcshellResult.partialFailures);
|
|
494
|
-
// 2026-04-25 eval Finding 13: mochikit-style scaffolds
|
|
495
|
-
// (`--test-style mochikit`) live under
|
|
496
|
-
// `engine/toolkit/content/tests/widgets/` with `chrome.toml`
|
|
497
|
-
// entries — neither the browser-chrome path nor the xpcshell
|
|
498
|
-
// path touches them. Without this pass, a `furnace create
|
|
499
|
-
// --with-tests --test-style mochikit` followed by `furnace
|
|
500
|
-
// remove` left the test file and its toml entry referencing a
|
|
501
|
-
// component that no longer exists.
|
|
502
|
-
const mochikitResult = await cleanupCustomMochikitTestFiles(name, projectRoot, journal);
|
|
503
|
-
testCleanupFailures.push(...mochikitResult.partialFailures);
|
|
504
|
-
}
|
|
489
|
+
const testCleanupFailures = freshType === 'custom' ? await cleanupAllCustomTestFiles(name, projectRoot, journal) : [];
|
|
505
490
|
// Remove entry from furnace.json
|
|
506
|
-
if (
|
|
507
|
-
|
|
491
|
+
if (freshType === 'stock') {
|
|
492
|
+
freshConfig.stock = freshConfig.stock.filter((s) => s !== name);
|
|
508
493
|
}
|
|
509
|
-
else if (
|
|
510
|
-
|
|
494
|
+
else if (freshType === 'override') {
|
|
495
|
+
freshConfig.overrides = Object.fromEntries(Object.entries(freshConfig.overrides).filter(([key]) => key !== name));
|
|
496
|
+
if (!freshConfig.stock.includes(name)) {
|
|
497
|
+
freshConfig.stock.push(name);
|
|
498
|
+
}
|
|
511
499
|
}
|
|
512
500
|
else {
|
|
513
|
-
|
|
501
|
+
freshConfig.custom = Object.fromEntries(Object.entries(freshConfig.custom).filter(([key]) => key !== name));
|
|
514
502
|
}
|
|
515
503
|
await snapshotFile(journal, furnacePaths.furnaceConfig);
|
|
516
|
-
await writeFurnaceConfig(projectRoot,
|
|
504
|
+
await writeFurnaceConfig(projectRoot, freshConfig);
|
|
517
505
|
// Drop stale per-file checksums inside the same transactional block.
|
|
518
506
|
// Snapshotting the state file into the rollback journal means the
|
|
519
507
|
// entire remove operation is a single atomic unit.
|
|
520
508
|
await snapshotFile(journal, furnacePaths.furnaceState);
|
|
521
|
-
await updateFurnaceState(projectRoot, (state) => dropChecksumsByPrefix(state, `${
|
|
509
|
+
await updateFurnaceState(projectRoot, (state) => dropChecksumsByPrefix(state, `${freshType}/${name}/`));
|
|
522
510
|
// Test-cleanup failures are warn-and-continue by design (test files
|
|
523
511
|
// are secondary artefacts), but the caller deserves a single summary
|
|
524
512
|
// line pointing at the residue so they don't have to re-scan earlier
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { tagNameToClassName } from '../../core/furnace-constants.js';
|
|
3
|
+
/** Escapes regex metacharacters so a user-supplied name is literal inside a RegExp. */
|
|
4
|
+
function escapeRegex(input) {
|
|
5
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6
|
+
}
|
|
7
|
+
function deriveTestStem(componentName, binaryName) {
|
|
8
|
+
const strippedName = componentName.startsWith('moz-') ? componentName.slice(4) : componentName;
|
|
9
|
+
const withoutBinaryPrefix = strippedName.startsWith(binaryName + '-')
|
|
10
|
+
? strippedName.slice(binaryName.length + 1)
|
|
11
|
+
: strippedName;
|
|
12
|
+
return withoutBinaryPrefix.replace(/-/g, '_');
|
|
13
|
+
}
|
|
14
|
+
/** Rewrites scaffolded browser-chrome test literals after a component rename. */
|
|
15
|
+
export function updateBrowserChromeTestContent(content, oldName, newName, binaryName) {
|
|
16
|
+
const oldClassName = tagNameToClassName(oldName);
|
|
17
|
+
const newClassName = tagNameToClassName(newName);
|
|
18
|
+
const oldUnderscored = oldName.replace(/-/g, '_');
|
|
19
|
+
const newUnderscored = newName.replace(/-/g, '_');
|
|
20
|
+
const oldTestStem = deriveTestStem(oldName, binaryName);
|
|
21
|
+
const newTestStem = deriveTestStem(newName, binaryName);
|
|
22
|
+
return content
|
|
23
|
+
.replace(new RegExp(escapeRegex(oldName), 'g'), newName)
|
|
24
|
+
.replace(new RegExp(escapeRegex(oldClassName), 'g'), newClassName)
|
|
25
|
+
.replace(new RegExp(escapeRegex(oldUnderscored), 'g'), newUnderscored)
|
|
26
|
+
.replace(new RegExp(escapeRegex(oldTestStem), 'g'), newTestStem);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=rename-browser-test.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FurnaceConfig } from '../../types/furnace.js';
|
|
2
|
+
/**
|
|
3
|
+
* Applies a component rename to scaffold-owned filenames only.
|
|
4
|
+
*/
|
|
5
|
+
export declare function renameComponentFileName(fileName: string, oldName: string, newName: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Re-keys a custom component config entry and same-config compose references.
|
|
8
|
+
*/
|
|
9
|
+
export declare function updateConfigForCustomRename(config: FurnaceConfig, oldName: string, newName: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Re-keys an override component config entry.
|
|
12
|
+
*/
|
|
13
|
+
export declare function updateConfigForOverrideRename(config: FurnaceConfig, oldName: string, newName: string): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Applies a component rename to scaffold-owned filenames only.
|
|
3
|
+
*/
|
|
4
|
+
export function renameComponentFileName(fileName, oldName, newName) {
|
|
5
|
+
if (fileName === oldName)
|
|
6
|
+
return newName;
|
|
7
|
+
if (fileName.startsWith(oldName + '.')) {
|
|
8
|
+
return newName + fileName.slice(oldName.length);
|
|
9
|
+
}
|
|
10
|
+
return fileName;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Re-keys a custom component config entry and same-config compose references.
|
|
14
|
+
*/
|
|
15
|
+
export function updateConfigForCustomRename(config, oldName, newName) {
|
|
16
|
+
const oldConfig = config.custom[oldName];
|
|
17
|
+
if (!oldConfig)
|
|
18
|
+
return;
|
|
19
|
+
config.custom[newName] = {
|
|
20
|
+
...oldConfig,
|
|
21
|
+
targetPath: oldConfig.targetPath.replace(new RegExp(`(^|/)${oldName}$`), `$1${newName}`),
|
|
22
|
+
};
|
|
23
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- idiomatic key removal from config record
|
|
24
|
+
delete config.custom[oldName];
|
|
25
|
+
for (const customConfig of Object.values(config.custom)) {
|
|
26
|
+
if (customConfig.composes) {
|
|
27
|
+
customConfig.composes = customConfig.composes.map((ref) => (ref === oldName ? newName : ref));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Re-keys an override component config entry.
|
|
33
|
+
*/
|
|
34
|
+
export function updateConfigForOverrideRename(config, oldName, newName) {
|
|
35
|
+
const oldConfig = config.overrides[oldName];
|
|
36
|
+
if (!oldConfig)
|
|
37
|
+
return;
|
|
38
|
+
config.overrides[newName] = { ...oldConfig };
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- idiomatic key removal from config record
|
|
40
|
+
delete config.overrides[oldName];
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=rename-helpers.js.map
|
|
@@ -14,52 +14,13 @@ 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 { updateBrowserChromeTestContent } from './rename-browser-test.js';
|
|
18
|
+
import { renameComponentFileName, updateConfigForCustomRename, updateConfigForOverrideRename, } from './rename-helpers.js';
|
|
17
19
|
import { renameXpcshellTestFiles } from './rename-xpcshell.js';
|
|
18
20
|
/** Escapes regex metacharacters so a user-supplied name is literal inside a RegExp. */
|
|
19
21
|
function escapeRegex(input) {
|
|
20
22
|
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
23
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Applies the component rename to a filename. Only replaces the leading
|
|
24
|
-
* component name when it is followed by `.` (extension) or equals the
|
|
25
|
-
* filename exactly; every other filename is returned unchanged so stray
|
|
26
|
-
* assets, editor backups, or files whose name coincidentally contains the
|
|
27
|
-
* old component name in the middle or at the end are not accidentally
|
|
28
|
-
* renamed.
|
|
29
|
-
*/
|
|
30
|
-
function renameComponentFileName(fileName, oldName, newName) {
|
|
31
|
-
if (fileName === oldName)
|
|
32
|
-
return newName;
|
|
33
|
-
if (fileName.startsWith(oldName + '.')) {
|
|
34
|
-
return newName + fileName.slice(oldName.length);
|
|
35
|
-
}
|
|
36
|
-
return fileName;
|
|
37
|
-
}
|
|
38
|
-
function updateConfigForCustomRename(config, oldName, newName) {
|
|
39
|
-
const oldConfig = config.custom[oldName];
|
|
40
|
-
if (!oldConfig)
|
|
41
|
-
return;
|
|
42
|
-
config.custom[newName] = {
|
|
43
|
-
...oldConfig,
|
|
44
|
-
targetPath: oldConfig.targetPath.replace(new RegExp(`(^|/)${oldName}$`), `$1${newName}`),
|
|
45
|
-
};
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- idiomatic key removal from config record
|
|
47
|
-
delete config.custom[oldName];
|
|
48
|
-
// Update composes references in other components
|
|
49
|
-
for (const customConfig of Object.values(config.custom)) {
|
|
50
|
-
if (customConfig.composes) {
|
|
51
|
-
customConfig.composes = customConfig.composes.map((ref) => (ref === oldName ? newName : ref));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function updateConfigForOverrideRename(config, oldName, newName) {
|
|
56
|
-
const oldConfig = config.overrides[oldName];
|
|
57
|
-
if (!oldConfig)
|
|
58
|
-
return;
|
|
59
|
-
config.overrides[newName] = { ...oldConfig };
|
|
60
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- idiomatic key removal from config record
|
|
61
|
-
delete config.overrides[oldName];
|
|
62
|
-
}
|
|
63
24
|
/**
|
|
64
25
|
* Derives the test file name for a component, matching the convention used by
|
|
65
26
|
* `furnace create --with-tests`.
|
|
@@ -98,7 +59,7 @@ async function renameTestFiles(engineDir, projectRoot, oldName, newName, journal
|
|
|
98
59
|
try {
|
|
99
60
|
await snapshotFile(journal, oldTestPath);
|
|
100
61
|
const content = await readText(oldTestPath);
|
|
101
|
-
await writeText(newTestPath, content);
|
|
62
|
+
await writeText(newTestPath, updateBrowserChromeTestContent(content, oldName, newName, binaryName));
|
|
102
63
|
await removeFile(oldTestPath);
|
|
103
64
|
info(`Renamed test file: ${oldTestFileName} → ${newTestFileName}`);
|
|
104
65
|
}
|
|
@@ -215,18 +176,37 @@ async function renameMochikitTestFiles(engineDir, oldName, newName, journal) {
|
|
|
215
176
|
* Performs the transactional rename mutation inside a furnace lock.
|
|
216
177
|
*/
|
|
217
178
|
async function performRenameMutations(args) {
|
|
218
|
-
const { projectRoot, oldName, newName
|
|
179
|
+
const { projectRoot, oldName, newName } = args;
|
|
219
180
|
const oldClassName = tagNameToClassName(oldName);
|
|
220
181
|
const newClassName = tagNameToClassName(newName);
|
|
221
|
-
// Capture the pre-rename deployed target path so we know what to
|
|
222
|
-
// clean up in the engine tree. `updateConfigForCustomRename` rewrites
|
|
223
|
-
// `targetPath` in-place once the mutation enters phase 2, so we read
|
|
224
|
-
// it here while it still points at the old name's deployment.
|
|
225
|
-
const oldCustomTargetPath = isCustom ? config.custom[oldName]?.targetPath : undefined;
|
|
226
182
|
await runFurnaceMutation(projectRoot, 'rename-rollback', async (ctx) => {
|
|
227
183
|
const journal = createRollbackJournal();
|
|
228
184
|
ctx.registerJournal(journal);
|
|
185
|
+
let newDir = args.newDir;
|
|
229
186
|
try {
|
|
187
|
+
const config = await loadFurnaceConfig(projectRoot);
|
|
188
|
+
const isCustom = oldName in config.custom;
|
|
189
|
+
const isOverride = oldName in config.overrides;
|
|
190
|
+
if (!isCustom && !isOverride) {
|
|
191
|
+
throw new FurnaceError(`Component "${oldName}" not found in furnace.json. Only custom and override components can be renamed.`, oldName);
|
|
192
|
+
}
|
|
193
|
+
if (newName in config.custom ||
|
|
194
|
+
newName in config.overrides ||
|
|
195
|
+
config.stock.includes(newName)) {
|
|
196
|
+
throw new FurnaceError(`A component named "${newName}" already exists in furnace.json.`, newName);
|
|
197
|
+
}
|
|
198
|
+
const componentType = isCustom ? 'custom' : 'override';
|
|
199
|
+
const componentDirLabel = isCustom ? 'custom' : 'overrides';
|
|
200
|
+
const baseDir = isCustom ? args.furnacePaths.customDir : args.furnacePaths.overridesDir;
|
|
201
|
+
const oldDir = join(baseDir, oldName);
|
|
202
|
+
newDir = join(baseDir, newName);
|
|
203
|
+
const oldCustomTargetPath = isCustom ? config.custom[oldName]?.targetPath : undefined;
|
|
204
|
+
if (!(await pathExists(oldDir))) {
|
|
205
|
+
throw new FurnaceError(`Component directory not found: components/${componentDirLabel}/${oldName}`, oldName);
|
|
206
|
+
}
|
|
207
|
+
if (await pathExists(newDir)) {
|
|
208
|
+
throw new FurnaceError(`Target directory already exists: components/${componentDirLabel}/${newName}`, newName);
|
|
209
|
+
}
|
|
230
210
|
await snapshotDir(journal, oldDir);
|
|
231
211
|
await snapshotFile(journal, args.furnaceConfigPath);
|
|
232
212
|
// 1. Create new directory with renamed files and updated content
|
|
@@ -472,6 +452,7 @@ export async function furnaceRenameCommand(projectRoot, oldName, newName) {
|
|
|
472
452
|
componentType,
|
|
473
453
|
config,
|
|
474
454
|
furnaceConfigPath: furnacePaths.furnaceConfig,
|
|
455
|
+
furnacePaths,
|
|
475
456
|
engineDir: paths.engine,
|
|
476
457
|
});
|
|
477
458
|
note(`Component renamed: ${oldName} → ${newName}\n\n` +
|
|
@@ -221,7 +221,7 @@ function filterFireForgeTempFiles(files) {
|
|
|
221
221
|
async function renderJsonStatus(files, paths, projectRoot, binaryName) {
|
|
222
222
|
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
223
223
|
const classified = await classifyFiles(files, paths.engine, paths.patches, binaryName, furnacePrefixes);
|
|
224
|
-
const
|
|
224
|
+
const outputFiles = classified.map((f) => {
|
|
225
225
|
const entry = {
|
|
226
226
|
file: f.file,
|
|
227
227
|
status: f.status.trim(),
|
|
@@ -236,6 +236,24 @@ async function renderJsonStatus(files, paths, projectRoot, binaryName) {
|
|
|
236
236
|
}
|
|
237
237
|
return entry;
|
|
238
238
|
});
|
|
239
|
+
const byClassification = {
|
|
240
|
+
unmanaged: 0,
|
|
241
|
+
'patch-backed': 0,
|
|
242
|
+
branding: 0,
|
|
243
|
+
furnace: 0,
|
|
244
|
+
conflict: 0,
|
|
245
|
+
};
|
|
246
|
+
for (const file of outputFiles) {
|
|
247
|
+
byClassification[file.classification]++;
|
|
248
|
+
}
|
|
249
|
+
const output = {
|
|
250
|
+
schemaVersion: 1,
|
|
251
|
+
summary: {
|
|
252
|
+
total: outputFiles.length,
|
|
253
|
+
byClassification,
|
|
254
|
+
},
|
|
255
|
+
files: outputFiles,
|
|
256
|
+
};
|
|
239
257
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
240
258
|
}
|
|
241
259
|
/**
|
|
@@ -267,7 +285,8 @@ async function assertEngineHasBaselineCommit(engineDir, options) {
|
|
|
267
285
|
// throw still falls through to the styled error renderer in
|
|
268
286
|
// withErrorHandling, leaving JSON consumers with non-JSON output on
|
|
269
287
|
// exactly the failure mode they care about catching.
|
|
270
|
-
process.stdout.write(JSON.stringify({ error: guidance, code: 'engine-baseline-missing' }) +
|
|
288
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 1, error: guidance, code: 'engine-baseline-missing' }) +
|
|
289
|
+
'\n');
|
|
271
290
|
}
|
|
272
291
|
throw new GeneralError(guidance);
|
|
273
292
|
}
|
|
@@ -307,7 +326,7 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
307
326
|
// `withErrorHandling` does not log: bin/fireforge.ts catches it,
|
|
308
327
|
// exits with the carried code, and stdout stays a single JSON line.
|
|
309
328
|
const emitJsonError = (code, message) => {
|
|
310
|
-
process.stdout.write(JSON.stringify({ error: message, code }) + '\n');
|
|
329
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 1, error: message, code }) + '\n');
|
|
311
330
|
throw new CommandError(ExitCode.GENERAL_ERROR);
|
|
312
331
|
};
|
|
313
332
|
// Ownership mode is a flat file→patch table; sources are the manifest's
|
|
@@ -201,8 +201,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
201
201
|
// here so `test --doctor` against an incomplete build surfaces the
|
|
202
202
|
// missing-bundle path instead of a cryptic `Browser process exited
|
|
203
203
|
// during spawn (exit code 1, signal none). stderr tail: (empty)`.
|
|
204
|
+
let launchablePath;
|
|
204
205
|
if (buildCheck.objDir) {
|
|
205
206
|
const bundleCheck = await hasRunnableBundle(paths.engine, projectConfig.binaryName, buildCheck.objDir);
|
|
207
|
+
launchablePath = bundleCheck.expectedPath;
|
|
206
208
|
if (!bundleCheck.runnable) {
|
|
207
209
|
const expectedSuffix = bundleCheck.expectedPath
|
|
208
210
|
? ` (expected at engine/${bundleCheck.expectedPath})`
|
|
@@ -287,6 +289,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
287
289
|
// (`info`/`warn`) is retained so TTY users keep the visual framing.
|
|
288
290
|
const directLine = formatMarionettePreflightLine(preflight);
|
|
289
291
|
process.stdout.write(`${directLine}\n`);
|
|
292
|
+
process.stdout.write(`Marionette preflight environment: objdir=${buildCheck.objDir ?? '(none)'}; binary=${projectConfig.binaryName}; app=${launchablePath ? `engine/${launchablePath}` : '(unknown)'}; port=${effectivePort ?? 2828}; elapsed=${preflight.durationMs}ms\n`);
|
|
290
293
|
reportMarionettePreflight(preflight);
|
|
291
294
|
if (testPaths.length === 0) {
|
|
292
295
|
if (!preflight.ok) {
|
|
@@ -60,14 +60,21 @@ function buildWatchmanConfigureTimeMessage() {
|
|
|
60
60
|
* @param watchmanPath - Optional absolute path to the resolved watchman binary; surfaced in the guidance so the operator can see whether FireForge actually found one.
|
|
61
61
|
* @returns User-facing failure guidance
|
|
62
62
|
*/
|
|
63
|
-
function
|
|
63
|
+
function hasWatchPermissionFailure(output) {
|
|
64
|
+
return /Operation not permitted|EPERM|EACCES/i.test(output);
|
|
65
|
+
}
|
|
66
|
+
function buildUnsupportedWatchMessage(exitCode, watchmanPath, output = '') {
|
|
64
67
|
const watchmanLine = watchmanPath
|
|
65
68
|
? ` - FireForge resolved watchman at ${watchmanPath} and prepended its directory to the mach subprocess PATH. If mach still did not see it, ensure that path is stable between runs.\n`
|
|
66
69
|
: '';
|
|
70
|
+
const permissionLine = hasWatchPermissionFailure(output)
|
|
71
|
+
? ' - macOS may be blocking watchman or Terminal/Codex from reading the engine directory. Grant Full Disk Access or Files and Folders access to your terminal app and watchman, then restart watchman with "watchman shutdown-server".\n'
|
|
72
|
+
: '';
|
|
67
73
|
return (`Watch failed with exit code ${exitCode}. Check the output above for details.\n\n` +
|
|
68
74
|
'Common causes:\n' +
|
|
69
75
|
' - watchman is not installed or not in PATH right now\n' +
|
|
70
76
|
' - watchman was installed only after the current obj-* directory was configured; delete obj-* and rebuild\n' +
|
|
77
|
+
permissionLine +
|
|
71
78
|
' - mach watch is unsupported in the current objdir or build environment\n' +
|
|
72
79
|
watchmanLine +
|
|
73
80
|
'\n' +
|
|
@@ -194,7 +201,7 @@ export async function watchCommand(projectRoot) {
|
|
|
194
201
|
throw new GeneralError(buildWatchmanConfigureTimeMessage());
|
|
195
202
|
}
|
|
196
203
|
// 130 is SIGINT (Ctrl+C), which is expected
|
|
197
|
-
throw new BuildError(buildUnsupportedWatchMessage(result.exitCode, watchmanPath), 'mach watch');
|
|
204
|
+
throw new BuildError(buildUnsupportedWatchMessage(result.exitCode, watchmanPath, combinedOutput), 'mach watch');
|
|
198
205
|
}
|
|
199
206
|
outro('Watch mode stopped');
|
|
200
207
|
}
|
|
@@ -19,7 +19,7 @@ export declare const SRC_DIR = "src";
|
|
|
19
19
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
20
20
|
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint", "typecheck", "markerComment"];
|
|
21
21
|
/** Supported config paths that can be read or set without --force. */
|
|
22
|
-
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
22
|
+
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
23
23
|
/**
|
|
24
24
|
* Gets all project paths based on a root directory.
|
|
25
25
|
* @param root - Root directory of the project
|
|
@@ -75,6 +75,10 @@ export function validateConfig(data) {
|
|
|
75
75
|
if (compatError) {
|
|
76
76
|
throw new ConfigError(compatError);
|
|
77
77
|
}
|
|
78
|
+
const firefoxSha256 = optionalConfigString(firefoxRec, 'sha256', 'firefox.sha256');
|
|
79
|
+
if (firefoxSha256 !== undefined && !/^[a-f0-9]{64}$/i.test(firefoxSha256)) {
|
|
80
|
+
throw new ConfigError('Config field "firefox.sha256" must be a 64-character SHA-256 hex digest');
|
|
81
|
+
}
|
|
78
82
|
// Optional configs
|
|
79
83
|
const config = {
|
|
80
84
|
name,
|
|
@@ -84,6 +88,7 @@ export function validateConfig(data) {
|
|
|
84
88
|
firefox: {
|
|
85
89
|
version: firefoxVersion,
|
|
86
90
|
product: firefoxProduct,
|
|
91
|
+
...(firefoxSha256 !== undefined ? { sha256: firefoxSha256.toLowerCase() } : {}),
|
|
87
92
|
},
|
|
88
93
|
};
|
|
89
94
|
// Build
|
package/dist/src/core/config.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
12
|
import { ConfigError, ConfigNotFoundError } from '../errors/config.js';
|
|
13
13
|
import { toError } from '../utils/errors.js';
|
|
14
|
-
import
|
|
14
|
+
import * as fsUtils from '../utils/fs.js';
|
|
15
15
|
import { getProjectPaths } from './config-paths.js';
|
|
16
16
|
import { validateConfig } from './config-validate.js';
|
|
17
17
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
@@ -21,6 +21,10 @@ export { CONFIG_FILENAME, FIREFORGE_DIR, getProjectPaths, STATE_FILENAME, SUPPOR
|
|
|
21
21
|
export { loadState, saveState, updateState } from './config-state.js';
|
|
22
22
|
export { validateConfig } from './config-validate.js';
|
|
23
23
|
// ---- config I/O (stays here because it bridges paths + validation) ----
|
|
24
|
+
async function configPathExists(path) {
|
|
25
|
+
const fs = fsUtils;
|
|
26
|
+
return (fs.pathExistsStrict ?? fsUtils.pathExists)(path);
|
|
27
|
+
}
|
|
24
28
|
/**
|
|
25
29
|
* Checks if a fireforge.json exists in the given directory.
|
|
26
30
|
* @param root - Root directory to check
|
|
@@ -28,7 +32,7 @@ export { validateConfig } from './config-validate.js';
|
|
|
28
32
|
*/
|
|
29
33
|
export async function configExists(root) {
|
|
30
34
|
const paths = getProjectPaths(root);
|
|
31
|
-
return
|
|
35
|
+
return configPathExists(paths.config);
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* Loads and validates the fireforge.json configuration.
|
|
@@ -38,11 +42,11 @@ export async function configExists(root) {
|
|
|
38
42
|
*/
|
|
39
43
|
export async function loadConfig(root) {
|
|
40
44
|
const paths = getProjectPaths(root);
|
|
41
|
-
if (!(await
|
|
45
|
+
if (!(await configPathExists(paths.config))) {
|
|
42
46
|
throw new ConfigNotFoundError(paths.config);
|
|
43
47
|
}
|
|
44
48
|
try {
|
|
45
|
-
const data = await readJson(paths.config);
|
|
49
|
+
const data = await fsUtils.readJson(paths.config);
|
|
46
50
|
return validateConfig(data);
|
|
47
51
|
}
|
|
48
52
|
catch (error) {
|
|
@@ -70,11 +74,11 @@ export async function loadConfig(root) {
|
|
|
70
74
|
*/
|
|
71
75
|
export async function loadRawConfigDocument(root) {
|
|
72
76
|
const paths = getProjectPaths(root);
|
|
73
|
-
if (!(await
|
|
77
|
+
if (!(await configPathExists(paths.config))) {
|
|
74
78
|
throw new ConfigNotFoundError(paths.config);
|
|
75
79
|
}
|
|
76
80
|
try {
|
|
77
|
-
const data = await readJson(paths.config);
|
|
81
|
+
const data = await fsUtils.readJson(paths.config);
|
|
78
82
|
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
79
83
|
throw new ConfigError(`Invalid fireforge.json at ${paths.config}: expected an object`);
|
|
80
84
|
}
|
|
@@ -110,7 +114,7 @@ export async function writeConfig(root, config) {
|
|
|
110
114
|
*/
|
|
111
115
|
export async function writeConfigDocument(root, config) {
|
|
112
116
|
const paths = getProjectPaths(root);
|
|
113
|
-
await writeJson(paths.config, config);
|
|
117
|
+
await fsUtils.writeJson(paths.config, config);
|
|
114
118
|
}
|
|
115
119
|
/**
|
|
116
120
|
* Runs an operation while holding a sidecar lock on `fireforge.json`.
|
|
@@ -35,8 +35,8 @@ function isProcessAlive(pid) {
|
|
|
35
35
|
process.kill(pid, 0);
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
|
-
catch {
|
|
39
|
-
return
|
|
38
|
+
catch (error) {
|
|
39
|
+
return getNodeErrorCode(error) !== 'ESRCH';
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
@@ -14,7 +14,7 @@ export declare function sha256File(filePath: string): Promise<string>;
|
|
|
14
14
|
* @param cacheDir - Cache directory
|
|
15
15
|
* @param onProgress - Optional progress callback
|
|
16
16
|
*/
|
|
17
|
-
export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback): Promise<void>;
|
|
17
|
+
export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback, expectedSha256?: string): Promise<void>;
|
|
18
18
|
/**
|
|
19
19
|
* Removes cached tarball, metadata, and partial download files for an archive.
|
|
20
20
|
* @param archive - Resolved archive descriptor
|