@hominis/fireforge 0.15.2 → 0.15.3

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.md +54 -0
  3. package/dist/src/commands/build.js +29 -2
  4. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +49 -0
  5. package/dist/src/commands/furnace/chrome-doc-templates.js +151 -0
  6. package/dist/src/commands/furnace/chrome-doc.d.ts +34 -0
  7. package/dist/src/commands/furnace/chrome-doc.js +168 -0
  8. package/dist/src/commands/furnace/create-mochikit.d.ts +30 -0
  9. package/dist/src/commands/furnace/create-mochikit.js +70 -0
  10. package/dist/src/commands/furnace/create-templates.d.ts +32 -0
  11. package/dist/src/commands/furnace/create-templates.js +69 -0
  12. package/dist/src/commands/furnace/create.d.ts +17 -0
  13. package/dist/src/commands/furnace/create.js +54 -16
  14. package/dist/src/commands/furnace/index.d.ts +2 -1
  15. package/dist/src/commands/furnace/index.js +20 -3
  16. package/dist/src/commands/lint.d.ts +13 -1
  17. package/dist/src/commands/lint.js +33 -7
  18. package/dist/src/core/build-audit.d.ts +46 -0
  19. package/dist/src/core/build-audit.js +251 -0
  20. package/dist/src/core/build-baseline.d.ts +59 -0
  21. package/dist/src/core/build-baseline.js +83 -0
  22. package/dist/src/core/build-prepare.d.ts +20 -1
  23. package/dist/src/core/build-prepare.js +89 -4
  24. package/dist/src/core/furnace-operation.d.ts +2 -1
  25. package/dist/src/core/furnace-operation.js +13 -7
  26. package/dist/src/core/mach-error-hints.d.ts +29 -0
  27. package/dist/src/core/mach-error-hints.js +43 -0
  28. package/dist/src/core/mach.d.ts +5 -2
  29. package/dist/src/core/mach.js +31 -4
  30. package/dist/src/core/patch-lint-diff-tag.d.ts +33 -0
  31. package/dist/src/core/patch-lint-diff-tag.js +83 -0
  32. package/dist/src/types/commands/options.d.ts +15 -0
  33. package/dist/src/types/commands/patches.d.ts +9 -0
  34. package/dist/src/types/furnace.d.ts +1 -1
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -36,12 +36,36 @@
36
36
 
37
37
  - New `furnace create --xpcshell` flag scaffolds an xpcshell test harness alongside (or instead of) the browser-chrome mochitest that `--with-tests` already produces. xpcshell runs headless without a `tabbrowser`, so storage-layer / observer-driven / module-loading code on forks that do not mount the upstream browser chrome (no `openLinkIn` → `URILoadingHelper`) can be covered without the harness complaining about a missing tab strip. Writes `test_<name>_module_loads.js` and an `xpcshell.toml` manifest into `engine/browser/base/content/test/<binary-name>-xpcshell/<component-name>/`. Registration in `XPCSHELL_TESTS_MANIFESTS` is left to the operator — the moz.build that should own the entry depends on where the component lives.
38
38
 
39
+ ### Build audit
40
+
41
+ - `fireforge build` now runs a warn-only post-build dist-tree audit after a successful mach build. The audit diffs engine-relative paths touched since the last successful build (stored as `.fireforge/last-build.json`) against the dist bundle, 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. Surfaces the class of bug where a new pref file or widget is edited but never registered in `moz.build` / `jar.mn` / `package-manifest.in` — the 2026-04-18 `hominis.js` pref-file incident is the motivating case. The audit is warn-only and never fails a successful build.
42
+ - Ends every build with a `Packaged: N updated, M stale, K missing, S skipped` summary so operators can distinguish a fast-because-incremental build from a fast-because-silently-skipped one without a post-build `find`.
43
+ - `fireforge build` auto-runs `mach configure` before the mach build step when any `moz.build`, `moz.configure`, or `Makefile.in` changed since the last successful build. Prevents the stale-backend trap where an incremental build skips work against a recursive-make backend that no longer matches the source tree. Emits a `Backend config changed; running mach configure first...` banner so the extra step is visible, and continues the build even if configure exits non-zero.
44
+ - Mach build failures with known-cryptic mozbuild errors now print actionable hints. First entry in the table: `mozbuild.preprocessor.Preprocessor.Error: no preprocessor directives found` prints `Use JS_PREFERENCE_FILES instead, or add at least one #filter / #expand directive to the file.` The hint table lives in `src/core/mach-error-hints.ts` and is extensible — new cryptic errors we diagnose get one-line hints added without touching the build wrapper.
45
+
46
+ ### Lint
47
+
48
+ - `fireforge lint --since <git-rev>` tags each lint issue as `[introduced]` (file touched in the diff since `<git-rev>`) or `[cumulative]` (pre-existing patch-state drift). Output gains the tag prefix and the summary line splits counts (e.g. `Lint: 2 introduced error(s), 0 introduced warning(s); 5 cumulative error(s), 1 cumulative warning(s)`). Exit code semantics are unchanged — an introduced OR cumulative error still fails lint — but triage of "is the diff I just produced clean?" no longer requires mentally subtracting pre-existing noise from every report. Without `--since` the output is unchanged.
49
+
50
+ ### Furnace chrome-doc
51
+
52
+ - New `furnace chrome-doc create <name>` subcommand scaffolds a top-level chrome document (xhtml + js + css + ftl) plus the three jar.mn registrations (`browser/base/jar.mn`, `browser/themes/shared/jar.inc.mn`, `browser/locales/jar.mn`). Default emits titlebar-buttonbox markup and a `windowtype="navigator:browser"` shell; `--no-titlebar` produces a frameless overlay with the macOS `.titlebar-button { display: none }` carve-out. Mirrors the workflow for custom elements (`furnace create`) so hand-authoring mistakes — the `*` preprocessor flag, the startup-topic observer, the platform titlebar inheritance — are eliminated. All writes go through a rollback journal under the signal-handler pathway: a Ctrl+C mid-scaffold restores every touched file.
53
+
54
+ ### Furnace create
55
+
56
+ - New `--test-style <mochikit|browser-chrome|xpcshell>` option for `furnace create --with-tests`. `mochikit` (the new default when `--with-tests` is set alone) scaffolds a `chrome://mochikit/...` test under `engine/toolkit/content/tests/widgets/test_<tag>.html` that loads the component module directly and smoke-asserts `customElements.whenDefined(tag)`. Runs today against forks whose top-level chrome document lacks a `tabbrowser` (the class of bug that forced `--xpcshell` for storage-layer code) because the harness doesn't traverse `URILoadingHelper.openLinkIn`. `browser-chrome` is the former default and requires a working tabbrowser; `xpcshell` is equivalent to `--xpcshell`. Backwards-compat: `--xpcshell` alone still works; conflicting flag combinations are rejected up-front.
57
+
58
+ ### Run / signals
59
+
60
+ - `fireforge run` (and every other command that never registers a furnace mutation) no longer prints the alarming `Received SIGTERM; rolling back in-flight furnace mutations…` line when the CLI receives SIGINT or SIGTERM. The global signal handler now gates the rollback banner on the presence of at least one live mutation in the registry — plain launches exit silently with code 130/143 as they always did. A new regression test asserts `warn` is not called when no operations are registered.
61
+
39
62
  ### Internal
