@hominis/fireforge 0.16.5 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +5 -3
  3. package/dist/src/commands/build.js +16 -7
  4. package/dist/src/commands/config.js +32 -20
  5. package/dist/src/commands/doctor.js +14 -1
  6. package/dist/src/commands/furnace/chrome-doc-tests.js +9 -2
  7. package/dist/src/commands/furnace/create-templates.d.ts +11 -0
  8. package/dist/src/commands/furnace/create-templates.js +11 -2
  9. package/dist/src/commands/furnace/init.js +97 -9
  10. package/dist/src/commands/furnace/rename.js +110 -0
  11. package/dist/src/commands/lint.js +55 -4
  12. package/dist/src/commands/resolve.d.ts +25 -1
  13. package/dist/src/commands/resolve.js +25 -15
  14. package/dist/src/commands/status.js +100 -122
  15. package/dist/src/commands/test.js +15 -2
  16. package/dist/src/commands/wire.js +34 -8
  17. package/dist/src/core/config.d.ts +33 -0
  18. package/dist/src/core/config.js +43 -0
  19. package/dist/src/core/furnace-config.d.ts +23 -2
  20. package/dist/src/core/furnace-config.js +26 -3
  21. package/dist/src/core/mach.d.ts +31 -0
  22. package/dist/src/core/mach.js +45 -1
  23. package/dist/src/core/marionette-port.d.ts +50 -0
  24. package/dist/src/core/marionette-port.js +215 -0
  25. package/dist/src/core/patch-manifest-consistency.d.ts +21 -1
  26. package/dist/src/core/patch-manifest-consistency.js +16 -1
  27. package/dist/src/core/status-classify.d.ts +54 -0
  28. package/dist/src/core/status-classify.js +134 -0
  29. package/dist/src/core/token-dark-mode.d.ts +49 -0
  30. package/dist/src/core/token-dark-mode.js +182 -0
  31. package/dist/src/core/token-manager.js +17 -33
  32. package/dist/src/core/wire-dom-fragment.d.ts +17 -0
  33. package/dist/src/core/wire-dom-fragment.js +40 -0
  34. package/package.json +1 -1
