@hominis/fireforge 0.15.9 → 0.16.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 +142 -0
- package/README.md +6 -2
- package/dist/src/cli.d.ts +4 -1
- package/dist/src/cli.js +6 -3
- package/dist/src/commands/config.js +16 -5
- package/dist/src/commands/download.js +31 -4
- package/dist/src/commands/export-all.js +96 -9
- package/dist/src/commands/export.js +10 -1
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +11 -1
- package/dist/src/commands/furnace/chrome-doc-templates.js +12 -2
- package/dist/src/commands/furnace/create.js +21 -3
- package/dist/src/commands/furnace/diff.js +22 -2
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/furnace/init.js +76 -2
- package/dist/src/commands/furnace/override.js +35 -12
- package/dist/src/commands/furnace/preview.js +46 -1
- package/dist/src/commands/furnace/rename.js +14 -3
- package/dist/src/commands/lint.js +26 -2
- package/dist/src/commands/package.js +16 -5
- package/dist/src/commands/re-export.js +25 -0
- package/dist/src/commands/rebase/patch-loop.js +19 -0
- package/dist/src/commands/register.js +2 -18
- package/dist/src/commands/run.js +23 -2
- package/dist/src/commands/status.js +42 -8
- package/dist/src/commands/test.js +6 -24
- package/dist/src/commands/token.js +14 -1
- package/dist/src/commands/watch.js +14 -2
- package/dist/src/commands/wire.js +35 -9
- package/dist/src/core/branding.d.ts +23 -0
- package/dist/src/core/branding.js +39 -0
- package/dist/src/core/browser-wire.js +68 -23
- package/dist/src/core/build-baseline.d.ts +14 -0
- package/dist/src/core/build-baseline.js +61 -1
- package/dist/src/core/config-mutate.d.ts +1 -1
- package/dist/src/core/config.d.ts +17 -0
- package/dist/src/core/config.js +35 -0
- package/dist/src/core/firefox.d.ts +16 -2
- package/dist/src/core/firefox.js +7 -2
- package/dist/src/core/furnace-config.d.ts +23 -0
- package/dist/src/core/furnace-config.js +38 -0
- package/dist/src/core/mach-build-artifacts.d.ts +41 -0
- package/dist/src/core/mach-build-artifacts.js +70 -0
- package/dist/src/core/mach-error-hints.js +38 -0
- package/dist/src/core/mach-mozconfig.d.ts +25 -0
- package/dist/src/core/mach-mozconfig.js +66 -0
- package/dist/src/core/mach.d.ts +12 -1
- package/dist/src/core/mach.js +14 -1
- package/dist/src/core/manifest-rules.js +22 -1
- package/dist/src/core/patch-lint.js +43 -20
- package/dist/src/core/test-stale-check.js +46 -1
- package/dist/src/core/token-manager.js +57 -4
- package/dist/src/core/token-scaffold.d.ts +36 -0
- package/dist/src/core/token-scaffold.js +74 -0
- package/dist/src/types/commands/options.d.ts +10 -0
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +12 -0
- package/dist/src/utils/paths.d.ts +19 -0
- package/dist/src/utils/paths.js +33 -0
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ import { buildPatchQueueContext, collectNewFileCreatorsByPath } from '../core/pa
|
|
|
12
12
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
13
13
|
import { GeneralError } from '../errors/base.js';
|
|
14
14
|
import { toError } from '../utils/errors.js';
|
|
15
|
-
import { pathExists, readText } from '../utils/fs.js';
|
|
15
|
+
import { FIREFORGE_TMP_PATH_PATTERN, pathExists, readText } from '../utils/fs.js';
|
|
16
16
|
import { info, intro, outro, verbose, warn } from '../utils/logger.js';
|
|
17
17
|
/**
|
|
18
18
|
* Status code descriptions for git status.
|
|
@@ -164,6 +164,21 @@ async function expandDirectoryEntries(files, engineDir) {
|
|
|
164
164
|
}
|
|
165
165
|
return { entries: expanded, truncations };
|
|
166
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Strips entries whose path matches the atomic-temp-file shape
|
|
169
|
+
* FireForge's own `writeText` produces (see
|
|
170
|
+
* {@link import('../utils/fs.js').FIREFORGE_TMP_PATH_PATTERN}). Those
|
|
171
|
+
* files only exist for the duration of a write + rename and should
|
|
172
|
+
* never appear in `status` output; filtering them here keeps every
|
|
173
|
+
* status mode (default, raw, unmanaged, ownership, json) symmetric so
|
|
174
|
+
* the operator never sees a `.mozconfig.fireforge-tmp-<pid>-<uuid>`
|
|
175
|
+
* entry mid-write. Files named for unrelated reasons (e.g. a user's
|
|
176
|
+
* `.bashrc.fireforge-tmp-backup` without the PID+UUID tail) do not
|
|
177
|
+
* match the pattern and pass through unfiltered.
|
|
178
|
+
*/
|
|
179
|
+
function filterFireForgeTempFiles(files) {
|
|
180
|
+
return files.filter((entry) => !FIREFORGE_TMP_PATH_PATTERN.test(entry.file));
|
|
181
|
+
}
|
|
167
182
|
/**
|
|
168
183
|
* Classifies files into patch-backed, unmanaged, or branding buckets.
|
|
169
184
|
*/
|
|
@@ -277,7 +292,11 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
277
292
|
const ownershipExpansion = (await isGitRepository(paths.engine))
|
|
278
293
|
? await expandDirectoryEntries(await getStatusWithCodes(paths.engine), paths.engine)
|
|
279
294
|
: { entries: [], truncations: [] };
|
|
280
|
-
|
|
295
|
+
// Filter atomic-write temp files (Finding #18) so a mid-flight
|
|
296
|
+
// `.fireforge-tmp-<pid>-<uuid>` artefact never shows up in any
|
|
297
|
+
// status mode. The pattern is tight enough to let legitimately
|
|
298
|
+
// similar names through.
|
|
299
|
+
const rawFilesOwnership = filterFireForgeTempFiles(ownershipExpansion.entries);
|
|
281
300
|
renderTruncationBanner(ownershipExpansion.truncations);
|
|
282
301
|
// Only walk the patch bodies when the directory actually exists.
|
|
283
302
|
// Fresh projects with no patch queue yet pass through with an empty
|
|
@@ -313,8 +332,28 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
313
332
|
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
314
333
|
}
|
|
315
334
|
const rawFiles = await getStatusWithCodes(paths.engine);
|
|
316
|
-
const { entries:
|
|
335
|
+
const { entries: expanded, truncations } = await expandDirectoryEntries(rawFiles, paths.engine);
|
|
336
|
+
// Strip atomic-write temp files (Finding #18) before every mode
|
|
337
|
+
// branch so raw / unmanaged / default / json all agree.
|
|
338
|
+
const files = filterFireForgeTempFiles(expanded);
|
|
317
339
|
renderTruncationBanner(truncations);
|
|
340
|
+
// `--json` callers expect machine-parseable output on every invocation,
|
|
341
|
+
// including the clean-tree case. Before this ordering fix a clean tree
|
|
342
|
+
// printed "No modified files" / "Working tree clean" via the human
|
|
343
|
+
// branch below and `--json` was silently ignored, so scripts that piped
|
|
344
|
+
// the output through a JSON parser broke precisely when there was
|
|
345
|
+
// nothing to report. Emit `[]` here and return before the human fallback.
|
|
346
|
+
if (options.json) {
|
|
347
|
+
await renderJsonStatus(files, paths, projectRoot, config.binaryName);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// `--raw` consumers parse the native `git status --porcelain` output
|
|
351
|
+
// directly. On a clean tree the raw mode should produce nothing on
|
|
352
|
+
// stdout — the human "Working tree clean" banner would contaminate the
|
|
353
|
+
// pipe. Short-circuit before the human clean-tree branch below.
|
|
354
|
+
if (options.raw && files.length === 0) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
318
357
|
if (files.length === 0) {
|
|
319
358
|
info('No modified files');
|
|
320
359
|
outro('Working tree clean');
|
|
@@ -325,11 +364,6 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
325
364
|
renderRawStatus(files);
|
|
326
365
|
return;
|
|
327
366
|
}
|
|
328
|
-
// JSON mode and default mode both need classification
|
|
329
|
-
if (options.json) {
|
|
330
|
-
await renderJsonStatus(files, paths, projectRoot, config.binaryName);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
367
|
// Patch-aware classification
|
|
334
368
|
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
335
369
|
const classified = await classifyFiles(files, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
|
|
@@ -11,28 +11,7 @@ import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
|
11
11
|
import { pathExists } from '../utils/fs.js';
|
|
12
12
|
import { info, intro, spinner, warn } from '../utils/logger.js';
|
|
13
13
|
import { pickDefined } from '../utils/options.js';
|
|
14
|
-
|
|
15
|
-
* Strips a leading "engine/" or "engine\\" prefix from a path if present.
|
|
16
|
-
* Users may specify paths like "engine/browser/modules/..." from the project
|
|
17
|
-
* root, but mach test expects paths relative to the engine directory.
|
|
18
|
-
*
|
|
19
|
-
* The match is case-insensitive because case-insensitive filesystems
|
|
20
|
-
* (default macOS, Windows) treat "Engine/" and "engine/" as the same
|
|
21
|
-
* directory, and a literal lowercase-only check left mach with a
|
|
22
|
-
* non-stripped prefix that resolved to a different path under the engine
|
|
23
|
-
* tree. Tab and other whitespace before the prefix is also ignored.
|
|
24
|
-
*
|
|
25
|
-
* @param testPath - Path as provided by the user
|
|
26
|
-
* @returns Path relative to the engine directory
|
|
27
|
-
*/
|
|
28
|
-
function normalizeTestPath(testPath) {
|
|
29
|
-
const trimmed = testPath.trim();
|
|
30
|
-
const match = /^engine[/\\]/i.exec(trimmed);
|
|
31
|
-
if (match) {
|
|
32
|
-
return trimmed.slice(match[0].length);
|
|
33
|
-
}
|
|
34
|
-
return trimmed;
|
|
35
|
-
}
|
|
14
|
+
import { stripEnginePrefix } from '../utils/paths.js';
|
|
36
15
|
async function assertTestPathsExist(engineDir, testPaths) {
|
|
37
16
|
const missingPaths = [];
|
|
38
17
|
for (const testPath of testPaths) {
|
|
@@ -198,8 +177,11 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
198
177
|
throw new GeneralError('Marionette preflight reported FAIL — see output above. Aborting before mach test runs.');
|
|
199
178
|
}
|
|
200
179
|
}
|
|
201
|
-
// Normalize test paths (strip engine/ prefix if present)
|
|
202
|
-
|
|
180
|
+
// Normalize test paths (strip engine/ prefix if present). Uses the
|
|
181
|
+
// shared `stripEnginePrefix` helper so `test`, `register`, `lint`, and
|
|
182
|
+
// `export` all accept the same prefix forms. Also trim to match the
|
|
183
|
+
// previous case-insensitive + leading-whitespace-tolerant contract.
|
|
184
|
+
const normalizedPaths = testPaths.map((p) => stripEnginePrefix(p).trim());
|
|
203
185
|
await assertTestPathsExist(paths.engine, normalizedPaths);
|
|
204
186
|
// Build extra args
|
|
205
187
|
const extraArgs = [];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { Option } from 'commander';
|
|
3
3
|
import { loadConfig } from '../core/config.js';
|
|
4
|
-
import { loadFurnaceConfig } from '../core/furnace-config.js';
|
|
4
|
+
import { furnaceConfigExists, loadFurnaceConfig } from '../core/furnace-config.js';
|
|
5
5
|
import { addToken, getTokensCssPath, validateTokenAdd, } from '../core/token-manager.js';
|
|
6
6
|
import { InvalidArgumentError } from '../errors/base.js';
|
|
7
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
7
8
|
import { toError } from '../utils/errors.js';
|
|
8
9
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
9
10
|
import { pickDefined } from '../utils/options.js';
|
|
@@ -36,6 +37,18 @@ async function normalizeTokenNameForProject(projectRoot, rawTokenName) {
|
|
|
36
37
|
*/
|
|
37
38
|
export async function tokenAddCommand(projectRoot, tokenName, value, options) {
|
|
38
39
|
intro('Token Add');
|
|
40
|
+
// Finding #15: a fresh project without furnace.json failed deep inside
|
|
41
|
+
// the token-manager's `assertTokenCategoryExists` with "Token CSS file
|
|
42
|
+
// not found: browser/themes/shared/<binary>-tokens.css" — technically
|
|
43
|
+
// correct, but the operator's actual next step is to initialize
|
|
44
|
+
// Furnace (which scaffolds the tokens CSS file among other things).
|
|
45
|
+
// Catching the uninitialized case here gives the right guidance up-
|
|
46
|
+
// front before the generic "file not found" error fires.
|
|
47
|
+
if (!(await furnaceConfigExists(projectRoot))) {
|
|
48
|
+
throw new FurnaceError('Token management requires Furnace to be initialized. ' +
|
|
49
|
+
'Tokens live in the Furnace-managed tokens CSS file, which `fireforge furnace init` scaffolds alongside the rest of the Furnace workspace.\n\n' +
|
|
50
|
+
'Run "fireforge furnace init" first, then rerun "fireforge token add ...".');
|
|
51
|
+
}
|
|
39
52
|
// Normalize token name using the configured Furnace token prefix when the
|
|
40
53
|
// user supplied a bare token name like "canvas-gap".
|
|
41
54
|
tokenName = await normalizeTokenNameForProject(projectRoot, tokenName);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
2
2
|
import { warnIfFurnaceStale } from '../core/furnace-staleness.js';
|
|
3
|
-
import { buildArtifactMismatchMessage, generateMozconfig, hasBuildArtifacts, watchWithOutput, } from '../core/mach.js';
|
|
3
|
+
import { buildArtifactMismatchMessage, generateMozconfig, hasBuildArtifacts, hasRunnableBundle, watchWithOutput, } from '../core/mach.js';
|
|
4
4
|
import { GeneralError } from '../errors/base.js';
|
|
5
5
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
6
6
|
import { toError } from '../utils/errors.js';
|
|
@@ -114,7 +114,19 @@ export async function watchCommand(projectRoot) {
|
|
|
114
114
|
throw new GeneralError(`Watch mode requires a completed build. ${detail}\n\n` +
|
|
115
115
|
"Run 'fireforge build' first to create the initial build, then run 'fireforge watch'.");
|
|
116
116
|
}
|
|
117
|
-
|
|
117
|
+
// Report bundle state alongside the "Using build artifacts..." banner
|
|
118
|
+
// so an operator watching a mid-build tree can see why `fireforge run`
|
|
119
|
+
// would refuse right now while watch is still going. Watch remains
|
|
120
|
+
// permissive (it exists to drive rebuilds) — this is informational.
|
|
121
|
+
// The `hasBuildArtifacts` check already passed at this point, so
|
|
122
|
+
// `objDir` is always defined.
|
|
123
|
+
const bundleCheck = buildCheck.objDir
|
|
124
|
+
? await hasRunnableBundle(paths.engine, config.binaryName, buildCheck.objDir)
|
|
125
|
+
: { runnable: false };
|
|
126
|
+
const bundleSuffix = bundleCheck.runnable
|
|
127
|
+
? ' (bundle: runnable)'
|
|
128
|
+
: ' (bundle: pending — watch will rebuild)';
|
|
129
|
+
info(`Using build artifacts from ${buildCheck.objDir}/${bundleSuffix}`);
|
|
118
130
|
// Advisory: warn when Furnace components have drifted since the last
|
|
119
131
|
// apply so the user doesn't launch watch-mode builds with stale
|
|
120
132
|
// components baked in. Mirrors the check in `fireforge run` — without
|
|
@@ -10,7 +10,7 @@ import { toError } from '../utils/errors.js';
|
|
|
10
10
|
import { pathExists } from '../utils/fs.js';
|
|
11
11
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
12
12
|
import { pickDefined } from '../utils/options.js';
|
|
13
|
-
import { isContainedRelativePath, isPathInsideRoot, toRootRelativePath } from '../utils/paths.js';
|
|
13
|
+
import { isContainedRelativePath, isExplicitAbsolutePath, isPathInsideRoot, stripEnginePrefix, toRootRelativePath, } from '../utils/paths.js';
|
|
14
14
|
const BROWSER_BASE_DIR = 'browser/base';
|
|
15
15
|
function printWireDryRun(engineDir, name, subscriptDir, domFilePath, domTargetPath, options) {
|
|
16
16
|
info('[dry-run] Would wire subscript:');
|
|
@@ -113,25 +113,51 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
113
113
|
}
|
|
114
114
|
subscriptDir = options.subscriptDir;
|
|
115
115
|
}
|
|
116
|
-
// Validate DOM fragment file exists and compute path relative to engine root
|
|
116
|
+
// Validate DOM fragment file exists and compute path relative to engine root.
|
|
117
|
+
//
|
|
118
|
+
// Accepts three shapes:
|
|
119
|
+
// - Absolute paths (`/project/engine/browser/base/content/foo.inc.xhtml`)
|
|
120
|
+
// - Repo-root-relative forms (`engine/browser/base/content/foo.inc.xhtml`)
|
|
121
|
+
// - Engine-relative forms (`browser/base/content/foo.inc.xhtml`)
|
|
122
|
+
//
|
|
123
|
+
// Before the engine-prefix normalization, passing an `engine/…`-prefixed
|
|
124
|
+
// relative path from the repo root double-rooted through
|
|
125
|
+
// `toRootRelativePath(engineDir, …)` — `resolve(engineDir, 'engine/…')`
|
|
126
|
+
// landed at `engineDir/engine/…`, which is still "inside" engineDir but
|
|
127
|
+
// named as a second-level `engine/…` entry. The computed `#include`
|
|
128
|
+
// then read `../../../engine/browser/base/content/foo.inc.xhtml`,
|
|
129
|
+
// packaging-breaking nonsense. For absolute inputs this pre-existing
|
|
130
|
+
// contract was fine — `toRootRelativePath` handles absolute candidates
|
|
131
|
+
// correctly — so we only strip the prefix when the input is relative.
|
|
117
132
|
let domFilePath;
|
|
118
133
|
if (options.dom) {
|
|
119
134
|
const paths = getProjectPaths(projectRoot);
|
|
120
|
-
|
|
135
|
+
const domCandidate = isExplicitAbsolutePath(options.dom)
|
|
136
|
+
? options.dom
|
|
137
|
+
: stripEnginePrefix(options.dom);
|
|
138
|
+
if (!(await pathExists(domCandidate))) {
|
|
121
139
|
throw new InvalidArgumentError(`DOM fragment file not found: ${options.dom}`, 'dom');
|
|
122
140
|
}
|
|
123
|
-
if (!isPathInsideRoot(paths.engine,
|
|
141
|
+
if (!isPathInsideRoot(paths.engine, domCandidate)) {
|
|
124
142
|
throw new InvalidArgumentError(`DOM fragment file must stay within engine/: ${options.dom}`, 'dom');
|
|
125
143
|
}
|
|
126
|
-
domFilePath = toRootRelativePath(paths.engine,
|
|
144
|
+
domFilePath = toRootRelativePath(paths.engine, domCandidate);
|
|
127
145
|
}
|
|
128
146
|
// Resolve the chrome document the `#include` directive will land in.
|
|
129
147
|
// Only consulted when `--dom` is supplied — we still resolve it here so
|
|
130
148
|
// the dry-run plan can print the target accurately.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
149
|
+
//
|
|
150
|
+
// `stripEnginePrefix` is applied so `--target engine/browser/base/browser.xhtml`
|
|
151
|
+
// and `--target browser/base/browser.xhtml` are treated identically,
|
|
152
|
+
// matching the `--dom` normalization above. Absolute `--target` paths
|
|
153
|
+
// stay absolute (the containment check downstream rejects them).
|
|
154
|
+
const normalizedTarget = options.target !== undefined && !isExplicitAbsolutePath(options.target)
|
|
155
|
+
? stripEnginePrefix(options.target)
|
|
156
|
+
: options.target;
|
|
157
|
+
if (normalizedTarget !== undefined && !isContainedRelativePath(normalizedTarget)) {
|
|
158
|
+
throw new InvalidArgumentError(`Target chrome document must stay within engine/: ${options.target ?? ''}`, 'target');
|
|
159
|
+
}
|
|
160
|
+
const domTargetPath = await resolveDomTargetPath(projectRoot, normalizedTarget);
|
|
135
161
|
if (domFilePath) {
|
|
136
162
|
const paths = getProjectPaths(projectRoot);
|
|
137
163
|
if (!options.dryRun && !(await pathExists(join(paths.engine, domTargetPath)))) {
|
|
@@ -6,6 +6,29 @@ export declare class BrandingError extends FireForgeError {
|
|
|
6
6
|
readonly code: 6;
|
|
7
7
|
get userMessage(): string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Error thrown when the generated `mozconfig` references a `--with-branding`
|
|
11
|
+
* directory that does not match the branding tree FireForge set up. The
|
|
12
|
+
* mismatch is a silent-corruption hazard — `mach configure` picks the value
|
|
13
|
+
* from mozconfig but the scaffolded branding lives elsewhere, so the build
|
|
14
|
+
* fails deep inside moz.build resolution with a confusing "path does not
|
|
15
|
+
* exist" message. Surface it as an actionable preflight instead.
|
|
16
|
+
*
|
|
17
|
+
* The root cause is that setup renders templates under `configs/` with
|
|
18
|
+
* `${binaryName}` baked in at setup time; a subsequent edit to
|
|
19
|
+
* `fireforge.json`'s `binaryName` (or a re-setup without re-templating)
|
|
20
|
+
* leaves those baked-in names stale while `setupBranding` continues to use
|
|
21
|
+
* the current `config.binaryName`. Both directions (mozconfig ahead of
|
|
22
|
+
* config, config ahead of mozconfig) produce the same class of build break.
|
|
23
|
+
*/
|
|
24
|
+
export declare class BrandingMozconfigMismatchError extends FireForgeError {
|
|
25
|
+
readonly expectedBrandingDir: string;
|
|
26
|
+
readonly mozconfigBrandingDir: string;
|
|
27
|
+
readonly reason: 'mozconfig-missing-branding' | 'name-mismatch' | 'branding-dir-missing';
|
|
28
|
+
readonly code: 6;
|
|
29
|
+
constructor(expectedBrandingDir: string, mozconfigBrandingDir: string, reason: 'mozconfig-missing-branding' | 'name-mismatch' | 'branding-dir-missing');
|
|
30
|
+
get userMessage(): string;
|
|
31
|
+
}
|
|
9
32
|
/**
|
|
10
33
|
* Full branding configuration.
|
|
11
34
|
*/
|
|
@@ -13,6 +13,45 @@ export class BrandingError extends FireForgeError {
|
|
|
13
13
|
return `Branding Error: ${this.message}\n\nBranding is required to set MOZ_APP_VENDOR, MOZ_MACBUNDLE_ID, and other Firefox identity values.`;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Error thrown when the generated `mozconfig` references a `--with-branding`
|
|
18
|
+
* directory that does not match the branding tree FireForge set up. The
|
|
19
|
+
* mismatch is a silent-corruption hazard — `mach configure` picks the value
|
|
20
|
+
* from mozconfig but the scaffolded branding lives elsewhere, so the build
|
|
21
|
+
* fails deep inside moz.build resolution with a confusing "path does not
|
|
22
|
+
* exist" message. Surface it as an actionable preflight instead.
|
|
23
|
+
*
|
|
24
|
+
* The root cause is that setup renders templates under `configs/` with
|
|
25
|
+
* `${binaryName}` baked in at setup time; a subsequent edit to
|
|
26
|
+
* `fireforge.json`'s `binaryName` (or a re-setup without re-templating)
|
|
27
|
+
* leaves those baked-in names stale while `setupBranding` continues to use
|
|
28
|
+
* the current `config.binaryName`. Both directions (mozconfig ahead of
|
|
29
|
+
* config, config ahead of mozconfig) produce the same class of build break.
|
|
30
|
+
*/
|
|
31
|
+
export class BrandingMozconfigMismatchError extends FireForgeError {
|
|
32
|
+
expectedBrandingDir;
|
|
33
|
+
mozconfigBrandingDir;
|
|
34
|
+
reason;
|
|
35
|
+
code = ExitCode.PATCH_ERROR;
|
|
36
|
+
constructor(expectedBrandingDir, mozconfigBrandingDir, reason) {
|
|
37
|
+
super(`Generated mozconfig references "${mozconfigBrandingDir}" but the active branding directory is "${expectedBrandingDir}".`);
|
|
38
|
+
this.expectedBrandingDir = expectedBrandingDir;
|
|
39
|
+
this.mozconfigBrandingDir = mozconfigBrandingDir;
|
|
40
|
+
this.reason = reason;
|
|
41
|
+
}
|
|
42
|
+
get userMessage() {
|
|
43
|
+
const diagnosis = this.reason === 'mozconfig-missing-branding'
|
|
44
|
+
? `The generated mozconfig does not contain a --with-branding directive (found "${this.mozconfigBrandingDir}"). FireForge expected to write one for binaryName "${this.expectedBrandingDir}".`
|
|
45
|
+
: this.reason === 'name-mismatch'
|
|
46
|
+
? `The generated mozconfig sets --with-branding="${this.mozconfigBrandingDir}" but FireForge set up branding under "${this.expectedBrandingDir}".`
|
|
47
|
+
: `The generated mozconfig sets --with-branding="${this.mozconfigBrandingDir}" but no moz.build exists under engine/${this.mozconfigBrandingDir}/.`;
|
|
48
|
+
return (`Branding Error: ${diagnosis}\n\n` +
|
|
49
|
+
'This usually means the rendered configs/ templates drifted from fireforge.json. Fix one of:\n' +
|
|
50
|
+
' 1. Edit configs/common.mozconfig so --with-branding uses ${binaryName} (or the current binaryName), then re-run "fireforge build".\n' +
|
|
51
|
+
' 2. Update fireforge.json so binaryName matches the --with-branding value baked into configs/.\n\n' +
|
|
52
|
+
'The mismatch is caught before mach builds because resolving the build against the wrong branding tree fails deep in moz.build with a confusing "path does not exist" message.');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
16
55
|
/**
|
|
17
56
|
* Sets up the custom branding directory for the browser.
|
|
18
57
|
*
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
|
+
import { GeneralError } from '../errors/base.js';
|
|
4
|
+
import { toError } from '../utils/errors.js';
|
|
3
5
|
import { toRootRelativePath } from '../utils/paths.js';
|
|
4
6
|
import { getProjectPaths } from './config.js';
|
|
7
|
+
import { createRollbackJournal, restoreRollbackJournal, snapshotFile } from './furnace-rollback.js';
|
|
5
8
|
import { registerBrowserContent } from './manifest-register.js';
|
|
9
|
+
import { DEFAULT_DOM_TARGET } from './wire-dom-fragment.js';
|
|
6
10
|
import { addDestroyToBrowserInit, addDomFragment, addInitToBrowserInit, addSubscriptToBrowserMain, } from './wire-targets.js';
|
|
7
11
|
export const DEFAULT_BROWSER_SUBSCRIPT_DIR = 'browser/base/content';
|
|
8
12
|
const BROWSER_BASE_DIR = 'browser/base';
|
|
@@ -36,31 +40,72 @@ export async function wireSubscript(root, name, options = {}) {
|
|
|
36
40
|
},
|
|
37
41
|
};
|
|
38
42
|
}
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Snapshot every file the five mutation steps might touch so a mid-sequence
|
|
44
|
+
// failure (most commonly the chrome-document insertion not finding an
|
|
45
|
+
// anchor) does not leave a half-wired browser behind. Before the rollback
|
|
46
|
+
// journal landed here, a failed `wire` would still have written new
|
|
47
|
+
// `loadSubScript` calls into browser-main.js, new init/destroy expressions
|
|
48
|
+
// into browser-init.js, and a new entry into browser/base/jar.mn — the
|
|
49
|
+
// operator then had to grep the engine tree for the partial mutation and
|
|
50
|
+
// hand-revert, or re-download. The snapshots cover the targets on every
|
|
51
|
+
// code path (init/destroy/DOM are conditional, so we snapshot only when
|
|
52
|
+
// the corresponding option would fire a write) plus the two files every
|
|
53
|
+
// wire touches.
|
|
54
|
+
const journal = createRollbackJournal();
|
|
55
|
+
const effectiveDomTargetPath = options.domFilePath
|
|
56
|
+
? toRootRelativePath(engineDir, options.domTargetPath ?? DEFAULT_DOM_TARGET)
|
|
57
|
+
: undefined;
|
|
58
|
+
await snapshotFile(journal, join(engineDir, 'browser/base/content/browser-main.js'));
|
|
59
|
+
if (options.init !== undefined || options.destroy !== undefined) {
|
|
60
|
+
await snapshotFile(journal, join(engineDir, 'browser/base/content/browser-init.js'));
|
|
45
61
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (options.destroy) {
|
|
49
|
-
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy);
|
|
62
|
+
if (effectiveDomTargetPath) {
|
|
63
|
+
await snapshotFile(journal, join(engineDir, effectiveDomTargetPath));
|
|
50
64
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
await snapshotFile(journal, join(engineDir, 'browser/base/jar.mn'));
|
|
66
|
+
try {
|
|
67
|
+
// 1. Add subscript to browser-main.js
|
|
68
|
+
const subscriptAdded = await addSubscriptToBrowserMain(engineDir, name);
|
|
69
|
+
// 2. Add init expression to browser-init.js (if provided)
|
|
70
|
+
let initAdded = false;
|
|
71
|
+
if (options.init) {
|
|
72
|
+
initAdded = await addInitToBrowserInit(engineDir, options.init, options.after);
|
|
73
|
+
}
|
|
74
|
+
// 3. Add destroy expression to browser-init.js onUnload() (if provided)
|
|
75
|
+
let destroyAdded = false;
|
|
76
|
+
if (options.destroy) {
|
|
77
|
+
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy);
|
|
78
|
+
}
|
|
79
|
+
// 4. Add #include directive to the top-level chrome document (if provided)
|
|
80
|
+
let domInserted = false;
|
|
81
|
+
if (options.domFilePath) {
|
|
82
|
+
domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath), options.domTargetPath);
|
|
83
|
+
}
|
|
84
|
+
// 5. Register in jar.mn
|
|
85
|
+
const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
|
|
86
|
+
return {
|
|
87
|
+
subscriptAdded,
|
|
88
|
+
initAdded,
|
|
89
|
+
destroyAdded,
|
|
90
|
+
domInserted,
|
|
91
|
+
jarMnResult,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Best-effort rollback: if the restore itself fails, surface both the
|
|
96
|
+
// original wire failure and the rollback failure so the operator knows
|
|
97
|
+
// the engine may be in a partially-wired state that needs manual
|
|
98
|
+
// attention. The original error's message is preserved so the user sees
|
|
99
|
+
// *why* the wire failed (e.g. "Could not find insertion point in chrome
|
|
100
|
+
// document") alongside any rollback diagnosis.
|
|
101
|
+
const originalMessage = toError(error).message;
|
|
102
|
+
try {
|
|
103
|
+
await restoreRollbackJournal(journal);
|
|
104
|
+
}
|
|
105
|
+
catch (rollbackError) {
|
|
106
|
+
throw new GeneralError(`Wire failed: ${originalMessage}. Automatic rollback also failed: ${toError(rollbackError).message}. The engine may contain partially-applied wire mutations; review "git status" under engine/ and revert manually.`);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
55
109
|
}
|
|
56
|
-
// 5. Register in jar.mn
|
|
57
|
-
const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
|
|
58
|
-
return {
|
|
59
|
-
subscriptAdded,
|
|
60
|
-
initAdded,
|
|
61
|
-
destroyAdded,
|
|
62
|
-
domInserted,
|
|
63
|
-
jarMnResult,
|
|
64
|
-
};
|
|
65
110
|
}
|
|
66
111
|
//# sourceMappingURL=browser-wire.js.map
|
|
@@ -31,6 +31,20 @@ export interface BuildBaseline {
|
|
|
31
31
|
* the project has since been renamed.
|
|
32
32
|
*/
|
|
33
33
|
binaryName: string;
|
|
34
|
+
/**
|
|
35
|
+
* Content hash per packageable engine path that was dirty at build
|
|
36
|
+
* time (modified-against-HEAD or untracked). Used by
|
|
37
|
+
* `checkStaleBuildForTest` to distinguish "this file's content was
|
|
38
|
+
* already in `dist/` when the build completed" from "this file has
|
|
39
|
+
* been edited since". Missing on baselines written before 0.16.0; the
|
|
40
|
+
* stale-check falls back to the path-only comparison in that case,
|
|
41
|
+
* so older baselines retain their existing behavior.
|
|
42
|
+
*
|
|
43
|
+
* Keys are engine-relative POSIX paths. Values are hex-encoded
|
|
44
|
+
* SHA-256 digests of the file contents at the moment the baseline
|
|
45
|
+
* was recorded.
|
|
46
|
+
*/
|
|
47
|
+
packageableFingerprints?: Record<string, string>;
|
|
34
48
|
}
|
|
35
49
|
/** Name of the last-build marker file under `.fireforge/`. */
|
|
36
50
|
export declare const BUILD_BASELINE_FILENAME = "last-build.json";
|
|
@@ -16,10 +16,17 @@
|
|
|
16
16
|
* on successful build completion; a failed build does not update it, so a
|
|
17
17
|
* subsequent run still audits against the last known-good tree.
|
|
18
18
|
*/
|
|
19
|
+
import { createHash } from 'node:crypto';
|
|
20
|
+
import { readFile } from 'node:fs/promises';
|
|
19
21
|
import { join } from 'node:path';
|
|
22
|
+
import { toError } from '../utils/errors.js';
|
|
20
23
|
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
24
|
+
import { verbose } from '../utils/logger.js';
|
|
25
|
+
import { isPackageablePath } from './build-audit.js';
|
|
21
26
|
import { FIREFORGE_DIR } from './config-paths.js';
|
|
22
|
-
import { getHead, isMissingHeadError } from './git.js';
|
|
27
|
+
import { getHead, hasChanges, isMissingHeadError } from './git.js';
|
|
28
|
+
import { git } from './git-base.js';
|
|
29
|
+
import { getUntrackedFiles } from './git-status.js';
|
|
23
30
|
/** Name of the last-build marker file under `.fireforge/`. */
|
|
24
31
|
export const BUILD_BASELINE_FILENAME = 'last-build.json';
|
|
25
32
|
/**
|
|
@@ -73,11 +80,64 @@ export async function writeBuildBaseline(projectRoot, engineDir, binaryName) {
|
|
|
73
80
|
throw error;
|
|
74
81
|
}
|
|
75
82
|
}
|
|
83
|
+
const packageableFingerprints = await collectPackageableFingerprints(engineDir);
|
|
76
84
|
const baseline = {
|
|
77
85
|
engineHeadSha,
|
|
78
86
|
builtAt: new Date().toISOString(),
|
|
79
87
|
binaryName,
|
|
88
|
+
...(packageableFingerprints !== undefined ? { packageableFingerprints } : {}),
|
|
80
89
|
};
|
|
81
90
|
await writeJson(getBuildBaselinePath(projectRoot), baseline);
|
|
82
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Reads the current engine workdir and computes a SHA-256 fingerprint
|
|
94
|
+
* for every packageable path that is either modified against HEAD or
|
|
95
|
+
* untracked. The stale-build preflight (`checkStaleBuildForTest`)
|
|
96
|
+
* compares the live fingerprint for each packageable-dirty file to
|
|
97
|
+
* the baseline's entry — paths where the hash matches are "the build
|
|
98
|
+
* already saw this exact content", paths where it differs (or that
|
|
99
|
+
* are new since the baseline) are genuinely stale.
|
|
100
|
+
*
|
|
101
|
+
* Returns `undefined` on any git failure so a broken probe never
|
|
102
|
+
* corrupts the on-disk baseline with `{}`; the stale-check then falls
|
|
103
|
+
* back to the pre-0.16.0 "path-only" behavior on the next test run.
|
|
104
|
+
*/
|
|
105
|
+
async function collectPackageableFingerprints(engineDir) {
|
|
106
|
+
try {
|
|
107
|
+
const dirtyPaths = new Set();
|
|
108
|
+
if (await hasChanges(engineDir)) {
|
|
109
|
+
const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
|
|
110
|
+
for (const line of worktreeDiff.split('\n')) {
|
|
111
|
+
const trimmed = line.trim();
|
|
112
|
+
if (trimmed)
|
|
113
|
+
dirtyPaths.add(trimmed);
|
|
114
|
+
}
|
|
115
|
+
for (const untracked of await getUntrackedFiles(engineDir)) {
|
|
116
|
+
dirtyPaths.add(untracked);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const packageable = [...dirtyPaths].filter(isPackageablePath);
|
|
120
|
+
if (packageable.length === 0) {
|
|
121
|
+
return {};
|
|
122
|
+
}
|
|
123
|
+
const fingerprints = {};
|
|
124
|
+
for (const relPath of packageable) {
|
|
125
|
+
try {
|
|
126
|
+
const buffer = await readFile(join(engineDir, relPath));
|
|
127
|
+
fingerprints[relPath] = createHash('sha256').update(buffer).digest('hex');
|
|
128
|
+
}
|
|
129
|
+
catch (fileError) {
|
|
130
|
+
// A file that disappeared between status probe and hash is
|
|
131
|
+
// expected in concurrent scenarios; skip it without failing the
|
|
132
|
+
// whole baseline write.
|
|
133
|
+
verbose(`Build baseline: skipping fingerprint for ${relPath} — ${toError(fileError).message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return fingerprints;
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
verbose(`Build baseline: packageable fingerprint probe failed — ${toError(error).message}`);
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
83
143
|
//# sourceMappingURL=build-baseline.js.map
|
|
@@ -12,4 +12,4 @@ import type { FireForgeConfig } from '../types/config.js';
|
|
|
12
12
|
* @returns The mutated config
|
|
13
13
|
*/
|
|
14
14
|
export declare function mutateConfig(config: FireForgeConfig, key: string, value: unknown, skipValidation?: false): FireForgeConfig;
|
|
15
|
-
export declare function mutateConfig(config: FireForgeConfig, key: string, value: unknown, skipValidation: true): Record<string, unknown>;
|
|
15
|
+
export declare function mutateConfig(config: FireForgeConfig | Record<string, unknown>, key: string, value: unknown, skipValidation: true): Record<string, unknown>;
|
|
@@ -25,6 +25,23 @@ export declare function configExists(root: string): Promise<boolean>;
|
|
|
25
25
|
* @throws Error if config doesn't exist or is invalid
|
|
26
26
|
*/
|
|
27
27
|
export declare function loadConfig(root: string): Promise<FireForgeConfig>;
|
|
28
|
+
/**
|
|
29
|
+
* Reads the raw `fireforge.json` document without running it through
|
|
30
|
+
* {@link validateConfig}. Returns every persisted key — including keys
|
|
31
|
+
* written via `fireforge config <key> --force` that `validateConfig`
|
|
32
|
+
* would strip from the typed result.
|
|
33
|
+
*
|
|
34
|
+
* Callers that need the validated, typed shape must still use
|
|
35
|
+
* {@link loadConfig}; this helper exists specifically for the `config`
|
|
36
|
+
* read path so `fireforge config <key>` can surface keys the write path
|
|
37
|
+
* accepted under `--force`.
|
|
38
|
+
*
|
|
39
|
+
* @param root - Root directory of the project
|
|
40
|
+
* @returns Raw config object as persisted on disk
|
|
41
|
+
* @throws ConfigNotFoundError when fireforge.json is missing
|
|
42
|
+
* @throws ConfigError when the file is not valid JSON
|
|
43
|
+
*/
|
|
44
|
+
export declare function loadRawConfigDocument(root: string): Promise<Record<string, unknown>>;
|
|
28
45
|
/**
|
|
29
46
|
* Writes a configuration to fireforge.json.
|
|
30
47
|
* @param root - Root directory of the project
|
package/dist/src/core/config.js
CHANGED
|
@@ -50,6 +50,41 @@ export async function loadConfig(root) {
|
|
|
50
50
|
throw new ConfigError(`Invalid fireforge.json at ${paths.config}: ${toError(error).message}`);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Reads the raw `fireforge.json` document without running it through
|
|
55
|
+
* {@link validateConfig}. Returns every persisted key — including keys
|
|
56
|
+
* written via `fireforge config <key> --force` that `validateConfig`
|
|
57
|
+
* would strip from the typed result.
|
|
58
|
+
*
|
|
59
|
+
* Callers that need the validated, typed shape must still use
|
|
60
|
+
* {@link loadConfig}; this helper exists specifically for the `config`
|
|
61
|
+
* read path so `fireforge config <key>` can surface keys the write path
|
|
62
|
+
* accepted under `--force`.
|
|
63
|
+
*
|
|
64
|
+
* @param root - Root directory of the project
|
|
65
|
+
* @returns Raw config object as persisted on disk
|
|
66
|
+
* @throws ConfigNotFoundError when fireforge.json is missing
|
|
67
|
+
* @throws ConfigError when the file is not valid JSON
|
|
68
|
+
*/
|
|
69
|
+
export async function loadRawConfigDocument(root) {
|
|
70
|
+
const paths = getProjectPaths(root);
|
|
71
|
+
if (!(await pathExists(paths.config))) {
|
|
72
|
+
throw new ConfigNotFoundError(paths.config);
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const data = await readJson(paths.config);
|
|
76
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
77
|
+
throw new ConfigError(`Invalid fireforge.json at ${paths.config}: expected an object`);
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof ConfigError || error instanceof ConfigNotFoundError) {
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
throw new ConfigError(`Invalid fireforge.json at ${paths.config}: ${toError(error).message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
53
88
|
/**
|
|
54
89
|
* Writes a configuration to fireforge.json.
|
|
55
90
|
* @param root - Root directory of the project
|