40
63
 
41
64
  - Extracted `furnace-apply-ftl.ts`, `furnace-config-tokens.ts`, and `create-templates.ts` to keep apply / config / scaffolding files under the per-file LOC budget after the new features landed. `parseStringArray` is now exported from `furnace-config.ts` for cross-module reuse.
42
65
  - New `src/core/marionette-preflight.ts` owns the `--doctor` probe and its teardown semantics.
43
66
  - Test mocks for `furnace-registration.js` now cover the new `addLocaleFtlJarMnEntry` / `removeLocaleFtlJarMnEntry` exports; `config.js` mocks in apply-batch tests now cover `loadConfig` because the apply path reads `markerComment` from fireforge.json.
44
- - Repo-wide scrub of fork-example mentions (`hominis.xhtml`, `HOMINIS` marker-comment examples, fixture tag names) in favour of a generic `mybrowser` / `MYBROWSER` placeholder. FireForge reads as fork-agnostic in docs and fixtures; the npm identity (`@hominis/fireforge`) is unchanged.
67
+ - Repo-wide scrub of fork-example mentions (`hominis.xhtml`, `HOMINIS` marker-comment examples, fixture tag names) in favour of a generic `mybrowser` / `MYBROWSER` placeholder. FireForge reads as fork-agnostic in docs and fixtures; the npm identity (`@hominis/fireforge`) is unchanged. Closes a v0.15.0 slip-through (one `@hominis/fireforge` reference remained in `src/core/furnace-operation.ts` as a generic example alongside the npm-identity occurrences; the code example is now fork-neutral).
68
+ - New modules landed under coverage-gate protection: `src/core/mach-error-hints.ts`, `src/core/build-audit.ts`, `src/core/build-baseline.ts`, `src/core/patch-lint-diff-tag.ts`, `src/commands/furnace/chrome-doc.ts`, `src/commands/furnace/chrome-doc-templates.ts`, `src/commands/furnace/create-mochikit.ts`. Per-module thresholds added to `scripts/check-coverage-thresholds.mjs`.
45
69
 
46
70
  ## 0.14.0
47
71
 
package/README.md CHANGED
@@ -293,6 +293,7 @@ There are three component types:
293
293
  fireforge furnace scan # discover components in the engine
294
294
  fireforge furnace override moz-button -t css-only # fork with CSS-only restyle
295
295
  fireforge furnace create moz-my-widget # scaffold a new component
296
+ fireforge furnace chrome-doc create mybrowser # scaffold a top-level chrome document
296
297
  fireforge furnace deploy # apply to engine/ + validate
297
298
  fireforge furnace status # workspace vs engine drift
298
299
  fireforge furnace diff moz-button # unified diff against baseline
@@ -300,6 +301,39 @@ fireforge furnace diff moz-button # unified diff against baseli
300
301
 
301
302
  `furnace deploy` validates components before applying. As always, errors block, warnings are advisory. `fireforge build` and `fireforge test --build` run apply automatically — when apply wrote files during a build, the build prints a `Furnace: source → engine sync wrote N component(s) …` banner naming every component that was synced, so it is obvious whether engine/ was freshly updated. Use `fireforge doctor --repair-furnace` if the engine gets out of sync.
