@hominis/fireforge 0.30.1 → 0.32.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 +36 -0
- package/README.md +22 -0
- package/dist/src/commands/export-all.js +9 -16
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +46 -1
- package/dist/src/commands/export.js +52 -113
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +3 -1
- package/dist/src/commands/lint-per-patch.js +265 -74
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +193 -88
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +68 -0
- package/dist/src/commands/test-run.js +97 -0
- package/dist/src/commands/test.js +214 -263
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +74 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +191 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -2
- package/dist/src/core/git-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -12
- package/dist/src/core/git-file-ops.js +84 -3
- package/dist/src/core/lint-cache.d.ts +0 -13
- package/dist/src/core/lint-cache.js +5 -5
- package/dist/src/core/mach.d.ts +22 -1
- package/dist/src/core/mach.js +27 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +263 -71
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- package/dist/src/core/patch-lint-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +24 -161
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +10 -3
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +3 -22
- package/dist/src/core/typecheck-shim.js +69 -7
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +122 -0
- package/dist/src/types/config.d.ts +11 -2
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -3,16 +3,15 @@ import { stat } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { Option } from 'commander';
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
|
-
import { appendHistory
|
|
6
|
+
import { appendHistory } from '../core/destructive.js';
|
|
7
7
|
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
8
8
|
import { getStatusWithCodes, isGitRepository } from '../core/git.js';
|
|
9
9
|
import { generateBinaryFilePatch, generateFullFilePatch } from '../core/git-diff.js';
|
|
10
10
|
import { isBinaryFile } from '../core/git-file-ops.js';
|
|
11
11
|
import { getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, } from '../core/git-status.js';
|
|
12
12
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
13
|
-
import { commitExportedPatch
|
|
14
|
-
import {
|
|
15
|
-
import { applyRenameMapToManifest, buildProjectedManifest, enforcePatchPolicy, } from '../core/patch-policy.js';
|
|
13
|
+
import { commitExportedPatch } from '../core/patch-export.js';
|
|
14
|
+
import { buildPatchQueueContext } from '../core/patch-lint.js';
|
|
16
15
|
import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
|
|
17
16
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
18
17
|
import { toError } from '../utils/errors.js';
|
|
@@ -21,9 +20,9 @@ import { info, intro, outro, spinner, verbose, warn } from '../utils/logger.js';
|
|
|
21
20
|
import { pickDefined } from '../utils/options.js';
|
|
22
21
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
23
22
|
import { parsePositiveIntegerFlag } from '../utils/validation.js';
|
|
24
|
-
import { commitPlacementExport,
|
|
25
|
-
import {
|
|
26
|
-
import { autoFixLicenseHeaders,
|
|
23
|
+
import { commitPlacementExport, renderDryRunPreview } from './export-flow.js';
|
|
24
|
+
import { gatePlacementPlan, patchMetadataExtras } from './export-placement-gate.js';
|
|
25
|
+
import { autoFixLicenseHeaders, promptExportPatchMetadata, runPatchLint, runSupersedeAndOverlapGates, } from './export-shared.js';
|
|
27
26
|
async function collectExportFiles(paths, files) {
|
|
28
27
|
const collectedFiles = new Set();
|
|
29
28
|
let fileStatuses;
|
|
@@ -100,20 +99,14 @@ async function generatePatchDiff(engineDir, allFiles) {
|
|
|
100
99
|
return diffs.join('\n');
|
|
101
100
|
}
|
|
102
101
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
102
|
+
* Validation + diff phase of `exportCommand`: checks flag combinations and
|
|
103
|
+
* the engine checkout, collects the export file set (honouring
|
|
104
|
+
* `--exclude-furnace`), generates the diff, auto-fixes license headers,
|
|
105
|
+
* and prompts for patch metadata. Returns `null` when the operator
|
|
106
|
+
* cancelled the metadata prompt (the command ends silently, matching the
|
|
107
|
+
* prompt's own cancel handling).
|
|
108
108
|
*/
|
|
109
|
-
|
|
110
|
-
// gate → dry-run/placement/default write. Splitting it further would
|
|
111
|
-
// spread the error-handling (spinner.error, try/catch) across multiple
|
|
112
|
-
// helpers and hurt readability more than it would help.
|
|
113
|
-
// eslint-disable-next-line max-lines-per-function
|
|
114
|
-
export async function exportCommand(projectRoot, files, options) {
|
|
115
|
-
const isDryRun = options.dryRun === true;
|
|
116
|
-
intro(isDryRun ? 'FireForge Export (dry run)' : 'FireForge Export');
|
|
109
|
+
async function prepareExport(projectRoot, files, options) {
|
|
117
110
|
// Placement flags are mutually exclusive with each other.
|
|
118
111
|
const placementFlagCount = [
|
|
119
112
|
options.order !== undefined,
|
|
@@ -170,7 +163,23 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
170
163
|
}
|
|
171
164
|
const metadata = await promptExportPatchMetadata(options, isInteractive, 'export', config);
|
|
172
165
|
if (!metadata)
|
|
166
|
+
return null;
|
|
167
|
+
return { paths, placementFlagCount, diff, config, isInteractive, metadata };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Runs the export command to export file changes as a patch.
|
|
171
|
+
* Accepts one or more file/directory paths and bundles them into a single patch.
|
|
172
|
+
* @param projectRoot - Root directory of the project
|
|
173
|
+
* @param files - File or directory paths to export (relative to engine/)
|
|
174
|
+
* @param options - Export options
|
|
175
|
+
*/
|
|
176
|
+
export async function exportCommand(projectRoot, files, options) {
|
|
177
|
+
const isDryRun = options.dryRun === true;
|
|
178
|
+
intro(isDryRun ? 'FireForge Export (dry run)' : 'FireForge Export');
|
|
179
|
+
const prepared = await prepareExport(projectRoot, files, options);
|
|
180
|
+
if (!prepared)
|
|
173
181
|
return;
|
|
182
|
+
const { paths, placementFlagCount, diff, config, isInteractive, metadata } = prepared;
|
|
174
183
|
const { patchName, selectedCategory, description } = metadata;
|
|
175
184
|
const s = spinner(isDryRun ? 'Planning export...' : 'Exporting patch...');
|
|
176
185
|
try {
|
|
@@ -184,76 +193,29 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
184
193
|
const exportIgnoreChecks = options.lintIgnore && options.lintIgnore.length > 0
|
|
185
194
|
? new Set(options.lintIgnore)
|
|
186
195
|
: undefined;
|
|
187
|
-
await
|
|
196
|
+
const patchQueueCtx = (await pathExists(paths.patches))
|
|
197
|
+
? await buildPatchQueueContext(paths.patches)
|
|
198
|
+
: undefined;
|
|
199
|
+
await runPatchLint(paths.engine, filesAffected, diff, config, options.skipLint, patchQueueCtx, exportIgnoreChecks, options.tier);
|
|
188
200
|
// Resolve placement (if any flag was given). Placement is mutually
|
|
189
201
|
// exclusive with supersede — the semantics overlap confusingly.
|
|
190
202
|
let placementPlan = null;
|
|
191
203
|
if (placementFlagCount > 0) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const conflicts = await projectPlacementForLint(paths.patches, placementPlan, diff);
|
|
201
|
-
const renamed = currentManifest !== null
|
|
202
|
-
? applyRenameMapToManifest(currentManifest, placementPlan.renameMap)
|
|
203
|
-
: buildProjectedManifest(null, []);
|
|
204
|
-
enforcePatchPolicy({
|
|
204
|
+
const gated = await gatePlacementPlan({
|
|
205
|
+
patchesDir: paths.patches,
|
|
206
|
+
options,
|
|
207
|
+
selectedCategory,
|
|
208
|
+
patchName,
|
|
209
|
+
description,
|
|
210
|
+
filesAffected,
|
|
211
|
+
diff,
|
|
205
212
|
config,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
filename: placementPlan.newFilename,
|
|
210
|
-
order: placementPlan.insertionOrder,
|
|
211
|
-
category: selectedCategory,
|
|
212
|
-
name: patchName,
|
|
213
|
-
description,
|
|
214
|
-
createdAt: new Date().toISOString(),
|
|
215
|
-
...buildPatchSourceMetadata(config.firefox),
|
|
216
|
-
filesAffected,
|
|
217
|
-
...(options.tier !== undefined ? { tier: options.tier } : {}),
|
|
218
|
-
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
219
|
-
? { lintIgnore: options.lintIgnore }
|
|
220
|
-
: {}),
|
|
221
|
-
},
|
|
222
|
-
]),
|
|
223
|
-
command: 'export',
|
|
224
|
-
forceUnsafe: options.forceUnsafe === true,
|
|
213
|
+
isDryRun,
|
|
214
|
+
s,
|
|
225
215
|
});
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
// enough to warrant a prompt (more than one rename) OR when the user
|
|
230
|
-
// asked for a dry-run. The dry-run branch must always print the
|
|
231
|
-
// placement summary — previously, single-rename/no-rename dry-runs
|
|
232
|
-
// exited silently with no filename or projected layout.
|
|
233
|
-
if (renameCount > 1 || isDryRun) {
|
|
234
|
-
s.stop();
|
|
235
|
-
const decision = await confirmDestructive({
|
|
236
|
-
operation: 'export-order',
|
|
237
|
-
title: `Export with placement at order ${placementPlan.insertionOrder}`,
|
|
238
|
-
summary,
|
|
239
|
-
yes: options.yes === true,
|
|
240
|
-
dryRun: isDryRun,
|
|
241
|
-
unsafeOverride: options.forceUnsafe === true,
|
|
242
|
-
conflicts,
|
|
243
|
-
});
|
|
244
|
-
if (decision === 'dry-run') {
|
|
245
|
-
outro('Dry run complete — no changes made');
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
if (decision === 'cancelled') {
|
|
249
|
-
outro('Export cancelled');
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
else if (conflicts && options.forceUnsafe !== true) {
|
|
254
|
-
s.stop();
|
|
255
|
-
throw new InvalidArgumentError(`Refusing to run export: ${conflicts.reason}. Pass --force-unsafe to override.`, '--force-unsafe');
|
|
256
|
-
}
|
|
216
|
+
if (gated === 'stop')
|
|
217
|
+
return;
|
|
218
|
+
placementPlan = gated;
|
|
257
219
|
}
|
|
258
220
|
// Dry-run path: compute the plan and print it, never write.
|
|
259
221
|
if (isDryRun && !placementPlan) {
|
|
@@ -267,10 +229,7 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
267
229
|
...buildPatchSourceMetadata(config.firefox),
|
|
268
230
|
explicitSupersede: options.supersede === true,
|
|
269
231
|
allowOverlap: options.allowOverlap === true,
|
|
270
|
-
...(options
|
|
271
|
-
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
272
|
-
? { lintIgnore: options.lintIgnore }
|
|
273
|
-
: {}),
|
|
232
|
+
...patchMetadataExtras(options),
|
|
274
233
|
config,
|
|
275
234
|
forceUnsafe: options.forceUnsafe === true,
|
|
276
235
|
});
|
|
@@ -291,10 +250,7 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
291
250
|
createdAt: new Date().toISOString(),
|
|
292
251
|
...buildPatchSourceMetadata(config.firefox),
|
|
293
252
|
filesAffected,
|
|
294
|
-
...(options
|
|
295
|
-
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
296
|
-
? { lintIgnore: options.lintIgnore }
|
|
297
|
-
: {}),
|
|
253
|
+
...patchMetadataExtras(options),
|
|
298
254
|
};
|
|
299
255
|
const committedPlan = await commitPlacementExport({
|
|
300
256
|
patchesDir: paths.patches,
|
|
@@ -336,29 +292,15 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
336
292
|
return;
|
|
337
293
|
}
|
|
338
294
|
// Default (no dry-run, no placement) path: the pre-existing behavior.
|
|
339
|
-
|
|
340
|
-
const shouldProceed = await confirmSupersedePatches(paths.patches, filesAffected, options.supersede, isInteractive, s);
|
|
341
|
-
if (!shouldProceed)
|
|
342
|
-
return;
|
|
343
|
-
// Overlap gate: pre-0.16.0 `export` only caught FULL-coverage
|
|
344
|
-
// supersedes, so a second export targeting a shared file like
|
|
345
|
-
// `browser/themes/shared/jar.inc.mn` happily created a queue where
|
|
346
|
-
// two patches both listed the same file in `filesAffected`. `verify`
|
|
347
|
-
// then failed immediately on "cross-patch filesAffected conflicts".
|
|
348
|
-
// `confirmSupersedePatches` might already have confirmed full
|
|
349
|
-
// supersedes above; pass their filenames through so we do not flag
|
|
350
|
-
// a file claimed by a patch that is about to be removed.
|
|
351
|
-
const willSupersede = await findAllPatchesForFiles(paths.patches, filesAffected);
|
|
352
|
-
const supersedingFilenames = new Set(willSupersede.map((p) => p.filename));
|
|
353
|
-
const shouldProceedPastOverlap = await guardOwnershipOverlap({
|
|
295
|
+
const shouldProceedPastGates = await runSupersedeAndOverlapGates({
|
|
354
296
|
patchesDir: paths.patches,
|
|
355
297
|
filesAffected,
|
|
356
|
-
|
|
298
|
+
supersede: options.supersede,
|
|
357
299
|
allowOverlap: options.allowOverlap === true,
|
|
358
300
|
isInteractive,
|
|
359
301
|
s,
|
|
360
302
|
});
|
|
361
|
-
if (!
|
|
303
|
+
if (!shouldProceedPastGates)
|
|
362
304
|
return;
|
|
363
305
|
const { patchFilename, superseded } = await commitExportedPatch({
|
|
364
306
|
patchesDir: paths.patches,
|
|
@@ -368,10 +310,7 @@ export async function exportCommand(projectRoot, files, options) {
|
|
|
368
310
|
diff,
|
|
369
311
|
filesAffected,
|
|
370
312
|
...buildPatchSourceMetadata(config.firefox),
|
|
371
|
-
...(options
|
|
372
|
-
...(options.lintIgnore !== undefined && options.lintIgnore.length > 0
|
|
373
|
-
? { lintIgnore: options.lintIgnore }
|
|
374
|
-
: {}),
|
|
313
|
+
...patchMetadataExtras(options),
|
|
375
314
|
config,
|
|
376
315
|
policyCommand: 'export',
|
|
377
316
|
forceUnsafe: options.forceUnsafe === true,
|
|
@@ -7,19 +7,6 @@
|
|
|
7
7
|
* upstream Firefox (browser.xhtml, privatebrowsing/aboutPrivateBrowsing.html,
|
|
8
8
|
* etc.) minus the fork-specific wiring. A fork author fills in the body.
|
|
9
9
|
*/
|
|
10
|
-
/**
|
|
11
|
-
* Sentinel attribute emitted on every `furnace chrome-doc create`-scaffolded
|
|
12
|
-
* root element. Platform modules (`DevToolsStartup`, `PageActions`,
|
|
13
|
-
* `SessionStore`, `DownloadsButton`, …) that observe
|
|
14
|
-
* `browser-delayed-startup-finished` and walk INTO the window assume the
|
|
15
|
-
* `browser.xhtml` DOM and throw on anything else. A fork-authored patch
|
|
16
|
-
* to such a module can use `hasAttribute(...)` against this sentinel as
|
|
17
|
-
* a cheap, fork-neutral guard to skip the walk on a custom chrome doc.
|
|
18
|
-
*
|
|
19
|
-
* Exposed as a named constant so test code and external checks can
|
|
20
|
-
* reference the exact attribute name without hardcoding the string.
|
|
21
|
-
*/
|
|
22
|
-
export declare const FURNACE_CHROME_DOC_SENTINEL = "data-furnace-chrome-doc";
|
|
23
10
|
/**
|
|
24
11
|
* XHTML shell for a top-level chrome document.
|
|
25
12
|
*
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* Exposed as a named constant so test code and external checks can
|
|
21
21
|
* reference the exact attribute name without hardcoding the string.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
const FURNACE_CHROME_DOC_SENTINEL = 'data-furnace-chrome-doc';
|
|
24
24
|
/**
|
|
25
25
|
* XHTML shell for a top-level chrome document.
|
|
26
26
|
*
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { FurnaceCreateOptions } from '../../types/commands/index.js';
|
|
2
|
-
|
|
3
|
-
export type ResolvedTestStyle = 'mochikit' | 'browser-chrome' | 'xpcshell' | 'none';
|
|
2
|
+
import type { ResolvedTestStyle } from '../../types/furnace.js';
|
|
4
3
|
/**
|
|
5
4
|
* Collapses `--with-tests`, `--xpcshell`, and `--test-style` into the single
|
|
6
5
|
* scaffold dispatch used inside the mutation phase.
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
4
|
-
import { applyAllComponents,
|
|
4
|
+
import { applyAllComponents, computeComponentChecksums, prefixChecksums, } from '../../core/furnace-apply.js';
|
|
5
5
|
import { logApplyResult } from '../../core/furnace-apply-output.js';
|
|
6
6
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, updateFurnaceState, } from '../../core/furnace-config.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { recordFurnaceRollbackFailure, runFurnaceMutation, } from '../../core/furnace-operation.js';
|
|
10
|
-
import { createRollbackJournal, restoreRollbackJournalOrThrow, } from '../../core/furnace-rollback.js';
|
|
7
|
+
import { reportJsconfigPathsSync } from '../../core/furnace-jsconfig.js';
|
|
8
|
+
import { runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
11
9
|
import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftError, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
|
|
12
10
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
13
|
-
import { toError } from '../../utils/errors.js';
|
|
14
11
|
import { pathExists } from '../../utils/fs.js';
|
|
15
12
|
import { info, intro, note, outro, spinner, warn } from '../../utils/logger.js';
|
|
16
13
|
import { runDeployValidation } from './validation-output.js';
|
|
@@ -116,115 +113,42 @@ async function persistSingleComponentState(projectRoot, appliedEntry, furnacePat
|
|
|
116
113
|
lastApply: new Date().toISOString(),
|
|
117
114
|
}));
|
|
118
115
|
}
|
|
119
|
-
/**
|
|
120
|
-
* True when an applied-result carries any signal that the deploy did not
|
|
121
|
-
* complete cleanly. Any such signal must trigger the rollback journal and
|
|
122
|
-
* must suppress state persistence, so both call sites read from here.
|
|
123
|
-
*
|
|
124
|
-
* An apply failure on a single-component deploy means one of:
|
|
125
|
-
* - `result.errors` has an entry (the apply body threw)
|
|
126
|
-
* - an `applied[].stepErrors` entry is present (a registration step
|
|
127
|
-
* failed after the copy succeeded)
|
|
128
|
-
*
|
|
129
|
-
* Both are treated identically for atomicity purposes: rollback runs,
|
|
130
|
-
* state stays on the previous checkpoint, and the caller raises a deploy
|
|
131
|
-
* failure so the operator sees the error.
|
|
132
|
-
*/
|
|
133
|
-
function namedDeployHasFailures(result) {
|
|
134
|
-
return result.errors.length > 0 || getStepFailureCount(result) > 0;
|
|
135
|
-
}
|
|
136
|
-
async function restoreNamedDeployRollback(rollbackJournal, name, projectRoot) {
|
|
137
|
-
if (!rollbackJournal)
|
|
138
|
-
return;
|
|
139
|
-
try {
|
|
140
|
-
await restoreRollbackJournalOrThrow(rollbackJournal, `Furnace deploy failed for "${name}"`);
|
|
141
|
-
}
|
|
142
|
-
catch (rollbackError) {
|
|
143
|
-
if (projectRoot) {
|
|
144
|
-
await recordFurnaceRollbackFailure(projectRoot, 'deploy-rollback', `component "${name}": ${toError(rollbackError).message}`);
|
|
145
|
-
}
|
|
146
|
-
throw rollbackError;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
116
|
/**
|
|
150
117
|
* Applies a single named override or custom component in targeted deploy mode.
|
|
151
118
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
119
|
+
* Delegates to {@link applyAllComponents} with a `componentName` filter so
|
|
120
|
+
* targeted deploys run the exact same pipeline as deploy-all — including
|
|
121
|
+
* workspace-deletion detection, engine orphan undeploy, and jar.mn /
|
|
122
|
+
* customElements.js re-sync. The previous implementation called the
|
|
123
|
+
* per-component apply helpers directly and never pruned: renaming a helper
|
|
124
|
+
* file in the workspace left the old deployed file and its stale jar.mn
|
|
125
|
+
* line in the engine (field report D1).
|
|
126
|
+
*
|
|
127
|
+
* `persistState: false` is load-bearing: the batch persist path *replaces*
|
|
128
|
+
* `appliedChecksums` wholesale with only this run's entries, which for a
|
|
129
|
+
* named deploy would wipe every other component's state. Named deploy keeps
|
|
130
|
+
* its per-component state merge ({@link persistSingleComponentState}) and
|
|
131
|
+
* its atomicity gate ({@link shouldPersistNamedDeployState}) at the call
|
|
132
|
+
* site. Rollback on failure happens inside `applyAllComponents`; the
|
|
133
|
+
* journal returned on success is ignored (the deploy keeps its files).
|
|
158
134
|
*
|
|
159
135
|
* @param name - Component name to apply
|
|
160
|
-
* @param engineDir - Firefox engine source directory
|
|
161
|
-
* @param furnacePaths - Resolved Furnace workspace paths
|
|
162
136
|
* @param config - Loaded Furnace configuration
|
|
163
137
|
* @param isDryRun - Whether file writes should be skipped
|
|
164
138
|
* @returns Apply result for the named component, or `stock` for stock-only entries
|
|
165
139
|
*/
|
|
166
|
-
async function applyNamedComponent(name,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
const result = {
|
|
172
|
-
applied: [],
|
|
173
|
-
skipped: [],
|
|
174
|
-
errors: [],
|
|
175
|
-
actions: [],
|
|
176
|
-
};
|
|
177
|
-
const overrideConfig = config.overrides[name];
|
|
178
|
-
const customConfig = config.custom[name];
|
|
179
|
-
if (overrideConfig) {
|
|
180
|
-
const componentDir = join(furnacePaths.overridesDir, name);
|
|
181
|
-
if (!(await pathExists(componentDir))) {
|
|
182
|
-
throw new FurnaceError(`Component directory not found: components/overrides/${name}`, name);
|
|
183
|
-
}
|
|
184
|
-
try {
|
|
185
|
-
const { affectedPaths: filesAffected, actions } = await applyOverrideComponent(engineDir, name, componentDir, overrideConfig, ftlDir, isDryRun, rollbackJournal);
|
|
186
|
-
if (isDryRun && actions) {
|
|
187
|
-
result.actions = actions;
|
|
188
|
-
}
|
|
189
|
-
result.applied.push({ name, type: 'override', filesAffected });
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
result.errors.push({ name, error: toError(error).message });
|
|
140
|
+
async function applyNamedComponent(name, config, isDryRun, projectRoot, operationContext) {
|
|
141
|
+
if (!(name in config.overrides) && !(name in config.custom)) {
|
|
142
|
+
if (config.stock.includes(name)) {
|
|
143
|
+
return 'stock';
|
|
193
144
|
}
|
|
194
|
-
|
|
195
|
-
await restoreNamedDeployRollback(rollbackJournal, name, projectRoot);
|
|
196
|
-
}
|
|
197
|
-
return result;
|
|
145
|
+
throw new FurnaceError(`Component "${name}" not found in furnace.json.`, name);
|
|
198
146
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, ftlDir, isDryRun, rollbackJournal, markerComment !== undefined ? { markerComment } : {});
|
|
206
|
-
if (isDryRun && actions) {
|
|
207
|
-
result.actions = actions;
|
|
208
|
-
}
|
|
209
|
-
result.applied.push({
|
|
210
|
-
name,
|
|
211
|
-
type: 'custom',
|
|
212
|
-
filesAffected,
|
|
213
|
-
...(stepErrors.length > 0 ? { stepErrors } : {}),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
result.errors.push({ name, error: toError(error).message });
|
|
218
|
-
}
|
|
219
|
-
if (!isDryRun && namedDeployHasFailures(result)) {
|
|
220
|
-
await restoreNamedDeployRollback(rollbackJournal, name, projectRoot);
|
|
221
|
-
}
|
|
222
|
-
return result;
|
|
223
|
-
}
|
|
224
|
-
if (config.stock.includes(name)) {
|
|
225
|
-
return 'stock';
|
|
226
|
-
}
|
|
227
|
-
throw new FurnaceError(`Component "${name}" not found in furnace.json.`, name);
|
|
147
|
+
return applyAllComponents(projectRoot, isDryRun, {
|
|
148
|
+
componentName: name,
|
|
149
|
+
persistState: false,
|
|
150
|
+
...(operationContext ? { operationContext } : {}),
|
|
151
|
+
});
|
|
228
152
|
}
|
|
229
153
|
/**
|
|
230
154
|
* Prints the deploy summary after apply and validation complete.
|
|
@@ -293,7 +217,7 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
293
217
|
throw new FurnaceError('No furnace.json found. Run "fireforge furnace create" or "fireforge furnace override" to get started.');
|
|
294
218
|
}
|
|
295
219
|
const config = await loadFurnaceConfig(projectRoot);
|
|
296
|
-
const
|
|
220
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
297
221
|
const overrideCount = Object.keys(config.overrides).length;
|
|
298
222
|
const customCount = Object.keys(config.custom).length;
|
|
299
223
|
if (overrideCount === 0 && customCount === 0) {
|
|
@@ -306,14 +230,6 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
306
230
|
// the plan before deciding whether to refresh the override or acknowledge
|
|
307
231
|
// the new baseline in furnace.json.
|
|
308
232
|
const forgeConfig = await loadConfig(projectRoot);
|
|
309
|
-
// 2026-04-26 eval Finding 6: when `markerComment` is unset in
|
|
310
|
-
// fireforge.json, default it to `binaryName.toUpperCase()` so the
|
|
311
|
-
// furnace-emitted edits to upstream files satisfy
|
|
312
|
-
// `lintModificationComments` on the next `lint`/`export` round-trip.
|
|
313
|
-
// The lint rule keys on the same uppercased binaryName, so the
|
|
314
|
-
// implicit default is identical to what the rule expects. Threaded
|
|
315
|
-
// through `applyNamedComponent` below.
|
|
316
|
-
const resolvedMarkerComment = resolveFurnaceMarkerComment(forgeConfig);
|
|
317
233
|
const driftEntries = findOverrideBaseVersionDrift(config, forgeConfig.firefox.version);
|
|
318
234
|
const force = options.force ?? false;
|
|
319
235
|
const scopedDrift = name ? driftEntries.filter((entry) => entry.name === name) : driftEntries;
|
|
@@ -326,7 +242,7 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
326
242
|
// `furnace deploy` runs only contend on the actual mutation.
|
|
327
243
|
const applyOutcome = await runFurnaceMutation(projectRoot, 'deploy-rollback', async (ctx) => {
|
|
328
244
|
if (name) {
|
|
329
|
-
const namedApplyResult = await applyNamedComponent(name,
|
|
245
|
+
const namedApplyResult = await applyNamedComponent(name, config, isDryRun, projectRoot, ctx);
|
|
330
246
|
if (namedApplyResult === 'stock') {
|
|
331
247
|
return { kind: 'stock' };
|
|
332
248
|
}
|
|
@@ -354,6 +270,12 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
354
270
|
const result = applyOutcome.result;
|
|
355
271
|
applySpinner.stop(isDryRun ? 'Planned actions calculated' : 'Components applied');
|
|
356
272
|
logApplyResult(result, isDryRun);
|
|
273
|
+
// Keep the consumer jsconfig's chrome-module `paths` in step with the
|
|
274
|
+
// deployed module set (field report D3). Only after a clean apply —
|
|
275
|
+
// a rolled-back deploy must not advance the typecheck mapping either.
|
|
276
|
+
if (result.errors.length === 0 && getStepFailureCount(result) === 0) {
|
|
277
|
+
await reportJsconfigPathsSync(projectRoot, config, isDryRun);
|
|
278
|
+
}
|
|
357
279
|
// --- Step 2: Validate (read-only, runs even in dry-run to show what would fail) ---
|
|
358
280
|
if (options.skipValidate) {
|
|
359
281
|
const applyErrors = result.errors.length + getStepFailureCount(result);
|
|
@@ -158,6 +158,54 @@ async function refreshSingleOverride(projectRoot, name, options = {}) {
|
|
|
158
158
|
}, { dryRun });
|
|
159
159
|
return { results, currentVersion };
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Refreshes every override sequentially, tallying merged/conflict/
|
|
163
|
+
* unchanged file counts. Per-override errors are expected (warned and
|
|
164
|
+
* recorded as failures) and do not abort the batch; only an error that
|
|
165
|
+
* escapes this function entirely warrants the caller's journal rollback.
|
|
166
|
+
*/
|
|
167
|
+
async function runBatchRefresh(projectRoot, overrideNames, options) {
|
|
168
|
+
let totalMerged = 0;
|
|
169
|
+
let totalConflicts = 0;
|
|
170
|
+
let totalUnchanged = 0;
|
|
171
|
+
let totalSkipped = 0;
|
|
172
|
+
const conflictComponents = [];
|
|
173
|
+
const failedOverrides = [];
|
|
174
|
+
for (const overrideName of overrideNames) {
|
|
175
|
+
try {
|
|
176
|
+
const { results } = await refreshSingleOverride(projectRoot, overrideName, options);
|
|
177
|
+
if (results.length === 0) {
|
|
178
|
+
totalSkipped++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
for (const r of results) {
|
|
182
|
+
if (r.status === 'merged')
|
|
183
|
+
totalMerged++;
|
|
184
|
+
else if (r.status === 'conflict') {
|
|
185
|
+
totalConflicts++;
|
|
186
|
+
if (!conflictComponents.includes(overrideName)) {
|
|
187
|
+
conflictComponents.push(overrideName);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (r.status === 'unchanged')
|
|
191
|
+
totalUnchanged++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const message = toError(error).message;
|
|
196
|
+
warn(`${overrideName}: ${message}`);
|
|
197
|
+
failedOverrides.push({ name: overrideName, message });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
totalMerged,
|
|
202
|
+
totalConflicts,
|
|
203
|
+
totalUnchanged,
|
|
204
|
+
totalSkipped,
|
|
205
|
+
conflictComponents,
|
|
206
|
+
failedOverrides,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
161
209
|
/**
|
|
162
210
|
* Runs the furnace refresh command to merge upstream Firefox changes into
|
|
163
211
|
* an override component using three-way merge.
|
|
@@ -208,12 +256,6 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
208
256
|
outro('Done');
|
|
209
257
|
return;
|
|
210
258
|
}
|
|
211
|
-
let totalMerged = 0;
|
|
212
|
-
let totalConflicts = 0;
|
|
213
|
-
let totalUnchanged = 0;
|
|
214
|
-
let totalSkipped = 0;
|
|
215
|
-
const conflictComponents = [];
|
|
216
|
-
const failedOverrides = [];
|
|
217
259
|
// Snapshot furnace.json before the batch loop so an unexpected failure
|
|
218
260
|
// (process crash, unhandled error) can be recovered from. Per-component
|
|
219
261
|
// errors caught below are expected and do not trigger a restore — only
|
|
@@ -223,33 +265,9 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
223
265
|
const furnacePaths = getFurnacePaths(projectRoot);
|
|
224
266
|
await snapshotFile(batchJournal, furnacePaths.furnaceConfig);
|
|
225
267
|
}
|
|
268
|
+
let tally;
|
|
226
269
|
try {
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const { results } = await refreshSingleOverride(projectRoot, overrideName, options);
|
|
230
|
-
if (results.length === 0) {
|
|
231
|
-
totalSkipped++;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
for (const r of results) {
|
|
235
|
-
if (r.status === 'merged')
|
|
236
|
-
totalMerged++;
|
|
237
|
-
else if (r.status === 'conflict') {
|
|
238
|
-
totalConflicts++;
|
|
239
|
-
if (!conflictComponents.includes(overrideName)) {
|
|
240
|
-
conflictComponents.push(overrideName);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
else if (r.status === 'unchanged')
|
|
244
|
-
totalUnchanged++;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
catch (error) {
|
|
248
|
-
const message = toError(error).message;
|
|
249
|
-
warn(`${overrideName}: ${message}`);
|
|
250
|
-
failedOverrides.push({ name: overrideName, message });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
270
|
+
tally = await runBatchRefresh(projectRoot, overrideNames, options);
|
|
253
271
|
}
|
|
254
272
|
catch (error) {
|
|
255
273
|
// Unexpected batch-level failure: restore furnace.json to its
|
|
@@ -259,6 +277,8 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
259
277
|
}
|
|
260
278
|
throw error;
|
|
261
279
|
}
|
|
280
|
+
const { totalMerged, totalConflicts, totalUnchanged, totalSkipped } = tally;
|
|
281
|
+
const { conflictComponents, failedOverrides } = tally;
|
|
262
282
|
const summary = `${overrideNames.length} override(s) processed, ${totalSkipped} already up-to-date\n` +
|
|
263
283
|
`${totalMerged} file(s) merged, ${totalUnchanged} unchanged, ${totalConflicts} conflict(s), ` +
|
|
264
284
|
`${failedOverrides.length} failed`;
|
|
@@ -3,6 +3,7 @@ import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
|
3
3
|
import { applyAllComponents } from '../../core/furnace-apply.js';
|
|
4
4
|
import { logApplyResult } from '../../core/furnace-apply-output.js';
|
|
5
5
|
import { furnaceConfigExists, loadFurnaceConfig } from '../../core/furnace-config.js';
|
|
6
|
+
import { reportJsconfigPathsSync } from '../../core/furnace-jsconfig.js';
|
|
6
7
|
import { runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
7
8
|
import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
|
|
8
9
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
@@ -68,6 +69,7 @@ export async function furnaceSyncCommand(projectRoot, options = {}) {
|
|
|
68
69
|
if (totalFailures > 0) {
|
|
69
70
|
throw new FurnaceError(`${totalFailures} component${totalFailures === 1 ? '' : 's'} failed to apply cleanly`);
|
|
70
71
|
}
|
|
72
|
+
await reportJsconfigPathsSync(projectRoot, config, false);
|
|
71
73
|
outro(`Sync complete — ${result.applied.length} applied, ${result.skipped.length} skipped`);
|
|
72
74
|
}
|
|
73
75
|
else {
|