@hominis/fireforge 0.18.2 → 0.18.5
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/README.md +29 -16
- package/dist/src/commands/build.js +27 -12
- package/dist/src/commands/config.js +56 -3
- package/dist/src/commands/discard.js +93 -1
- package/dist/src/commands/doctor.js +17 -4
- package/dist/src/commands/download.js +21 -0
- package/dist/src/commands/export-all.js +35 -6
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +59 -8
- package/dist/src/commands/furnace/chrome-doc-templates.js +95 -12
- package/dist/src/commands/furnace/chrome-doc.js +24 -2
- package/dist/src/commands/furnace/deploy.js +10 -1
- package/dist/src/commands/furnace/init.js +28 -2
- package/dist/src/commands/furnace/remove.js +68 -0
- package/dist/src/commands/import.js +9 -1
- package/dist/src/commands/lint.js +78 -13
- package/dist/src/commands/patch/delete.js +2 -4
- package/dist/src/commands/patch/lint-ignore.js +2 -4
- package/dist/src/commands/patch/reorder.js +2 -4
- package/dist/src/commands/patch/tier.js +2 -4
- package/dist/src/commands/status.js +39 -1
- package/dist/src/commands/test.js +20 -1
- package/dist/src/commands/token.js +1 -1
- package/dist/src/core/furnace-apply.js +11 -3
- package/dist/src/core/furnace-config.js +19 -0
- package/dist/src/core/furnace-marker.d.ts +16 -0
- package/dist/src/core/furnace-marker.js +23 -0
- package/dist/src/core/git.js +66 -10
- package/dist/src/core/license-headers.d.ts +8 -0
- package/dist/src/core/license-headers.js +15 -1
- package/dist/src/core/manifest-rules.js +9 -1
- package/dist/src/core/patch-identifier-suggest.d.ts +25 -0
- package/dist/src/core/patch-identifier-suggest.js +108 -0
- package/dist/src/core/patch-lint.js +8 -0
- package/dist/src/core/register-shared-css.d.ts +28 -0
- package/dist/src/core/register-shared-css.js +67 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,6 +143,9 @@ fireforge re-export --all --scan
|
|
|
143
143
|
# Preview what an export would do without writing
|
|
144
144
|
fireforge export browser/base/content/browser.js --dry-run
|
|
145
145
|
|
|
146
|
+
# Same preview surface for the aggregate path
|
|
147
|
+
fireforge export-all --name "all-changes" --category ui --dry-run
|
|
148
|
+
|
|
146
149
|
# Insert a new patch at a specific position
|
|
147
150
|
fireforge export browser/base/content/browser.js --order 3 --name "inserted" --category ui
|
|
148
151
|
fireforge export browser/base/content/browser.js --before 005-ui-sidebar.patch --name "prelim"
|
|
@@ -158,7 +161,7 @@ fireforge re-export --files browser/base/content/browser.js 002-ui-toolbar
|
|
|
158
161
|
fireforge re-export --all --scan --stamp
|
|
159
162
|
```
|
|
160
163
|
|
|
161
|
-
`export` refuses when the new patch's `filesAffected` would overlap with files already claimed by another non-superseded patch. Repartitioning ownership is a deliberate operation: the message points at `fireforge re-export --files <paths> <patch>` as the safe primitive. Pass `--allow-overlap` to acknowledge the conflict and proceed anyway — the resulting queue will fail `fireforge verify` immediately, so this is an intentional escape hatch, not a default.
|
|
164
|
+
`export` refuses when the new patch's `filesAffected` would overlap with files already claimed by another non-superseded patch. Repartitioning ownership is a deliberate operation: the message points at `fireforge re-export --files <paths> <patch>` as the safe primitive. Pass `--allow-overlap` to acknowledge the conflict and proceed anyway — the resulting queue will fail `fireforge verify` immediately, so this is an intentional escape hatch, not a default. The flag covers cross-patch _modification_ overlap, where two patches both edit the same file. It does NOT bypass the new-file creation guard: two patches creating the same path on `/dev/null` cannot coexist in any apply order, so that case stays a hard refusal regardless of `--allow-overlap`.
|
|
162
165
|
|
|
163
166
|
`re-export --scan` also prompts before broadening a patch with more than a handful of newly discovered files or with files spanning multiple directories. The gate keeps the common refresh case frictionless (small, same-directory additions) while catching the failure mode where `--scan` silently pulls an adjacent feature into the wrong patch. Non-interactive mode requires `--yes` to acknowledge a broad expansion; dry-run previews never require confirmation.
|
|
164
167
|
|
|
@@ -267,16 +270,16 @@ fireforge status --json # machine-readable classified output
|
|
|
267
270
|
|
|
268
271
|
Then fix with the appropriate primitive:
|
|
269
272
|
|
|
270
|
-
| Problem | Fix
|
|
271
|
-
| ---------------------------------------------- |
|
|
272
|
-
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files`
|
|
273
|
-
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>`
|
|
274
|
-
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>`
|
|
275
|
-
| Ordinal gaps after deletes/splits | `fireforge patch compact`
|
|
276
|
-
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>`
|
|
277
|
-
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest`
|
|
278
|
-
| Dangling widget / locale registration in patch | Re-run `fireforge export` without `--exclude-furnace` to capture the source files, or revert furnace changes
|
|
279
|
-
| Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset`
|
|
273
|
+
| Problem | Fix |
|
|
274
|
+
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
275
|
+
| Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files` |
|
|
276
|
+
| A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>` |
|
|
277
|
+
| Wrong patch ordering | `fireforge patch reorder <patch> --to <N>` |
|
|
278
|
+
| Ordinal gaps after deletes/splits | `fireforge patch compact` |
|
|
279
|
+
| A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>` |
|
|
280
|
+
| Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
|
|
281
|
+
| Dangling widget / locale registration in patch | Re-run `fireforge export` without `--exclude-furnace` to capture the source files, or revert furnace changes |
|
|
282
|
+
| Unmanaged changes you want to discard | `fireforge discard <file>` (also accepts a directory path to discard everything beneath it) or `fireforge reset` |
|
|
280
283
|
|
|
281
284
|
Every destructive command defaults to an interactive confirmation with a change summary. `--dry-run` previews without writing; `--yes` skips the prompt for CI; `--force-unsafe` bypasses structural refusals when you have context the linter cannot see. Do not hand-edit `patches.json` as the file is owned by FireForge — `doctor --repair-patches-manifest` reconstructs missing metadata, and `fireforge re-export <filename> --description "<text>"` overwrites recovered entries with operator-supplied metadata through the tool. `fireforge verify` cross-checks every registration hunk in each patch body against the files the queue and engine supply, so a patch that registers a widget / locale without carrying its source surfaces as a `dangling-registration` error rather than slipping through as "Verify clean"; `fireforge export-all --exclude-furnace` refuses up-front when it would produce that shape.
|
|
282
285
|
|
|
@@ -355,11 +358,11 @@ fireforge furnace chrome-doc create mybrowser --with-tests # + xpcshell packagin
|
|
|
355
358
|
|
|
356
359
|
The command writes:
|
|
357
360
|
|
|
358
|
-
- `engine/browser/base/content/<name>.xhtml` — XHTML shell
|
|
361
|
+
- `engine/browser/base/content/<name>.xhtml` — XHTML shell. `data-l10n-id` is bound on the leaf `<title>` only (binding it on the root `<window>` would let Fluent's first-paint translation pass overwrite the entire body subtree, the standard `data-l10n-id`-on-non-leaf failure mode). The `<head>` loads `chrome://global/content/customElements.js` ahead of the per-doc subscript so any `<moz-*>` widget the author drops into the body resolves through the toolkit registry instead of silently degrading to `HTMLUnknownElement` — matches the `webrtcIndicator.xhtml` shape upstream uses for non-`browser.xhtml` chrome documents. Under `--with-titlebar` (the default) the root carries the `navigator:browser` minimum attribute set: `windowtype="navigator:browser"`, `customtitlebar="true"`, default `width="1024"` / `height="640"`, and `persist="screenX screenY width height sizemode"` so XULStore remembers geometry across restarts; without these a fork shipping the scaffold verbatim opens at the OS intrinsic minimum size on first launch and forgets the user's window position.
|
|
359
362
|
- `engine/browser/base/content/<name>.js` — startup-topic observer fired on first idle.
|
|
360
|
-
- `engine/browser/themes/shared/<name>-chrome.css` — scoped CSS;
|
|
363
|
+
- `engine/browser/themes/shared/<name>-chrome.css` — scoped CSS. Under `--with-titlebar` the buttonbox container is a `-moz-window-dragging: drag` region and `.titlebar-buttonbox` opts into the platform-native `-moz-window-button-box` appearance so the OS renders traffic-light / minimize-maximize-close controls in their canonical positions; under `--no-titlebar` the macOS `.titlebar-button { display: none }` carve-out is emitted instead so frameless overlays don't inherit the platform window controls `global.css` applies by default.
|
|
361
364
|
- `engine/browser/locales/en-US/browser/<name>.ftl` — Fluent stub keyed on `<name>-window-title`.
|
|
362
|
-
- Appends the corresponding `jar.mn` / `jar.inc.mn` / `locales/jar.mn`
|
|
365
|
+
- 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.
|
|
363
366
|
- 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.
|
|
364
367
|
|
|
365
368
|
Writes are transactional: a SIGINT mid-scaffold rolls back every touched file. Requires an existing engine — run `fireforge download` first.
|
|
@@ -426,6 +429,8 @@ fireforge config customKey "value" --force
|
|
|
426
429
|
|
|
427
430
|
Writes are serialised behind a sidecar lock — two concurrent `fireforge config` invocations against the same `fireforge.json` (for example, parallel automation steps) queue instead of racing the read-modify-write. The lock is released automatically on process exit; stale locks from a crashed earlier command are reclaimed on the next invocation via the PID-alive probe.
|
|
428
431
|
|
|
432
|
+
Re-setting a key to its current value is a no-op: `fireforge.json` is not rewritten, key ordering is preserved, and the success log surfaces `<key> = <value> (unchanged)` instead of a fresh `Set …` line. This means automation that idempotently runs `fireforge config <key> <value>` no longer produces spurious diffs in `fireforge.json`.
|
|
433
|
+
|
|
429
434
|
### Patch queue management
|
|
430
435
|
|
|
431
436
|
```bash
|
|
@@ -466,10 +471,14 @@ fireforge package
|
|
|
466
471
|
fireforge watch
|
|
467
472
|
|
|
468
473
|
# Add a CSS design token (requires `fireforge furnace init` first; see the Furnace/Tokens section below)
|
|
469
|
-
|
|
474
|
+
# The `--` separator is required because the token name itself starts with `--`,
|
|
475
|
+
# which Commander would otherwise read as an option flag. Bare names without `--`
|
|
476
|
+
# are accepted directly and get the configured `tokenPrefix` prepended.
|
|
477
|
+
fireforge token add --category 'Colors — General' --mode static -- --my-color 'light-dark(#fff, #000)'
|
|
478
|
+
fireforge token add --category 'Colors — General' --mode static my-color '#fff' # bare-name form
|
|
470
479
|
```
|
|
471
480
|
|
|
472
|
-
Tokens live in the Furnace-managed tokens CSS file (`engine/browser/themes/shared/<binaryName>-tokens.css`), scaffolded by `fireforge furnace init` alongside `furnace.json`. The scaffold seeds a default set of categories (`Colors — General`, `Colors — Canvas`, `Colors — Experiment`, `Spacing`); add a category by hand as a `/* = My Category = */` comment inside the `:root { … }` block if you need another. `fireforge furnace init`
|
|
481
|
+
Tokens live in the Furnace-managed tokens CSS file (`engine/browser/themes/shared/<binaryName>-tokens.css`), scaffolded by `fireforge furnace init` alongside `furnace.json`. The scaffold seeds a default set of categories (`Colors — General`, `Colors — Canvas`, `Colors — Experiment`, `Spacing`); add a category by hand as a `/* = My Category = */` comment inside the `:root { … }` block if you need another. `fireforge furnace init` does three more things in the same step so the file is owned end-to-end by tooling: it registers the tokens CSS path in `patchLint.rawColorAllowlist` (so raw color literals inside it are not flagged by `fireforge lint`); it adds the matching `skin/classic/browser/<binaryName>-tokens.css (../shared/<binaryName>-tokens.css)` entry to `browser/themes/shared/jar.inc.mn` (so `fireforge status` does not flag the file as unmanaged or unregistered); and it derives `tokenPrefix: --<binaryName>-` from `fireforge.json`'s `binaryName` so `fireforge token coverage` has a prefix to key off on the very first run. Projects that prefer a different prefix can override it in `furnace.json` after init.
|
|
473
482
|
|
|
474
483
|
### Diff-scoped lint (`lint --since`)
|
|
475
484
|
|
|
@@ -511,6 +520,10 @@ The build also auto-runs `mach configure` before the mach build step when any `m
|
|
|
511
520
|
|
|
512
521
|
Mach build failures with known-cryptic mozbuild errors now print actionable hints. Example: a `JS_PREFERENCE_PP_FILES` entry with no `#filter` / `#expand` directives now prints `Hint: ...use JS_PREFERENCE_FILES instead, or add at least one #filter / #expand directive to the file.` alongside the raw mach traceback.
|
|
513
522
|
|
|
523
|
+
When mach prints a post-build `config.status is out of date …` or `Config object not found by mach. / Configure complete!` banner on a successful build, FireForge surfaces a one-line annotation immediately before its own `Build completed in Xm Ys!` outro explaining that the banner is a known side effect of tool-managed branding edits applied before the build and that the build does not need to be re-run. The FireForge exit code remains authoritative regardless of the mach guard text.
|
|
524
|
+
|
|
525
|
+
The reported `Build completed in Xm Ys!` duration is wall-clock measured with `Date.now()`, so it includes any time the host spent suspended (laptop sleep, system idle) during the build. Treat it as wall-clock-with-sleep, not active CPU time, when comparing builds across machines or sessions.
|
|
526
|
+
|
|
514
527
|
### Relocated workspaces: `fireforge build --rewrite-mozinfo`
|
|
515
528
|
|
|
516
529
|
When a workspace is moved to a new path (e.g. the project directory was renamed or relocated on disk), `obj-*/mozinfo.json` still records the old `topsrcdir` / `topobjdir`. The pre-flight detects the mismatch and aborts with a "delete and rebuild" instruction — correct but expensive; a fresh clean build typically runs ~20 minutes and discards ~14 GB of intact obj artefacts on a moved checkout.
|
|
@@ -157,18 +157,33 @@ export async function buildCommand(projectRoot, options) {
|
|
|
157
157
|
throw new BuildError(`Build failed with exit code ${result.exitCode}`, options.ui ? 'mach build faster' : 'mach build');
|
|
158
158
|
}
|
|
159
159
|
// Tool-managed branding edits that land on `browser/moz.configure`
|
|
160
|
-
// before the build cause mach's post-build guard to print
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
160
|
+
// before the build cause mach's post-build guard to print one of two
|
|
161
|
+
// banners that read like build failures even though the build
|
|
162
|
+
// completed cleanly:
|
|
163
|
+
//
|
|
164
|
+
// 1) "config.status is out of date with respect to ..."
|
|
165
|
+
// 2) "Config object not found by mach. / Configure complete! /
|
|
166
|
+
// Be sure to run |mach build| to pick up any changes."
|
|
167
|
+
//
|
|
168
|
+
// 2026-04-21 eval covered (1); 2026-04-26 eval Finding 8 reproduced
|
|
169
|
+
// (2) on a successful build. The pre-fix pattern only matched (1),
|
|
170
|
+
// so operators on the (2) path saw mach's own "Configure complete!"
|
|
171
|
+
// and "run |mach build|" lines unexplained between mach's
|
|
172
|
+
// "Your build was successful!" and FireForge's own "Build completed
|
|
173
|
+
// in Xm Ys" outro — a contradictory tail. Both shapes now route
|
|
174
|
+
// through the same annotation, emitted BEFORE FireForge's outro so
|
|
175
|
+
// the operator's last terminal line is the explanation, not the
|
|
176
|
+
// confusing mach guard text.
|
|
177
|
+
const staleConfigurePatterns = [
|
|
178
|
+
/config\.status is out of date/i,
|
|
179
|
+
/Config object not found by mach\.[\s\S]*Configure complete!/i,
|
|
180
|
+
];
|
|
181
|
+
const captured = `${result.stdout}\n${result.stderr}`;
|
|
182
|
+
if (staleConfigurePatterns.some((p) => p.test(captured))) {
|
|
183
|
+
info('Note: mach printed a post-build "Configure complete!" / "config.status is out of date" ' +
|
|
184
|
+
'banner. That is a known side effect of tool-managed branding edits applied before the ' +
|
|
185
|
+
'build and does not mean the build is stale or that you need to rerun mach — the FireForge ' +
|
|
186
|
+
'exit code is authoritative.');
|
|
172
187
|
}
|
|
173
188
|
// Warn-only post-build audit: surfaces silent packaging drops (files
|
|
174
189
|
// edited in engine/ but never registered for packaging) against the
|
|
@@ -111,6 +111,7 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
111
111
|
}
|
|
112
112
|
const parsedValue = parseValue(value, key);
|
|
113
113
|
const keyIsKnown = SUPPORTED_CONFIG_PATHS.includes(key);
|
|
114
|
+
let unchanged;
|
|
114
115
|
try {
|
|
115
116
|
// Serialise the read → mutate → write round-trip behind the sidecar
|
|
116
117
|
// config lock so two concurrent `fireforge config` invocations can't
|
|
@@ -122,7 +123,21 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
122
123
|
// were never enough on their own — the lost update happens before
|
|
123
124
|
// the rename, inside the read-modify step. Readers stay lock-free
|
|
124
125
|
// (see `withConfigFileLock` docstring).
|
|
125
|
-
await withConfigFileLock(projectRoot, async () => {
|
|
126
|
+
unchanged = await withConfigFileLock(projectRoot, async () => {
|
|
127
|
+
// 2026-04-26 eval Finding 11: short-circuit when the new value
|
|
128
|
+
// matches the current on-disk value. Pre-fix, every set ran
|
|
129
|
+
// through `mutateConfig` + `writeConfig`, which round-trips
|
|
130
|
+
// through `JSON.stringify` and rewrites the file even when no
|
|
131
|
+
// semantic change happened — the rewrite reorders top-level
|
|
132
|
+
// keys (`license`, `markerComment`, etc.) on every harmless
|
|
133
|
+
// re-set, producing diff churn for no reason. The check uses
|
|
134
|
+
// the raw on-disk document so forced-keys round-trip the same
|
|
135
|
+
// as known keys.
|
|
136
|
+
const rawConfig = await loadRawConfigDocument(projectRoot);
|
|
137
|
+
const currentValue = getNestedValue(rawConfig, key);
|
|
138
|
+
if (deepEqual(currentValue, parsedValue)) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
126
141
|
// `--force` is intended as an escape hatch for *unknown* keys; it
|
|
127
142
|
// should not also let the user write a structurally invalid value
|
|
128
143
|
// for a *known* key. Apply strict validation whenever the key is
|
|
@@ -133,7 +148,6 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
133
148
|
// keys (which `validateConfig` would strip) survive the round-trip.
|
|
134
149
|
// Without this, writing a second --force key would silently drop
|
|
135
150
|
// every earlier forced key from fireforge.json.
|
|
136
|
-
const rawConfig = await loadRawConfigDocument(projectRoot);
|
|
137
151
|
const updatedConfig = mutateConfig(rawConfig, key, parsedValue, true);
|
|
138
152
|
await writeConfigDocument(projectRoot, updatedConfig);
|
|
139
153
|
}
|
|
@@ -142,15 +156,54 @@ export async function configCommand(projectRoot, key, value, options = {}) {
|
|
|
142
156
|
const updatedConfig = mutateConfig(config, key, parsedValue);
|
|
143
157
|
await writeConfig(projectRoot, updatedConfig);
|
|
144
158
|
}
|
|
159
|
+
return false;
|
|
145
160
|
});
|
|
146
161
|
}
|
|
147
162
|
catch (error) {
|
|
148
163
|
throw new InvalidArgumentError(`Invalid value for "${key}": ${toError(error).message}`, key);
|
|
149
164
|
}
|
|
150
|
-
|
|
165
|
+
if (unchanged) {
|
|
166
|
+
info(`${key} = ${formatValue(parsedValue)} (unchanged)`);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
success(`Set ${key} = ${formatValue(parsedValue)}`);
|
|
170
|
+
}
|
|
151
171
|
}
|
|
152
172
|
outro('');
|
|
153
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Structural equality check covering the shapes that
|
|
176
|
+
* `fireforge config` accepts: primitives (strings, numbers, booleans),
|
|
177
|
+
* `null`, arrays of primitives, and nested objects. Used to short-circuit
|
|
178
|
+
* no-op writes (Finding 11) — when the parsed value matches the current
|
|
179
|
+
* on-disk value, skip the mutate + write step entirely.
|
|
180
|
+
*/
|
|
181
|
+
function deepEqual(a, b) {
|
|
182
|
+
if (a === b)
|
|
183
|
+
return true;
|
|
184
|
+
if (a === null || b === null)
|
|
185
|
+
return a === b;
|
|
186
|
+
if (typeof a !== typeof b)
|
|
187
|
+
return false;
|
|
188
|
+
if (typeof a !== 'object')
|
|
189
|
+
return false;
|
|
190
|
+
if (Array.isArray(a)) {
|
|
191
|
+
if (!Array.isArray(b))
|
|
192
|
+
return false;
|
|
193
|
+
if (a.length !== b.length)
|
|
194
|
+
return false;
|
|
195
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
196
|
+
}
|
|
197
|
+
if (Array.isArray(b))
|
|
198
|
+
return false;
|
|
199
|
+
const ar = a;
|
|
200
|
+
const br = b;
|
|
201
|
+
const keysA = Object.keys(ar);
|
|
202
|
+
const keysB = Object.keys(br);
|
|
203
|
+
if (keysA.length !== keysB.length)
|
|
204
|
+
return false;
|
|
205
|
+
return keysA.every((k) => deepEqual(ar[k], br[k]));
|
|
206
|
+
}
|
|
154
207
|
/** Registers the config command on the CLI program. */
|
|
155
208
|
export function registerConfig(program, { getProjectRoot, withErrorHandling }) {
|
|
156
209
|
program
|
|
@@ -11,6 +11,82 @@ import { toError } from '../utils/errors.js';
|
|
|
11
11
|
import { pathExists } from '../utils/fs.js';
|
|
12
12
|
import { info, intro, isCancel, outro, spinner, warn } from '../utils/logger.js';
|
|
13
13
|
import { pickDefined } from '../utils/options.js';
|
|
14
|
+
/**
|
|
15
|
+
* Discards every status entry whose path lives under `dirPath`. Used by
|
|
16
|
+
* `discardCommand` as a directory-recursion fallback when the operator
|
|
17
|
+
* passed a directory path that contains modified or untracked entries
|
|
18
|
+
* but is not itself a status entry.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the single-file path's confirmation, dry-run, and Furnace-aware
|
|
21
|
+
* warning behaviour so the contract stays consistent. Each per-entry
|
|
22
|
+
* discard runs sequentially under its own try/catch so a failure on one
|
|
23
|
+
* file is reported but does not block the remaining files in the batch.
|
|
24
|
+
*/
|
|
25
|
+
async function discardDirectoryEntries(projectRoot, engineDir, dirPath, entries, options) {
|
|
26
|
+
if (!options.yes && !options.dryRun) {
|
|
27
|
+
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
28
|
+
if (!isInteractive) {
|
|
29
|
+
throw new InvalidArgumentError('Interactive confirmation not available. Use --yes flag to discard without confirmation.', 'Use: fireforge discard <directory> --yes');
|
|
30
|
+
}
|
|
31
|
+
const confirmed = await confirm({
|
|
32
|
+
message: `Discard changes to ${entries.length} file${entries.length === 1 ? '' : 's'} under ${dirPath}/?`,
|
|
33
|
+
initialValue: false,
|
|
34
|
+
});
|
|
35
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
36
|
+
outro('Discard cancelled');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (options.dryRun) {
|
|
41
|
+
info(`Would discard changes to ${entries.length} file(s) under ${dirPath}/:`);
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const target = entry.originalPath && entry.originalPath !== entry.file
|
|
44
|
+
? `${entry.originalPath} -> ${entry.file}`
|
|
45
|
+
: entry.file;
|
|
46
|
+
info(` ${target}`);
|
|
47
|
+
}
|
|
48
|
+
outro('Dry run complete — no changes made');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const s = spinner(`Discarding ${entries.length} file(s) under ${dirPath}/...`);
|
|
52
|
+
let succeeded = 0;
|
|
53
|
+
const failures = [];
|
|
54
|
+
try {
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
try {
|
|
57
|
+
await discardStatusEntry(engineDir, entry);
|
|
58
|
+
succeeded += 1;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
failures.push(`${entry.file}: ${toError(error).message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
s.stop(`Discarded ${succeeded} of ${entries.length} file(s) under ${dirPath}/${failures.length > 0 ? ` (${failures.length} failed)` : ''}`);
|
|
65
|
+
for (const failure of failures) {
|
|
66
|
+
warn(` ${failure}`);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
70
|
+
const dirIsFurnace = [...furnacePrefixes].some((prefix) => `${dirPath}/`.startsWith(prefix) || prefix.startsWith(`${dirPath}/`));
|
|
71
|
+
if (dirIsFurnace) {
|
|
72
|
+
warn('These paths are managed by Furnace. Run "fireforge furnace apply" to redeploy components if needed.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Furnace config may not exist — skip silently
|
|
77
|
+
}
|
|
78
|
+
if (failures.length > 0) {
|
|
79
|
+
throw new GeneralError(`Failed to discard ${failures.length} file(s) under ${dirPath}/. See warnings above.`);
|
|
80
|
+
}
|
|
81
|
+
outro(`${succeeded} file(s) restored to original state`);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (!(error instanceof GeneralError)) {
|
|
85
|
+
s.error('Discard failed');
|
|
86
|
+
}
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
14
90
|
/**
|
|
15
91
|
* Runs the discard command to revert changes to a specific file.
|
|
16
92
|
* @param projectRoot - Root directory of the project
|
|
@@ -31,7 +107,23 @@ export async function discardCommand(projectRoot, file, options = {}) {
|
|
|
31
107
|
// Check if the file has changes
|
|
32
108
|
const statusEntries = await expandUntrackedDirectoryEntries(paths.engine, await getWorkingTreeStatus(paths.engine));
|
|
33
109
|
const statusEntry = statusEntries.find((entry) => entry.file === file || entry.originalPath === file);
|
|
110
|
+
// Directory recursion fallback: when the explicit path does not match a
|
|
111
|
+
// single status entry but DOES correspond to one or more entries below
|
|
112
|
+
// it, treat the input as a directory and discard everything inside.
|
|
113
|
+
// 2026-04-25 eval Finding 20: `discard browser/components/storybook/
|
|
114
|
+
// stories/furnace --yes` failed with "no changes to discard" even
|
|
115
|
+
// though `status --unmanaged` listed 23 files under that directory —
|
|
116
|
+
// operators were forced to discard each file individually or fall
|
|
117
|
+
// back to non-FireForge cleanup commands. Match against the
|
|
118
|
+
// directory-with-trailing-slash form so a path like `foo/bar` doesn't
|
|
119
|
+
// accidentally match `foo/bar2/file`.
|
|
34
120
|
if (!statusEntry) {
|
|
121
|
+
const dirPrefix = file.endsWith('/') ? file : `${file}/`;
|
|
122
|
+
const dirEntries = statusEntries.filter((entry) => entry.file.startsWith(dirPrefix) || entry.originalPath?.startsWith(dirPrefix));
|
|
123
|
+
if (dirEntries.length > 0) {
|
|
124
|
+
await discardDirectoryEntries(projectRoot, paths.engine, file, dirEntries, options);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
35
127
|
throw new GeneralError(`File "${file}" has no changes to discard.`);
|
|
36
128
|
}
|
|
37
129
|
if (!options.yes && !options.dryRun) {
|
|
@@ -91,7 +183,7 @@ export async function discardCommand(projectRoot, file, options = {}) {
|
|
|
91
183
|
export function registerDiscard(program, { getProjectRoot, withErrorHandling }) {
|
|
92
184
|
program
|
|
93
185
|
.command('discard <file>')
|
|
94
|
-
.description('Discard changes to a specific file (deletes untracked files)')
|
|
186
|
+
.description('Discard changes to a specific file (deletes untracked files). Pass a directory path to discard every modified or untracked file beneath it; the operation walks the status output and reverts each match individually.')
|
|
95
187
|
.option('--dry-run', 'Show what would be discarded without doing it')
|
|
96
188
|
.option('-y, --yes', 'Skip confirmation prompt')
|
|
97
189
|
.action(withErrorHandling(async (file, options) => {
|
|
@@ -9,7 +9,7 @@ import { ExitCode } from '../errors/codes.js';
|
|
|
9
9
|
import { toError } from '../utils/errors.js';
|
|
10
10
|
import { pathExists } from '../utils/fs.js';
|
|
11
11
|
import { error, info, intro, outro, success, warn } from '../utils/logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import { findExecutable } from '../utils/process.js';
|
|
13
13
|
import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
|
|
14
14
|
import { inspectEngineWorkingTree } from './doctor-working-tree.js';
|
|
15
15
|
/**
|
|
@@ -255,9 +255,22 @@ const DOCTOR_CHECKS = [
|
|
|
255
255
|
// failure site.
|
|
256
256
|
name: 'Watchman available',
|
|
257
257
|
run: async () => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
// Resolve the absolute path so the OK row names what doctor actually
|
|
259
|
+
// found. The 2026-04-25 eval flagged a confusing case where the
|
|
260
|
+
// operator's interactive shell returned no result for `which
|
|
261
|
+
// watchman` but doctor still printed "OK" — the cause was a
|
|
262
|
+
// PATH-export discrepancy between the shell and the spawned
|
|
263
|
+
// subprocess, and surfacing the resolved path makes the discrepancy
|
|
264
|
+
// visible without users having to re-run with a verbose flag.
|
|
265
|
+
const path = await findExecutable('watchman');
|
|
266
|
+
if (path) {
|
|
267
|
+
return {
|
|
268
|
+
name: 'Watchman available',
|
|
269
|
+
passed: true,
|
|
270
|
+
severity: 'ok',
|
|
271
|
+
message: `OK (${path})`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
261
274
|
return warning('Watchman available', 'watchman is not installed or not on PATH. "fireforge watch" requires it.', 'Install watchman (brew install watchman / dnf install watchman / https://facebook.github.io/watchman/), then re-run doctor.');
|
|
262
275
|
},
|
|
263
276
|
},
|
|
@@ -69,6 +69,25 @@ async function cleanPatchTouchedFiles(engineDir, patchesDir, preExistingDirty) {
|
|
|
69
69
|
}
|
|
70
70
|
return { hadQueue: true, restored: toClean.length, preserved: preserved.length };
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Prints a one-line nudge pointing at `fireforge import` when the project
|
|
74
|
+
* carries a non-empty patch queue but the just-downloaded engine has not
|
|
75
|
+
* yet had any patches applied. The post-download spinner closes with
|
|
76
|
+
* "Patch-touched files already match baseline" because a fresh tree IS at
|
|
77
|
+
* baseline, but the 2026-04-25 eval saw operators read that as "patches
|
|
78
|
+
* are restored" and skip the import step. The note is suppressed when
|
|
79
|
+
* patches/ is missing or the manifest is empty so unconfigured projects
|
|
80
|
+
* stay quiet.
|
|
81
|
+
*/
|
|
82
|
+
async function noteUnappliedPatches(patchesDir) {
|
|
83
|
+
if (!(await pathExists(patchesDir)))
|
|
84
|
+
return;
|
|
85
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
86
|
+
if (!manifest || manifest.patches.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
const n = manifest.patches.length;
|
|
89
|
+
info(`Note: ${n} patch${n === 1 ? '' : 'es'} in patches/ have not been applied to this fresh engine. Run "fireforge import" to apply them.`);
|
|
90
|
+
}
|
|
72
91
|
/**
|
|
73
92
|
* Stops `restoreSpinner` with a message that reflects what actually
|
|
74
93
|
* happened. Three branches: empty queue → explicit no-op; queue present but
|
|
@@ -151,6 +170,7 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
151
170
|
downloadedVersion: version,
|
|
152
171
|
baseCommit,
|
|
153
172
|
});
|
|
173
|
+
await noteUnappliedPatches(paths.patches);
|
|
154
174
|
outro(`Firefox ${version} is ready! (resumed from partial init)`);
|
|
155
175
|
return;
|
|
156
176
|
}
|
|
@@ -299,6 +319,7 @@ export async function downloadCommand(projectRoot, options) {
|
|
|
299
319
|
downloadedVersion: version,
|
|
300
320
|
baseCommit,
|
|
301
321
|
});
|
|
322
|
+
await noteUnappliedPatches(paths.patches);
|
|
302
323
|
outro(`Firefox ${version} is ready!`);
|
|
303
324
|
}
|
|
304
325
|
/** Registers the download command on the CLI program. */
|
|
@@ -15,6 +15,7 @@ import { ensureDir, pathExists } from '../utils/fs.js';
|
|
|
15
15
|
import { info, intro, outro, spinner } from '../utils/logger.js';
|
|
16
16
|
import { pickDefined } from '../utils/options.js';
|
|
17
17
|
import { PATCH_CATEGORIES } from '../utils/validation.js';
|
|
18
|
+
import { renderDryRunPreview } from './export-flow.js';
|
|
18
19
|
import { autoFixLicenseHeaders, confirmSupersedePatches, guardOwnershipOverlap, promptExportPatchMetadata, runPatchLint, } from './export-shared.js';
|
|
19
20
|
async function checkBrandingManagedFiles(paths, config) {
|
|
20
21
|
const changedFiles = await getWorkingTreeStatus(paths.engine);
|
|
@@ -158,7 +159,8 @@ async function checkDuplicateNewFileCreations(paths, diff) {
|
|
|
158
159
|
.join('\n');
|
|
159
160
|
throw new GeneralError('Export-all refuses to capture new-file creations that are already claimed by existing patches.\n\n' +
|
|
160
161
|
`Conflicting creations:\n${conflictList}\n\n` +
|
|
161
|
-
'Only one patch may create a given path
|
|
162
|
+
'Only one patch may create a given path — two creation hunks on /dev/null cannot coexist in any apply order, so this case is structurally unrecoverable rather than verify-failing. The --allow-overlap escape hatch covers cross-patch MODIFICATION overlap (which yields a queue that fails verify but still applies); it deliberately does NOT cover this case. ' +
|
|
163
|
+
'Run "fireforge export <path> [...]" with an explicit file list that omits the already-claimed path(s), or resolve the conflict via "fireforge patch delete" / "fireforge re-export --files" before retrying export-all.');
|
|
162
164
|
}
|
|
163
165
|
/**
|
|
164
166
|
* Runs the export-all command to export all changes as a patch.
|
|
@@ -166,7 +168,8 @@ async function checkDuplicateNewFileCreations(paths, diff) {
|
|
|
166
168
|
* @param options - Export options
|
|
167
169
|
*/
|
|
168
170
|
export async function exportAllCommand(projectRoot, options = {}) {
|
|
169
|
-
|
|
171
|
+
const isDryRun = options.dryRun === true;
|
|
172
|
+
intro(isDryRun ? 'FireForge Export All (dry run)' : 'FireForge Export All');
|
|
170
173
|
const paths = getProjectPaths(projectRoot);
|
|
171
174
|
// Check if engine exists
|
|
172
175
|
if (!(await pathExists(paths.engine))) {
|
|
@@ -236,13 +239,38 @@ export async function exportAllCommand(projectRoot, options = {}) {
|
|
|
236
239
|
if (!metadata)
|
|
237
240
|
return;
|
|
238
241
|
const { patchName, selectedCategory, description } = metadata;
|
|
239
|
-
// Ensure patches directory exists
|
|
240
|
-
|
|
241
|
-
|
|
242
|
+
// Ensure patches directory exists. Skip during a dry-run so the command
|
|
243
|
+
// is purely read-only — `--dry-run` callers should be safe to invoke
|
|
244
|
+
// against a project that has never exported a patch without leaving the
|
|
245
|
+
// empty `patches/` directory behind.
|
|
246
|
+
if (!isDryRun) {
|
|
247
|
+
await ensureDir(paths.patches);
|
|
248
|
+
}
|
|
249
|
+
const s = spinner(isDryRun ? 'Planning export-all...' : 'Exporting all changes...');
|
|
242
250
|
try {
|
|
243
251
|
// Extract affected files from diff
|
|
244
252
|
const filesAffected = extractAffectedFiles(diff);
|
|
245
253
|
await runPatchLint(paths.engine, filesAffected, diff, config, options.skipLint);
|
|
254
|
+
// Dry-run: enumerate filename, metadata, and supersede coverage without
|
|
255
|
+
// writing. Mirrors `fireforge export --dry-run` so the same preview
|
|
256
|
+
// surface is available for both targeted and aggregate exports. Runs
|
|
257
|
+
// AFTER lint so the operator sees the same lint output they would on
|
|
258
|
+
// a real run; runs BEFORE the supersede confirmation prompt because
|
|
259
|
+
// confirming a dry-run is meaningless.
|
|
260
|
+
if (isDryRun) {
|
|
261
|
+
s.stop('Plan ready');
|
|
262
|
+
await renderDryRunPreview({
|
|
263
|
+
patchesDir: paths.patches,
|
|
264
|
+
category: selectedCategory,
|
|
265
|
+
name: patchName,
|
|
266
|
+
description,
|
|
267
|
+
filesAffected,
|
|
268
|
+
sourceEsrVersion: config.firefox.version,
|
|
269
|
+
explicitSupersede: options.supersede === true,
|
|
270
|
+
});
|
|
271
|
+
outro('Dry run complete — no changes made');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
246
274
|
// Check how many existing patches would be superseded
|
|
247
275
|
const shouldProceed = await confirmSupersedePatches(paths.patches, filesAffected, options.supersede, isInteractive, s);
|
|
248
276
|
if (!shouldProceed)
|
|
@@ -299,7 +327,8 @@ export function registerExportAll(program, { getProjectRoot, withErrorHandling }
|
|
|
299
327
|
.option('--supersede', 'Allow superseding multiple existing patches')
|
|
300
328
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
301
329
|
.option('--exclude-furnace', 'Export the non-Furnace subset of the aggregate diff instead of refusing when Furnace-managed files are modified. Furnace-managed files are still deployed by "fireforge furnace apply"; this flag only changes whether export-all aborts or filters in their presence.')
|
|
302
|
-
.option('--allow-overlap', 'Acknowledge cross-patch ownership overlap with non-superseded patches (the resulting queue fails verify)')
|
|
330
|
+
.option('--allow-overlap', 'Acknowledge cross-patch ownership overlap with non-superseded patches (the resulting queue fails verify). Does not bypass the new-file creation guard — two patches creating the same path is structurally unrecoverable, so that case still refuses regardless of this flag.')
|
|
331
|
+
.option('--dry-run', 'Print the export-all plan (filename, metadata, files affected, supersede preview) without writing anything to patches/. Lint still runs so the operator sees the same lint output a real run would produce.')
|
|
303
332
|
.action(withErrorHandling(async (options) => {
|
|
304
333
|
const { category, ...rest } = options;
|
|
305
334
|
await exportAllCommand(getProjectRoot(), {
|
|
@@ -24,12 +24,29 @@ export declare const FURNACE_CHROME_DOC_SENTINEL = "data-furnace-chrome-doc";
|
|
|
24
24
|
* XHTML shell for a top-level chrome document.
|
|
25
25
|
*
|
|
26
26
|
* The emitted document:
|
|
27
|
-
* -
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* - When `withTitlebar` is true, declares the `navigator:browser` minimum
|
|
28
|
+
* set: `windowtype`, `customtitlebar`, default `width`/`height`, and a
|
|
29
|
+
* `persist` allowlist for screen position + size + sizemode. Without
|
|
30
|
+
* these, a fork-owned chrome doc that ships as the main window opens
|
|
31
|
+
* at the OS intrinsic minimum size on first launch and forgets the
|
|
32
|
+
* user's last-known geometry across restarts. The titlebar-buttonbox
|
|
33
|
+
* placeholder is emitted alongside so platform-native window controls
|
|
34
|
+
* render with the matching CSS rules from `generateChromeDocCss`.
|
|
35
|
+
* - Loads `chrome://global/content/customElements.js` in `<head>` ahead
|
|
36
|
+
* of the per-doc subscript. Without it, every `<moz-*>` widget the
|
|
37
|
+
* author drops into the body silently degrades to `HTMLUnknownElement`
|
|
38
|
+
* and the upstream a11y/keyboard semantics that motivated the use of
|
|
39
|
+
* the toolkit widget in the first place are lost. Matches the
|
|
40
|
+
* `webrtcIndicator.xhtml` shape upstream uses for non-`browser.xhtml`
|
|
41
|
+
* chrome documents.
|
|
31
42
|
* - Links the per-document CSS at `chrome://browser/content/<name>-chrome.css`
|
|
32
43
|
* and the Fluent bundle `browser/<name>.ftl`.
|
|
44
|
+
* - Keeps `data-l10n-id` on the leaf `<title>` only. Binding the same key
|
|
45
|
+
* on the root `<window>` would cause Fluent's first-paint translation
|
|
46
|
+
* pass to overwrite the entire body subtree with the message's text
|
|
47
|
+
* value (the standard `data-l10n-id`-on-non-leaf failure mode), since
|
|
48
|
+
* the FTL stub gives `<name>-window-title` a value rather than an
|
|
49
|
+
* attribute-only message.
|
|
33
50
|
* - Carries the `data-furnace-chrome-doc="<name>"` sentinel so fork-side
|
|
34
51
|
* patches to upstream platform modules (DevToolsStartup, PageActions, …)
|
|
35
52
|
* that assume `browser.xhtml`'s DOM can guard against it cheaply. See
|
|
@@ -46,10 +63,21 @@ export declare function generateChromeDocXhtml(name: string, withTitlebar: boole
|
|
|
46
63
|
*/
|
|
47
64
|
export declare function generateChromeDocJs(name: string, licenseHeader: string): string;
|
|
48
65
|
/**
|
|
49
|
-
* Scoped CSS for a chrome document.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
66
|
+
* Scoped CSS for a chrome document.
|
|
67
|
+
*
|
|
68
|
+
* When `withTitlebar` is true, the matching navigator:browser minimum
|
|
69
|
+
* CSS is emitted alongside the layout rules: the buttonbox container is
|
|
70
|
+
* a draggable region (`-moz-window-dragging: drag`) so the user can drag
|
|
71
|
+
* the window from the title bar, and the buttonbox itself opts into the
|
|
72
|
+
* platform-native window-button-box appearance so the OS renders the
|
|
73
|
+
* traffic-light / minimize-maximize-close controls in their canonical
|
|
74
|
+
* positions. Without these rules the buttonbox markup still draws but
|
|
75
|
+
* is unstyled and non-draggable, which is the failure mode a fork that
|
|
76
|
+
* ships the scaffold verbatim hits on first launch.
|
|
77
|
+
*
|
|
78
|
+
* When `withTitlebar` is false the macOS `.titlebar-button { display: none }`
|
|
79
|
+
* carve-out is emitted so frameless overlay-style documents don't inherit
|
|
80
|
+
* the platform window controls that `global.css` applies by default.
|
|
53
81
|
*/
|
|
54
82
|
export declare function generateChromeDocCss(name: string, withTitlebar: boolean, licenseHeader: string): string;
|
|
55
83
|
/** Fluent stub — one placeholder message keyed to the window title. */
|
|
@@ -92,3 +120,26 @@ export declare function jarIncMnEntryForChromeDoc(name: string): string;
|
|
|
92
120
|
* "jar.mn: Cannot find ${name}.ftl".
|
|
93
121
|
*/
|
|
94
122
|
export declare function localeJarMnEntryForChromeDoc(name: string): string;
|
|
123
|
+
/**
|
|
124
|
+
* Returns true when `jarMnContents` already carries a `[localization]`-style
|
|
125
|
+
* wildcard rooted at `%browser/` whose pattern would already pick up a
|
|
126
|
+
* scaffolded `browser/<name>.ftl` file. Recognises:
|
|
127
|
+
*
|
|
128
|
+
* - `(%browser/**\/*.ftl)` — recursive (the upstream shape).
|
|
129
|
+
* - `(%browser/*.ftl)` — flat.
|
|
130
|
+
*
|
|
131
|
+
* Forks that have migrated entirely to `[localization]` wildcards typically
|
|
132
|
+
* keep no per-file `locale/...` entries for FTL at all; appending one
|
|
133
|
+
* there is dead weight at best, and an outright build break when the fork
|
|
134
|
+
* has also dropped the `% locale browser …` registration. The chrome-doc
|
|
135
|
+
* scaffolder consults this predicate before its locales/jar.mn append and
|
|
136
|
+
* skips the per-file write when the wildcard already covers the scaffold's
|
|
137
|
+
* target path.
|
|
138
|
+
*
|
|
139
|
+
* Conservative by design: only wildcards rooted at `%browser/` count, and
|
|
140
|
+
* a `(%browser/foo.ftl)`-style explicit reference (no `*`) is not treated
|
|
141
|
+
* as a capture. A fork with a narrower wildcard (e.g. `(%browser/about/*.ftl)`)
|
|
142
|
+
* is correctly NOT captured by this predicate, because that wildcard would
|
|
143
|
+
* not pick up the top-level `browser/<name>.ftl` the scaffold writes.
|
|
144
|
+
*/
|
|
145
|
+
export declare function localesFtlWildcardCapturesScaffoldedName(jarMnContents: string): boolean;
|