302
303
 
304
+ ### Scaffolding top-level chrome documents
305
+
306
+ Custom elements live under `toolkit/content/widgets`, but a fork's top-level chrome document (`browser.xhtml` equivalents like `mybrowser.xhtml`, `about:*` panels, onboarding flows) lives at `browser/base/content/` and needs jar.mn, jar.inc.mn, and locales/jar.mn entries to reach the packaged bundle. `furnace chrome-doc create <name>` handles that boilerplate:
307
+
308
+ ```bash
309
+ fireforge furnace chrome-doc create mybrowser # full chrome (titlebar + windowtype)
310
+ fireforge furnace chrome-doc create overlay --no-titlebar # frameless overlay
311
+ ```
312
+
313
+ The command writes:
314
+
315
+ - `engine/browser/base/content/<name>.xhtml` — XHTML shell, optional titlebar-buttonbox, Fluent `<link>`.
316
+ - `engine/browser/base/content/<name>.js` — startup-topic observer fired on first idle.
317
+ - `engine/browser/themes/shared/<name>-chrome.css` — scoped CSS; emits the macOS `.titlebar-button { display: none }` carve-out under `--no-titlebar`.
318
+ - `engine/browser/locales/en-US/browser/<name>.ftl` — Fluent stub keyed on `<name>-window-title`.
319
+ - Appends the corresponding `jar.mn` / `jar.inc.mn` / `locales/jar.mn` entries.
320
+
321
+ Writes are transactional: a SIGINT mid-scaffold rolls back every touched file. Requires an existing engine — run `fireforge download` first.
322
+
323
+ ### Picking a test harness for `furnace create`
324
+
325
+ `furnace create --with-tests` defaults to a MochiKit test at `engine/toolkit/content/tests/widgets/test_<tag>.html`. MochiKit tests load the component module via `chrome://global/` and don't need a `tabbrowser`, so they run against any fork — including bespoke chrome documents (`mybrowser.xhtml`-class) that deliberately omit the upstream browser chrome.
326
+
327
+ Three styles are available via `--test-style`:
328
+
329
+ | Style | When to use |
330
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
331
+ | `mochikit` | Default. Pure-UI custom elements. Runs today against non-tabbrowser chrome. Emits `test_<tag>.html` under `toolkit/content/tests/widgets/`. |
332
+ | `browser-chrome` | Element talks to the browser window (open URLs, manipulate tabs). Requires a working `tabbrowser` in the chrome document. Emits the `browser_<bin>_<tag>.js` scaffold and registers it in `browser/base/moz.build`. |
333
+ | `xpcshell` | Storage-layer, observer-driven, or ESM-loading code. Headless, no tabbrowser. Emits `test_<name>_module_loads.js` + `xpcshell.toml` (registration in `XPCSHELL_TESTS_MANIFESTS` is left to the operator). |
334
+
335
+ `--xpcshell` is preserved as an alias for `--test-style=xpcshell`; conflicting flag combinations (`--xpcshell --test-style=mochikit`) are rejected.
336
+
303
337
  ## Additional Commands
304
338
 
305
339
  The commands below cover project configuration, patch queue management, build packaging and development utilities. Run `fireforge <command> --help` for full option details.
@@ -348,6 +382,26 @@ fireforge watch
348
382
  fireforge token --name "--my-color" --value "light-dark(#fff, #000)"
349
383
  ```
350
384
 
385
+ ### Diff-scoped lint (`lint --since`)
386
+
387
+ `fireforge lint --since <git-rev>` tags each issue as `[introduced]` or `[cumulative]` based on whether its file changed since `<git-rev>`:
388
+
389
+ ```bash
390
+ fireforge lint --since HEAD~1 # just the current commit
391
+ fireforge lint --since main # everything since main
392
+ fireforge lint --since abc1234 # since a specific SHA
393
+ ```
394
+
395
+ The summary line splits counts — e.g. `Lint: 2 introduced error(s), 0 introduced warning(s); 5 cumulative error(s), 1 cumulative warning(s)` — so triage of "did my diff introduce any of these?" is a one-glance check on a large patch series. Exit code still fails on any error (introduced or cumulative); the flag is purely a display / audit tool. Without `--since`, output is unchanged.
396
+
397
+ ### Post-build audit and auto-configure
398
+
399
+ `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.
400
+
401
+ The build also auto-runs `mach configure` before the mach build step when any `moz.build`, `moz.configure`, or `Makefile.in` changed since the last successful build. Prevents incremental builds from silently skipping work against a stale recursive-make backend. Emits a `Backend config changed; running mach configure first...` banner when it fires.
402
+
403
+ 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.
404
+
351
405
  ## Configuration
352
406
 
353
407
  `fireforge.json` at your project root:
@@ -1,11 +1,14 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
3
3
  import { validateBrandOverride } from '../core/brand-validation.js';
4
+ import { auditBuildArtifacts } from '../core/build-audit.js';
5
+ import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
4
6
  import { prepareBuildEnvironment } from '../core/build-prepare.js';
5
7
  import { getProjectPaths, loadConfig } from '../core/config.js';
6
8
  import { build, buildArtifactMismatchMessage, buildUI, hasBuildArtifacts } from '../core/mach.js';
