@hominis/fireforge 0.15.3 → 0.15.4

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 CHANGED
@@ -47,6 +47,11 @@
47
47
 
48
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
49
 
50
+ ### Fork chrome-doc assumptions
51
+
52
+ - `fireforge doctor`'s `Furnace engine paths` check now reads `furnace.json.tokenHostDocuments` instead of hardcoding `browser/base/content/browser.xhtml`. Forks that replaced the stock chrome document were emitting a permanent "browser.xhtml missing" warning every doctor run; reusing the same field the `missing-token-link` validator already consumes means a fork configures chrome-doc paths once and both checks agree. Defaults to `['browser/base/content/browser.xhtml']` when unset — behaviour is unchanged for forks that ship the stock chrome document.
53
+ - `fireforge wire --dom` no longer hardcodes `browser/base/content/browser.xhtml` as the target chrome document. The target now resolves in this order: explicit `--target <path>` flag → first entry of `furnace.json.tokenHostDocuments` → upstream `browser/base/content/browser.xhtml`. Forks that replaced browser.xhtml were getting a cryptic "could not find insertion point in browser.xhtml" error that read like a FireForge bug; the resolved path now propagates into the dry-run plan, the success message, and the insertion-failure error so the actual target is surfaced. When the resolved target does not exist on disk, wire fails up-front with a pointer to `tokenHostDocuments` / `--target` rather than blowing up in the AST pass. The `addDomFragment` core now computes the `#include` directive relative to the target's own directory instead of a hardcoded `browser/base/content/`, so a target that lives elsewhere in the engine tree gets a correctly resolved include path.
54
+
50
55
  ### Furnace chrome-doc
51
56
 
52
57
  - 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.
package/README.md CHANGED
@@ -429,7 +429,7 @@ Mach build failures with known-cryptic mozbuild errors now print actionable hint
429
429
 
430
430
  **`markerComment`** (optional). Appended as a ` // <marker>:` suffix to every line FireForge writes into upstream Firefox source files (starting with `customElements.js`). Keeps fork modifications discoverable and makes re-apply idempotent without hand-tagging entries after each `furnace apply`. Reject list: empty strings, leading/trailing whitespace, newlines, `*/` (would close an enclosing block comment), control characters.
431
431
 
432
- **`furnace.json.tokenHostDocuments`** (optional). List of chrome XHTML documents the `missing-token-link` validator scans for the tokens CSS link. Forks with a second chrome host (e.g. `mybrowser.xhtml` alongside `browser.xhtml`) should list every document that may own the link — the rule fires only when NONE of them link the tokens CSS. Defaults to `["browser/base/content/browser.xhtml"]` when omitted.
432
+ **`furnace.json.tokenHostDocuments`** (optional). List of chrome XHTML documents the `missing-token-link` validator scans for the tokens CSS link. Forks with a second chrome host (e.g. `mybrowser.xhtml` alongside `browser.xhtml`) should list every document that may own the link — the rule fires only when NONE of them link the tokens CSS. Defaults to `["browser/base/content/browser.xhtml"]` when omitted. `fireforge doctor`'s engine-paths probe reads the same field when confirming the chrome document exists on disk, and `fireforge wire --dom` uses the first entry as the default target for its `#include` directive (override per-invocation with `--target <path>`). Forks that fully replaced `browser.xhtml` with a custom top-level chrome document configure this field once and both checks agree.
433
433
 
434
434
  ### `furnace create --localized` for `MozLitElement`
435
435
 
