@hominis/fireforge 0.19.6 → 0.20.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 +25 -0
- package/README.md +9 -0
- 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/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/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 +30 -45
- 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 +27 -47
- 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-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 +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { text } from '@clack/prompts';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
5
|
-
import { createDefaultFurnaceConfig,
|
|
5
|
+
import { createDefaultFurnaceConfig, furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
6
|
import { resolveFtlChromeSubPath, tagNameToClassName } from '../../core/furnace-constants.js';
|
|
7
7
|
import { recordFurnaceRollbackFailure, runFurnaceMutation, } from '../../core/furnace-operation.js';
|
|
8
8
|
import { CUSTOM_ELEMENT_TAG_PATTERN, CUSTOM_ELEMENT_TAG_RULES, } from '../../core/furnace-registration-validate.js';
|
|
@@ -21,6 +21,7 @@ import { resolveCreateFeatures } from './create-features.js';
|
|
|
21
21
|
import { scaffoldMochikitTestFiles } from './create-mochikit.js';
|
|
22
22
|
import { assertCustomEntryPersisted } from './create-readback.js';
|
|
23
23
|
import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
24
|
+
import { validateCreateAgainstConfig } from './create-validation.js';
|
|
24
25
|
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
25
26
|
async function loadAuthoringFurnaceConfig(projectRoot) {
|
|
26
27
|
if (await furnaceConfigExists(projectRoot)) {
|
|
@@ -41,18 +42,6 @@ function validateTagName(name) {
|
|
|
41
42
|
return `Name ${CUSTOM_ELEMENT_TAG_RULES}`;
|
|
42
43
|
return undefined;
|
|
43
44
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Checks if a component name conflicts with existing entries in furnace.json.
|
|
46
|
-
*/
|
|
47
|
-
function checkNameConflict(config, name) {
|
|
48
|
-
if (name in config.custom) {
|
|
49
|
-
return `A custom component named "${name}" already exists in furnace.json`;
|
|
50
|
-
}
|
|
51
|
-
if (name in config.overrides) {
|
|
52
|
-
return `An override component named "${name}" already exists in furnace.json`;
|
|
53
|
-
}
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
45
|
/**
|
|
57
46
|
* Scaffolds browser mochitest files for a newly created custom component.
|
|
58
47
|
* @param componentName - Custom element tag name
|
|
@@ -247,11 +236,16 @@ async function performCreateMutations(args) {
|
|
|
247
236
|
const testFiles = [];
|
|
248
237
|
let files;
|
|
249
238
|
try {
|
|
239
|
+
const freshConfig = await loadAuthoringFurnaceConfig(args.projectRoot);
|
|
240
|
+
validateCreateAgainstConfig(freshConfig, args.componentName, args.allowPrefixMismatch, args.composes);
|
|
241
|
+
if (await pathExists(args.componentDir)) {
|
|
242
|
+
throw new FurnaceError(`Directory already exists: components/custom/${args.componentName}`, args.componentName);
|
|
243
|
+
}
|
|
250
244
|
// Record the componentDir creation entry immediately after registration
|
|
251
245
|
// so signal-driven rollback can clean it up even if writeComponentFiles
|
|
252
246
|
// is interrupted mid-ensureDir.
|
|
253
247
|
recordCreatedDir(journal, args.componentDir);
|
|
254
|
-
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license,
|
|
248
|
+
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, resolveFtlChromeSubPath(freshConfig.ftlBasePath), args.sharedFtl, journal);
|
|
255
249
|
const customEntry = {
|
|
256
250
|
description: args.description,
|
|
257
251
|
targetPath: `toolkit/content/widgets/${args.componentName}`,
|
|
@@ -264,9 +258,9 @@ async function performCreateMutations(args) {
|
|
|
264
258
|
if (args.sharedFtl) {
|
|
265
259
|
customEntry.sharedFtl = args.sharedFtl;
|
|
266
260
|
}
|
|
267
|
-
|
|
261
|
+
freshConfig.custom[args.componentName] = customEntry;
|
|
268
262
|
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
269
|
-
await writeFurnaceConfig(args.projectRoot,
|
|
263
|
+
await writeFurnaceConfig(args.projectRoot, freshConfig);
|
|
270
264
|
await assertCustomEntryPersisted(args.projectRoot, args.componentName);
|
|
271
265
|
if (args.testStyle === 'browser-chrome') {
|
|
272
266
|
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
@@ -310,41 +304,6 @@ async function resolveDescription(isInteractive, options) {
|
|
|
310
304
|
}
|
|
311
305
|
return description;
|
|
312
306
|
}
|
|
313
|
-
/**
|
|
314
|
-
* Validates the `--compose` targets against registered components and runs
|
|
315
|
-
* cycle detection if the new component is introduced into the graph. Throws
|
|
316
|
-
* on any failure; returns when the graph is clean.
|
|
317
|
-
*/
|
|
318
|
-
function validateComposesTargets(config, componentName, composes) {
|
|
319
|
-
if (!composes || composes.length === 0)
|
|
320
|
-
return;
|
|
321
|
-
const known = new Set([
|
|
322
|
-
...config.stock,
|
|
323
|
-
...Object.keys(config.overrides),
|
|
324
|
-
...Object.keys(config.custom),
|
|
325
|
-
]);
|
|
326
|
-
for (const tag of composes) {
|
|
327
|
-
if (tag === componentName) {
|
|
328
|
-
throw new FurnaceError(`Component "${componentName}" cannot compose itself.`);
|
|
329
|
-
}
|
|
330
|
-
if (!known.has(tag)) {
|
|
331
|
-
throw new FurnaceError(`Cannot compose unknown component "${tag}". ` +
|
|
332
|
-
'The referenced component must be registered as stock, override, or custom.');
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
// Check for cycles that would be introduced by adding this component.
|
|
336
|
-
const tempCustom = {
|
|
337
|
-
...config.custom,
|
|
338
|
-
[componentName]: {
|
|
339
|
-
description: '',
|
|
340
|
-
targetPath: `toolkit/content/widgets/${componentName}`,
|
|
341
|
-
register: true,
|
|
342
|
-
localized: false,
|
|
343
|
-
composes,
|
|
344
|
-
},
|
|
345
|
-
};
|
|
346
|
-
detectComposesCycles(tempCustom);
|
|
347
|
-
}
|
|
348
307
|
/**
|
|
349
308
|
* Runs the furnace create command to scaffold a new custom component.
|
|
350
309
|
* @param projectRoot - Root directory of the project
|
|
@@ -391,39 +350,14 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
391
350
|
// succeeds so a cancelled create in a fresh project does not strand a new
|
|
392
351
|
// furnace.json behind.
|
|
393
352
|
const config = await loadAuthoringFurnaceConfig(projectRoot);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (conflict) {
|
|
397
|
-
throw new FurnaceError(conflict, componentName);
|
|
398
|
-
}
|
|
353
|
+
const composes = options.compose;
|
|
354
|
+
validateCreateAgainstConfig(config, componentName, options.allowPrefixMismatch, composes);
|
|
399
355
|
// Check if it already exists in the engine source tree
|
|
400
356
|
if (await pathExists(paths.engine)) {
|
|
401
357
|
if (await isComponentInEngine(paths.engine, componentName)) {
|
|
402
358
|
throw new FurnaceError(`"${componentName}" already exists in the engine source tree. Use "fireforge furnace override" instead.`, componentName);
|
|
403
359
|
}
|
|
404
360
|
}
|
|
405
|
-
// Refuse if name doesn't match componentPrefix, unless
|
|
406
|
-
// --allow-prefix-mismatch was explicitly passed.
|
|
407
|
-
//
|
|
408
|
-
// Pre-0.16.0 this was a bare `warn()` and the create flow continued,
|
|
409
|
-
// which produced a class of validation runs where the command reported
|
|
410
|
-
// success, scaffolded files under components/custom/<name>/, and
|
|
411
|
-
// registered tests in browser/base/moz.build, but the component
|
|
412
|
-
// wasn't a good citizen of the fork's convention — subsequent
|
|
413
|
-
// follow-up commands (list, status, rename) behaved inconsistently.
|
|
414
|
-
// Refusing up-front leaves the workspace untouched on a bad name and
|
|
415
|
-
// forces an intentional `--allow-prefix-mismatch` for the rare case
|
|
416
|
-
// where the mismatch is deliberate.
|
|
417
|
-
if (config.componentPrefix &&
|
|
418
|
-
!componentName.startsWith(config.componentPrefix) &&
|
|
419
|
-
!options.allowPrefixMismatch) {
|
|
420
|
-
throw new InvalidArgumentError(`Name "${componentName}" does not start with the configured prefix "${config.componentPrefix}". ` +
|
|
421
|
-
'Use a prefixed name (e.g. "' +
|
|
422
|
-
config.componentPrefix +
|
|
423
|
-
componentName +
|
|
424
|
-
'"), update `componentPrefix` in furnace.json, ' +
|
|
425
|
-
'or pass --allow-prefix-mismatch to create the component anyway.', 'name');
|
|
426
|
-
}
|
|
427
361
|
// --- Resolve description ---
|
|
428
362
|
const description = await resolveDescription(isInteractive, options);
|
|
429
363
|
// --- Resolve features ---
|
|
@@ -447,10 +381,6 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
447
381
|
if (await pathExists(componentDir)) {
|
|
448
382
|
throw new FurnaceError(`Directory already exists: components/custom/${componentName}`, componentName);
|
|
449
383
|
}
|
|
450
|
-
// --- Validate --compose targets BEFORE any writes so a failed validation
|
|
451
|
-
// does not strand component files behind.
|
|
452
|
-
const composes = options.compose;
|
|
453
|
-
validateComposesTargets(config, componentName, composes);
|
|
454
384
|
// --- Normalize and validate --shared-ftl ahead of any writes. Shares the
|
|
455
385
|
// structural rules with furnace-config.ts so the command and the on-disk
|
|
456
386
|
// schema cannot diverge. Pass the resolved `localized` rather than a
|
|
@@ -492,10 +422,6 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
492
422
|
// state via the shared rollback journal. The mutation runs under the
|
|
493
423
|
// furnace-wide lock and is registered with the global SIGINT/SIGTERM
|
|
494
424
|
// rollback pathway.
|
|
495
|
-
// Derive the FTL chrome sub-path from the configured ftlBasePath so the
|
|
496
|
-
// generated `.mjs` calls `insertFTLIfNeeded` at a URI that actually matches
|
|
497
|
-
// the locale jar.mn entry `furnace apply` will write.
|
|
498
|
-
const ftlChromeSubPath = resolveFtlChromeSubPath(config.ftlBasePath);
|
|
499
425
|
const { files, testFiles } = await runFurnaceMutation(projectRoot, 'create-rollback', (ctx) => performCreateMutations({
|
|
500
426
|
projectRoot,
|
|
501
427
|
componentName,
|
|
@@ -507,12 +433,11 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
507
433
|
sharedFtl,
|
|
508
434
|
componentDir,
|
|
509
435
|
furnacePaths,
|
|
510
|
-
|
|
436
|
+
allowPrefixMismatch: options.allowPrefixMismatch,
|
|
511
437
|
forgeConfig,
|
|
512
438
|
paths,
|
|
513
439
|
license,
|
|
514
440
|
testStyle,
|
|
515
|
-
ftlChromeSubPath,
|
|
516
441
|
operationContext: ctx,
|
|
517
442
|
}));
|
|
518
443
|
note(formatSuccessNote({
|
|
@@ -213,6 +213,7 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
213
213
|
let totalUnchanged = 0;
|
|
214
214
|
let totalSkipped = 0;
|
|
215
215
|
const conflictComponents = [];
|
|
216
|
+
const failedOverrides = [];
|
|
216
217
|
// Snapshot furnace.json before the batch loop so an unexpected failure
|
|
217
218
|
// (process crash, unhandled error) can be recovered from. Per-component
|
|
218
219
|
// errors caught below are expected and do not trigger a restore — only
|
|
@@ -244,7 +245,9 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
catch (error) {
|
|
247
|
-
|
|
248
|
+
const message = toError(error).message;
|
|
249
|
+
warn(`${overrideName}: ${message}`);
|
|
250
|
+
failedOverrides.push({ name: overrideName, message });
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
253
|
}
|
|
@@ -257,12 +260,18 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
257
260
|
throw error;
|
|
258
261
|
}
|
|
259
262
|
const summary = `${overrideNames.length} override(s) processed, ${totalSkipped} already up-to-date\n` +
|
|
260
|
-
`${totalMerged} file(s) merged, ${totalUnchanged} unchanged, ${totalConflicts} conflict(s)
|
|
263
|
+
`${totalMerged} file(s) merged, ${totalUnchanged} unchanged, ${totalConflicts} conflict(s), ` +
|
|
264
|
+
`${failedOverrides.length} failed`;
|
|
261
265
|
if (conflictComponents.length > 0) {
|
|
262
266
|
warn(`Conflicts in: ${conflictComponents.join(', ')}. ` +
|
|
263
267
|
'Resolve conflict markers, then re-run refresh for those components to update baseVersion.');
|
|
264
268
|
}
|
|
265
269
|
note(summary, dryRun ? 'Dry Run Summary' : 'Refresh Summary');
|
|
270
|
+
if (failedOverrides.length > 0) {
|
|
271
|
+
outro(dryRun ? 'Dry run completed with failures' : 'Refresh completed with failures');
|
|
272
|
+
throw new FurnaceError(`Failed to refresh ${failedOverrides.length} override(s): ` +
|
|
273
|
+
failedOverrides.map((failure) => `${failure.name}: ${failure.message}`).join('; '));
|
|
274
|
+
}
|
|
266
275
|
outro(dryRun ? 'Dry run complete' : 'Refresh complete');
|
|
267
276
|
}
|
|
268
277
|
//# sourceMappingURL=refresh.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Removes every checksum entry owned by the removed component.
|
|
3
|
+
*/
|
|
4
|
+
export function dropChecksumsByPrefix(state, prefix) {
|
|
5
|
+
const result = { ...state };
|
|
6
|
+
if (state.appliedChecksums) {
|
|
7
|
+
result.appliedChecksums = Object.fromEntries(Object.entries(state.appliedChecksums).filter(([k]) => !k.startsWith(prefix)));
|
|
8
|
+
}
|
|
9
|
+
if (state.engineChecksums) {
|
|
10
|
+
result.engineChecksums = Object.fromEntries(Object.entries(state.engineChecksums).filter(([k]) => !k.startsWith(prefix)));
|
|
11
|
+
}
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=remove-state.js.map
|
|
@@ -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,24 @@ 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));
|
|
511
496
|
}
|
|
512
497
|
else {
|
|
513
|
-
|
|
498
|
+
freshConfig.custom = Object.fromEntries(Object.entries(freshConfig.custom).filter(([key]) => key !== name));
|
|
514
499
|
}
|
|
515
500
|
await snapshotFile(journal, furnacePaths.furnaceConfig);
|
|
516
|
-
await writeFurnaceConfig(projectRoot,
|
|
501
|
+
await writeFurnaceConfig(projectRoot, freshConfig);
|
|
517
502
|
// Drop stale per-file checksums inside the same transactional block.
|
|
518
503
|
// Snapshotting the state file into the rollback journal means the
|
|
519
504
|
// entire remove operation is a single atomic unit.
|
|
520
505
|
await snapshotFile(journal, furnacePaths.furnaceState);
|
|
521
|
-
await updateFurnaceState(projectRoot, (state) => dropChecksumsByPrefix(state, `${
|
|
506
|
+
await updateFurnaceState(projectRoot, (state) => dropChecksumsByPrefix(state, `${freshType}/${name}/`));
|
|
522
507
|
// Test-cleanup failures are warn-and-continue by design (test files
|
|
523
508
|
// are secondary artefacts), but the caller deserves a single summary
|
|
524
509
|
// line pointing at the residue so they don't have to re-scan earlier
|
|
@@ -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,12 @@ 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 { renameComponentFileName, updateConfigForCustomRename, updateConfigForOverrideRename, } from './rename-helpers.js';
|
|
17
18
|
import { renameXpcshellTestFiles } from './rename-xpcshell.js';
|
|
18
19
|
/** Escapes regex metacharacters so a user-supplied name is literal inside a RegExp. */
|
|
19
20
|
function escapeRegex(input) {
|
|
20
21
|
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
22
|
}
|
|
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
23
|
/**
|
|
64
24
|
* Derives the test file name for a component, matching the convention used by
|
|
65
25
|
* `furnace create --with-tests`.
|
|
@@ -215,18 +175,37 @@ async function renameMochikitTestFiles(engineDir, oldName, newName, journal) {
|
|
|
215
175
|
* Performs the transactional rename mutation inside a furnace lock.
|
|
216
176
|
*/
|
|
217
177
|
async function performRenameMutations(args) {
|
|
218
|
-
const { projectRoot, oldName, newName
|
|
178
|
+
const { projectRoot, oldName, newName } = args;
|
|
219
179
|
const oldClassName = tagNameToClassName(oldName);
|
|
220
180
|
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
181
|
await runFurnaceMutation(projectRoot, 'rename-rollback', async (ctx) => {
|
|
227
182
|
const journal = createRollbackJournal();
|
|
228
183
|
ctx.registerJournal(journal);
|
|
184
|
+
let newDir = args.newDir;
|
|
229
185
|
try {
|
|
186
|
+
const config = await loadFurnaceConfig(projectRoot);
|
|
187
|
+
const isCustom = oldName in config.custom;
|
|
188
|
+
const isOverride = oldName in config.overrides;
|
|
189
|
+
if (!isCustom && !isOverride) {
|
|
190
|
+
throw new FurnaceError(`Component "${oldName}" not found in furnace.json. Only custom and override components can be renamed.`, oldName);
|
|
191
|
+
}
|
|
192
|
+
if (newName in config.custom ||
|
|
193
|
+
newName in config.overrides ||
|
|
194
|
+
config.stock.includes(newName)) {
|
|
195
|
+
throw new FurnaceError(`A component named "${newName}" already exists in furnace.json.`, newName);
|
|
196
|
+
}
|
|
197
|
+
const componentType = isCustom ? 'custom' : 'override';
|
|
198
|
+
const componentDirLabel = isCustom ? 'custom' : 'overrides';
|
|
199
|
+
const baseDir = isCustom ? args.furnacePaths.customDir : args.furnacePaths.overridesDir;
|
|
200
|
+
const oldDir = join(baseDir, oldName);
|
|
201
|
+
newDir = join(baseDir, newName);
|
|
202
|
+
const oldCustomTargetPath = isCustom ? config.custom[oldName]?.targetPath : undefined;
|
|
203
|
+
if (!(await pathExists(oldDir))) {
|
|
204
|
+
throw new FurnaceError(`Component directory not found: components/${componentDirLabel}/${oldName}`, oldName);
|
|
205
|
+
}
|
|
206
|
+
if (await pathExists(newDir)) {
|
|
207
|
+
throw new FurnaceError(`Target directory already exists: components/${componentDirLabel}/${newName}`, newName);
|
|
208
|
+
}
|
|
230
209
|
await snapshotDir(journal, oldDir);
|
|
231
210
|
await snapshotFile(journal, args.furnaceConfigPath);
|
|
232
211
|
// 1. Create new directory with renamed files and updated content
|
|
@@ -472,6 +451,7 @@ export async function furnaceRenameCommand(projectRoot, oldName, newName) {
|
|
|
472
451
|
componentType,
|
|
473
452
|
config,
|
|
474
453
|
furnaceConfigPath: furnacePaths.furnaceConfig,
|
|
454
|
+
furnacePaths,
|
|
475
455
|
engineDir: paths.engine,
|
|
476
456
|
});
|
|
477
457
|
note(`Component renamed: ${oldName} → ${newName}\n\n` +
|
|
@@ -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`.
|