@hominis/fireforge 0.21.0 → 0.21.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 +1 -0
- package/dist/src/commands/config.js +5 -0
- package/dist/src/commands/export-all.js +1 -0
- package/dist/src/commands/export-flow.d.ts +1 -0
- package/dist/src/commands/export-flow.js +21 -1
- package/dist/src/commands/export.js +1 -0
- package/dist/src/commands/furnace/create-templates.js +10 -3
- package/dist/src/commands/furnace/create.js +1 -0
- package/dist/src/commands/furnace/deploy.js +1 -1
- package/dist/src/commands/furnace/validation-output.d.ts +2 -2
- package/dist/src/commands/furnace/validation-output.js +20 -4
- package/dist/src/core/furnace-config-order.d.ts +7 -0
- package/dist/src/core/furnace-config-order.js +86 -0
- package/dist/src/core/furnace-config.js +13 -1
- package/dist/src/core/furnace-validate.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
### Hardening
|
|
11
11
|
|
|
12
|
+
- **Eval 0.21.0 release-gate fixes.** `export --dry-run` now performs the same supersede and cross-patch ownership checks as real export before calling a plan safe; `furnace deploy --dry-run` validates successful custom-component plans against projected jar.mn registrations; generated Furnace components and browser-chrome test scaffolds are strict-checkJs and lazy-custom-element ready; chrome-doc packaging xpcshell tests no longer trip component-orphan validation; supported optional config keys such as `firefox.sha256` print `(not set)` when absent; and Furnace manifest writes preserve existing top-level/component ordering while appending new entries predictably.
|
|
12
13
|
- **Override removal demotes back to stock.** Removing a Furnace override restores engine files, deletes the override workspace, clears override checksums, and re-adds the component to `stock` tracking instead of dropping it from `furnace.json`. Optional Furnace config fields, including `platformPrefixes`, are preserved across the write.
|
|
13
14
|
- **Rename updates browser-chrome test bodies.** `furnace rename` now rewrites generated browser-chrome mochitest contents as well as filenames and `browser.toml`, preventing stale `waitForElement("<old>")` references after a component rename.
|
|
14
15
|
- **UI build preflight is stricter.** `fireforge build --ui` now refuses before `mach build faster` when the current objdir lacks a completed launchable bundle, guiding fresh imports and partial builds through a full `fireforge build` first.
|
|
@@ -99,6 +99,11 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
99
99
|
const rawConfig = await loadRawConfigDocument(projectRoot);
|
|
100
100
|
const currentValue = getNestedValue(rawConfig, key);
|
|
101
101
|
if (currentValue === undefined) {
|
|
102
|
+
if (SUPPORTED_CONFIG_PATHS.includes(key)) {
|
|
103
|
+
info(`${key} = ${formatValue(currentValue)}`);
|
|
104
|
+
outro('');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
102
107
|
throw new InvalidArgumentError(`Unknown config key: ${key}`);
|
|
103
108
|
}
|
|
104
109
|
else {
|
|
@@ -267,6 +267,7 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
267
267
|
filesAffected,
|
|
268
268
|
sourceEsrVersion: config.firefox.version,
|
|
269
269
|
explicitSupersede: options.supersede === true,
|
|
270
|
+
allowOverlap: options.allowOverlap === true,
|
|
270
271
|
});
|
|
271
272
|
outro('Dry run complete — no changes made');
|
|
272
273
|
return;
|
|
@@ -82,6 +82,7 @@ export interface DryRunPreviewInput {
|
|
|
82
82
|
filesAffected: string[];
|
|
83
83
|
sourceEsrVersion: string;
|
|
84
84
|
explicitSupersede: boolean;
|
|
85
|
+
allowOverlap: boolean;
|
|
85
86
|
/** Optional `PatchMetadata.tier` opt-in carried from the CLI. */
|
|
86
87
|
tier?: 'branding';
|
|
87
88
|
/** Optional `PatchMetadata.lintIgnore` carried from the CLI. */
|
|
@@ -13,10 +13,11 @@ import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFi
|
|
|
13
13
|
import { withPatchDirectoryLock } from '../core/patch-lock.js';
|
|
14
14
|
import { addPatchToManifest, loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
|
|
15
15
|
import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
|
|
16
|
-
import { InvalidArgumentError } from '../errors/base.js';
|
|
16
|
+
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
17
17
|
import { toError } from '../utils/errors.js';
|
|
18
18
|
import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
|
|
19
19
|
import { info, warn } from '../utils/logger.js';
|
|
20
|
+
import { findPartialOwnershipOverlap } from './export-shared.js';
|
|
20
21
|
function buildFilenameForPlacement(category, name, order, width) {
|
|
21
22
|
const padded = String(order).padStart(Math.max(3, width), '0');
|
|
22
23
|
return `${padded}-${category}-${sanitizeName(name)}.patch`;
|
|
@@ -296,6 +297,11 @@ export async function commitPlacementExport(input) {
|
|
|
296
297
|
*/
|
|
297
298
|
export async function renderDryRunPreview(input) {
|
|
298
299
|
const supersedeDetails = await findAllPatchesForFilesWithDetails(input.patchesDir, input.filesAffected);
|
|
300
|
+
const supersedingFilenames = new Set(supersedeDetails.map((detail) => detail.patch.filename));
|
|
301
|
+
const manifest = await loadPatchesManifest(input.patchesDir);
|
|
302
|
+
const overlap = manifest !== null
|
|
303
|
+
? findPartialOwnershipOverlap(manifest, input.filesAffected, supersedingFilenames)
|
|
304
|
+
: new Map();
|
|
299
305
|
const plan = await planExport({
|
|
300
306
|
patchesDir: input.patchesDir,
|
|
301
307
|
category: input.category,
|
|
@@ -329,5 +335,19 @@ export async function renderDryRunPreview(input) {
|
|
|
329
335
|
else {
|
|
330
336
|
info('\n[dry-run] No patches would be superseded.');
|
|
331
337
|
}
|
|
338
|
+
if (overlap.size > 0) {
|
|
339
|
+
const entries = [...overlap.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
340
|
+
warn(`\n[dry-run] Would create cross-patch ownership overlap on ${String(entries.length)} file${entries.length === 1 ? '' : 's'}:`);
|
|
341
|
+
for (const [file, owners] of entries) {
|
|
342
|
+
warn(` - ${file} already claimed by: ${owners.join(', ')}`);
|
|
343
|
+
}
|
|
344
|
+
warn('The real export would leave the queue verify-failing. Repartition ownership with `fireforge re-export --files <paths> <existing-patch>` before exporting, or pass --allow-overlap to acknowledge the conflict.');
|
|
345
|
+
if (!input.allowOverlap) {
|
|
346
|
+
throw new GeneralError('Dry-run detected cross-patch ownership overlap. Pass --allow-overlap to preview the acknowledged conflict, or repartition ownership via `fireforge re-export --files`.');
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
info('[dry-run] No cross-patch ownership overlap detected.');
|
|
351
|
+
}
|
|
332
352
|
}
|
|
333
353
|
//# sourceMappingURL=export-flow.js.map
|
|
@@ -233,6 +233,7 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
233
233
|
filesAffected,
|
|
234
234
|
sourceEsrVersion: config.firefox.version,
|
|
235
235
|
explicitSupersede: options.supersede === true,
|
|
236
|
+
allowOverlap: options.allowOverlap === true,
|
|
236
237
|
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
237
238
|
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
238
239
|
? { lintIgnore: options.lintIgnore }
|
|
@@ -39,12 +39,18 @@ window.MozXULElement?.insertFTLIfNeeded("${ftlPath}");
|
|
|
39
39
|
? `
|
|
40
40
|
connectedCallback() {
|
|
41
41
|
super.connectedCallback();
|
|
42
|
-
this
|
|
42
|
+
const { shadowRoot } = this;
|
|
43
|
+
if (shadowRoot) {
|
|
44
|
+
this.ownerDocument.l10n?.connectRoot(shadowRoot);
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
disconnectedCallback() {
|
|
46
49
|
super.disconnectedCallback();
|
|
47
|
-
this
|
|
50
|
+
const { shadowRoot } = this;
|
|
51
|
+
if (shadowRoot) {
|
|
52
|
+
this.ownerDocument.l10n?.disconnectRoot(shadowRoot);
|
|
53
|
+
}
|
|
48
54
|
}
|
|
49
55
|
`
|
|
50
56
|
: '';
|
|
@@ -59,6 +65,7 @@ ${ftlModulePreamble}
|
|
|
59
65
|
* @tagname ${name}
|
|
60
66
|
*/
|
|
61
67
|
class ${className} extends MozLitElement {
|
|
68
|
+
/** @type {Record<string, unknown>} */
|
|
62
69
|
static properties = {};
|
|
63
70
|
|
|
64
71
|
constructor() {
|
|
@@ -72,7 +79,7 @@ ${lifecycleHooks}
|
|
|
72
79
|
\`;
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
|
-
customElements.define("${name}", ${className});
|
|
82
|
+
customElements.define("${name}", /** @type {CustomElementConstructor} */ (${className}));
|
|
76
83
|
`;
|
|
77
84
|
}
|
|
78
85
|
/** Generates the .css file content for a custom component. */
|
|
@@ -364,7 +364,7 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
364
364
|
}
|
|
365
365
|
const validateSpinner = spinner(isDryRun ? 'Validating (read-only)...' : 'Validating...');
|
|
366
366
|
const failedComponents = getFailedComponentNames(result);
|
|
367
|
-
const validation = await runDeployValidation(validateSpinner, name, config, furnacePaths, failedComponents, isDryRun, projectRoot);
|
|
367
|
+
const validation = await runDeployValidation(validateSpinner, name, config, furnacePaths, failedComponents, isDryRun, projectRoot, result.actions);
|
|
368
368
|
if (validation.done)
|
|
369
369
|
return;
|
|
370
370
|
const { totalErrors, totalWarnings, componentCount, skippedValidationCount } = validation;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { getFurnacePaths } from '../../core/furnace-config.js';
|
|
2
|
-
import type { FurnaceConfig, ValidationIssue } from '../../types/furnace.js';
|
|
2
|
+
import type { DryRunAction, FurnaceConfig, ValidationIssue } from '../../types/furnace.js';
|
|
3
3
|
import { type SpinnerHandle } from '../../utils/logger.js';
|
|
4
4
|
/**
|
|
5
5
|
* Displays validation issues and returns aggregated error and warning counts.
|
|
@@ -27,4 +27,4 @@ export type ValidationResult = {
|
|
|
27
27
|
* @param projectRoot - Root directory of the project
|
|
28
28
|
* @returns Validation counts, or `done: true` if the caller should early-return
|
|
29
29
|
*/
|
|
30
|
-
export declare function runDeployValidation(validateSpinner: SpinnerHandle, name: string | undefined, config: FurnaceConfig, furnacePaths: ReturnType<typeof getFurnacePaths>, failedComponents: Set<string>, isDryRun: boolean, projectRoot: string): Promise<ValidationResult>;
|
|
30
|
+
export declare function runDeployValidation(validateSpinner: SpinnerHandle, name: string | undefined, config: FurnaceConfig, furnacePaths: ReturnType<typeof getFurnacePaths>, failedComponents: Set<string>, isDryRun: boolean, projectRoot: string, dryRunActions?: DryRunAction[]): Promise<ValidationResult>;
|
|
@@ -24,6 +24,18 @@ export function displayValidationIssues(issues) {
|
|
|
24
24
|
}
|
|
25
25
|
return [errors, warnings];
|
|
26
26
|
}
|
|
27
|
+
function filterProjectedDryRunIssues(issues, actions) {
|
|
28
|
+
if (!actions || actions.length === 0)
|
|
29
|
+
return issues;
|
|
30
|
+
const plannedJarRegistrations = new Set(actions.filter((action) => action.action === 'register-jar').map((action) => action.component));
|
|
31
|
+
return issues.filter((issue) => {
|
|
32
|
+
if (plannedJarRegistrations.has(issue.component) &&
|
|
33
|
+
(issue.check === 'missing-jar-mn-mjs' || issue.check === 'missing-jar-mn-css')) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
27
39
|
function resolveNamedValidationTarget(name, config, furnacePaths) {
|
|
28
40
|
if (name in config.overrides) {
|
|
29
41
|
return {
|
|
@@ -53,7 +65,7 @@ function resolveNamedValidationTarget(name, config, furnacePaths) {
|
|
|
53
65
|
* @param projectRoot - Root directory of the project
|
|
54
66
|
* @returns Validation counts, or `done: true` if the caller should early-return
|
|
55
67
|
*/
|
|
56
|
-
export async function runDeployValidation(validateSpinner, name, config, furnacePaths, failedComponents, isDryRun, projectRoot) {
|
|
68
|
+
export async function runDeployValidation(validateSpinner, name, config, furnacePaths, failedComponents, isDryRun, projectRoot, dryRunActions) {
|
|
57
69
|
let totalErrors = 0;
|
|
58
70
|
let totalWarnings = 0;
|
|
59
71
|
let componentCount = 0;
|
|
@@ -75,7 +87,8 @@ export async function runDeployValidation(validateSpinner, name, config, furnace
|
|
|
75
87
|
validateSpinner.stop('Validation failed');
|
|
76
88
|
throw new FurnaceError(`Component directory not found for "${name}".`, name);
|
|
77
89
|
}
|
|
78
|
-
const
|
|
90
|
+
const rawIssues = await validateComponent(target.componentDir, name, target.type, config, projectRoot);
|
|
91
|
+
const issues = isDryRun ? filterProjectedDryRunIssues(rawIssues, dryRunActions) : rawIssues;
|
|
79
92
|
componentCount = 1;
|
|
80
93
|
validateSpinner.stop('Validation complete');
|
|
81
94
|
if (issues.length === 0) {
|
|
@@ -96,11 +109,14 @@ export async function runDeployValidation(validateSpinner, name, config, furnace
|
|
|
96
109
|
continue;
|
|
97
110
|
}
|
|
98
111
|
componentCount++;
|
|
99
|
-
|
|
112
|
+
const projectedIssues = isDryRun
|
|
113
|
+
? filterProjectedDryRunIssues(issues, dryRunActions)
|
|
114
|
+
: issues;
|
|
115
|
+
if (projectedIssues.length === 0) {
|
|
100
116
|
success(`${componentName} — all checks passed`);
|
|
101
117
|
}
|
|
102
118
|
else {
|
|
103
|
-
const [errors, warnings] = displayValidationIssues(
|
|
119
|
+
const [errors, warnings] = displayValidationIssues(projectedIssues);
|
|
104
120
|
totalErrors += errors;
|
|
105
121
|
totalWarnings += warnings;
|
|
106
122
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { FurnaceConfig } from '../types/furnace.js';
|
|
2
|
+
/**
|
|
3
|
+
* Orders furnace.json output using the existing file as the primary key
|
|
4
|
+
* sequence, preserving unknown extension keys and appending newly supported
|
|
5
|
+
* fields only when needed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function orderFurnaceConfigForWrite(existing: Record<string, unknown> | undefined, config: FurnaceConfig): Record<string, unknown>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { isObject } from '../utils/validation.js';
|
|
2
|
+
const FURNACE_CONFIG_TOP_LEVEL_KEYS = new Set([
|
|
3
|
+
'version',
|
|
4
|
+
'componentPrefix',
|
|
5
|
+
'tokenPrefix',
|
|
6
|
+
'tokenAllowlist',
|
|
7
|
+
'platformPrefixes',
|
|
8
|
+
'runtimeVariables',
|
|
9
|
+
'tokenHostDocuments',
|
|
10
|
+
'ftlBasePath',
|
|
11
|
+
'scanPaths',
|
|
12
|
+
'stock',
|
|
13
|
+
'overrides',
|
|
14
|
+
'custom',
|
|
15
|
+
]);
|
|
16
|
+
function orderObjectLikeExisting(existing, next) {
|
|
17
|
+
if (!existing)
|
|
18
|
+
return next;
|
|
19
|
+
const ordered = {};
|
|
20
|
+
for (const key of Object.keys(existing)) {
|
|
21
|
+
if (Object.hasOwn(next, key)) {
|
|
22
|
+
ordered[key] = next[key];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
for (const key of Object.keys(next)) {
|
|
26
|
+
if (!Object.hasOwn(ordered, key)) {
|
|
27
|
+
ordered[key] = next[key];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return ordered;
|
|
31
|
+
}
|
|
32
|
+
function orderComponentMapLikeExisting(existing, next) {
|
|
33
|
+
if (!isObject(next))
|
|
34
|
+
return next;
|
|
35
|
+
if (!isObject(existing))
|
|
36
|
+
return next;
|
|
37
|
+
const ordered = {};
|
|
38
|
+
for (const key of Object.keys(existing)) {
|
|
39
|
+
if (Object.hasOwn(next, key)) {
|
|
40
|
+
const existingValue = existing[key];
|
|
41
|
+
const nextValue = next[key];
|
|
42
|
+
ordered[key] =
|
|
43
|
+
isObject(existingValue) && isObject(nextValue)
|
|
44
|
+
? orderObjectLikeExisting(existingValue, nextValue)
|
|
45
|
+
: nextValue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
for (const key of Object.keys(next)) {
|
|
49
|
+
if (!Object.hasOwn(ordered, key)) {
|
|
50
|
+
ordered[key] = next[key];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return ordered;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Orders furnace.json output using the existing file as the primary key
|
|
57
|
+
* sequence, preserving unknown extension keys and appending newly supported
|
|
58
|
+
* fields only when needed.
|
|
59
|
+
*/
|
|
60
|
+
export function orderFurnaceConfigForWrite(existing, config) {
|
|
61
|
+
const next = config;
|
|
62
|
+
if (!existing)
|
|
63
|
+
return next;
|
|
64
|
+
const ordered = {};
|
|
65
|
+
for (const key of Object.keys(existing)) {
|
|
66
|
+
if (key === 'overrides' || key === 'custom') {
|
|
67
|
+
ordered[key] = orderComponentMapLikeExisting(existing[key], next[key]);
|
|
68
|
+
}
|
|
69
|
+
else if (Object.hasOwn(next, key)) {
|
|
70
|
+
ordered[key] = next[key];
|
|
71
|
+
}
|
|
72
|
+
else if (!FURNACE_CONFIG_TOP_LEVEL_KEYS.has(key)) {
|
|
73
|
+
ordered[key] = existing[key];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const key of Object.keys(next)) {
|
|
77
|
+
if (Object.hasOwn(ordered, key))
|
|
78
|
+
continue;
|
|
79
|
+
ordered[key] =
|
|
80
|
+
key === 'overrides' || key === 'custom'
|
|
81
|
+
? orderComponentMapLikeExisting(existing[key], next[key])
|
|
82
|
+
: next[key];
|
|
83
|
+
}
|
|
84
|
+
return ordered;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=furnace-config-order.js.map
|
|
@@ -8,6 +8,7 @@ import { isObject, isString } from '../utils/validation.js';
|
|
|
8
8
|
import { FIREFORGE_DIR } from './config.js';
|
|
9
9
|
import { parseStringArray } from './furnace-config-array-utils.js';
|
|
10
10
|
import { parseCustomConfig } from './furnace-config-custom.js';
|
|
11
|
+
import { orderFurnaceConfigForWrite } from './furnace-config-order.js';
|
|
11
12
|
import { validateRuntimeVariables, validateTokenHostDocuments } from './furnace-config-tokens.js';
|
|
12
13
|
import { resolveFtlDir } from './furnace-constants.js';
|
|
13
14
|
import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
|
|
@@ -410,7 +411,18 @@ export async function loadFurnaceConfig(root) {
|
|
|
410
411
|
*/
|
|
411
412
|
export async function writeFurnaceConfig(root, config) {
|
|
412
413
|
const paths = getFurnacePaths(root);
|
|
413
|
-
|
|
414
|
+
let existing;
|
|
415
|
+
if (await pathExists(paths.furnaceConfig)) {
|
|
416
|
+
try {
|
|
417
|
+
const raw = await readJson(paths.furnaceConfig);
|
|
418
|
+
if (isObject(raw))
|
|
419
|
+
existing = raw;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
existing = undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
await writeJson(paths.furnaceConfig, orderFurnaceConfigForWrite(existing, config));
|
|
414
426
|
}
|
|
415
427
|
/**
|
|
416
428
|
* Stamps every override's `baseVersion` to the supplied version. Used by
|
|
@@ -233,6 +233,9 @@ async function findOrphanXpcshellScaffolds(root, config) {
|
|
|
233
233
|
for (const entry of entries) {
|
|
234
234
|
if (known.has(entry))
|
|
235
235
|
continue;
|
|
236
|
+
const chromeDocPackagingTest = join(parentAbs, entry, `test_${entry}_packaging.js`);
|
|
237
|
+
if (await pathExists(chromeDocPackagingTest))
|
|
238
|
+
continue;
|
|
236
239
|
issues.push({
|
|
237
240
|
component: entry,
|
|
238
241
|
severity: 'error',
|