@@ -319,12 +319,19 @@ const furnaceEnginePathsCheck = {
319
319
  dependsOn: ['Furnace configuration'],
320
320
  skipIf: (ctx) => !ctx.furnaceConfigExists || !ctx.furnaceConfig || !ctx.engineExists,
321
321
  run: async (ctx) => {
322
+ // Forks that replace browser.xhtml with a custom top-level chrome
323
+ // document enumerate their chrome docs in furnace.json.tokenHostDocuments.
324
+ // Reuse the same field for the engine-path probe so those forks stop
325
+ // seeing a permanent "browser.xhtml missing" warning.
326
+ const hostDocs = ctx.furnaceConfig?.tokenHostDocuments && ctx.furnaceConfig.tokenHostDocuments.length > 0
327
+ ? ctx.furnaceConfig.tokenHostDocuments
328
+ : ['browser/base/content/browser.xhtml'];
322
329
  const expectedPaths = [
323
330
  CUSTOM_ELEMENTS_JS,
324
331
  JAR_MN,
325
332
  'toolkit/content/widgets',
326
333
  resolveFtlDir(ctx.furnaceConfig?.ftlBasePath),
327
- 'browser/base/content/browser.xhtml',
334
+ ...hostDocs,
328
335
  ];
329
336
  const missing = [];
330
337
  for (const relative of expectedPaths) {
@@ -2,7 +2,9 @@
2
2
  import { join, relative } from 'node:path';
3
3
  import { DEFAULT_BROWSER_SUBSCRIPT_DIR, wireSubscript } from '../core/browser-wire.js';
4
4
  import { getProjectPaths, loadConfig } from '../core/config.js';
5
+ import { furnaceConfigExists as checkFurnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
5
6
  import { consumeParserFallbackEvents } from '../core/parser-fallback.js';
7
+ import { DEFAULT_DOM_TARGET } from '../core/wire-dom-fragment.js';
6
8
  import { InvalidArgumentError } from '../errors/base.js';
7
9
  import { toError } from '../utils/errors.js';
8
10
  import { pathExists } from '../utils/fs.js';
@@ -10,7 +12,7 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
10
12
  import { pickDefined } from '../utils/options.js';
11
13
  import { isContainedRelativePath, isPathInsideRoot, toRootRelativePath } from '../utils/paths.js';
12
14
  const BROWSER_BASE_DIR = 'browser/base';
13
- function printWireDryRun(engineDir, name, subscriptDir, domFilePath, options) {
15
+ function printWireDryRun(engineDir, name, subscriptDir, domFilePath, domTargetPath, options) {
14
16
  info('[dry-run] Would wire subscript:');
15
17
  info(` source: ${subscriptDir}/${name}.js`);
16
18
  info(` browser-main.js: loadSubScript("chrome://browser/content/${name}.js")`);
@@ -22,12 +24,45 @@ function printWireDryRun(engineDir, name, subscriptDir, domFilePath, options) {
22
24
  }
23
25
  if (domFilePath) {
24
26
  const includePath = relative(join(engineDir, subscriptDir), join(engineDir, domFilePath)).replace(/\\/g, '/');
25
- info(` browser.xhtml: #include ${includePath}`);
27
+ info(` ${domTargetPath}: #include ${includePath}`);
26
28
  }
27
29
  const relPath = relative(join(engineDir, BROWSER_BASE_DIR), join(engineDir, subscriptDir)).replace(/\\/g, '/');
28
30
  info(` jar.mn: content/browser/${name}.js (${relPath}/${name}.js)`);
29
31
  outro('Dry run complete');
30
32
  }
33
+ /**
34
+ * Resolves the chrome document the `#include` directive is inserted into.
35
+ *
36
+ * Preference order:
37
+ * 1. `--target <path>` CLI flag (explicit caller intent)
38
+ * 2. First entry of `furnace.json.tokenHostDocuments` (fork-configured
39
+ * chrome doc list; already consumed by the missing-token-link
40
+ * validator and the doctor check)
41
+ * 3. `browser/base/content/browser.xhtml` (upstream default)
42
+ *
43
+ * Step 2 is silent — a missing / invalid furnace.json falls through to the
44
+ * upstream default rather than surfacing a warning, because forks that don't
45
+ * use furnace shouldn't have to configure anything.
46
+ */
47
+ async function resolveDomTargetPath(projectRoot, explicit) {
48
+ if (explicit !== undefined) {
49
+ return explicit;
50
+ }
51
+ if (await checkFurnaceConfigExists(projectRoot)) {
52
+ try {
53
+ const furnaceConfig = await loadFurnaceConfig(projectRoot);
54
+ const first = furnaceConfig.tokenHostDocuments?.[0];
55
+ if (first !== undefined && first.length > 0) {
56
+ return first;
57
+ }
58
+ }
59
+ catch {
60
+ // Fall through to default — a broken furnace.json should not block
61
+ // the wire command. The doctor surfaces that issue separately.
62
+ }
63
+ }
64
+ return DEFAULT_DOM_TARGET;
65
+ }
31
66
  /**
32
67
  * Validates a subscript name supplied on the command line. Subscripts are
33
68
  * resolved into filenames under the subscript directory and registered in
@@ -90,6 +125,21 @@ export async function wireCommand(projectRoot, name, options = {}) {
90
125
  }
91
126
  domFilePath = toRootRelativePath(paths.engine, options.dom);
92
127
  }
128
+ // Resolve the chrome document the `#include` directive will land in.
129
+ // Only consulted when `--dom` is supplied — we still resolve it here so
130
+ // the dry-run plan can print the target accurately.
131
+ if (options.target !== undefined && !isContainedRelativePath(options.target)) {
132
+ throw new InvalidArgumentError(`Target chrome document must stay within engine/: ${options.target}`, 'target');
133
+ }
134
+ const domTargetPath = await resolveDomTargetPath(projectRoot, options.target);
135
+ if (domFilePath) {
136
+ const paths = getProjectPaths(projectRoot);
137
+ if (!options.dryRun && !(await pathExists(join(paths.engine, domTargetPath)))) {
138
+ throw new InvalidArgumentError(`Chrome document not found in engine: ${domTargetPath}\n` +
139
+ 'Set "tokenHostDocuments" in furnace.json (first entry is used by wire) ' +
140
+ 'or pass --target <path>.', 'target');
141
+ }
142
+ }
93
143
  // Verify the subscript file exists in engine/ (skip for dry-run)
94
144
  if (!options.dryRun) {
95
145
  const paths = getProjectPaths(projectRoot);
@@ -100,13 +150,14 @@ export async function wireCommand(projectRoot, name, options = {}) {
100
150
  }
101
151
  }
102
152
  if (options.dryRun) {
103
- printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, options);
153
+ printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, domTargetPath, options);
104
154
  return;
105
155
  }
106
156
  const result = await wireSubscript(projectRoot, name, {
107
157
  ...(options.init !== undefined ? { init: options.init } : {}),
108
158
  ...(options.destroy !== undefined ? { destroy: options.destroy } : {}),
109
159
  ...(domFilePath !== undefined ? { domFilePath } : {}),
160
+ ...(domFilePath !== undefined && domTargetPath !== DEFAULT_DOM_TARGET ? { domTargetPath } : {}),
110
161
  ...(options.after !== undefined ? { after: options.after } : {}),
111
162
  ...(subscriptDir !== DEFAULT_BROWSER_SUBSCRIPT_DIR ? { subscriptDir } : {}),
112
163
  dryRun: false,
@@ -140,10 +191,10 @@ export async function wireCommand(projectRoot, name, options = {}) {
140
191
  }
141
192
  if (domFilePath) {
142
193
  if (result.domInserted) {
143
- success(`Inserted #include directive into browser.xhtml`);
194
+ success(`Inserted #include directive into ${domTargetPath}`);
144
195
  }
145
196
  else {
146
- info(`#include directive already present in browser.xhtml (skipped)`);
197
+ info(`#include directive already present in ${domTargetPath} (skipped)`);
147
198
  }
148
199
  }
149
200
  if (result.jarMnResult.skipped) {
@@ -161,10 +212,12 @@ export function registerWire(program, { getProjectRoot, withErrorHandling }) {
161
212
  .description('Wire a chrome subscript into the browser')
162
213
  .option('--init <expression>', 'Init expression for browser-init.js onLoad()')
163
214
  .option('--destroy <expression>', 'Destroy expression for browser-init.js onUnload()')
164
- .option('--dom <file>', 'XHTML fragment file to insert into browser.xhtml')
215
+ .option('--dom <file>', 'XHTML fragment file to insert into the chrome document')
165
216
  .option('--dry-run', 'Show what would be changed without writing')
166
217
  .option('--after <name>', 'Insert init block after the block for this name')
167
218
  .option('--subscript-dir <dir>', 'Subscript directory relative to engine/ (default: browser/base/content)')
219
+ .option('--target <path>', 'Chrome document to insert --dom into, relative to engine/ ' +
220
+ '(default: first entry of furnace.json tokenHostDocuments, else browser/base/content/browser.xhtml)')
168
221
  .action(withErrorHandling(async (name, options) => {
169
222
  await wireCommand(getProjectRoot(), name, pickDefined(options));
170
223
  }));
@@ -22,6 +22,14 @@ export interface WireOptions {
22
22
  destroy?: string | undefined;
23
23
  /** Path to `.inc.xhtml` file relative to engine root */
24
24
  domFilePath?: string | undefined;
25
+ /**
26
+ * Top-level chrome document the DOM fragment's `#include` directive is
27
+ * inserted into, relative to engine/. Defaults to
28
+ * `browser/base/content/browser.xhtml`. Forks that replace browser.xhtml
29
+ * with a custom chrome document (e.g. `mybrowser.xhtml`) pass the
30
+ * replacement path here.
31
+ */
32
+ domTargetPath?: string | undefined;
25
33
  /** Dry run — don't write any files */
26
34
  dryRun?: boolean | undefined;
27
35
  /** Insert init block after the block containing this name */
@@ -48,10 +48,10 @@ export async function wireSubscript(root, name, options = {}) {
48
48
  if (options.destroy) {
49
49
  destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy);
50
50
  }
51
- // 4. Add #include directive to browser.xhtml (if provided)
51
+ // 4. Add #include directive to the top-level chrome document (if provided)
52
52
  let domInserted = false;
53
53
  if (options.domFilePath) {
54
- domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath));
54
+ domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath), options.domTargetPath);
55
55
  }
56
56
  // 5. Register in jar.mn
57
57
  const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
@@ -1,6 +1,13 @@
1
1
  /**
2
- * browser.xhtml — DOM fragment insertion.
2
+ * Top-level chrome document — DOM fragment insertion.
3
+ *
4
+ * Default target is `browser/base/content/browser.xhtml`. Forks that replace
5
+ * browser.xhtml with a custom top-level chrome document pass the replacement
6
+ * path in via `targetPath`; the insertion logic is shape-agnostic (looks for
7
+ * `#include browser-sets.inc`, then falls back to `<html:body>`), so any
8
+ * browser.xhtml-shaped xhtml works.
3
9
  */
10
+ export declare const DEFAULT_DOM_TARGET = "browser/base/content/browser.xhtml";
4
11
  /**
5
12
  * Tokenizer-based implementation for DOM fragment insertion.
6
13
  */
@@ -10,14 +17,19 @@ export declare function addDomFragmentTokenized(content: string, includeDirectiv
10
17
  */
11
18
  export declare function legacyAddDomFragment(content: string, includeDirective: string): string;
12
19
  /**
13
- * Inserts a `#include` directive for an `.inc.xhtml` file into browser.xhtml,
14
- * before `#include browser-sets.inc`.
20
+ * Inserts a `#include` directive for an `.inc.xhtml` file into the top-level
21
+ * chrome document (default: `browser/base/content/browser.xhtml`), before
22
+ * `#include browser-sets.inc`.
15
23
  *
16
24
  * If the file's content was previously inlined (detected by root element id=),
17
25
  * the inlined block is automatically replaced with the `#include` directive.
18
26
  *
19
27
  * @param engineDir - Engine source root
20
28
  * @param domFilePath - Path to the `.inc.xhtml` file relative to engine root
29
+ * @param targetPath - Chrome document to insert into, relative to engine
30
+ * root. Defaults to {@link DEFAULT_DOM_TARGET}. Forks that replace
31
+ * browser.xhtml with a custom top-level chrome document pass the
32
+ * replacement path here.
21
33
  * @returns true if inserted, false if already present
22
34
  */
23
- export declare function addDomFragment(engineDir: string, domFilePath: string): Promise<boolean>;
35
+ export declare function addDomFragment(engineDir: string, domFilePath: string, targetPath?: string): Promise<boolean>;
@@ -1,15 +1,21 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * browser.xhtml — DOM fragment insertion.
3
+ * Top-level chrome document — DOM fragment insertion.
4
+ *
5
+ * Default target is `browser/base/content/browser.xhtml`. Forks that replace
6
+ * browser.xhtml with a custom top-level chrome document pass the replacement
7
+ * path in via `targetPath`; the insertion logic is shape-agnostic (looks for
8
+ * `#include browser-sets.inc`, then falls back to `<html:body>`), so any
9
+ * browser.xhtml-shaped xhtml works.
4
10
  */
5
- import { join, relative } from 'node:path';
11
+ import { dirname, join, relative } from 'node:path';
6
12
  import { GeneralError } from '../errors/base.js';
7
13
  import { pathExists, readText, writeText } from '../utils/fs.js';
8
14
  import { toRootRelativePath } from '../utils/paths.js';
9
15
  import { escapeRegex } from '../utils/regex.js';
10
16
  import { withParserFallback } from './parser-fallback.js';
11
17
  import { tokenizeXhtml } from './wire-utils.js';
12
- const BROWSER_XHTML = 'browser/base/content/browser.xhtml';
18
+ export const DEFAULT_DOM_TARGET = 'browser/base/content/browser.xhtml';
13
19
  /**
14
20
  * Tokenizer-based implementation for DOM fragment insertion.
15
21
  */
@@ -36,7 +42,7 @@ export function addDomFragmentTokenized(content, includeDirective) {
36
42
  }
37
43
  }
38
44
  if (insertIndex === -1) {
39
- throw new GeneralError('Could not find insertion point in browser.xhtml');
45
+ throw new GeneralError('Could not find insertion point in chrome document');
40
46
  }
41
47
  lines.splice(insertIndex, 0, includeDirective);
42
48
  return lines.join('\n');
@@ -64,32 +70,41 @@ export function legacyAddDomFragment(content, includeDirective) {
64
70
  }
65
71
  }