@@ -0,0 +1,134 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Status classifier: partitions engine file changes into
4
+ * patch-backed / unmanaged / branding / furnace / conflict buckets.
5
+ *
6
+ * Extracted from `src/commands/status.ts` so that command file stays
7
+ * under the per-file line budget as the number of buckets grows.
8
+ */
9
+ import { join } from 'node:path';
10
+ import { toError } from '../utils/errors.js';
11
+ import { readText } from '../utils/fs.js';
12
+ import { verbose } from '../utils/logger.js';
13
+ import { isBrandingManagedPath } from './branding.js';
14
+ import { computePatchedContent } from './patch-apply.js';
15
+ import { loadPatchesManifest } from './patch-manifest.js';
16
+ function getPrimaryStatusCode(status) {
17
+ if (status.includes('?'))
18
+ return '?';
19
+ if (status.includes('!'))
20
+ return '!';
21
+ for (const code of status) {
22
+ if (code !== ' ') {
23
+ return code;
24
+ }
25
+ }
26
+ return status;
27
+ }
28
+ /**
29
+ * Classifies files into patch-backed, unmanaged, branding, furnace, or
30
+ * conflict buckets.
31
+ *
32
+ * Tracks patch ownership as a `Map<file, patchFilename[]>` rather than
33
+ * a plain `Set<file>` so the classifier can surface cross-patch
34
+ * ownership conflicts the same way the human `--ownership` mode does.
35
+ * The 2026-04-21 eval's `status --json` run reported
36
+ * `classification: "unmanaged"` on two files (`browser/base/jar.mn`,
37
+ * `browser/themes/shared/jar.inc.mn`) that `--ownership` correctly
38
+ * flagged as `CONFLICT`; the JSON output was effectively lying to
39
+ * machine consumers about the nature of the drift.
40
+ */
41
+ export async function classifyFiles(files, engineDir, patchesDir, binaryName, furnacePrefixes) {
42
+ const manifest = await loadPatchesManifest(patchesDir);
43
+ // Build a multimap from file path → list of claiming patch
44
+ // filenames so we can detect cross-patch ownership conflicts. The
45
+ // previous `Set<string>` captured only whether a path was claimed,
46
+ // not by whom, and collapsed multi-owner files into the single-owner
47
+ // branch where the expected-vs-actual content comparison then routed
48
+ // them into `unmanaged` when the content didn't match either owner's
49
+ // expectation.
50
+ const patchClaims = new Map();
51
+ if (manifest) {
52
+ for (const patch of manifest.patches) {
53
+ for (const f of patch.filesAffected) {
54
+ const owners = patchClaims.get(f);
55
+ if (owners) {
56
+ owners.push(patch.filename);
57
+ }
58
+ else {
59
+ patchClaims.set(f, [patch.filename]);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ const results = [];
65
+ for (const entry of files) {
66
+ // Branding check first
67
+ if (isBrandingManagedPath(entry.file, binaryName)) {
68
+ results.push({ ...entry, classification: 'branding' });
69
+ continue;
70
+ }
71
+ // Furnace-managed component paths
72
+ if (furnacePrefixes.size > 0) {
73
+ let isFurnace = false;
74
+ for (const prefix of furnacePrefixes) {
75
+ if (entry.file.startsWith(prefix)) {
76
+ isFurnace = true;
77
+ break;
78
+ }
79
+ }
80
+ if (isFurnace) {
81
+ results.push({ ...entry, classification: 'furnace' });
82
+ continue;
83
+ }
84
+ }
85
+ const owners = patchClaims.get(entry.file);
86
+ // Multiple patches claim this file — surface the cross-patch
87
+ // ownership conflict regardless of whether the current content
88
+ // matches any single claim. `--ownership` reports the same state
89
+ // as `CONFLICT`; `--json` must agree so machine consumers of the
90
+ // two views see the same truth.
91
+ if (owners && owners.length >= 2) {
92
+ results.push({
93
+ ...entry,
94
+ classification: 'conflict',
95
+ claimedBy: [...owners],
96
+ });
97
+ continue;
98
+ }
99
+ // Not in any patch → unmanaged
100
+ if (!owners) {
101
+ results.push({ ...entry, classification: 'unmanaged' });
102
+ continue;
103
+ }
104
+ // File is claimed by exactly one patch — compare content.
105
+ const primaryCode = getPrimaryStatusCode(entry.status);
106
+ if (primaryCode === 'D') {
107
+ // Deleted file: patch-backed only if patch expects deletion
108
+ const expected = await computePatchedContent(patchesDir, engineDir, entry.file);
109
+ results.push({
110
+ ...entry,
111
+ classification: expected === null ? 'patch-backed' : 'unmanaged',
112
+ });
113
+ continue;
114
+ }
115
+ // File exists on disk — compare actual vs expected
116
+ try {
117
+ const [expected, actual] = await Promise.all([
118
+ computePatchedContent(patchesDir, engineDir, entry.file),
119
+ readText(join(engineDir, entry.file)),
120
+ ]);
121
+ results.push({
122
+ ...entry,
123
+ classification: actual === expected ? 'patch-backed' : 'unmanaged',
124
+ });
125
+ }
126
+ catch (error) {
127
+ verbose(`Treating ${entry.file} as unmanaged because patch-backed classification failed: ${toError(error).message}`);
128
+ // If we can't read the file, treat as unmanaged
129
+ results.push({ ...entry, classification: 'unmanaged' });
130
+ }
131
+ }
132
+ return results;
133
+ }
134
+ //# sourceMappingURL=status-classify.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Dark-mode insertion helpers for the tokens CSS scaffold.
3
+ *
4
+ * The 2026-04-21 eval reproduced a bug where `fireforge token add
5
+ * --mode override --dark-value ...` landed the dark declaration
6
+ * AFTER the nested `:root { }` inside the
7
+ * `@media (prefers-color-scheme: dark)` block had already closed,
8
+ * producing a declaration outside any rule block. The helpers here
9
+ * scan the comment-stripped source lines to find the *inner* `:root`
10
+ * block's closing `}` and return a line index the caller can splice
11
+ * into. When the inner `:root` is missing (a scaffold that drifted
12
+ * from the default), the fallback helper returns the outer `@media`
13
+ * block's close so the caller can materialise a fresh `:root` wrapper
14
+ * rather than dropping the dark value.
15
+ */
16
+ /**
17
+ * Strips the content of `/* ... *\/` block comments from an array of
18
+ * CSS source lines while preserving each line's length. Indexed scans
19
+ * over the returned mirror line up with the original, so callers that
20
+ * compute an insertion index against the stripped array can splice
21
+ * into the original array at the same index.
22
+ *
23
+ * We blank the comment body with spaces (rather than removing it) so
24
+ * any downstream consumer that indexes by column — or derives an
25
+ * insertion index as a line number in the original array — still
26
+ * agrees on line numbers.
27
+ */
28
+ export declare function stripBlockCommentsInLines(lines: string[]): string[];
29
+ /**
30
+ * Finds the closing `}` line of the nested `:root { ... }` block inside
31
+ * a `@media (prefers-color-scheme: dark)` block. Returns `-1` when the
32
+ * media block exists but the nested `:root` block is missing; returns
33
+ * `null` when the `@media` block itself is absent.
34
+ *
35
+ * Runs the scan over a comment-stripped mirror of the source lines so
36
+ * braces inside CSS comments (`/* before { after *\/`) do not offset
37
+ * the depth counter. The scan is deliberately line-indexed so callers
38
+ * can splice into the original `lines` array at the returned index.
39
+ */
40
+ export declare function findDarkRootInsertionIndex(lines: string[]): number | null;
41
+ /**
42
+ * Finds the closing `}` of the outermost
43
+ * `@media (prefers-color-scheme: dark)` block. Used as the fallback
44
+ * landing site when the scaffold has no nested `:root { }` — the
45
+ * insertion helper uses this index to splice a brand-new `:root`
46
+ * wrapper containing the dark declaration, rather than dropping the
47
+ * value.
48
+ */
49
+ export declare function findDarkMediaCloseIndex(lines: string[]): number;
@@ -0,0 +1,182 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Dark-mode insertion helpers for the tokens CSS scaffold.
4
+ *
5
+ * The 2026-04-21 eval reproduced a bug where `fireforge token add
6
+ * --mode override --dark-value ...` landed the dark declaration
7
+ * AFTER the nested `:root { }` inside the
8
+ * `@media (prefers-color-scheme: dark)` block had already closed,
9
+ * producing a declaration outside any rule block. The helpers here
10
+ * scan the comment-stripped source lines to find the *inner* `:root`
11
+ * block's closing `}` and return a line index the caller can splice
12
+ * into. When the inner `:root` is missing (a scaffold that drifted
13
+ * from the default), the fallback helper returns the outer `@media`
14
+ * block's close so the caller can materialise a fresh `:root` wrapper
15
+ * rather than dropping the dark value.
16
+ */
17
+ /**
18
+ * Strips the content of `/* ... *\/` block comments from an array of
19
+ * CSS source lines while preserving each line's length. Indexed scans
20
+ * over the returned mirror line up with the original, so callers that
21
+ * compute an insertion index against the stripped array can splice
22
+ * into the original array at the same index.
23
+ *
24
+ * We blank the comment body with spaces (rather than removing it) so
25
+ * any downstream consumer that indexes by column — or derives an
26
+ * insertion index as a line number in the original array — still
27
+ * agrees on line numbers.
28
+ */
29
+ export function stripBlockCommentsInLines(lines) {
30
+ const out = [];
31
+ let inBlockComment = false;
32
+ for (const original of lines) {
33
+ let line = '';
34
+ for (let i = 0; i < original.length; i++) {
35
+ if (inBlockComment) {
36
+ if (original[i] === '*' && original[i + 1] === '/') {
37
+ line += ' ';
38
+ i += 1;
39
+ inBlockComment = false;
40
+ }
41
+ else {
42
+ line += ' ';
43
+ }
44
+ }
45
+ else if (original[i] === '/' && original[i + 1] === '*') {
46
+ line += ' ';
47
+ i += 1;
48
+ inBlockComment = true;
49
+ }
50
+ else {
51
+ // `original[i]` is provably defined here (the bounds check is
52
+ // the loop condition), but TS narrows it to `string | undefined`.
53
+ // Default to empty string so the concat stays well-typed.
54
+ line += original[i] ?? '';
55
+ }
56
+ }
57
+ out.push(line);
58
+ }
59
+ return out;
60
+ }
61
+ /**
62
+ * Finds the closing `}` line of the nested `:root { ... }` block inside
63
+ * a `@media (prefers-color-scheme: dark)` block. Returns `-1` when the
64
+ * media block exists but the nested `:root` block is missing; returns
65
+ * `null` when the `@media` block itself is absent.
66
+ *
67
+ * Runs the scan over a comment-stripped mirror of the source lines so
68
+ * braces inside CSS comments (`/* before { after *\/`) do not offset
69
+ * the depth counter. The scan is deliberately line-indexed so callers
70
+ * can splice into the original `lines` array at the returned index.
71
+ */
72
+ export function findDarkRootInsertionIndex(lines) {
73
+ const stripped = stripBlockCommentsInLines(lines);
74
+ let darkMediaLine = -1;
75
+ for (let i = 0; i < stripped.length; i++) {
76
+ if (/prefers-color-scheme:\s*dark/.test(stripped[i] ?? '')) {
77
+ darkMediaLine = i;
78
+ break;
79
+ }
80
+ }
81
+ if (darkMediaLine === -1)
82
+ return null;
83
+ // Walk the comment-stripped lines after the @media header and find
84
+ // the first `:root {` opener inside the block. The opening brace of
85
+ // the selector may live on the same line as the selector name or on
86
+ // the following line; either shape is tolerated.
87
+ let rootOpenLine = -1;
88
+ for (let i = darkMediaLine; i < stripped.length; i++) {
89
+ const line = stripped[i] ?? '';
90
+ if (/(^|[\s,{])\s*:root\b/.test(line)) {
91
+ // Brace on the same line?
92
+ if (/:root[^{}]*\{/.test(line)) {
93
+ rootOpenLine = i;
94
+ break;
95
+ }
96
+ // Otherwise scan forward for the opening brace, stopping at the
97
+ // first `}` or second selector that would mean the `:root`
98
+ // declaration never opened a block.
99
+ for (let j = i + 1; j < stripped.length; j++) {
100
+ const next = stripped[j] ?? '';
101
+ if (/\{/.test(next)) {
102
+ rootOpenLine = j;
103
+ break;
104
+ }
105
+ if (/[};]/.test(next))
106
+ break;
107
+ }
108
+ if (rootOpenLine !== -1)
109
+ break;
110
+ }
111
+ }
112
+ if (rootOpenLine === -1)
113
+ return -1;
114
+ // Depth-count starting from the `:root` opener. The first `{`
115
+ // encountered sets the entry depth to the initial counter value; the
116
+ // closing brace that returns to that depth terminates the block.
117
+ let depth = 0;
118
+ let entryDepth = 0;
119
+ let enteredBlock = false;
120
+ for (let i = rootOpenLine; i < stripped.length; i++) {
121
+ const line = stripped[i] ?? '';
122
+ for (const ch of line) {
123
+ if (ch === '{') {
124
+ depth++;
125
+ if (!enteredBlock) {
126
+ entryDepth = depth - 1;
127
+ enteredBlock = true;
128
+ }
129
+ }
130
+ else if (ch === '}') {
131
+ depth--;
132
+ }
133
+ }
134
+ if (enteredBlock && depth === entryDepth) {
135
+ return i;
136
+ }
137
+ }
138
+ return -1;
139
+ }
140
+ /**
141
+ * Finds the closing `}` of the outermost
142
+ * `@media (prefers-color-scheme: dark)` block. Used as the fallback
143
+ * landing site when the scaffold has no nested `:root { }` — the
144
+ * insertion helper uses this index to splice a brand-new `:root`
145
+ * wrapper containing the dark declaration, rather than dropping the
146
+ * value.
147
+ */
148
+ export function findDarkMediaCloseIndex(lines) {
149
+ const stripped = stripBlockCommentsInLines(lines);
150
+ let darkMediaLine = -1;
151
+ for (let i = 0; i < stripped.length; i++) {
152
+ if (/prefers-color-scheme:\s*dark/.test(stripped[i] ?? '')) {
153
+ darkMediaLine = i;
154
+ break;
155
+ }
156
+ }
157
+ if (darkMediaLine === -1)
158
+ return -1;
159
+ let depth = 0;
160
+ let entryDepth = 0;
161
+ let enteredBlock = false;
162
+ for (let i = darkMediaLine; i < stripped.length; i++) {
163
+ const line = stripped[i] ?? '';
164
+ for (const ch of line) {
165
+ if (ch === '{') {
166
+ depth++;
167
+ if (!enteredBlock) {
168
+ entryDepth = depth - 1;
169
+ enteredBlock = true;
170
+ }
171
+ }
172
+ else if (ch === '}') {
173
+ depth--;
174
+ }
175
+ }
176
+ if (enteredBlock && depth === entryDepth) {
177
+ return i;
178
+ }
179
+ }
180
+ return -1;
181
+ }
182
+ //# sourceMappingURL=token-dark-mode.js.map
@@ -10,6 +10,7 @@ import { validateTokenName } from '../utils/validation.js';
10
10
  import { getProjectPaths, loadConfig } from './config.js';
11
11
  import { loadFurnaceConfig } from './furnace-config.js';
12
12
  import { findTableAfterHeading, findTableByColumns, insertRow, rewriteTableRows, updateCellByKey, } from './markdown-table.js';
13
+ import { findDarkMediaCloseIndex, findDarkRootInsertionIndex } from './token-dark-mode.js';
13
14
  /** Returns the token CSS path relative to engine root for a given binary name. */
14
15
  export function getTokensCssPath(binaryName) {
15
16
  return `browser/themes/shared/${binaryName}-tokens.css`;
@@ -271,41 +272,24 @@ function findCategorySection(lines, category, tokensCssPath) {
271
272
  function insertDarkModeOverride(lines, options) {
272
273
  if (options.mode !== 'override' || !options.darkValue)
273
274
  return;
274
- let darkMediaLine = -1;
275
- for (let i = 0; i < lines.length; i++) {
276
- if (/prefers-color-scheme:\s*dark/.test(lines[i] ?? '')) {
277
- darkMediaLine = i;
278
- break;
279
- }
280
- }
281
- if (darkMediaLine === -1)
275
+ const insertionIndex = findDarkRootInsertionIndex(lines);
276
+ if (insertionIndex === null)
277
+ return; // No @media block at all.
278
+ const darkEntry = ` ${options.tokenName}: ${options.darkValue};`;
279
+ if (insertionIndex === -1) {
280
+ // @media block exists but has no nested :root { } — the scaffold
281
+ // drifted. Warn and fall back to appending a fresh nested :root
282
+ // block right before the @media block's closing brace so the
283
+ // generated CSS still parses, rather than dropping the dark value
284
+ // on the floor or producing a declaration outside any rule.
285
+ warn(`Dark-mode override block for "${options.tokenName}" could not find a nested ":root { }" inside @media (prefers-color-scheme: dark). Appending a fresh ":root { }" block — review the tokens CSS scaffold.`);
286
+ const outerCloseIndex = findDarkMediaCloseIndex(lines);
287
+ if (outerCloseIndex === -1)
288
+ return;
289
+ lines.splice(outerCloseIndex, 0, ' :root {', darkEntry, ' }');
282
290
  return;
283
- // Find the closing } of the @media block
284
- let darkBlockEnd = lines.length;
285
- let depth = 0;
286
- let entryDepth = 0;
287
- let enteredBlock = false;
288
- for (let i = darkMediaLine; i < lines.length; i++) {
289
- const line = lines[i] ?? '';
290
- for (const ch of line) {
291
- if (ch === '{') {
292
- depth++;
293
- if (!enteredBlock) {
294
- entryDepth = depth - 1;
295
- enteredBlock = true;
296
- }
297
- }
298
- if (ch === '}')
299
- depth--;
300
- }
301
- if (enteredBlock && depth === entryDepth) {
302
- darkBlockEnd = i;
303
- break;
304
- }
305
291
  }
306
- // Insert the dark value before the closing }
307
- const darkEntry = ` ${options.tokenName}: ${options.darkValue};`;
308
- lines.splice(darkBlockEnd, 0, darkEntry);
292
+ lines.splice(insertionIndex, 0, darkEntry);
309
293
  }
310
294
  /**
311
295
  * Adds a token declaration to the CSS file in the correct category section.
@@ -16,6 +16,23 @@ export declare function addDomFragmentTokenized(content: string, includeDirectiv
16
16
  * Legacy line-based implementation preserved as fallback.
17
17
  */
18
18
  export declare function legacyAddDomFragment(content: string, includeDirective: string): string;
19
+ /**
20
+ * Dry-run precheck for `addDomFragment`. Reads the resolved chrome
21
+ * document and verifies it either already contains the `#include`
22
+ * directive (the idempotent-skip case) OR offers a locatable insertion
23
+ * point via {@link addDomFragmentTokenized} / {@link legacyAddDomFragment}.
24
+ * Throws the same `Could not find insertion point in chrome document`
25
+ * error the real run would throw when neither condition holds.
26
+ *
27
+ * Motivating case (2026-04-21 eval, Finding #12): `fireforge wire ...
28
+ * --dry-run` previewed a plausible mutation plan against
29
+ * `tokenHostDocuments[0]`, then `fireforge wire ...` without
30
+ * `--dry-run` threw `Could not find insertion point in chrome document`
31
+ * on the same arguments. The real run had always called the insertion
32
+ * helpers; dry-run did not. This helper runs the same check in the
33
+ * preview pass so plan and execution disagree less.
34
+ */
35
+ export declare function probeDomFragmentInsertionPoint(engineDir: string, domFilePath: string, targetPath?: string): Promise<void>;
19
36
  /**
20
37
  * Inserts a `#include` directive for an `.inc.xhtml` file into the top-level
21
38
  * chrome document (default: `browser/base/content/browser.xhtml`), before
@@ -75,6 +75,46 @@ export function legacyAddDomFragment(content, includeDirective) {
75
75
  lines.splice(insertIndex, 0, includeDirective);
76
76
  return lines.join('\n');
77
77
  }
78
+ /**
79
+ * Dry-run precheck for `addDomFragment`. Reads the resolved chrome
80
+ * document and verifies it either already contains the `#include`
81
+ * directive (the idempotent-skip case) OR offers a locatable insertion
82
+ * point via {@link addDomFragmentTokenized} / {@link legacyAddDomFragment}.
83
+ * Throws the same `Could not find insertion point in chrome document`
84
+ * error the real run would throw when neither condition holds.
85
+ *
86
+ * Motivating case (2026-04-21 eval, Finding #12): `fireforge wire ...
87
+ * --dry-run` previewed a plausible mutation plan against
88
+ * `tokenHostDocuments[0]`, then `fireforge wire ...` without
89
+ * `--dry-run` threw `Could not find insertion point in chrome document`
90
+ * on the same arguments. The real run had always called the insertion
91
+ * helpers; dry-run did not. This helper runs the same check in the
92
+ * preview pass so plan and execution disagree less.
93
+ */
94
+ export async function probeDomFragmentInsertionPoint(engineDir, domFilePath, targetPath = DEFAULT_DOM_TARGET) {
95
+ const targetAbsPath = join(engineDir, targetPath);
96
+ if (!(await pathExists(targetAbsPath))) {
97
+ // The callers in `wire.ts` run their own existence probe before
98
+ // invoking this helper, but a well-behaved probe is paranoid — if
99
+ // something changed between the two checks, fail with the same
100
+ // error the real run would surface.
101
+ throw new GeneralError(`${targetPath} not found in engine`);
102
+ }
103
+ const safeDomFilePath = toRootRelativePath(engineDir, domFilePath);
104
+ const targetDir = dirname(targetPath);
105
+ const includePath = relative(targetDir, safeDomFilePath).replace(/\\/g, '/');
106
+ const includeDirective = `#include ${includePath}`;
107
+ const content = await readText(targetAbsPath);
108
+ if (new RegExp(`^${escapeRegex(includeDirective)}$`, 'm').test(content)) {
109
+ // Already wired — the real run would idempotent-skip here, so
110
+ // dry-run is allowed to proceed too.
111
+ return;
112
+ }
113
+ // Check the tokenised and legacy insertion paths symmetrically with
114
+ // the real run. Either helper returning without throwing is sufficient
115
+ // evidence that the real run can land the directive.
116
+ withParserFallback(() => addDomFragmentTokenized(content, includeDirective), () => legacyAddDomFragment(content, includeDirective), targetPath);
117
+ }
78
118
  /**
79
119
  * Inserts a `#include` directive for an `.inc.xhtml` file into the top-level
80
120
  * chrome document (default: `browser/base/content/browser.xhtml`), before
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.16.5",
3
+ "version": "0.17.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",