@hominis/fireforge 0.20.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +13 -4
- package/dist/src/commands/build.js +19 -1
- package/dist/src/commands/furnace/chrome-doc-remove.d.ts +13 -0
- package/dist/src/commands/furnace/chrome-doc-remove.js +142 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +32 -0
- package/dist/src/commands/furnace/chrome-doc.js +113 -1
- package/dist/src/commands/furnace/index.js +14 -0
- package/dist/src/commands/furnace/remove.js +3 -0
- package/dist/src/commands/furnace/rename-browser-test.d.ts +2 -0
- package/dist/src/commands/furnace/rename-browser-test.js +28 -0
- package/dist/src/commands/furnace/rename.js +2 -1
- package/dist/src/commands/status.js +22 -3
- package/dist/src/commands/test.js +3 -0
- package/dist/src/commands/watch.js +9 -2
- package/dist/src/core/furnace-config.js +4 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.21.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- **Chrome-doc previews and cleanup.** `furnace chrome-doc create` now supports `--dry-run`, validating the same target files and jar registrations without writing. New `furnace chrome-doc remove <name>` removes scaffolded chrome-doc files, jar entries, and optional xpcshell packaging-test directories, with `--dry-run` and `--yes` support.
|
|
8
|
+
- **Versioned `status --json` schema.** The JSON output is now an object with `schemaVersion`, `summary`, and `files` instead of a bare array. Error paths also emit versioned JSON objects with `code` and `error`.
|
|
9
|
+
|
|
10
|
+
### Hardening
|
|
11
|
+
|
|
12
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
15
|
+
- **Interrupt and diagnostics polish.** Signal-driven Furnace preview teardown has regression coverage for stale lock cleanup, `chrome-doc-rollback` markers round-trip through Furnace state validation, watch-mode permission failures name the macOS privacy remediation, and `test --doctor` prints the probed objdir, binary/app path, port, and elapsed time.
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
- **README — Storybook first-run and audit posture.** The Furnace preview docs now call out the upstream Storybook npm install and audit output as Firefox Storybook workspace dependency state, not FireForge package dependency state.
|
|
20
|
+
- **README — chrome-doc lifecycle and JSON schema.** The Furnace and status sections document chrome-doc dry-runs/removal, UI-build preconditions, watch privacy guidance, and the new versioned `status --json` object.
|
|
21
|
+
|
|
3
22
|
## 0.20.0
|
|
4
23
|
|
|
5
24
|
### Features
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
|
|
|
36
36
|
- **Python 3** (required by Firefox's `mach` build system).
|
|
37
37
|
- **Git**
|
|
38
38
|
- Platform build tools: Xcode on macOS, `build-essential` on Linux, Visual Studio Build Tools on Windows.
|
|
39
|
-
- **Watchman** (optional, only required by `fireforge watch`). Install via `brew install watchman` (macOS), `dnf install watchman` (Fedora), or follow the upstream [Meta docs](https://facebook.github.io/watchman/). `fireforge doctor` surfaces a warning row when it is not on `PATH` so the dependency is visible during the usual onboarding sweep rather than at the watch-mode failure site. `fireforge watch` resolves watchman's absolute path via `which` / `where` and prepends its directory to the subprocess `PATH` it hands mach, so a homebrew-installed watchman at `/opt/homebrew/bin/watchman` (absent from the Node subprocess's default `PATH` on macOS) is still visible to `mach watch` without the operator having to re-export `PATH` manually.
|
|
39
|
+
- **Watchman** (optional, only required by `fireforge watch`). Install via `brew install watchman` (macOS), `dnf install watchman` (Fedora), or follow the upstream [Meta docs](https://facebook.github.io/watchman/). `fireforge doctor` surfaces a warning row when it is not on `PATH` so the dependency is visible during the usual onboarding sweep rather than at the watch-mode failure site. `fireforge watch` resolves watchman's absolute path via `which` / `where` and prepends its directory to the subprocess `PATH` it hands mach, so a homebrew-installed watchman at `/opt/homebrew/bin/watchman` (absent from the Node subprocess's default `PATH` on macOS) is still visible to `mach watch` without the operator having to re-export `PATH` manually. If `mach watch` reports `Operation not permitted` / `EPERM` on macOS, FireForge points at the usual privacy fix: grant Full Disk Access or Files and Folders access to the terminal/Codex app and watchman, then restart watchman with `watchman shutdown-server`.
|
|
40
40
|
|
|
41
41
|
### Setup
|
|
42
42
|
|
|
@@ -290,9 +290,11 @@ When a patch queue drifts, e.g. due to overlapping new-file creations, forward i
|
|
|
290
290
|
fireforge verify # fsck: manifest + cross-patch lint
|
|
291
291
|
fireforge lint # includes the same cross-patch rules
|
|
292
292
|
fireforge status --ownership # flat path → owning patch table
|
|
293
|
-
fireforge status --json # machine-readable classified
|
|
293
|
+
fireforge status --json # machine-readable classified object
|
|
294
294
|
```
|
|
295
295
|
|
|
296
|
+
`status --json` emits a versioned object: `{ "schemaVersion": 1, "summary": { "total": <n>, "byClassification": { ... } }, "files": [...] }`. Error paths also emit a JSON object with `schemaVersion`, `code`, and `error` before exiting non-zero, so scripts can parse both clean and failing runs.
|
|
297
|
+
|
|
296
298
|
Then fix with the appropriate primitive:
|
|
297
299
|
|
|
298
300
|
| Problem | Fix |
|
|
@@ -379,6 +381,9 @@ Custom elements live under `toolkit/content/widgets`, but a fork's top-level chr
|
|
|
379
381
|
fireforge furnace chrome-doc create mybrowser # full chrome (titlebar + windowtype)
|
|
380
382
|
fireforge furnace chrome-doc create overlay --no-titlebar # frameless overlay
|
|
381
383
|
fireforge furnace chrome-doc create mybrowser --with-tests # + xpcshell packaging-verification test
|
|
384
|
+
fireforge furnace chrome-doc create mybrowser --dry-run # preview without writing
|
|
385
|
+
fireforge furnace chrome-doc remove mybrowser --dry-run # preview cleanup
|
|
386
|
+
fireforge furnace chrome-doc remove mybrowser --yes # remove files + registrations
|
|
382
387
|
```
|
|
383
388
|
|
|
384
389
|
The command writes:
|
|
@@ -390,7 +395,7 @@ The command writes:
|
|
|
390
395
|
- Appends the corresponding `jar.mn` / `jar.inc.mn` entries. The locales/jar.mn append is suppressed when the fork's existing `engine/browser/locales/jar.mn` already carries a `[localization] (%browser/**/*.ftl)` (or `(%browser/*.ftl)`) wildcard that would already pick up the scaffolded FTL — on those forks a per-file `locale/<name>.ftl` entry would be dead weight at best and an outright build break when the fork has dropped the `% locale browser …` registration the per-file entry depends on. Forks still on the legacy registration get the per-file entry as before.
|
|
391
396
|
- When `--with-tests` is set, also scaffolds an xpcshell test + `xpcshell.toml` under `engine/browser/base/content/test/<binary>-xpcshell/<name>/` that probes the packaged app directory (`Services.dirsvc.get("XCurProcD")/chrome/browser/...`) directly rather than going through `chrome://` URI resolution — see "Platform module compatibility" and the xpcshell chrome-URI note further down for why direct filesystem probing is the reliable way to verify chrome-doc packaging. Registration in `XPCSHELL_TESTS_MANIFESTS` is left to the operator because the owning moz.build depends on the fork layout.
|
|
392
397
|
|
|
393
|
-
Writes are transactional: a SIGINT mid-scaffold rolls back every touched file. Requires an existing engine — run `fireforge download` first.
|
|
398
|
+
Writes are transactional: a SIGINT mid-scaffold rolls back every touched file. `--dry-run` validates the same paths and registrations without acquiring the mutation lock or writing. `furnace chrome-doc remove <name>` removes the scaffolded source files, jar registrations, and optional xpcshell packaging-test directory; use its `--dry-run` first when cleaning an experimental document. Requires an existing engine — run `fireforge download` first.
|
|
394
399
|
|
|
395
400
|
#### Platform module compatibility
|
|
396
401
|
|
|
@@ -439,6 +444,8 @@ Three styles are available via `--test-style`:
|
|
|
439
444
|
|
|
440
445
|
`furnace create`, `furnace remove`, and `furnace rename` re-read `furnace.json` inside the mutation lock before writing, so concurrent component edits preserve sibling entries instead of writing back a stale outer snapshot. `furnace refresh --all` continues past per-component refresh failures, reports the failed count, and exits non-zero with the failed override names after finishing the rest of the selection.
|
|
441
446
|
|
|
447
|
+
`furnace preview` starts Firefox's upstream Storybook workspace. On the first run, `mach storybook` may install roughly a thousand npm packages under `engine/browser/components/storybook/` and may print npm audit counts from Storybook's transitive dependencies. FireForge frames that output as upstream Storybook dependency state; it does not mean FireForge's own package dependencies were installed into the project.
|
|
448
|
+
|
|
442
449
|
## Additional Commands
|
|
443
450
|
|
|
444
451
|
The commands below cover project configuration, patch queue management, build packaging and development utilities. Run `fireforge <command> --help` for full option details.
|
|
@@ -540,6 +547,8 @@ Aggregate patch-size findings (`large-patch-files`, `large-patch-lines`) describ
|
|
|
540
547
|
|
|
541
548
|
`fireforge build` is a transactional step: after a successful mach build it audits the dist bundle against engine-relative paths touched since the last successful build, and warns per file that is packageable-by-convention (`.js`/`.mjs`/`.css`/`.ftl`/`.xhtml`/`app/profile/…`) but has no matching artifact or whose dist mtime is older than the source. Ends every build with a `Packaged: N updated, M stale, K missing, S skipped` summary. The audit is warn-only — it never fails a build that mach reported green.
|
|
542
549
|
|
|
550
|
+
`fireforge build --ui` is intentionally a fast rebuild path, not a bootstrap path. It now refuses before invoking `mach build faster` unless the current objdir has a completed launchable bundle; fresh imports and partial builds should run a full `fireforge build` first.
|
|
551
|
+
|
|
543
552
|
The audit applies seven routing rules to suppress false positives that previously trained operators to ignore its warnings:
|
|
544
553
|
|
|
545
554
|
- **jar.mn registrations are authoritative.** When the source under audit is claimed by a `(source)` reference in an ancestor `jar.mn`, the audit walks the registration to compute the expected target path (e.g. `content/browser/mybrowser.js`) and probes the dist tree for a candidate whose absolute path ends with that suffix. Picking the correct artifact from a same-basename collision no longer depends on path-similarity scoring. If the registration target is missing from dist, the warning names the `jar.mn` entry so "registration is intact, packaging dropped the file" is distinguishable from "source is unregistered". This is the fix for the class of false positive where `engine/browser/base/content/<name>.js` (registered in `browser/base/jar.mn`) collided with an unrelated `browser/defaults/preferences/<name>.js` added by a separate patch; the heuristic could not distinguish them, so the audit falsely reported the correctly-packaged chrome resource as missing.
|
|
@@ -614,7 +623,7 @@ fireforge test --doctor
|
|
|
614
623
|
fireforge test --doctor browser/base/content/test/foo/browser_bar.js
|
|
615
624
|
```
|
|
616
625
|
|
|
617
|
-
Spawns the built browser headless, waits for a marionette handshake on `127.0.0.1:2828`, and reports PASS/FAIL with the tail of the browser's stderr on FAIL. Distinguishes "marionette wedged" (socket silent) from "mach test discovery failed" — both otherwise surface as a silent 360-second hang followed by `Passed: 0, Failed: 0`. Useful as a prefix on routine `fireforge test` invocations when marionette has been flaky.
|
|
626
|
+
Spawns the built browser headless, waits for a marionette handshake on `127.0.0.1:2828`, and reports PASS/FAIL with the tail of the browser's stderr on FAIL. The success output also names the objdir, binary, app path, port, and elapsed probe time so CI logs show exactly what was probed. Distinguishes "marionette wedged" (socket silent) from "mach test discovery failed" — both otherwise surface as a silent 360-second hang followed by `Passed: 0, Failed: 0`. Useful as a prefix on routine `fireforge test` invocations when marionette has been flaky.
|
|
618
627
|
|
|
619
628
|
The probe is a cascade of six layered checks — engine-present → mach-available → python-available → profile-creatable → browser-spawns → marionette-handshake. Each failure is tagged `[layer N/6: <name>]` so the first broken layer is surfaced immediately instead of the whole cascade blocking on the final socket poll. When the browser binary crashes at startup (missing dylib, wrong CPU arch, corrupt profile) the cascade fails at layer 5 within the settle window, not after the full socket timeout.
|
|
620
629
|
|
|
@@ -5,7 +5,7 @@ import { auditBuildArtifacts } from '../core/build-audit.js';
|
|
|
5
5
|
import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
|
|
6
6
|
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
7
7
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
8
|
-
import { attemptMozinfoRewrite, build, buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, runMach, withBuildLock, } from '../core/mach.js';
|
|
8
|
+
import { attemptMozinfoRewrite, build, buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, runMach, withBuildLock, } from '../core/mach.js';
|
|
9
9
|
import { GeneralError } from '../errors/base.js';
|
|
10
10
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
11
11
|
import { toError } from '../utils/errors.js';
|
|
@@ -106,6 +106,24 @@ export async function buildCommand(projectRoot, options) {
|
|
|
106
106
|
throw new GeneralError(mismatchMessage);
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
if (options.ui) {
|
|
110
|
+
if (!buildCheck.exists || !buildCheck.objDir) {
|
|
111
|
+
const detail = buildCheck.objDir
|
|
112
|
+
? `Build artifacts incomplete in ${buildCheck.objDir}/`
|
|
113
|
+
: 'No completed obj-* build artifacts found.';
|
|
114
|
+
throw new GeneralError(`UI-only builds require a completed full build first. ${detail}\n\n` +
|
|
115
|
+
'Run "fireforge build" and let it finish, then retry "fireforge build --ui".');
|
|
116
|
+
}
|
|
117
|
+
const bundleCheck = await hasRunnableBundle(paths.engine, config.binaryName, buildCheck.objDir);
|
|
118
|
+
if (!bundleCheck.runnable) {
|
|
119
|
+
const expectedSuffix = bundleCheck.expectedPath
|
|
120
|
+
? ` Expected launchable binary at engine/${bundleCheck.expectedPath}.`
|
|
121
|
+
: '';
|
|
122
|
+
throw new GeneralError(`UI-only builds require a completed full build first.${expectedSuffix}\n\n` +
|
|
123
|
+
'Freshly imported or partially built trees cannot use `mach build faster` yet. ' +
|
|
124
|
+
'Run "fireforge build" and let it finish, then retry "fireforge build --ui".');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
109
127
|
// Log brand info if specified
|
|
110
128
|
if (options.brand) {
|
|
111
129
|
verbose(`Building with brand: ${options.brand}`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fireforge furnace chrome-doc remove <name>` — removes the files and
|
|
3
|
+
* registrations created by `furnace chrome-doc create`.
|
|
4
|
+
*/
|
|
5
|
+
/** Options for `furnace chrome-doc remove`. */
|
|
6
|
+
export interface FurnaceChromeDocRemoveOptions {
|
|
7
|
+
/** Skip confirmation. Required for real non-interactive removal. */
|
|
8
|
+
yes?: boolean;
|
|
9
|
+
/** Print the removal plan without writing files. */
|
|
10
|
+
dryRun?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** Runs `furnace chrome-doc remove <name>`. */
|
|
13
|
+
export declare function furnaceChromeDocRemoveCommand(projectRoot: string, name: string, options?: FurnaceChromeDocRemoveOptions): Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `fireforge furnace chrome-doc remove <name>` — removes the files and
|
|
4
|
+
* registrations created by `furnace chrome-doc create`.
|
|
5
|
+
*/
|
|
6
|
+
import { readdir } from 'node:fs/promises';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { confirm } from '@clack/prompts';
|
|
9
|
+
import { loadConfig } from '../../core/config.js';
|
|
10
|
+
import { runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
11
|
+
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
12
|
+
import { FurnaceError } from '../../errors/furnace.js';
|
|
13
|
+
import { pathExists, readText, removeDir, removeFile, writeText } from '../../utils/fs.js';
|
|
14
|
+
import { cancel, info, intro, isCancel, note, outro } from '../../utils/logger.js';
|
|
15
|
+
import { buildChromeDocPlan, validateChromeDocName } from './chrome-doc.js';
|
|
16
|
+
function removeExactLine(content, line) {
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
const filtered = lines.filter((candidate) => candidate !== line);
|
|
19
|
+
return filtered.join('\n').replace(/\n*$/, '\n');
|
|
20
|
+
}
|
|
21
|
+
async function removeChromeDocJarEntryIfPresent(engineDir, file, entry, journal) {
|
|
22
|
+
const jarPath = join(engineDir, file);
|
|
23
|
+
if (!(await pathExists(jarPath))) {
|
|
24
|
+
throw new FurnaceError(`Required jar file ${jarPath} does not exist; cannot remove chrome-doc entry. Check that the fork's engine layout matches the expected browser/ and locales/ tree.`);
|
|
25
|
+
}
|
|
26
|
+
const existing = await readText(jarPath);
|
|
27
|
+
if (!existing.includes(entry)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
await snapshotFile(journal, jarPath);
|
|
31
|
+
await writeText(jarPath, removeExactLine(existing, entry));
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
async function removeEmptyDirIfPresent(dirPath, journal) {
|
|
35
|
+
if (!(await pathExists(dirPath)))
|
|
36
|
+
return false;
|
|
37
|
+
const entries = await readdir(dirPath);
|
|
38
|
+
if (entries.length > 0)
|
|
39
|
+
return false;
|
|
40
|
+
await snapshotDir(journal, dirPath);
|
|
41
|
+
await removeDir(dirPath);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
function renderChromeDocRemoveDryRun(name, plan) {
|
|
45
|
+
const jarLines = plan.jarEntries.map(({ file, entry, present }) => ` engine/${file}: ${present ? 'would remove' : 'not present'} ${entry.trim()}`);
|
|
46
|
+
const testLines = plan.testDir !== undefined
|
|
47
|
+
? ['', 'Would remove test directory if present:', ` engine/${plan.testDir}/`]
|
|
48
|
+
: [];
|
|
49
|
+
return [
|
|
50
|
+
`[dry-run] Chrome document "${name}" removal plan`,
|
|
51
|
+
'',
|
|
52
|
+
'Would remove source files if present:',
|
|
53
|
+
...plan.files.map((f) => ` engine/${f}`),
|
|
54
|
+
...testLines,
|
|
55
|
+
'',
|
|
56
|
+
'Jar registrations:',
|
|
57
|
+
...jarLines,
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
async function performChromeDocRemoveMutations(args) {
|
|
61
|
+
const journal = createRollbackJournal();
|
|
62
|
+
args.operationContext.registerJournal(journal);
|
|
63
|
+
let removedFiles = 0;
|
|
64
|
+
let removedJarEntries = 0;
|
|
65
|
+
let removedTestDir = false;
|
|
66
|
+
try {
|
|
67
|
+
for (const file of args.plan.files) {
|
|
68
|
+
const filePath = join(args.engineDir, file);
|
|
69
|
+
if (await pathExists(filePath)) {
|
|
70
|
+
await snapshotFile(journal, filePath);
|
|
71
|
+
await removeFile(filePath);
|
|
72
|
+
removedFiles++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const { file, entry } of args.plan.jarEntries) {
|
|
76
|
+
if (await removeChromeDocJarEntryIfPresent(args.engineDir, file, entry, journal)) {
|
|
77
|
+
removedJarEntries++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const testDir = join(args.engineDir, 'browser/base/content/test', `${args.binaryName}-xpcshell`, args.name);
|
|
81
|
+
if (await pathExists(testDir)) {
|
|
82
|
+
await snapshotDir(journal, testDir);
|
|
83
|
+
await removeDir(testDir);
|
|
84
|
+
removedTestDir = true;
|
|
85
|
+
}
|
|
86
|
+
await removeEmptyDirIfPresent(join(args.engineDir, 'browser/base/content/test', `${args.binaryName}-xpcshell`), journal);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
await restoreRollbackJournalOrThrow(journal, `Failed to remove chrome-doc "${args.name}"`);
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
return { removedFiles, removedJarEntries, removedTestDir };
|
|
93
|
+
}
|
|
94
|
+
/** Runs `furnace chrome-doc remove <name>`. */
|
|
95
|
+
export async function furnaceChromeDocRemoveCommand(projectRoot, name, options = {}) {
|
|
96
|
+
intro('Furnace chrome-doc remove');
|
|
97
|
+
validateChromeDocName(name);
|
|
98
|
+
const forgeConfig = await loadConfig(projectRoot);
|
|
99
|
+
const engineDir = join(projectRoot, 'engine');
|
|
100
|
+
if (!(await pathExists(engineDir))) {
|
|
101
|
+
throw new FurnaceError('Engine directory not found. Run "fireforge download" first before removing a chrome-doc.');
|
|
102
|
+
}
|
|
103
|
+
const plan = await buildChromeDocPlan({
|
|
104
|
+
engineDir,
|
|
105
|
+
name,
|
|
106
|
+
withTests: true,
|
|
107
|
+
binaryName: forgeConfig.binaryName,
|
|
108
|
+
includeLocaleEntryWhenWildcard: true,
|
|
109
|
+
});
|
|
110
|
+
if (options.dryRun) {
|
|
111
|
+
note(renderChromeDocRemoveDryRun(name, plan), name);
|
|
112
|
+
outro('Dry run complete');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
116
|
+
if (!options.yes && !isInteractive) {
|
|
117
|
+
throw new FurnaceError(`Cannot remove chrome-doc "${name}" in non-interactive mode without --yes flag.`, name);
|
|
118
|
+
}
|
|
119
|
+
if (!options.yes && isInteractive) {
|
|
120
|
+
const confirmed = await confirm({
|
|
121
|
+
message: `Remove chrome document "${name}" and its scaffolded registrations?`,
|
|
122
|
+
});
|
|
123
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
124
|
+
cancel('Remove cancelled');
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const result = await runFurnaceMutation(projectRoot, 'chrome-doc-rollback', (ctx) => performChromeDocRemoveMutations({
|
|
129
|
+
name,
|
|
130
|
+
engineDir,
|
|
131
|
+
plan,
|
|
132
|
+
binaryName: forgeConfig.binaryName,
|
|
133
|
+
operationContext: ctx,
|
|
134
|
+
}));
|
|
135
|
+
info(`Removed ${result.removedFiles} source file${result.removedFiles === 1 ? '' : 's'} and ` +
|
|
136
|
+
`${result.removedJarEntries} jar registration${result.removedJarEntries === 1 ? '' : 's'} for "${name}".`);
|
|
137
|
+
if (result.removedTestDir) {
|
|
138
|
+
info('Removed xpcshell packaging test directory.');
|
|
139
|
+
}
|
|
140
|
+
outro('Chrome document removed');
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=chrome-doc-remove.js.map
|
|
@@ -35,7 +35,39 @@ export interface FurnaceChromeDocCreateOptions {
|
|
|
35
35
|
* because the owning moz.build depends on the fork's layout.
|
|
36
36
|
*/
|
|
37
37
|
withTests?: boolean;
|
|
38
|
+
/** Print the scaffold plan without writing files. */
|
|
39
|
+
dryRun?: boolean;
|
|
38
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Validates a chrome-doc name. Lowercase ASCII, optional hyphens, no
|
|
43
|
+
* leading digit — the name is used verbatim in CSS selectors, jar.mn
|
|
44
|
+
* entries, FTL keys, and file basenames, so anything outside that
|
|
45
|
+
* character set would break at least one downstream consumer.
|
|
46
|
+
* @param name Chrome-doc name (file basename without extension).
|
|
47
|
+
* @throws InvalidArgumentError when the name is unusable.
|
|
48
|
+
*/
|
|
49
|
+
export declare function validateChromeDocName(name: string): void;
|
|
50
|
+
export interface ChromeDocPlan {
|
|
51
|
+
files: string[];
|
|
52
|
+
dirs: string[];
|
|
53
|
+
jarEntries: Array<{
|
|
54
|
+
file: string;
|
|
55
|
+
entry: string;
|
|
56
|
+
present: boolean;
|
|
57
|
+
}>;
|
|
58
|
+
localeWildcardCapturesFtl: boolean;
|
|
59
|
+
testDir?: string;
|
|
60
|
+
testFiles: string[];
|
|
61
|
+
}
|
|
62
|
+
/** Builds the shared create/remove plan for a top-level chrome document. */
|
|
63
|
+
export declare function buildChromeDocPlan(args: {
|
|
64
|
+
engineDir: string;
|
|
65
|
+
name: string;
|
|
66
|
+
withTests: boolean;
|
|
67
|
+
binaryName: string;
|
|
68
|
+
validateCreateConflicts?: boolean;
|
|
69
|
+
includeLocaleEntryWhenWildcard?: boolean;
|
|
70
|
+
}): Promise<ChromeDocPlan>;
|
|
39
71
|
/**
|
|
40
72
|
* Runs `furnace chrome-doc create <name>`.
|
|
41
73
|
* @param projectRoot Root directory of the project.
|
|
@@ -37,7 +37,7 @@ const CHROME_DOC_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
|
37
37
|
* @param name Chrome-doc name (file basename without extension).
|
|
38
38
|
* @throws InvalidArgumentError when the name is unusable.
|
|
39
39
|
*/
|
|
40
|
-
function validateChromeDocName(name) {
|
|
40
|
+
export function validateChromeDocName(name) {
|
|
41
41
|
if (!name.trim()) {
|
|
42
42
|
throw new InvalidArgumentError('Chrome-doc name is required', 'name');
|
|
43
43
|
}
|
|
@@ -45,6 +45,106 @@ function validateChromeDocName(name) {
|
|
|
45
45
|
throw new InvalidArgumentError('Chrome-doc name must be lowercase ASCII, may contain hyphens, and must not start with a digit (e.g. mybrowser, about-onboarding).', 'name');
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/** Builds the shared create/remove plan for a top-level chrome document. */
|
|
49
|
+
export async function buildChromeDocPlan(args) {
|
|
50
|
+
const contentDir = join(args.engineDir, 'browser/base/content');
|
|
51
|
+
const sharedThemeDir = join(args.engineDir, 'browser/themes/shared');
|
|
52
|
+
const localeDir = join(args.engineDir, 'browser/locales/en-US/browser');
|
|
53
|
+
const dirs = [contentDir, sharedThemeDir, localeDir];
|
|
54
|
+
const xhtmlPath = join(contentDir, `${args.name}.xhtml`);
|
|
55
|
+
if (args.validateCreateConflicts && (await pathExists(xhtmlPath))) {
|
|
56
|
+
throw new FurnaceError(`${args.name}.xhtml already exists at ${xhtmlPath}. Remove it or choose a different name.`);
|
|
57
|
+
}
|
|
58
|
+
const jarMnPath = join(args.engineDir, 'browser/base/jar.mn');
|
|
59
|
+
const jarIncMnPath = join(args.engineDir, 'browser/themes/shared/jar.inc.mn');
|
|
60
|
+
const localeJarMnPath = join(args.engineDir, 'browser/locales/jar.mn');
|
|
61
|
+
for (const requiredJarPath of [jarMnPath, jarIncMnPath, localeJarMnPath]) {
|
|
62
|
+
if (!(await pathExists(requiredJarPath))) {
|
|
63
|
+
throw new FurnaceError(`Required jar file ${requiredJarPath} does not exist; cannot register chrome-doc entry. Check that the fork's engine layout matches the expected browser/ and locales/ tree.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const [jarMn, jarIncMn, localeJarMn] = await Promise.all([
|
|
67
|
+
readText(jarMnPath),
|
|
68
|
+
readText(jarIncMnPath),
|
|
69
|
+
readText(localeJarMnPath),
|
|
70
|
+
]);
|
|
71
|
+
const localeWildcardCapturesFtl = localesFtlWildcardCapturesScaffoldedName(localeJarMn);
|
|
72
|
+
const jarEntries = [];
|
|
73
|
+
for (const entry of jarMnEntriesForChromeDoc(args.name)) {
|
|
74
|
+
jarEntries.push({
|
|
75
|
+
file: 'browser/base/jar.mn',
|
|
76
|
+
entry,
|
|
77
|
+
present: jarMn.includes(entry),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const cssEntry = jarIncMnEntryForChromeDoc(args.name);
|
|
81
|
+
jarEntries.push({
|
|
82
|
+
file: 'browser/themes/shared/jar.inc.mn',
|
|
83
|
+
entry: cssEntry,
|
|
84
|
+
present: jarIncMn.includes(cssEntry),
|
|
85
|
+
});
|
|
86
|
+
if (!localeWildcardCapturesFtl || args.includeLocaleEntryWhenWildcard) {
|
|
87
|
+
const ftlEntry = localeJarMnEntryForChromeDoc(args.name);
|
|
88
|
+
jarEntries.push({
|
|
89
|
+
file: 'browser/locales/jar.mn',
|
|
90
|
+
entry: ftlEntry,
|
|
91
|
+
present: localeJarMn.includes(ftlEntry),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const files = [
|
|
95
|
+
`browser/base/content/${args.name}.xhtml`,
|
|
96
|
+
`browser/base/content/${args.name}.js`,
|
|
97
|
+
`browser/themes/shared/${args.name}-chrome.css`,
|
|
98
|
+
`browser/locales/en-US/browser/${args.name}.ftl`,
|
|
99
|
+
];
|
|
100
|
+
const testFiles = [];
|
|
101
|
+
let testDir;
|
|
102
|
+
if (args.withTests) {
|
|
103
|
+
const testParentDir = `${args.binaryName}-xpcshell`;
|
|
104
|
+
testDir = `browser/base/content/test/${testParentDir}/${args.name}`;
|
|
105
|
+
testFiles.push(`${testDir}/${chromeDocPackagingTestFileName(args.name)}`, `${testDir}/xpcshell.toml`);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
files,
|
|
109
|
+
dirs,
|
|
110
|
+
jarEntries,
|
|
111
|
+
localeWildcardCapturesFtl,
|
|
112
|
+
...(testDir ? { testDir } : {}),
|
|
113
|
+
testFiles,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function renderChromeDocCreateDryRun(name, plan) {
|
|
117
|
+
const dirLines = plan.dirs.map((dir) => ` ${dir}`);
|
|
118
|
+
const jarLines = plan.jarEntries.map(({ file, entry, present }) => ` engine/${file}: ${present ? 'already present' : 'would add'} ${entry.trim()}`);
|
|
119
|
+
const localeLine = plan.localeWildcardCapturesFtl
|
|
120
|
+
? [
|
|
121
|
+
'',
|
|
122
|
+
'Locale jar.mn already has a [localization] wildcard that captures the FTL;',
|
|
123
|
+
'no per-file locale entry would be added.',
|
|
124
|
+
]
|
|
125
|
+
: [];
|
|
126
|
+
const testLines = plan.testFiles.length > 0
|
|
127
|
+
? [
|
|
128
|
+
'',
|
|
129
|
+
'Would create xpcshell packaging test files:',
|
|
130
|
+
...plan.testFiles.map((f) => ` engine/${f}`),
|
|
131
|
+
]
|
|
132
|
+
: [];
|
|
133
|
+
return [
|
|
134
|
+
`[dry-run] Chrome document "${name}" scaffold plan`,
|
|
135
|
+
'',
|
|
136
|
+
'Directories checked/created as needed:',
|
|
137
|
+
...dirLines,
|
|
138
|
+
'',
|
|
139
|
+
'Would create source files:',
|
|
140
|
+
...plan.files.map((f) => ` engine/${f}`),
|
|
141
|
+
...testLines,
|
|
142
|
+
'',
|
|
143
|
+
'Jar registrations:',
|
|
144
|
+
...jarLines,
|
|
145
|
+
...localeLine,
|
|
146
|
+
].join('\n');
|
|
147
|
+
}
|
|
48
148
|
/**
|
|
49
149
|
* Appends a line to a jar.mn-style file when that exact line is not
|
|
50
150
|
* already present. Captures the pre-write contents in the journal so a
|
|
@@ -192,6 +292,18 @@ export async function furnaceChromeDocCreateCommand(projectRoot, name, options =
|
|
|
192
292
|
}
|
|
193
293
|
const withTitlebar = options.titlebar ?? true;
|
|
194
294
|
const withTests = options.withTests ?? false;
|
|
295
|
+
const plan = await buildChromeDocPlan({
|
|
296
|
+
engineDir,
|
|
297
|
+
name,
|
|
298
|
+
withTests,
|
|
299
|
+
binaryName: forgeConfig.binaryName,
|
|
300
|
+
validateCreateConflicts: true,
|
|
301
|
+
});
|
|
302
|
+
if (options.dryRun) {
|
|
303
|
+
note(renderChromeDocCreateDryRun(name, plan), name);
|
|
304
|
+
outro('Dry run complete');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
195
307
|
const written = await runFurnaceMutation(projectRoot, 'chrome-doc-rollback', (ctx) => performChromeDocMutations({
|
|
196
308
|
name,
|
|
197
309
|
license,
|
|
@@ -3,6 +3,7 @@ import { Option } from 'commander';
|
|
|
3
3
|
import { pickDefined } from '../../utils/options.js';
|
|
4
4
|
import { furnaceApplyCommand } from './apply.js';
|
|
5
5
|
import { furnaceChromeDocCreateCommand } from './chrome-doc.js';
|
|
6
|
+
import { furnaceChromeDocRemoveCommand } from './chrome-doc-remove.js';
|
|
6
7
|
import { furnaceCreateCommand } from './create.js';
|
|
7
8
|
import { furnaceDeployCommand } from './deploy.js';
|
|
8
9
|
import { furnaceDiffCommand } from './diff.js';
|
|
@@ -86,6 +87,10 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
86
87
|
.action(withErrorHandling(async (name, options) => {
|
|
87
88
|
await furnaceCreateCommand(getProjectRoot(), name, options);
|
|
88
89
|
}));
|
|
90
|
+
registerChromeDocCommands(furnace, context);
|
|
91
|
+
}
|
|
92
|
+
function registerChromeDocCommands(furnace, context) {
|
|
93
|
+
const { getProjectRoot, withErrorHandling } = context;
|
|
89
94
|
const chromeDoc = furnace
|
|
90
95
|
.command('chrome-doc')
|
|
91
96
|
.description('Scaffold top-level chrome documents (xhtml + js + css + ftl + jar.mn)');
|
|
@@ -94,9 +99,18 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
94
99
|
.description('Scaffold a new top-level chrome document')
|
|
95
100
|
.option('--no-titlebar', 'Frameless overlay-style document (omits titlebar-buttonbox)')
|
|
96
101
|
.option('--with-tests', 'Scaffold an xpcshell packaging-verification test that probes XCurProcD/chrome/browser/... directly (bypasses the xpcshell chrome:// URI limitation).')
|
|
102
|
+
.option('--dry-run', 'Show the chrome-doc scaffold plan without writing')
|
|
97
103
|
.action(withErrorHandling(async (name, options) => {
|
|
98
104
|
await furnaceChromeDocCreateCommand(getProjectRoot(), name, pickDefined(options));
|
|
99
105
|
}));
|
|
106
|
+
chromeDoc
|
|
107
|
+
.command('remove <name>')
|
|
108
|
+
.description('Remove a scaffolded top-level chrome document')
|
|
109
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
110
|
+
.option('--dry-run', 'Show the chrome-doc removal plan without writing')
|
|
111
|
+
.action(withErrorHandling(async (name, options) => {
|
|
112
|
+
await furnaceChromeDocRemoveCommand(getProjectRoot(), name, pickDefined(options));
|
|
113
|
+
}));
|
|
100
114
|
}
|
|
101
115
|
/**
|
|
102
116
|
* Registers Furnace commands for authoring, inspection, and maintenance:
|
|
@@ -493,6 +493,9 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
493
493
|
}
|
|
494
494
|
else if (freshType === 'override') {
|
|
495
495
|
freshConfig.overrides = Object.fromEntries(Object.entries(freshConfig.overrides).filter(([key]) => key !== name));
|
|
496
|
+
if (!freshConfig.stock.includes(name)) {
|
|
497
|
+
freshConfig.stock.push(name);
|
|
498
|
+
}
|
|
496
499
|
}
|
|
497
500
|
else {
|
|
498
501
|
freshConfig.custom = Object.fromEntries(Object.entries(freshConfig.custom).filter(([key]) => key !== name));
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { tagNameToClassName } from '../../core/furnace-constants.js';
|
|
3
|
+
/** Escapes regex metacharacters so a user-supplied name is literal inside a RegExp. */
|
|
4
|
+
function escapeRegex(input) {
|
|
5
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
6
|
+
}
|
|
7
|
+
function deriveTestStem(componentName, binaryName) {
|
|
8
|
+
const strippedName = componentName.startsWith('moz-') ? componentName.slice(4) : componentName;
|
|
9
|
+
const withoutBinaryPrefix = strippedName.startsWith(binaryName + '-')
|
|
10
|
+
? strippedName.slice(binaryName.length + 1)
|
|
11
|
+
: strippedName;
|
|
12
|
+
return withoutBinaryPrefix.replace(/-/g, '_');
|
|
13
|
+
}
|
|
14
|
+
/** Rewrites scaffolded browser-chrome test literals after a component rename. */
|
|
15
|
+
export function updateBrowserChromeTestContent(content, oldName, newName, binaryName) {
|
|
16
|
+
const oldClassName = tagNameToClassName(oldName);
|
|
17
|
+
const newClassName = tagNameToClassName(newName);
|
|
18
|
+
const oldUnderscored = oldName.replace(/-/g, '_');
|
|
19
|
+
const newUnderscored = newName.replace(/-/g, '_');
|
|
20
|
+
const oldTestStem = deriveTestStem(oldName, binaryName);
|
|
21
|
+
const newTestStem = deriveTestStem(newName, binaryName);
|
|
22
|
+
return content
|
|
23
|
+
.replace(new RegExp(escapeRegex(oldName), 'g'), newName)
|
|
24
|
+
.replace(new RegExp(escapeRegex(oldClassName), 'g'), newClassName)
|
|
25
|
+
.replace(new RegExp(escapeRegex(oldUnderscored), 'g'), newUnderscored)
|
|
26
|
+
.replace(new RegExp(escapeRegex(oldTestStem), 'g'), newTestStem);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=rename-browser-test.js.map
|
|
@@ -14,6 +14,7 @@ import { FurnaceError } from '../../errors/furnace.js';
|
|
|
14
14
|
import { toError } from '../../utils/errors.js';
|
|
15
15
|
import { copyFile, ensureDir, pathExists, readText, removeDir, removeFile, writeText, } from '../../utils/fs.js';
|
|
16
16
|
import { info, intro, note, outro, warn } from '../../utils/logger.js';
|
|
17
|
+
import { updateBrowserChromeTestContent } from './rename-browser-test.js';
|
|
17
18
|
import { renameComponentFileName, updateConfigForCustomRename, updateConfigForOverrideRename, } from './rename-helpers.js';
|
|
18
19
|
import { renameXpcshellTestFiles } from './rename-xpcshell.js';
|
|
19
20
|
/** Escapes regex metacharacters so a user-supplied name is literal inside a RegExp. */
|
|
@@ -58,7 +59,7 @@ async function renameTestFiles(engineDir, projectRoot, oldName, newName, journal
|
|
|
58
59
|
try {
|
|
59
60
|
await snapshotFile(journal, oldTestPath);
|
|
60
61
|
const content = await readText(oldTestPath);
|
|
61
|
-
await writeText(newTestPath, content);
|
|
62
|
+
await writeText(newTestPath, updateBrowserChromeTestContent(content, oldName, newName, binaryName));
|
|
62
63
|
await removeFile(oldTestPath);
|
|
63
64
|
info(`Renamed test file: ${oldTestFileName} → ${newTestFileName}`);
|
|
64
65
|
}
|
|
@@ -221,7 +221,7 @@ function filterFireForgeTempFiles(files) {
|
|
|
221
221
|
async function renderJsonStatus(files, paths, projectRoot, binaryName) {
|
|
222
222
|
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
223
223
|
const classified = await classifyFiles(files, paths.engine, paths.patches, binaryName, furnacePrefixes);
|
|
224
|
-
const
|
|
224
|
+
const outputFiles = classified.map((f) => {
|
|
225
225
|
const entry = {
|
|
226
226
|
file: f.file,
|
|
227
227
|
status: f.status.trim(),
|
|
@@ -236,6 +236,24 @@ async function renderJsonStatus(files, paths, projectRoot, binaryName) {
|
|
|
236
236
|
}
|
|
237
237
|
return entry;
|
|
238
238
|
});
|
|
239
|
+
const byClassification = {
|
|
240
|
+
unmanaged: 0,
|
|
241
|
+
'patch-backed': 0,
|
|
242
|
+
branding: 0,
|
|
243
|
+
furnace: 0,
|
|
244
|
+
conflict: 0,
|
|
245
|
+
};
|
|
246
|
+
for (const file of outputFiles) {
|
|
247
|
+
byClassification[file.classification]++;
|
|
248
|
+
}
|
|
249
|
+
const output = {
|
|
250
|
+
schemaVersion: 1,
|
|
251
|
+
summary: {
|
|
252
|
+
total: outputFiles.length,
|
|
253
|
+
byClassification,
|
|
254
|
+
},
|
|
255
|
+
files: outputFiles,
|
|
256
|
+
};
|
|
239
257
|
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
240
258
|
}
|
|
241
259
|
/**
|
|
@@ -267,7 +285,8 @@ async function assertEngineHasBaselineCommit(engineDir, options) {
|
|
|
267
285
|
// throw still falls through to the styled error renderer in
|
|
268
286
|
// withErrorHandling, leaving JSON consumers with non-JSON output on
|
|
269
287
|
// exactly the failure mode they care about catching.
|
|
270
|
-
process.stdout.write(JSON.stringify({ error: guidance, code: 'engine-baseline-missing' }) +
|
|
288
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 1, error: guidance, code: 'engine-baseline-missing' }) +
|
|
289
|
+
'\n');
|
|
271
290
|
}
|
|
272
291
|
throw new GeneralError(guidance);
|
|
273
292
|
}
|
|
@@ -307,7 +326,7 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
307
326
|
// `withErrorHandling` does not log: bin/fireforge.ts catches it,
|
|
308
327
|
// exits with the carried code, and stdout stays a single JSON line.
|
|
309
328
|
const emitJsonError = (code, message) => {
|
|
310
|
-
process.stdout.write(JSON.stringify({ error: message, code }) + '\n');
|
|
329
|
+
process.stdout.write(JSON.stringify({ schemaVersion: 1, error: message, code }) + '\n');
|
|
311
330
|
throw new CommandError(ExitCode.GENERAL_ERROR);
|
|
312
331
|
};
|
|
313
332
|
// Ownership mode is a flat file→patch table; sources are the manifest's
|
|
@@ -201,8 +201,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
201
201
|
// here so `test --doctor` against an incomplete build surfaces the
|
|
202
202
|
// missing-bundle path instead of a cryptic `Browser process exited
|
|
203
203
|
// during spawn (exit code 1, signal none). stderr tail: (empty)`.
|
|
204
|
+
let launchablePath;
|
|
204
205
|
if (buildCheck.objDir) {
|
|
205
206
|
const bundleCheck = await hasRunnableBundle(paths.engine, projectConfig.binaryName, buildCheck.objDir);
|
|
207
|
+
launchablePath = bundleCheck.expectedPath;
|
|
206
208
|
if (!bundleCheck.runnable) {
|
|
207
209
|
const expectedSuffix = bundleCheck.expectedPath
|
|
208
210
|
? ` (expected at engine/${bundleCheck.expectedPath})`
|
|
@@ -287,6 +289,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
287
289
|
// (`info`/`warn`) is retained so TTY users keep the visual framing.
|
|
288
290
|
const directLine = formatMarionettePreflightLine(preflight);
|
|
289
291
|
process.stdout.write(`${directLine}\n`);
|
|
292
|
+
process.stdout.write(`Marionette preflight environment: objdir=${buildCheck.objDir ?? '(none)'}; binary=${projectConfig.binaryName}; app=${launchablePath ? `engine/${launchablePath}` : '(unknown)'}; port=${effectivePort ?? 2828}; elapsed=${preflight.durationMs}ms\n`);
|
|
290
293
|
reportMarionettePreflight(preflight);
|
|
291
294
|
if (testPaths.length === 0) {
|
|
292
295
|
if (!preflight.ok) {
|
|
@@ -60,14 +60,21 @@ function buildWatchmanConfigureTimeMessage() {
|
|
|
60
60
|
* @param watchmanPath - Optional absolute path to the resolved watchman binary; surfaced in the guidance so the operator can see whether FireForge actually found one.
|
|
61
61
|
* @returns User-facing failure guidance
|
|
62
62
|
*/
|
|
63
|
-
function
|
|
63
|
+
function hasWatchPermissionFailure(output) {
|
|
64
|
+
return /Operation not permitted|EPERM|EACCES/i.test(output);
|
|
65
|
+
}
|
|
66
|
+
function buildUnsupportedWatchMessage(exitCode, watchmanPath, output = '') {
|
|
64
67
|
const watchmanLine = watchmanPath
|
|
65
68
|
? ` - FireForge resolved watchman at ${watchmanPath} and prepended its directory to the mach subprocess PATH. If mach still did not see it, ensure that path is stable between runs.\n`
|
|
66
69
|
: '';
|
|
70
|
+
const permissionLine = hasWatchPermissionFailure(output)
|
|
71
|
+
? ' - macOS may be blocking watchman or Terminal/Codex from reading the engine directory. Grant Full Disk Access or Files and Folders access to your terminal app and watchman, then restart watchman with "watchman shutdown-server".\n'
|
|
72
|
+
: '';
|
|
67
73
|
return (`Watch failed with exit code ${exitCode}. Check the output above for details.\n\n` +
|
|
68
74
|
'Common causes:\n' +
|
|
69
75
|
' - watchman is not installed or not in PATH right now\n' +
|
|
70
76
|
' - watchman was installed only after the current obj-* directory was configured; delete obj-* and rebuild\n' +
|
|
77
|
+
permissionLine +
|
|
71
78
|
' - mach watch is unsupported in the current objdir or build environment\n' +
|
|
72
79
|
watchmanLine +
|
|
73
80
|
'\n' +
|
|
@@ -194,7 +201,7 @@ export async function watchCommand(projectRoot) {
|
|
|
194
201
|
throw new GeneralError(buildWatchmanConfigureTimeMessage());
|
|
195
202
|
}
|
|
196
203
|
// 130 is SIGINT (Ctrl+C), which is expected
|
|
197
|
-
throw new BuildError(buildUnsupportedWatchMessage(result.exitCode, watchmanPath), 'mach watch');
|
|
204
|
+
throw new BuildError(buildUnsupportedWatchMessage(result.exitCode, watchmanPath, combinedOutput), 'mach watch');
|
|
198
205
|
}
|
|
199
206
|
outro('Watch mode stopped');
|
|
200
207
|
}
|
|
@@ -195,6 +195,9 @@ export function validateFurnaceConfig(data) {
|
|
|
195
195
|
if (migrated['tokenAllowlist'] !== undefined) {
|
|
196
196
|
config.tokenAllowlist = parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
197
197
|
}
|
|
198
|
+
if (migrated['platformPrefixes'] !== undefined) {
|
|
199
|
+
config.platformPrefixes = parseStringArray(migrated['platformPrefixes'], 'platformPrefixes');
|
|
200
|
+
}
|
|
198
201
|
if (migrated['runtimeVariables'] !== undefined) {
|
|
199
202
|
config.runtimeVariables = parseStringArray(migrated['runtimeVariables'], 'runtimeVariables');
|
|
200
203
|
}
|
|
@@ -242,6 +245,7 @@ const PENDING_REPAIR_OPERATIONS = [
|
|
|
242
245
|
'deploy-rollback',
|
|
243
246
|
'remove-rollback',
|
|
244
247
|
'create-rollback',
|
|
248
|
+
'chrome-doc-rollback',
|
|
245
249
|
'override-rollback',
|
|
246
250
|
'scan-rollback',
|
|
247
251
|
'rename-rollback',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hominis/fireforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "FireForge — a build tool for customizing Firefox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
71
71
|
"fast-check": "^4.6.0",
|
|
72
72
|
"husky": "^9.1.7",
|
|
73
|
-
"lint-staged": "^
|
|
73
|
+
"lint-staged": "^17.0.4",
|
|
74
74
|
"prettier": "^3.7.4",
|
|
75
75
|
"tsx": "^4.7.0",
|
|
76
76
|
"typescript": "~6.0.0",
|