66
72
  if (insertIndex === -1) {
67
- throw new GeneralError('Could not find insertion point in browser.xhtml');
73
+ throw new GeneralError('Could not find insertion point in chrome document');
68
74
  }
69
75
  lines.splice(insertIndex, 0, includeDirective);
70
76
  return lines.join('\n');
71
77
  }
72
78
  /**
73
- * Inserts a `#include` directive for an `.inc.xhtml` file into browser.xhtml,
74
- * before `#include browser-sets.inc`.
79
+ * Inserts a `#include` directive for an `.inc.xhtml` file into the top-level
80
+ * chrome document (default: `browser/base/content/browser.xhtml`), before
81
+ * `#include browser-sets.inc`.
75
82
  *
76
83
  * If the file's content was previously inlined (detected by root element id=),
77
84
  * the inlined block is automatically replaced with the `#include` directive.
78
85
  *
79
86
  * @param engineDir - Engine source root
80
87
  * @param domFilePath - Path to the `.inc.xhtml` file relative to engine root
88
+ * @param targetPath - Chrome document to insert into, relative to engine
89
+ * root. Defaults to {@link DEFAULT_DOM_TARGET}. Forks that replace
90
+ * browser.xhtml with a custom top-level chrome document pass the
91
+ * replacement path here.
81
92
  * @returns true if inserted, false if already present
82
93
  */
