@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.
- package/CHANGELOG.md +19 -0
- package/README.md +5 -3
- package/dist/src/commands/build.js +16 -7
- package/dist/src/commands/config.js +32 -20
- package/dist/src/commands/doctor.js +14 -1
- package/dist/src/commands/furnace/chrome-doc-tests.js +9 -2
- package/dist/src/commands/furnace/create-templates.d.ts +11 -0
- package/dist/src/commands/furnace/create-templates.js +11 -2
- package/dist/src/commands/furnace/init.js +97 -9
- package/dist/src/commands/furnace/rename.js +110 -0
- package/dist/src/commands/lint.js +55 -4
- package/dist/src/commands/resolve.d.ts +25 -1
- package/dist/src/commands/resolve.js +25 -15
- package/dist/src/commands/status.js +100 -122
- package/dist/src/commands/test.js +15 -2
- package/dist/src/commands/wire.js +34 -8
- package/dist/src/core/config.d.ts +33 -0
- package/dist/src/core/config.js +43 -0
- package/dist/src/core/furnace-config.d.ts +23 -2
- package/dist/src/core/furnace-config.js +26 -3
- package/dist/src/core/mach.d.ts +31 -0
- package/dist/src/core/mach.js +45 -1
- package/dist/src/core/marionette-port.d.ts +50 -0
- package/dist/src/core/marionette-port.js +215 -0
- package/dist/src/core/patch-manifest-consistency.d.ts +21 -1
- package/dist/src/core/patch-manifest-consistency.js +16 -1
- package/dist/src/core/status-classify.d.ts +54 -0
- package/dist/src/core/status-classify.js +134 -0
- package/dist/src/core/token-dark-mode.d.ts +49 -0
- package/dist/src/core/token-dark-mode.js +182 -0
- package/dist/src/core/token-manager.js +17 -33
- package/dist/src/core/wire-dom-fragment.d.ts +17 -0
- package/dist/src/core/wire-dom-fragment.js +40 -0
- 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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|