7
9
  import { GeneralError } from '../errors/base.js';
8
10
  import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
11
+ import { toError } from '../utils/errors.js';
9
12
  import { checkDiskSpace, pathExists } from '../utils/fs.js';
10
13
  import { error, info, intro, outro, verbose, warn } from '../utils/logger.js';
11
14
  import { pickDefined } from '../utils/options.js';
@@ -60,8 +63,13 @@ export async function buildCommand(projectRoot, options) {
60
63
  // Future: Load brand-specific config from fireforge.json brands section
61
64
  info(`Brand: ${options.brand}`);
62
65
  }
63
- // Shared pre-flight: branding, Furnace, mozconfig
64
- await prepareBuildEnvironment(projectRoot, paths, config);
66
+ // Read the previous build baseline BEFORE prepareBuildEnvironment so the
67
+ // auto-configure step there can detect moz.build-family changes since the
68
+ // last successful build. The post-build audit below reuses the same
69
+ // baseline to diff engine changes against dist artifacts.
70
+ const previousBaseline = await readBuildBaseline(projectRoot);
71
+ // Shared pre-flight: branding, Furnace, mozconfig, auto-configure
72
+ await prepareBuildEnvironment(projectRoot, paths, config, { previousBaseline });
65
73
  const jobs = resolveJobCount(options, config.build?.jobs);
66
74
  // Run build
67
75
  info(`Starting ${buildType.toLowerCase()} build...`);
@@ -90,6 +98,25 @@ export async function buildCommand(projectRoot, options) {
90
98
  error(`Build failed after ${timeStr}`);
91
99
  throw new BuildError(`Build failed with exit code ${exitCode}`, options.ui ? 'mach build faster' : 'mach build');
92
100
  }
101
+ // Warn-only post-build audit: surfaces silent packaging drops (files
102
+ // edited in engine/ but never registered for packaging) against the
103
+ // previous-build baseline. Never fails the build; the worst case is a
104
+ // warning an operator chooses to investigate.
105
+ try {
106
+ await auditBuildArtifacts(projectRoot, paths.engine, previousBaseline);
107
+ }
108
+ catch (auditError) {
109
+ verbose(`Audit skipped: ${toError(auditError).message}`);
110
+ }
111
+ // Record a fresh baseline only on clean success so the next run audits
112
+ // against this build's HEAD. A failed build keeps the prior baseline so
113
+ // the next attempt still catches long-standing packaging drops.
114
+ try {
115
+ await writeBuildBaseline(projectRoot, paths.engine, config.binaryName);
116
+ }
117
+ catch (baselineError) {
118
+ verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
119
+ }
93
120
  outro(`Build completed in ${timeStr}!`);
94
121
  }
95
122
  /** Registers the build command on the CLI program. */
