@hominis/fireforge 0.15.1 → 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.
- package/CHANGELOG.md +39 -3
- package/README.md +76 -3
- package/dist/src/commands/build.js +41 -3
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +49 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +151 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +34 -0
- package/dist/src/commands/furnace/chrome-doc.js +168 -0
- package/dist/src/commands/furnace/create-mochikit.d.ts +30 -0
- package/dist/src/commands/furnace/create-mochikit.js +70 -0
- package/dist/src/commands/furnace/create-templates.d.ts +53 -0
- package/dist/src/commands/furnace/create-templates.js +118 -0
- package/dist/src/commands/furnace/create-xpcshell.d.ts +27 -0
- package/dist/src/commands/furnace/create-xpcshell.js +53 -0
- package/dist/src/commands/furnace/create.d.ts +17 -0
- package/dist/src/commands/furnace/create.js +59 -12
- package/dist/src/commands/furnace/index.d.ts +2 -1
- package/dist/src/commands/furnace/index.js +20 -2
- package/dist/src/commands/lint.d.ts +13 -1
- package/dist/src/commands/lint.js +33 -7
- package/dist/src/commands/setup.d.ts +1 -1
- package/dist/src/commands/setup.js +3 -2
- package/dist/src/core/build-audit.d.ts +46 -0
- package/dist/src/core/build-audit.js +251 -0
- package/dist/src/core/build-baseline.d.ts +59 -0
- package/dist/src/core/build-baseline.js +83 -0
- package/dist/src/core/build-prepare.d.ts +20 -1
- package/dist/src/core/build-prepare.js +94 -4
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -1
- package/dist/src/core/furnace-config-tokens.d.ts +6 -0
- package/dist/src/core/furnace-config-tokens.js +15 -0
- package/dist/src/core/furnace-config.js +10 -4
- package/dist/src/core/furnace-operation.d.ts +2 -1
- package/dist/src/core/furnace-operation.js +13 -7
- package/dist/src/core/furnace-registration-ast.d.ts +2 -2
- package/dist/src/core/furnace-registration-ast.js +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +18 -7
- package/dist/src/core/furnace-validate-helpers.d.ts +31 -1
- package/dist/src/core/furnace-validate-helpers.js +101 -18
- package/dist/src/core/furnace-validate-registration.d.ts +1 -1
- package/dist/src/core/furnace-validate-registration.js +1 -1
- package/dist/src/core/mach-error-hints.d.ts +29 -0
- package/dist/src/core/mach-error-hints.js +43 -0
- package/dist/src/core/mach.d.ts +5 -2
- package/dist/src/core/mach.js +31 -4
- package/dist/src/core/marionette-preflight.d.ts +14 -7
- package/dist/src/core/marionette-preflight.js +94 -44
- package/dist/src/core/patch-lint-cross.d.ts +1 -1
- package/dist/src/core/patch-lint-cross.js +1 -1
- package/dist/src/core/patch-lint-diff-tag.d.ts +33 -0
- package/dist/src/core/patch-lint-diff-tag.js +83 -0
- package/dist/src/core/patch-lint.js +29 -9
- package/dist/src/types/commands/options.d.ts +25 -0
- package/dist/src/types/commands/patches.d.ts +9 -0
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +13 -2
- package/package.json +1 -1
|
@@ -148,28 +148,88 @@ function isWithinLocalizedElement(content, matchIndex) {
|
|
|
148
148
|
const tagContent = contentBefore.slice(lastTagOpen, matchIndex + 1);
|
|
149
149
|
return /data-l10n-id\s*=/.test(tagContent);
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Detects hardcoded user-visible template text that should usually be
|
|
153
|
+
* localized.
|
|
154
|
+
*
|
|
155
|
+
* Scoped to three positive contexts rather than scanning the whole file,
|
|
156
|
+
* because a bare `>…<` regex catches JS comparisons (`if (x > 0 && y <
|
|
157
|
+
* 100)`), diagnostic strings (`console.error("Failed <id> lookup")`), and
|
|
158
|
+
* identifier literals that are never shown to a user. Only matches that
|
|
159
|
+
* actually enter a UI render path count:
|
|
160
|
+
*
|
|
161
|
+
* 1. Content inside a Lit `` html`…` `` tagged template literal.
|
|
162
|
+
* 2. The string literal on the RHS of `.textContent = "…"` or
|
|
163
|
+
* `.innerHTML = "…"`.
|
|
164
|
+
* 3. The string literal assigned to an XUL-widget `label=`,
|
|
165
|
+
* `title=`, or `tooltiptext=` attribute when constructing DOM in JS.
|
|
166
|
+
*
|
|
167
|
+
* A file-wide `// furnace-ignore: hardcoded-text` comment suppresses all
|
|
168
|
+
* findings (matches the pre-existing escape hatch).
|
|
169
|
+
*/
|
|
152
170
|
export function containsHardcodedTemplateText(content) {
|
|
153
171
|
if (/furnace-ignore:\s*hardcoded-text/.test(content)) {
|
|
154
172
|
return false;
|
|
155
173
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
return (hasFlaggedTextInLitTemplates(content) ||
|
|
175
|
+
hasFlaggedTextInDomAssignment(content) ||
|
|
176
|
+
hasFlaggedTextInXulAttribute(content));
|
|
177
|
+
}
|
|
178
|
+
function isFlaggableText(text) {
|
|
179
|
+
const trimmed = text.trim();
|
|
180
|
+
if (!trimmed)
|
|
181
|
+
return false;
|
|
182
|
+
if (/\$\{/.test(trimmed))
|
|
183
|
+
return false;
|
|
184
|
+
if (Array.from(trimmed).length <= 1)
|
|
185
|
+
return false;
|
|
186
|
+
if (isSymbolOnlyText(trimmed))
|
|
187
|
+
return false;
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
function hasFlaggedTextInLitTemplates(content) {
|
|
191
|
+
// Match `html\`…\`` regions, anchored on a non-identifier char before `html`
|
|
192
|
+
// so substrings like `otherhtml` do not spuriously open a template.
|
|
193
|
+
const htmlPattern = /(?:^|[^a-zA-Z0-9_$])html`([\s\S]*?)`/g;
|
|
194
|
+
let litMatch;
|
|
195
|
+
while ((litMatch = htmlPattern.exec(content)) !== null) {
|
|
196
|
+
const region = litMatch[1] ?? '';
|
|
197
|
+
const textPattern = />([^<$\s][^<$]*)</g;
|
|
198
|
+
let textMatch;
|
|
199
|
+
while ((textMatch = textPattern.exec(region)) !== null) {
|
|
200
|
+
const text = textMatch[1] ?? '';
|
|
201
|
+
if (!isFlaggableText(text))
|
|
202
|
+
continue;
|
|
203
|
+
if (isWithinLocalizedElement(region, textMatch.index))
|
|
204
|
+
continue;
|
|
205
|
+
return true;
|
|
171
206
|
}
|
|
172
|
-
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
function hasFlaggedTextInDomAssignment(content) {
|
|
211
|
+
// `<expr>.textContent = "abc"` and `<expr>.innerHTML = "abc"` — these are
|
|
212
|
+
// user-visible render paths. Template-literal RHS is excluded (usually
|
|
213
|
+
// dynamic), matching the `${` guard used elsewhere in this helper.
|
|
214
|
+
const assignPattern = /\.(?:textContent|innerHTML)\s*=\s*(["'])((?:\\.|(?!\1).)*)\1/g;
|
|
215
|
+
let match;
|
|
216
|
+
while ((match = assignPattern.exec(content)) !== null) {
|
|
217
|
+
const text = match[2] ?? '';
|
|
218
|
+
if (isFlaggableText(text))
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
function hasFlaggedTextInXulAttribute(content) {
|
|
224
|
+
// Assignments like `node.setAttribute("label", "Save")` or JS-built XUL
|
|
225
|
+
// attributes `label="…"` / `title="…"` / `tooltiptext="…"` in template
|
|
226
|
+
// literals outside Lit blocks. Covers DOM built via createXULElement.
|
|
227
|
+
const setAttrPattern = /setAttribute\s*\(\s*["'](?:label|title|tooltiptext)["']\s*,\s*(["'])((?:\\.|(?!\1).)*)\1/g;
|
|
228
|
+
let setAttrMatch;
|
|
229
|
+
while ((setAttrMatch = setAttrPattern.exec(content)) !== null) {
|
|
230
|
+
const text = setAttrMatch[2] ?? '';
|
|
231
|
+
if (isFlaggableText(text))
|
|
232
|
+
return true;
|
|
173
233
|
}
|
|
174
234
|
return false;
|
|
175
235
|
}
|
|
@@ -214,6 +274,27 @@ export function collectCssVariableReferences(cssContent) {
|
|
|
214
274
|
}
|
|
215
275
|
return referencedVariables;
|
|
216
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Collects CSS custom property *declarations* — names appearing on the
|
|
279
|
+
* left-hand side of a `--name:` declaration. Used to auto-exempt
|
|
280
|
+
* component-local runtime variables from the token-prefix check: if the
|
|
281
|
+
* component both declares and consumes a variable in its own CSS file, it
|
|
282
|
+
* is a local runtime channel, not a design-token reference.
|
|
283
|
+
*
|
|
284
|
+
* The anchor `(?:^|[{;,\s])` rules out `var(--name)` occurrences (which are
|
|
285
|
+
* always preceded by `(`), so references are not mistaken for declarations.
|
|
286
|
+
*/
|
|
287
|
+
export function collectCssVariableDeclarations(cssContent) {
|
|
288
|
+
const declared = new Set();
|
|
289
|
+
const pattern = /(?:^|[{;,\s])(--[\w-]+)\s*:/g;
|
|
290
|
+
let match;
|
|
291
|
+
while ((match = pattern.exec(cssContent)) !== null) {
|
|
292
|
+
const name = match[1];
|
|
293
|
+
if (name)
|
|
294
|
+
declared.add(name);
|
|
295
|
+
}
|
|
296
|
+
return declared;
|
|
297
|
+
}
|
|
217
298
|
async function collectInheritedOverrideVariables(tagName, config, root) {
|
|
218
299
|
const inheritedVariables = new Set();
|
|
219
300
|
const basePath = config.overrides[tagName]?.basePath;
|
|
@@ -234,12 +315,14 @@ async function collectInheritedOverrideVariables(tagName, config, root) {
|
|
|
234
315
|
/** Builds token-validation context from the config allowlist and inherited override CSS. */
|
|
235
316
|
export async function getTokenPrefixContext(tagName, type, config, root) {
|
|
236
317
|
const allowlist = new Set(config.tokenAllowlist ?? []);
|
|
318
|
+
const runtimeVariables = new Set(config.runtimeVariables ?? []);
|
|
237
319
|
if (type !== 'override' || !root) {
|
|
238
|
-
return { allowlist, inheritedOverrideVars: new Set() };
|
|
320
|
+
return { allowlist, inheritedOverrideVars: new Set(), runtimeVariables };
|
|
239
321
|
}
|
|
240
322
|
return {
|
|
241
323
|
allowlist,
|
|
242
324
|
inheritedOverrideVars: await collectInheritedOverrideVariables(tagName, config, root),
|
|
325
|
+
runtimeVariables,
|
|
243
326
|
};
|
|
244
327
|
}
|
|
245
328
|
//# sourceMappingURL=furnace-validate-helpers.js.map
|
|
@@ -35,7 +35,7 @@ export declare function validateJarMnEntries(root: string, config: FurnaceConfig
|
|
|
35
35
|
* linked in at least one chrome host document. Without the link, tokens
|
|
36
36
|
* silently resolve to nothing at runtime.
|
|
37
37
|
*
|
|
38
|
-
* Forks with multiple chrome host documents (e.g. `
|
|
38
|
+
* Forks with multiple chrome host documents (e.g. `mybrowser.xhtml` beside
|
|
39
39
|
* `browser.xhtml`) can enumerate them via `tokenHostDocuments` in
|
|
40
40
|
* furnace.json; the warning fires only when NONE of the configured
|
|
41
41
|
* documents link the tokens CSS.
|
|
@@ -216,7 +216,7 @@ const DEFAULT_TOKEN_HOST_DOCUMENTS = ['browser/base/content/browser.xhtml'];
|
|
|
216
216
|
* linked in at least one chrome host document. Without the link, tokens
|
|
217
217
|
* silently resolve to nothing at runtime.
|
|
218
218
|
*
|
|
219
|
-
* Forks with multiple chrome host documents (e.g. `
|
|
219
|
+
* Forks with multiple chrome host documents (e.g. `mybrowser.xhtml` beside
|
|
220
220
|
* `browser.xhtml`) can enumerate them via `tokenHostDocuments` in
|
|
221
221
|
* furnace.json; the warning fires only when NONE of the configured
|
|
222
222
|
* documents link the tokens CSS.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern-based translator for cryptic mozbuild / mach errors.
|
|
3
|
+
*
|
|
4
|
+
* Each entry maps a stderr regex to an actionable hint. The goal is not to
|
|
5
|
+
* parse every mach failure — it's to convert the handful of errors whose
|
|
6
|
+
* message is non-obvious into a one-line "here's what to change". New
|
|
7
|
+
* entries should only be added when a concrete diagnosis of the cryptic
|
|
8
|
+
* output has been established; low-confidence hints would train operators
|
|
9
|
+
* to ignore the translator.
|
|
10
|
+
*/
|
|
11
|
+
/** A single translator entry. */
|
|
12
|
+
export interface MachErrorHint {
|
|
13
|
+
/** Pattern to search within the captured mach stderr. */
|
|
14
|
+
pattern: RegExp;
|
|
15
|
+
/** Actionable, one-line hint to surface alongside the raw mach output. */
|
|
16
|
+
hint: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Registered hint patterns. Order-sensitive: the first match wins per
|
|
20
|
+
* pattern, but multiple distinct patterns may fire for the same stderr.
|
|
21
|
+
*/
|
|
22
|
+
export declare const MACH_ERROR_HINTS: MachErrorHint[];
|
|
23
|
+
/**
|
|
24
|
+
* Scans captured stderr for known mach errors and returns matching hints.
|
|
25
|
+
* Pure function — safe to call on any string; never throws.
|
|
26
|
+
* @param stderr Captured mach stderr.
|
|
27
|
+
* @returns Ordered, de-duplicated list of hint strings. Empty when nothing matches.
|
|
28
|
+
*/
|
|
29
|
+
export declare function explainMachError(stderr: string): string[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Pattern-based translator for cryptic mozbuild / mach errors.
|
|
4
|
+
*
|
|
5
|
+
* Each entry maps a stderr regex to an actionable hint. The goal is not to
|
|
6
|
+
* parse every mach failure — it's to convert the handful of errors whose
|
|
7
|
+
* message is non-obvious into a one-line "here's what to change". New
|
|
8
|
+
* entries should only be added when a concrete diagnosis of the cryptic
|
|
9
|
+
* output has been established; low-confidence hints would train operators
|
|
10
|
+
* to ignore the translator.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Registered hint patterns. Order-sensitive: the first match wins per
|
|
14
|
+
* pattern, but multiple distinct patterns may fire for the same stderr.
|
|
15
|
+
*/
|
|
16
|
+
export const MACH_ERROR_HINTS = [
|
|
17
|
+
{
|
|
18
|
+
pattern: /mozbuild\.preprocessor\.Preprocessor\.Error[\s\S]*?no preprocessor directives found/,
|
|
19
|
+
hint: 'A file registered under JS_PREFERENCE_PP_FILES contains no preprocessor directives. ' +
|
|
20
|
+
'Use JS_PREFERENCE_FILES instead, or add at least one #filter / #expand directive to the file.',
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Scans captured stderr for known mach errors and returns matching hints.
|
|
25
|
+
* Pure function — safe to call on any string; never throws.
|
|
26
|
+
* @param stderr Captured mach stderr.
|
|
27
|
+
* @returns Ordered, de-duplicated list of hint strings. Empty when nothing matches.
|
|
28
|
+
*/
|
|
29
|
+
export function explainMachError(stderr) {
|
|
30
|
+
if (!stderr) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const hits = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
for (const { pattern, hint } of MACH_ERROR_HINTS) {
|
|
36
|
+
if (pattern.test(stderr) && !seen.has(hint)) {
|
|
37
|
+
seen.add(hint);
|
|
38
|
+
hits.push(hint);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return hits;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=mach-error-hints.js.map
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -57,14 +57,17 @@ export declare function bootstrap(engineDir: string): Promise<number>;
|
|
|
57
57
|
*/
|
|
58
58
|
export declare function bootstrapWithOutput(engineDir: string): Promise<MachCommandResult>;
|
|
59
59
|
/**
|
|
60
|
-
* Runs a full mach build.
|
|
60
|
+
* Runs a full mach build. On a non-zero exit, any matched error hints are
|
|
61
|
+
* surfaced on top of the raw mach output so operators get an actionable
|
|
62
|
+
* nudge alongside the cryptic mozbuild traceback.
|
|
61
63
|
* @param engineDir - Path to the engine directory
|
|
62
64
|
* @param jobs - Number of parallel jobs (optional)
|
|
63
65
|
* @returns Exit code
|
|
64
66
|
*/
|
|
65
67
|
export declare function build(engineDir: string, jobs?: number): Promise<number>;
|
|
66
68
|
/**
|
|
67
|
-
* Runs a fast UI-only build.
|
|
69
|
+
* Runs a fast UI-only build. On a non-zero exit, any matched error hints are
|
|
70
|
+
* surfaced on top of the raw mach output.
|
|
68
71
|
* @param engineDir - Path to the engine directory
|
|
69
72
|
* @returns Exit code
|
|
70
73
|
*/
|
package/dist/src/core/mach.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { MachNotFoundError } from '../errors/build.js';
|
|
4
4
|
import { pathExists } from '../utils/fs.js';
|
|
5
|
+
import { warn } from '../utils/logger.js';
|
|
5
6
|
import { exec, execInherit, execInheritCapture, execStream } from '../utils/process.js';
|
|
7
|
+
import { explainMachError } from './mach-error-hints.js';
|
|
6
8
|
import { getPython } from './mach-python.js';
|
|
7
9
|
// Re-export sub-modules so existing `from './mach.js'` imports keep working.
|
|
8
10
|
export { buildArtifactMismatchMessage, hasBuildArtifacts, } from './mach-build-artifacts.js';
|
|
@@ -109,7 +111,23 @@ export async function bootstrapWithOutput(engineDir) {
|
|
|
109
111
|
return runMachInheritCapture(['bootstrap', '--application-choice', 'browser'], engineDir);
|
|
110
112
|
}
|
|
111
113
|
/**
|
|
112
|
-
*
|
|
114
|
+
* Prints any matched {@link MachErrorHint} hints for the captured stderr.
|
|
115
|
+
* No-op when nothing matches. Always called before a non-zero exit propagates
|
|
116
|
+
* so the hint sits immediately below the raw mach error in the operator's
|
|
117
|
+
* terminal.
|
|
118
|
+
*/
|
|
119
|
+
function surfaceMachErrorHints(stderr) {
|
|
120
|
+
const hints = explainMachError(stderr);
|
|
121
|
+
if (hints.length === 0)
|
|
122
|
+
return;
|
|
123
|
+
for (const hint of hints) {
|
|
124
|
+
warn(`Hint: ${hint}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Runs a full mach build. On a non-zero exit, any matched error hints are
|
|
129
|
+
* surfaced on top of the raw mach output so operators get an actionable
|
|
130
|
+
* nudge alongside the cryptic mozbuild traceback.
|
|
113
131
|
* @param engineDir - Path to the engine directory
|
|
114
132
|
* @param jobs - Number of parallel jobs (optional)
|
|
115
133
|
* @returns Exit code
|
|
@@ -119,15 +137,24 @@ export async function build(engineDir, jobs) {
|
|
|
119
137
|
if (jobs !== undefined) {
|
|
120
138
|
args.push('-j', String(jobs));
|
|
121
139
|
}
|
|
122
|
-
|
|
140
|
+
const result = await runMachInheritCapture(args, engineDir);
|
|
141
|
+
if (result.exitCode !== 0) {
|
|
142
|
+
surfaceMachErrorHints(result.stderr);
|
|
143
|
+
}
|
|
144
|
+
return result.exitCode;
|
|
123
145
|
}
|
|
124
146
|
/**
|
|
125
|
-
* Runs a fast UI-only build.
|
|
147
|
+
* Runs a fast UI-only build. On a non-zero exit, any matched error hints are
|
|
148
|
+
* surfaced on top of the raw mach output.
|
|
126
149
|
* @param engineDir - Path to the engine directory
|
|
127
150
|
* @returns Exit code
|
|
128
151
|
*/
|
|
129
152
|
export async function buildUI(engineDir) {
|
|
130
|
-
|
|
153
|
+
const result = await runMachInheritCapture(['build', 'faster'], engineDir);
|
|
154
|
+
if (result.exitCode !== 0) {
|
|
155
|
+
surfaceMachErrorHints(result.stderr);
|
|
156
|
+
}
|
|
157
|
+
return result.exitCode;
|
|
131
158
|
}
|
|
132
159
|
/**
|
|
133
160
|
* Runs the built browser.
|
|
@@ -6,18 +6,17 @@
|
|
|
6
6
|
* to discover"; this helper surfaces the failure in under a minute with a
|
|
7
7
|
* clear PASS/FAIL line and the tail of the browser's stderr.
|
|
8
8
|
*
|
|
9
|
-
* The probe is
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* the socket proves a handshake payload is being produced.
|
|
9
|
+
* The probe is a cascade of layered checks (engine → mach → python →
|
|
10
|
+
* profile → spawn → handshake). Each layer has a tight per-attempt budget
|
|
11
|
+
* so a broken earlier layer fails fast with a specific diagnosis rather
|
|
12
|
+
* than blocking on the final socket poll for the full overall budget.
|
|
14
13
|
*/
|
|
15
14
|
import { spawn } from 'node:child_process';
|
|
16
15
|
import net from 'node:net';
|
|
17
16
|
export interface MarionettePreflightResult {
|
|
18
17
|
ok: boolean;
|
|
19
18
|
durationMs: number;
|
|
20
|
-
/** Human-readable summary. */
|
|
19
|
+
/** Human-readable summary. On FAIL, prefixed with `[layer N/6: <name>]`. */
|
|
21
20
|
detail: string;
|
|
22
21
|
}
|
|
23
22
|
export interface MarionettePreflightOptions {
|
|
@@ -25,13 +24,21 @@ export interface MarionettePreflightOptions {
|
|
|
25
24
|
timeoutMs?: number;
|
|
26
25
|
/** Overrides marionette TCP port — primarily used in tests. */
|
|
27
26
|
port?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Grace window after spawn() before the browser is considered "running
|
|
29
|
+
* OK." Catches immediate crashes (missing dylib, wrong CPU arch, corrupt
|
|
30
|
+
* profile) at the spawn layer rather than the handshake layer. Default:
|
|
31
|
+
* {@link SPAWN_SETTLE_MS}. Tests may set this to 0 to skip the settle.
|
|
32
|
+
*/
|
|
33
|
+
spawnSettleMs?: number;
|
|
28
34
|
/** Test seam: spawn and socket connect factories. */
|
|
29
35
|
spawner?: typeof spawn;
|
|
30
36
|
connect?: typeof net.createConnection;
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* Runs the marionette preflight. Returns PASS on first byte read from the
|
|
34
|
-
* marionette socket within the budget; FAIL otherwise
|
|
40
|
+
* marionette socket within the budget; FAIL otherwise, with a diagnostic
|
|
41
|
+
* identifying which layer of the cascade broke. Always tears down the
|
|
35
42
|
* spawned browser before returning.
|
|
36
43
|
*/
|
|
37
44
|
export declare function runMarionettePreflight(engineDir: string, options?: MarionettePreflightOptions): Promise<MarionettePreflightResult>;
|
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
* to discover"; this helper surfaces the failure in under a minute with a
|
|
8
8
|
* clear PASS/FAIL line and the tail of the browser's stderr.
|
|
9
9
|
*
|
|
10
|
-
* The probe is
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* the socket proves a handshake payload is being produced.
|
|
10
|
+
* The probe is a cascade of layered checks (engine → mach → python →
|
|
11
|
+
* profile → spawn → handshake). Each layer has a tight per-attempt budget
|
|
12
|
+
* so a broken earlier layer fails fast with a specific diagnosis rather
|
|
13
|
+
* than blocking on the final socket poll for the full overall budget.
|
|
15
14
|
*/
|
|
16
15
|
import { spawn } from 'node:child_process';
|
|
17
16
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
@@ -28,60 +27,112 @@ const MARIONETTE_PORT = 2828;
|
|
|
28
27
|
const DEFAULT_PREFLIGHT_TIMEOUT_MS = 30_000;
|
|
29
28
|
/** Per-attempt socket connect timeout. Polling continues until the overall budget expires. */
|
|
30
29
|
const SOCKET_ATTEMPT_TIMEOUT_MS = 2_000;
|
|
30
|
+
/**
|
|
31
|
+
* Grace window after spawn() returns before we accept the child as
|
|
32
|
+
* "spawned OK". A browser binary that exits immediately (missing dylib,
|
|
33
|
+
* wrong CPU arch, corrupt profile) must be caught here — not 30 seconds
|
|
34
|
+
* later at the socket layer.
|
|
35
|
+
*/
|
|
36
|
+
const SPAWN_SETTLE_MS = 750;
|
|
31
37
|
/** Tail of stderr preserved for FAIL diagnostics. */
|
|
32
38
|
const STDERR_TAIL_LIMIT = 8 * 1024;
|
|
39
|
+
/**
|
|
40
|
+
* Layer names, ordered by the probe sequence. Surfaced in `detail` so the
|
|
41
|
+
* operator sees which layer failed without having to guess.
|
|
42
|
+
*/
|
|
43
|
+
const LAYER_NAMES = [
|
|
44
|
+
'engine-present',
|
|
45
|
+
'mach-available',
|
|
46
|
+
'python-available',
|
|
47
|
+
'profile-creatable',
|
|
48
|
+
'browser-spawns',
|
|
49
|
+
'marionette-handshake',
|
|
50
|
+
];
|
|
51
|
+
function layerTag(name) {
|
|
52
|
+
const index = LAYER_NAMES.indexOf(name) + 1;
|
|
53
|
+
return `[layer ${index}/${LAYER_NAMES.length}: ${name}]`;
|
|
54
|
+
}
|
|
33
55
|
/**
|
|
34
56
|
* Runs the marionette preflight. Returns PASS on first byte read from the
|
|
35
|
-
* marionette socket within the budget; FAIL otherwise
|
|
57
|
+
* marionette socket within the budget; FAIL otherwise, with a diagnostic
|
|
58
|
+
* identifying which layer of the cascade broke. Always tears down the
|
|
36
59
|
* spawned browser before returning.
|
|
37
60
|
*/
|
|
38
61
|
export async function runMarionettePreflight(engineDir, options = {}) {
|
|
39
62
|
const timeoutMs = options.timeoutMs ?? DEFAULT_PREFLIGHT_TIMEOUT_MS;
|
|
63
|
+
const spawnSettleMs = options.spawnSettleMs ?? SPAWN_SETTLE_MS;
|
|
40
64
|
const port = options.port ?? MARIONETTE_PORT;
|
|
41
65
|
const spawnerFn = options.spawner ?? spawn;
|
|
42
66
|
const connectFn = options.connect ?? net.createConnection;
|
|
43
67
|
const startedAt = Date.now();
|
|
44
68
|
const elapsed = () => Date.now() - startedAt;
|
|
69
|
+
// Layer 1: engine directory exists.
|
|
45
70
|
if (!(await pathExists(engineDir))) {
|
|
46
|
-
return
|
|
47
|
-
ok: false,
|
|
48
|
-
durationMs: elapsed(),
|
|
49
|
-
detail: 'Engine directory not found — run "fireforge download" first.',
|
|
50
|
-
};
|
|
71
|
+
return fail('engine-present', 'Engine directory not found — run "fireforge download" first.', elapsed());
|
|
51
72
|
}
|
|
73
|
+
// Layer 2: mach binary resolves in the engine.
|
|
52
74
|
try {
|
|
53
75
|
await ensureMach(engineDir);
|
|
54
76
|
}
|
|
55
77
|
catch (error) {
|
|
56
|
-
return {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
return fail('mach-available', `mach not available in engine: ${error.message}`, elapsed());
|
|
79
|
+
}
|
|
80
|
+
// Layer 3: Python that mach requires is discoverable.
|
|
81
|
+
let python;
|
|
82
|
+
try {
|
|
83
|
+
python = await getPython(engineDir);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
return fail('python-available', `Python interpreter required by mach is not available: ${error.message}`, elapsed());
|
|
87
|
+
}
|
|
88
|
+
// Layer 4: throwaway browser profile directory is creatable.
|
|
89
|
+
let profileDir;
|
|
90
|
+
try {
|
|
91
|
+
profileDir = await mkdtemp(join(tmpdir(), 'fireforge-marionette-'));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
return fail('profile-creatable', `Could not create a throwaway browser profile in ${tmpdir()}: ${error.message}`, elapsed());
|
|
61
95
|
}
|
|
62
|
-
const python = await getPython(engineDir);
|
|
63
|
-
const profileDir = await mkdtemp(join(tmpdir(), 'fireforge-marionette-'));
|
|
64
96
|
let child;
|
|
65
97
|
let stderrTail = '';
|
|
66
98
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
99
|
+
// Layer 5: browser spawns and does not crash within the settle window.
|
|
100
|
+
try {
|
|
101
|
+
child = spawnerFn(python, [
|
|
102
|
+
join(engineDir, 'mach'),
|
|
103
|
+
'run',
|
|
104
|
+
'--marionette',
|
|
105
|
+
'--headless',
|
|
106
|
+
'--no-remote',
|
|
107
|
+
'-profile',
|
|
108
|
+
profileDir,
|
|
109
|
+
], {
|
|
110
|
+
cwd: engineDir,
|
|
111
|
+
env: { ...process.env, MOZ_HEADLESS: '1' },
|
|
112
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return fail('browser-spawns', `Could not spawn mach run: ${error.message}`, elapsed());
|
|
117
|
+
}
|
|
118
|
+
const spawnedChild = child;
|
|
80
119
|
child.stderr?.on('data', (data) => {
|
|
81
120
|
const chunk = data.toString();
|
|
82
121
|
stderrTail = (stderrTail + chunk).slice(-STDERR_TAIL_LIMIT);
|
|
83
122
|
});
|
|
84
|
-
|
|
123
|
+
// Short settle window — catches "binary exits immediately" failures
|
|
124
|
+
// (missing dylib, wrong CPU arch, corrupt profile) before the socket
|
|
125
|
+
// poll swallows the full overall budget waiting for bytes that will
|
|
126
|
+
// never come.
|
|
127
|
+
const settleDeadline = Math.min(spawnSettleMs, Math.max(0, timeoutMs - elapsed()));
|
|
128
|
+
if (settleDeadline > 0) {
|
|
129
|
+
await delay(settleDeadline);
|
|
130
|
+
}
|
|
131
|
+
if (hasChildExited(spawnedChild)) {
|
|
132
|
+
return fail('browser-spawns', `Browser process exited during spawn (exit code ${String(spawnedChild.exitCode)}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
|
|
133
|
+
`stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`, elapsed());
|
|
134
|
+
}
|
|
135
|
+
// Layer 6: marionette handshake within the remaining budget.
|
|
85
136
|
const socketResult = await waitForMarionetteSocket(port, connectFn, () => {
|
|
86
137
|
return elapsed() < timeoutMs && !hasChildExited(spawnedChild);
|
|
87
138
|
});
|
|
@@ -95,19 +146,11 @@ export async function runMarionettePreflight(engineDir, options = {}) {
|
|
|
95
146
|
// Child may have exited before the socket was ever ready — surface that
|
|
96
147
|
// distinctly from "socket never answered" so the operator has a lead.
|
|
97
148
|
if (hasChildExited(spawnedChild)) {
|
|
98
|
-
return {
|
|
99
|
-
|
|
100
|
-
durationMs: elapsed(),
|
|
101
|
-
detail: `Browser process exited before marionette handshake (exit code ${String(spawnedChild.exitCode)}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
|
|
102
|
-
`stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`,
|
|
103
|
-
};
|
|
149
|
+
return fail('marionette-handshake', `Browser process exited before marionette handshake (exit code ${String(spawnedChild.exitCode)}, signal ${spawnedChild.signalCode ?? 'none'}). ` +
|
|
150
|
+
`stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`, elapsed());
|
|
104
151
|
}
|
|
105
|
-
return {
|
|
106
|
-
|
|
107
|
-
durationMs: elapsed(),
|
|
108
|
-
detail: `Marionette socket on 127.0.0.1:${port} did not respond within ${timeoutMs}ms. ` +
|
|
109
|
-
`stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`,
|
|
110
|
-
};
|
|
152
|
+
return fail('marionette-handshake', `Marionette socket on 127.0.0.1:${port} did not respond within ${timeoutMs}ms. ` +
|
|
153
|
+
`stderr tail: ${stderrTail.trim().slice(-2_000) || '(empty)'}`, elapsed());
|
|
111
154
|
}
|
|
112
155
|
finally {
|
|
113
156
|
if (child && !hasChildExited(child)) {
|
|
@@ -137,6 +180,13 @@ export async function runMarionettePreflight(engineDir, options = {}) {
|
|
|
137
180
|
}
|
|
138
181
|
}
|
|
139
182
|
}
|
|
183
|
+
function fail(layer, message, durationMs) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
durationMs,
|
|
187
|
+
detail: `${layerTag(layer)} ${message}`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
140
190
|
/** Returns true when the child process has exited (normal or signaled). */
|
|
141
191
|
function hasChildExited(child) {
|
|
142
192
|
return child.exitCode !== null || child.signalCode !== null;
|
|
@@ -91,7 +91,7 @@ export declare function collectNewFileCreatorsByPath(ctx: PatchQueueContext): Ma
|
|
|
91
91
|
/**
|
|
92
92
|
* Cross-patch lint rule: the same path is newly created (`--- /dev/null →
|
|
93
93
|
* +++ b/path`) by more than one patch. This is the failure mode that
|
|
94
|
-
* motivated the rule —
|
|
94
|
+
* motivated the rule — a fork landed three patches each trying to create
|
|
95
95
|
* the same file, and the error surfaced only when import rolled back
|
|
96
96
|
* mid-apply.
|
|
97
97
|
*
|
|
@@ -114,7 +114,7 @@ export function collectNewFileCreatorsByPath(ctx) {
|
|
|
114
114
|
/**
|
|
115
115
|
* Cross-patch lint rule: the same path is newly created (`--- /dev/null →
|
|
116
116
|
* +++ b/path`) by more than one patch. This is the failure mode that
|
|
117
|
-
* motivated the rule —
|
|
117
|
+
* motivated the rule — a fork landed three patches each trying to create
|
|
118
118
|
* the same file, and the error surfaced only when import rolled back
|
|
119
119
|
* mid-apply.
|
|
120
120
|
*
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff-scoping for `fireforge lint`.
|
|
3
|
+
*
|
|
4
|
+
* Pre-existing patch-state errors and errors introduced by the current task
|
|
5
|
+
* print identically today, so triaging "is the diff I just produced clean?"
|
|
6
|
+
* requires mentally subtracting the pre-existing noise. This module
|
|
7
|
+
* classifies each lint issue as either `introduced` (the file was touched
|
|
8
|
+
* since the user-supplied git revision) or `cumulative` (pre-existing
|
|
9
|
+
* drift), without changing what the underlying rules emit.
|
|
10
|
+
*/
|
|
11
|
+
import type { PatchLintIssue } from '../types/commands/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* Collects engine-relative paths that changed between `rev` and `HEAD`,
|
|
14
|
+
* plus any workdir modifications and untracked files. An empty set means
|
|
15
|
+
* "no diff we can prove" — downstream treats every issue as `cumulative`
|
|
16
|
+
* in that case (operator ran `lint --since HEAD` with no pending work).
|
|
17
|
+
* @param engineDir Path to the engine git repository.
|
|
18
|
+
* @param rev Git revision to diff against (e.g. `HEAD`, a branch, a SHA).
|
|
19
|
+
*/
|
|
20
|
+
export declare function collectDiffFilePaths(engineDir: string, rev: string): Promise<Set<string>>;
|
|
21
|
+
/**
|
|
22
|
+
* Annotates a list of lint issues with `introduced` / `cumulative` tags
|
|
23
|
+
* based on whether the issue's file is part of the supplied diff set.
|
|
24
|
+
* Mutates each issue in place AND returns the list for chaining.
|
|
25
|
+
*
|
|
26
|
+
* Issues with no file (`issue.file === ''`) — e.g. cross-patch rules that
|
|
27
|
+
* describe queue-wide state — are always `cumulative` under `--since`
|
|
28
|
+
* because they describe drift accumulated across many commits, not a
|
|
29
|
+
* single current-task edit.
|
|
30
|
+
* @param issues Issues returned by the lint orchestrator.
|
|
31
|
+
* @param diffFiles File paths touched since the user's revision.
|
|
32
|
+
*/
|
|
33
|
+
export declare function tagLintIssues(issues: PatchLintIssue[], diffFiles: Set<string>): PatchLintIssue[];
|