83
- export async function addDomFragment(engineDir, domFilePath) {
84
- const browserXhtmlPath = join(engineDir, BROWSER_XHTML);
94
+ export async function addDomFragment(engineDir, domFilePath, targetPath = DEFAULT_DOM_TARGET) {
95
+ const targetAbsPath = join(engineDir, targetPath);
85
96
  const safeDomFilePath = toRootRelativePath(engineDir, domFilePath);
86
- if (!(await pathExists(browserXhtmlPath))) {
87
- throw new GeneralError(`${BROWSER_XHTML} not found in engine`);
97
+ if (!(await pathExists(targetAbsPath))) {
98
+ throw new GeneralError(`${targetPath} not found in engine`);
88
99
  }
89
- // Compute include path relative to browser/base/content/ (where browser.xhtml lives)
90
- const includePath = relative('browser/base/content', safeDomFilePath).replace(/\\/g, '/');
100
+ // Compute include path relative to the target's directory — the `#include`
101
+ // directive is resolved by the preprocessor relative to the file that
102
+ // contains it, so this must track the chrome doc's location, not a
103
+ // hardcoded `browser/base/content/`.
104
+ const targetDir = dirname(targetPath);
105
+ const includePath = relative(targetDir, safeDomFilePath).replace(/\\/g, '/');
91
106
  const includeDirective = `#include ${includePath}`;
92
- let content = await readText(browserXhtmlPath);
107
+ let content = await readText(targetAbsPath);
93
108
  // Idempotency: check if the #include directive already exists (line-anchored to avoid substring matches)
94
109
  if (new RegExp(`^${escapeRegex(includeDirective)}$`, 'm').test(content)) {
95
110
  return false;
@@ -116,14 +131,14 @@ export async function addDomFragment(engineDir, domFilePath) {
116
131
  }
117
132
  lines.splice(startIdx, endIdx - startIdx, includeDirective);
118
133
  content = lines.join('\n');
119
- await writeText(browserXhtmlPath, content);
134
+ await writeText(targetAbsPath, content);
120
135
  return true;
121
136
  }
122
137
  }
123
138
  }
124
139
  // Normal insertion
125
- const { value } = withParserFallback(() => addDomFragmentTokenized(content, includeDirective), () => legacyAddDomFragment(content, includeDirective), BROWSER_XHTML);
126
- await writeText(browserXhtmlPath, value);
140
+ const { value } = withParserFallback(() => addDomFragmentTokenized(content, includeDirective), () => legacyAddDomFragment(content, includeDirective), targetPath);
141
+ await writeText(targetAbsPath, value);
127
142
  return true;
128
143
  }
129
144
  //# sourceMappingURL=wire-dom-fragment.js.map
@@ -309,6 +309,13 @@ export interface WireOptions {
309
309
  dryRun?: boolean;
310
310
  after?: string;
311
311
  subscriptDir?: string;
312
+ /**
313
+ * Chrome document the DOM fragment's `#include` is inserted into, relative
314
+ * to engine/. Defaults to the first entry of
315
+ * `furnace.json.tokenHostDocuments` when set, otherwise
316
+ * `browser/base/content/browser.xhtml`.
317
+ */
318
+ target?: string;
312
319
  }
313
320
  /**
314
321
  * Options for the register command.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.15.3",
3
+ "version": "0.15.4",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",