@@ -0,0 +1,49 @@
1
+ /**
2
+ * File-content templates for `fireforge furnace chrome-doc create`.
3
+ * Extracted so the command entrypoint stays under the per-file LOC budget
4
+ * and each template can be exercised in isolation.
5
+ *
6
+ * The templates here mirror the shape of top-level chrome documents in
7
+ * upstream Firefox (browser.xhtml, privatebrowsing/aboutPrivateBrowsing.html,
8
+ * etc.) minus the fork-specific wiring. A fork author fills in the body.
9
+ */
10
+ /**
11
+ * XHTML shell for a top-level chrome document.
12
+ *
13
+ * The emitted document:
14
+ * - Declares `windowtype="navigator:browser"` when `withTitlebar` is true
15
+ * so chrome-wide stylesheets that target the browser window still apply.
16
+ * - Emits a titlebar-buttonbox placeholder when `withTitlebar` is true so
17
+ * platform-native window controls render.
18
+ * - Links the per-document CSS at `chrome://browser/content/<name>-chrome.css`
19
+ * and the Fluent bundle `browser/<name>.ftl`.
20
+ */
21
+ export declare function generateChromeDocXhtml(name: string, withTitlebar: boolean, license: string): string;
22
+ /**
23
+ * ESM bootstrap script for the chrome document.
24
+ *
25
+ * The generated script fires a startup observer topic on the first idle
26
+ * callback so other chrome code can wait on the document being ready.
27
+ * Mirrors the pattern FireForge-built forks use to coordinate per-window
28
+ * init across multiple top-level documents.
29
+ */
30
+ export declare function generateChromeDocJs(name: string, licenseHeader: string): string;
31
+ /**
32
+ * Scoped CSS for a chrome document. When `withTitlebar` is false the
33
+ * macOS `.titlebar-button { display: none }` carve-out is emitted so
34
+ * frameless overlay-style documents don't inherit the platform window
35
+ * controls that `global.css` applies by default.
36
+ */
37
+ export declare function generateChromeDocCss(name: string, withTitlebar: boolean, licenseHeader: string): string;
38
+ /** Fluent stub — one placeholder message keyed to the window title. */
39
+ export declare function generateChromeDocFtl(name: string, licenseHeader: string): string;
40
+ /**
41
+ * Single-line jar.mn entry that registers an xhtml + js pair under
42
+ * `content/browser/`. Emits the `*` preprocessor flag so both files flow
43
+ * through `#filter substitution` for FireForge brand-name substitution.
44
+ */
45
+ export declare function jarMnEntriesForChromeDoc(name: string): string[];
46
+ /** jar.inc.mn entry that registers the scoped CSS under `content/browser/`. */
47
+ export declare function jarIncMnEntryForChromeDoc(name: string): string;
48
+ /** locales/jar.mn entry that registers the `.ftl` under the browser locale bundle. */
49
+ export declare function localeJarMnEntryForChromeDoc(name: string): string;
@@ -0,0 +1,151 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * File-content templates for `fireforge furnace chrome-doc create`.
4
+ * Extracted so the command entrypoint stays under the per-file LOC budget
5
+ * and each template can be exercised in isolation.
6
+ *
7
+ * The templates here mirror the shape of top-level chrome documents in
8
+ * upstream Firefox (browser.xhtml, privatebrowsing/aboutPrivateBrowsing.html,
9
+ * etc.) minus the fork-specific wiring. A fork author fills in the body.
10
+ */
11
+ /**
12
+ * XHTML shell for a top-level chrome document.
13
+ *
14
+ * The emitted document:
15
+ * - Declares `windowtype="navigator:browser"` when `withTitlebar` is true
16
+ * so chrome-wide stylesheets that target the browser window still apply.
17
+ * - Emits a titlebar-buttonbox placeholder when `withTitlebar` is true so
18
+ * platform-native window controls render.
19
+ * - Links the per-document CSS at `chrome://browser/content/<name>-chrome.css`
20
+ * and the Fluent bundle `browser/<name>.ftl`.
21
+ */
22
+ export function generateChromeDocXhtml(name, withTitlebar, license) {
23
+ const windowAttr = withTitlebar ? ' windowtype="navigator:browser"' : '';
24
+ const titlebarMarkup = withTitlebar
25
+ ? `
26
+ <hbox class="titlebar-buttonbox-container">
27
+ <hbox class="titlebar-buttonbox"></hbox>
28
+ </hbox>`
29
+ : '';
30
+ return `<?xml version="1.0"?>
31
+ <!-- SPDX-License-Identifier: ${license} -->
32
+ <!DOCTYPE window>
33
+ <window
34
+ xmlns="http://www.w3.org/1999/xhtml"
35
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
36
+ id="${name}-window"${windowAttr}
37
+ data-l10n-id="${name}-window-title"
38
+ role="application">
39
+ <head>
40
+ <meta charset="utf-8" />
41
+ <title data-l10n-id="${name}-window-title"></title>
42
+ <link rel="localization" href="browser/${name}.ftl" />
43
+ <link rel="stylesheet" href="chrome://global/skin/global.css" />
44
+ <link rel="stylesheet" href="chrome://browser/content/${name}-chrome.css" />
45
+ <script src="chrome://browser/content/${name}.js"></script>
46
+ </head>
47
+ <body>${titlebarMarkup}
48
+ <main id="${name}-main"></main>
49
+ </body>
50
+ </window>
51
+ `;
52
+ }
53
+ /**
54
+ * ESM bootstrap script for the chrome document.
55
+ *
56
+ * The generated script fires a startup observer topic on the first idle
57
+ * callback so other chrome code can wait on the document being ready.
58
+ * Mirrors the pattern FireForge-built forks use to coordinate per-window
59
+ * init across multiple top-level documents.
60
+ */
61
+ export function generateChromeDocJs(name, licenseHeader) {
62
+ const topic = `${name}-startup`;
63
+ return `${licenseHeader}
64
+
65
+ "use strict";
66
+
67
+ // Fire a startup topic on first idle so subscribers can defer their own
68
+ // init until this document is ready. Observers see the document element as
69
+ // the subject; use \`aSubject instanceof Ci.nsIDOMWindow\` if you need the
70
+ // containing window instead.
71
+ window.addEventListener(
72
+ "DOMContentLoaded",
73
+ () => {
74
+ window.requestIdleCallback?.(() => {
75
+ try {
76
+ Services.obs.notifyObservers(document.documentElement, "${topic}");
77
+ } catch (error) {
78
+ // Observer notifications should never block document init — log
79
+ // but don't throw so a missing observer service (headless / test
80
+ // harness) still leaves the document usable.
81
+ console.error("Failed to fire ${topic} observer:", error);
82
+ }
83
+ });
84
+ },
85
+ { once: true }
86
+ );
87
+ `;
88
+ }
89
+ /**
90
+ * Scoped CSS for a chrome document. When `withTitlebar` is false the
91
+ * macOS `.titlebar-button { display: none }` carve-out is emitted so
92
+ * frameless overlay-style documents don't inherit the platform window
93
+ * controls that `global.css` applies by default.
94
+ */
95
+ export function generateChromeDocCss(name, withTitlebar, licenseHeader) {
96
+ const titlebarOverrides = withTitlebar
97
+ ? ''
98
+ : `
99
+
100
+ /* Frameless overlay — suppress the platform titlebar buttons that
101
+ global.css inherits on macOS. Without this carve-out the traffic-light
102
+ controls render even though the document has no titlebar-buttonbox. */
103
+ :root[windowtype="navigator:browser"] .titlebar-button {
104
+ display: none;
105
+ }
106
+ `;
107
+ return `${licenseHeader}
108
+
109
+ :root {
110
+ --${name}-padding: 16px;
111
+ }
112
+
113
+ #${name}-window {
114
+ display: flex;
115
+ flex-direction: column;
116
+ min-height: 100vh;
117
+ }
118
+
119
+ #${name}-main {
120
+ flex: 1;
121
+ padding: var(--${name}-padding);
122
+ }${titlebarOverrides}
123
+ `;
124
+ }
125
+ /** Fluent stub — one placeholder message keyed to the window title. */
126
+ export function generateChromeDocFtl(name, licenseHeader) {
127
+ return `${licenseHeader}
128
+
129
+ ${name}-window-title = ${name}
130
+ `;
131
+ }
132
+ /**
133
+ * Single-line jar.mn entry that registers an xhtml + js pair under
134
+ * `content/browser/`. Emits the `*` preprocessor flag so both files flow
135
+ * through `#filter substitution` for FireForge brand-name substitution.
136
+ */
137
+ export function jarMnEntriesForChromeDoc(name) {
138
+ return [
139
+ `* content/browser/${name}.xhtml (content/${name}.xhtml)`,
140
+ ` content/browser/${name}.js (content/${name}.js)`,
141
+ ];
142
+ }
143
+ /** jar.inc.mn entry that registers the scoped CSS under `content/browser/`. */
144
+ export function jarIncMnEntryForChromeDoc(name) {
145
+ return ` content/browser/${name}-chrome.css (shared/${name}-chrome.css)`;
146
+ }
147
+ /** locales/jar.mn entry that registers the `.ftl` under the browser locale bundle. */
148
+ export function localeJarMnEntryForChromeDoc(name) {
149
+ return ` locale/browser/${name}.ftl (%${name}.ftl)`;
150
+ }
151
+ //# sourceMappingURL=chrome-doc-templates.js.map
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `fireforge furnace chrome-doc create <name>` — scaffolds a top-level
3
+ * chrome document (xhtml + js + css + ftl + jar.mn registrations).
4
+ *
5
+ * Motivation: `furnace create` covers custom elements under
6
+ * `toolkit/content/widgets/`, but top-level chrome documents (the
7
+ * `mybrowser.xhtml`-class entry points a fork adds alongside or instead
8
+ * of `browser.xhtml`) are today hand-authored with error-prone jar.mn +
9
+ * jar.inc.mn + locales/jar.mn glue. The `*` preprocessor flag, the
10
+ * macOS titlebar-button carve-out, the startup-topic observer, and the
11
+ * Fluent linkage each have silent-break failure modes.
12
+ *
13
+ * This command writes the four source files and appends three jar.mn
14
+ * entries under a rollback journal identical in shape to `furnace create`.
15
+ * A SIGINT mid-scaffold restores every touched file; a successful run
16
+ * leaves the tree ready for `fireforge build`.
17
+ */
18
+ /** Options for `furnace chrome-doc create`. */
19
+ export interface FurnaceChromeDocCreateOptions {
20
+ /**
21
+ * Emit the titlebar-buttonbox markup and leave platform window controls
22
+ * visible. Defaults to `true` because the common case is a full chrome
23
+ * document (not a frameless overlay). Frameless callers pass
24
+ * `--no-titlebar`.
25
+ */
26
+ titlebar?: boolean;
27
+ }
28
+ /**
29
+ * Runs `furnace chrome-doc create <name>`.
30
+ * @param projectRoot Root directory of the project.
31
+ * @param name Chrome-doc name (e.g. `mybrowser`, `aboutonboarding`).
32
+ * @param options CLI-provided options.
33
+ */
34
+ export declare function furnaceChromeDocCreateCommand(projectRoot: string, name: string, options?: FurnaceChromeDocCreateOptions): Promise<void>;
@@ -0,0 +1,168 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `fireforge furnace chrome-doc create <name>` — scaffolds a top-level
4
+ * chrome document (xhtml + js + css + ftl + jar.mn registrations).
5
+ *
6
+ * Motivation: `furnace create` covers custom elements under
7
+ * `toolkit/content/widgets/`, but top-level chrome documents (the
8
+ * `mybrowser.xhtml`-class entry points a fork adds alongside or instead
9
+ * of `browser.xhtml`) are today hand-authored with error-prone jar.mn +
10
+ * jar.inc.mn + locales/jar.mn glue. The `*` preprocessor flag, the
11
+ * macOS titlebar-button carve-out, the startup-topic observer, and the
12
+ * Fluent linkage each have silent-break failure modes.
13
+ *
14
+ * This command writes the four source files and appends three jar.mn
15
+ * entries under a rollback journal identical in shape to `furnace create`.
16
+ * A SIGINT mid-scaffold restores every touched file; a successful run
17
+ * leaves the tree ready for `fireforge build`.
18
+ */
19
+ import { join } from 'node:path';
20
+ import { loadConfig } from '../../core/config.js';
21
+ import { runFurnaceMutation } from '../../core/furnace-operation.js';
22
+ import { createRollbackJournal, recordCreatedDir, restoreRollbackJournalOrThrow, snapshotFile, } from '../../core/furnace-rollback.js';
23
+ import { DEFAULT_LICENSE, getLicenseHeader } from '../../core/license-headers.js';
24
+ import { InvalidArgumentError } from '../../errors/base.js';
25
+ import { FurnaceError } from '../../errors/furnace.js';
26
+ import { pathExists, readText, writeText } from '../../utils/fs.js';
27
+ import { intro, note, outro } from '../../utils/logger.js';
28
+ import { generateChromeDocCss, generateChromeDocFtl, generateChromeDocJs, generateChromeDocXhtml, jarIncMnEntryForChromeDoc, jarMnEntriesForChromeDoc, localeJarMnEntryForChromeDoc, } from './chrome-doc-templates.js';
29
+ /** Chrome-doc name shape: lowercase ASCII, optional hyphens, no leading digit. */
30
+ const CHROME_DOC_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
31
+ /**
32
+ * Validates a chrome-doc name. Lowercase ASCII, optional hyphens, no
33
+ * leading digit — the name is used verbatim in CSS selectors, jar.mn
34
+ * entries, FTL keys, and file basenames, so anything outside that
35
+ * character set would break at least one downstream consumer.
36
+ * @param name Chrome-doc name (file basename without extension).
37
+ * @throws InvalidArgumentError when the name is unusable.
38
+ */
39
+ function validateChromeDocName(name) {
40
+ if (!name.trim()) {
41
+ throw new InvalidArgumentError('Chrome-doc name is required', 'name');
42
+ }
43
+ if (!CHROME_DOC_NAME_PATTERN.test(name)) {
44
+ 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');
45
+ }
46
+ }
47
+ /**
48
+ * Appends a line to a jar.mn-style file when that exact line is not
49
+ * already present. Captures the pre-write contents in the journal so a
50
+ * mid-run interruption restores the file to its original state.
51
+ */
52
+ async function appendJarEntryIfAbsent(filePath, entry, journal) {
53
+ if (!(await pathExists(filePath))) {
54
+ // Target jar.mn doesn't exist in this tree layout. We do NOT create it
55
+ // — a fork that moved the jar file needs the operator to choose a
56
+ // placement. The command surfaces this as a FurnaceError so the user
57
+ // can investigate rather than silently writing to a non-canonical path.
58
+ throw new FurnaceError(`Required jar file ${filePath} does not exist; cannot register chrome-doc entry. Check that the fork's engine layout matches the expected browser/ and locales/ tree.`);
59
+ }
60
+ const existing = await readText(filePath);
61
+ if (existing.includes(entry)) {
62
+ return;
63
+ }
64
+ await snapshotFile(journal, filePath);
65
+ const withEntry = existing.trimEnd() + '\n' + entry + '\n';
66
+ await writeText(filePath, withEntry);
67
+ }
68
+ /**
69
+ * Writes the xhtml/js/css/ftl source files plus the three jar.mn
70
+ * registrations under a rollback journal. Any interruption leaves the
71
+ * tree in its pre-command state.
72
+ */
73
+ async function performChromeDocMutations(args) {
74
+ const journal = createRollbackJournal();
75
+ args.operationContext.registerJournal(journal);
76
+ // XHTML uses an inline XML comment since getLicenseHeader has no XML
77
+ // style — the SPDX convention is a single-line comment at the top.
78
+ const jsHeader = getLicenseHeader(args.license, 'js');
79
+ const cssHeader = getLicenseHeader(args.license, 'css');
80
+ const ftlHeader = getLicenseHeader(args.license, 'hash');
81
+ const written = [];
82
+ try {
83
+ const contentDir = join(args.engineDir, 'browser/base/content');
84
+ const sharedThemeDir = join(args.engineDir, 'browser/themes/shared');
85
+ const localeDir = join(args.engineDir, 'browser/locales/en-US/browser');
86
+ for (const dir of [contentDir, sharedThemeDir, localeDir]) {
87
+ if (!(await pathExists(dir))) {
88
+ recordCreatedDir(journal, dir);
89
+ const { ensureDir } = await import('../../utils/fs.js');
90
+ await ensureDir(dir);
91
+ }
92
+ }
93
+ const xhtmlPath = join(contentDir, `${args.name}.xhtml`);
94
+ if (await pathExists(xhtmlPath)) {
95
+ throw new FurnaceError(`${args.name}.xhtml already exists at ${xhtmlPath}. Remove it or choose a different name.`);
96
+ }
97
+ await snapshotFile(journal, xhtmlPath);
98
+ await writeText(xhtmlPath, generateChromeDocXhtml(args.name, args.withTitlebar, args.license));
99
+ written.push(`browser/base/content/${args.name}.xhtml`);
100
+ const jsPath = join(contentDir, `${args.name}.js`);
101
+ await snapshotFile(journal, jsPath);
102
+ await writeText(jsPath, generateChromeDocJs(args.name, jsHeader));
103
+ written.push(`browser/base/content/${args.name}.js`);
104
+ const cssPath = join(sharedThemeDir, `${args.name}-chrome.css`);
105
+ await snapshotFile(journal, cssPath);
106
+ await writeText(cssPath, generateChromeDocCss(args.name, args.withTitlebar, cssHeader));
107
+ written.push(`browser/themes/shared/${args.name}-chrome.css`);
108
+ const ftlPath = join(localeDir, `${args.name}.ftl`);
109
+ await snapshotFile(journal, ftlPath);
110
+ await writeText(ftlPath, generateChromeDocFtl(args.name, ftlHeader));
111
+ written.push(`browser/locales/en-US/browser/${args.name}.ftl`);
112
+ // jar.mn registrations — XHTML + JS go through the `*` preprocessor
113
+ // for brand substitution, CSS goes through jar.inc.mn, FTL through
114
+ // the locale jar.
115
+ const jarMnPath = join(args.engineDir, 'browser/base/jar.mn');
116
+ for (const entry of jarMnEntriesForChromeDoc(args.name)) {
117
+ await appendJarEntryIfAbsent(jarMnPath, entry, journal);
118
+ }
119
+ written.push('browser/base/jar.mn');
120
+ const jarIncMnPath = join(args.engineDir, 'browser/themes/shared/jar.inc.mn');
121
+ await appendJarEntryIfAbsent(jarIncMnPath, jarIncMnEntryForChromeDoc(args.name), journal);
122
+ written.push('browser/themes/shared/jar.inc.mn');
123
+ const localeJarMnPath = join(args.engineDir, 'browser/locales/jar.mn');
124
+ await appendJarEntryIfAbsent(localeJarMnPath, localeJarMnEntryForChromeDoc(args.name), journal);
125
+ written.push('browser/locales/jar.mn');
126
+ }
127
+ catch (error) {
128
+ await restoreRollbackJournalOrThrow(journal, `Failed to scaffold chrome-doc "${args.name}"`);
129
+ throw error;
130
+ }
131
+ return written;
132
+ }
133
+ /**
134
+ * Runs `furnace chrome-doc create <name>`.
135
+ * @param projectRoot Root directory of the project.
136
+ * @param name Chrome-doc name (e.g. `mybrowser`, `aboutonboarding`).
137
+ * @param options CLI-provided options.
138
+ */
139
+ export async function furnaceChromeDocCreateCommand(projectRoot, name, options = {}) {
140
+ intro('Furnace chrome-doc create');
141
+ validateChromeDocName(name);
142
+ const forgeConfig = await loadConfig(projectRoot);
143
+ const license = forgeConfig.license ?? DEFAULT_LICENSE;
144
+ const engineDir = join(projectRoot, 'engine');
145
+ if (!(await pathExists(engineDir))) {
146
+ throw new FurnaceError('Engine directory not found. Run "fireforge download" first to scaffold a chrome-doc.');
147
+ }
148
+ const withTitlebar = options.titlebar ?? true;
149
+ const written = await runFurnaceMutation(projectRoot, 'chrome-doc-rollback', (ctx) => performChromeDocMutations({
150
+ name,
151
+ license,
152
+ engineDir,
153
+ withTitlebar,
154
+ operationContext: ctx,
155
+ }));
156
+ note([
157
+ `Chrome document "${name}" scaffolded:`,
158
+ ...written.map((f) => ` engine/${f}`),
159
+ '',
160
+ 'Next steps:',
161
+ ` 1. Edit engine/browser/base/content/${name}.xhtml and fill in the body.`,
162
+ ` 2. Localize strings in engine/browser/locales/en-US/browser/${name}.ftl.`,
163
+ ` 3. Run "fireforge build" to validate packaging (post-build audit will flag`,
164
+ ' any entry whose file does not land in the dist bundle).',
165
+ ].join('\n'), name);
166
+ outro('Chrome document created');
167
+ }
168
+ //# sourceMappingURL=chrome-doc.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * MochiKit (chrome://mochikit) test-harness scaffolder for
3
+ * `fireforge furnace create --test-style=mochikit`.
4
+ *
5
+ * Motivation: browser-chrome mochitests require a `tabbrowser` to exist in
6
+ * the top-level chrome document. Forks with a bespoke chrome document
7
+ * (e.g. `mybrowser.xhtml`) that deliberately omits tabbrowser cannot run
8
+ * browser-chrome tests today. MochiKit tests load the component module
9
+ * directly via `chrome://global/` and assert against `customElements`, so
10
+ * they work against any fork that registers the upstream toolkit test
11
+ * manifest tree — including those without a tabbrowser.
12
+ */
13
+ import { type RollbackJournal } from '../../core/furnace-rollback.js';
14
+ import type { ProjectLicense } from '../../types/config.js';
15
+ /**
16
+ * Scaffolds a MochiKit test for a newly created custom component under
17
+ * `engine/toolkit/content/tests/widgets/`. Mirrors the layout stock
18
+ * Firefox widgets (moz-button, moz-toggle, etc.) use, so an operator who
19
+ * already added the `widgets/` tree to their test-manifest registration
20
+ * picks the new test up automatically.
21
+ *
22
+ * Appends a per-test entry to the existing `chrome.toml` when present,
23
+ * writes a fresh `[DEFAULT]`-headed one otherwise. The caller is still
24
+ * responsible for ensuring the `toolkit/content/tests/widgets/chrome.toml`
25
+ * path is registered somewhere in the moz.build tree; most forks inherit
26
+ * this from upstream via `TEST_HARNESS_FILES += [...]`.
27
+ */
28
+ export declare function scaffoldMochikitTestFiles(componentName: string, license: ProjectLicense, paths: {
29
+ engine: string;
30
+ }, journal?: RollbackJournal): Promise<string[]>;