@hominis/fireforge 0.15.7 → 0.15.9
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 +44 -0
- package/README.md +103 -12
- package/dist/src/commands/export-shared.d.ts +6 -1
- package/dist/src/commands/export-shared.js +7 -2
- package/dist/src/commands/furnace/create-dry-run.d.ts +7 -0
- package/dist/src/commands/furnace/create-dry-run.js +7 -2
- package/dist/src/commands/furnace/create-features.d.ts +24 -0
- package/dist/src/commands/furnace/create-features.js +56 -0
- package/dist/src/commands/furnace/create-templates.d.ts +9 -5
- package/dist/src/commands/furnace/create-templates.js +14 -6
- package/dist/src/commands/furnace/create.js +34 -39
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/lint.d.ts +20 -0
- package/dist/src/commands/lint.js +157 -44
- package/dist/src/commands/re-export-files.js +6 -2
- package/dist/src/commands/re-export.js +37 -4
- package/dist/src/commands/run.d.ts +15 -1
- package/dist/src/commands/run.js +202 -7
- package/dist/src/commands/test.js +97 -2
- package/dist/src/core/furnace-apply-ftl.d.ts +5 -3
- package/dist/src/core/furnace-apply-ftl.js +6 -2
- package/dist/src/core/furnace-apply-helpers.js +14 -4
- package/dist/src/core/furnace-config-custom.d.ts +14 -0
- package/dist/src/core/furnace-config-custom.js +64 -0
- package/dist/src/core/furnace-config.js +2 -39
- package/dist/src/core/furnace-validate-accessibility.d.ts +9 -2
- package/dist/src/core/furnace-validate-accessibility.js +17 -3
- package/dist/src/core/furnace-validate-helpers.d.ts +13 -1
- package/dist/src/core/furnace-validate-helpers.js +19 -0
- package/dist/src/core/furnace-validate-structure.js +6 -2
- package/dist/src/core/furnace-validate.js +6 -3
- package/dist/src/core/mach.d.ts +26 -0
- package/dist/src/core/mach.js +25 -1
- package/dist/src/core/patch-lint.d.ts +6 -1
- package/dist/src/core/patch-lint.js +14 -1
- package/dist/src/core/shared-ftl.d.ts +28 -0
- package/dist/src/core/shared-ftl.js +42 -0
- package/dist/src/core/smoke-patterns.d.ts +45 -0
- package/dist/src/core/smoke-patterns.js +100 -0
- package/dist/src/core/xpcshell-appdir.d.ts +143 -0
- package/dist/src/core/xpcshell-appdir.js +273 -0
- package/dist/src/errors/codes.d.ts +13 -0
- package/dist/src/errors/codes.js +13 -0
- package/dist/src/errors/run.d.ts +16 -0
- package/dist/src/errors/run.js +22 -0
- package/dist/src/types/commands/options.d.ts +58 -0
- package/dist/src/types/commands/patches.d.ts +22 -0
- package/dist/src/types/furnace.d.ts +39 -0
- package/dist/src/utils/process.d.ts +63 -0
- package/dist/src/utils/process.js +122 -0
- package/package.json +1 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Auto-injects `--app-path=<abs>` into `mach test` invocations whose nearest
|
|
4
|
+
* xpcshell.toml sets `firefox-appdir = "browser"` (or `<appname>-appdir = …`)
|
|
5
|
+
* but whose `appname` is not `firefox`.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this exists
|
|
8
|
+
*
|
|
9
|
+
* The upstream xpcshell harness computes the manifest key for the appdir
|
|
10
|
+
* override as `mozInfo["appname"] + "-appdir"`. On a stock Firefox build the
|
|
11
|
+
* key is `firefox-appdir`, so the very common `firefox-appdir = "browser"`
|
|
12
|
+
* directive is honoured. On a rebranded fork (appname=`hominis`,
|
|
13
|
+
* `mybrowser`, …) the harness looks for `hominis-appdir` / `mybrowser-appdir`
|
|
14
|
+
* — the literal `firefox-appdir` line is silently ignored, `appPath` falls
|
|
15
|
+
* back to `xrePath`, and every `resource:///modules/…` import throws
|
|
16
|
+
* `Failed to load resource:///modules/<name>.sys.mjs` because xpcshell now
|
|
17
|
+
* resolves the `resource:///` prefix one level above the real app root.
|
|
18
|
+
*
|
|
19
|
+
* ## Strategy
|
|
20
|
+
*
|
|
21
|
+
* 1. For each test path the operator handed us, find the nearest
|
|
22
|
+
* `xpcshell.toml`. If none exists, the test is not an xpcshell test and
|
|
23
|
+
* nothing to inject.
|
|
24
|
+
* 2. Read the manifest's `[DEFAULT]` section. Look for `<appname>-appdir`
|
|
25
|
+
* first — if present, the harness already finds it and there's nothing to
|
|
26
|
+
* do. Fall back to `firefox-appdir`. This ordering matches upstream
|
|
27
|
+
* precedence and avoids overriding an operator who already migrated.
|
|
28
|
+
* 3. If only `firefox-appdir` is present and `appname != "firefox"`, compute
|
|
29
|
+
* the absolute app dir path against the active `obj-X/dist` tree
|
|
30
|
+
* (probing `dist/bin/<value>` first, then any `dist/<bundle>.app/Contents/
|
|
31
|
+
* Resources/<value>` for the macOS packaged layout) and return it as
|
|
32
|
+
* the value to pass to `--app-path`.
|
|
33
|
+
* 4. If multiple test paths disagree on the resolved value (e.g. one
|
|
34
|
+
* manifest sets `browser`, another sets `xulrunner`), refuse injection
|
|
35
|
+
* and return null — the operator can drop down to `--mach-arg`.
|
|
36
|
+
*
|
|
37
|
+
* Operator escape hatches: `--mach-arg=--app-path=…` always wins (handled in
|
|
38
|
+
* test.ts; we skip injection when `--app-path=` already appears in the
|
|
39
|
+
* forwarded args).
|
|
40
|
+
*/
|
|
41
|
+
import { readdir } from 'node:fs/promises';
|
|
42
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
43
|
+
import { pathExists, readJson, readText } from '../utils/fs.js';
|
|
44
|
+
import { isObject, isString } from '../utils/validation.js';
|
|
45
|
+
/**
|
|
46
|
+
* `[DEFAULT]` section parser shaped to the narrow case we need: pull a
|
|
47
|
+
* single key/value out without depending on a real TOML parser. Avoids
|
|
48
|
+
* pulling a TOML dep into the test path for a one-shot lookup.
|
|
49
|
+
*
|
|
50
|
+
* Accepts:
|
|
51
|
+
* - Single- or double-quoted values
|
|
52
|
+
* - Whitespace either side of `=`
|
|
53
|
+
* - Continuation comments (`#` or `;`) at the end of the line
|
|
54
|
+
* - Bare unquoted bareword values (e.g. `firefox-appdir = browser`) — some
|
|
55
|
+
* operators omit the quotes and the harness honours either form.
|
|
56
|
+
*
|
|
57
|
+
* Returns `undefined` when the key is absent or sits outside `[DEFAULT]`.
|
|
58
|
+
*/
|
|
59
|
+
export function parseAppdirFromToml(tomlText, key) {
|
|
60
|
+
const lines = tomlText.split(/\r?\n/);
|
|
61
|
+
let inDefault = false;
|
|
62
|
+
let sectionSeen = false;
|
|
63
|
+
// Anchored on the start of the line so a `# firefox-appdir = "…"`-style
|
|
64
|
+
// comment further along the file is not mistaken for the directive.
|
|
65
|
+
const escapedKey = escapeRegex(key);
|
|
66
|
+
const keyPattern = new RegExp('^\\s*' + escapedKey + '\\s*=\\s*(.+?)\\s*(?:[#;].*)?$');
|
|
67
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
68
|
+
const line = lines[i] ?? '';
|
|
69
|
+
const sectionMatch = /^\s*\[([^\]]+)\]\s*$/.exec(line);
|
|
70
|
+
if (sectionMatch) {
|
|
71
|
+
sectionSeen = true;
|
|
72
|
+
inDefault = sectionMatch[1]?.trim().toUpperCase() === 'DEFAULT';
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// The implicit pre-section region of an xpcshell.toml is treated as
|
|
76
|
+
// [DEFAULT] by the upstream parser, so we honour the same convention.
|
|
77
|
+
const inImplicitDefault = !sectionSeen;
|
|
78
|
+
if (!inDefault && !inImplicitDefault)
|
|
79
|
+
continue;
|
|
80
|
+
const match = keyPattern.exec(line);
|
|
81
|
+
if (!match)
|
|
82
|
+
continue;
|
|
83
|
+
const raw = (match[1] ?? '').trim();
|
|
84
|
+
const value = stripQuotes(raw);
|
|
85
|
+
if (value === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
return { value, lineIndex: i };
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
function escapeRegex(input) {
|
|
92
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
93
|
+
}
|
|
94
|
+
function stripQuotes(raw) {
|
|
95
|
+
if (raw.length === 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
98
|
+
return raw.slice(1, -1);
|
|
99
|
+
}
|
|
100
|
+
// Bareword: must not contain whitespace; otherwise we are looking at
|
|
101
|
+
// commentary that the regex's optional comment tail did not strip.
|
|
102
|
+
if (/\s/.test(raw))
|
|
103
|
+
return undefined;
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Walks up from `startPath` (a file or directory under `engineDir`) and
|
|
108
|
+
* returns the absolute path of the first sibling `xpcshell.toml` found.
|
|
109
|
+
* Stops at `engineDir` (inclusive) and returns null on miss.
|
|
110
|
+
*
|
|
111
|
+
* Special-cases `startPath` itself when it already ends with
|
|
112
|
+
* `xpcshell.toml` — operators sometimes pass a manifest path directly.
|
|
113
|
+
*/
|
|
114
|
+
export async function findNearestXpcshellManifest(engineDir, startPath) {
|
|
115
|
+
const absStart = resolve(engineDir, startPath);
|
|
116
|
+
if (absStart.toLowerCase().endsWith(`${sep}xpcshell.toml`)) {
|
|
117
|
+
return (await pathExists(absStart)) ? absStart : null;
|
|
118
|
+
}
|
|
119
|
+
const engineAbs = resolve(engineDir);
|
|
120
|
+
let current = absStart;
|
|
121
|
+
// First iteration walks down to a directory; subsequent ones walk up.
|
|
122
|
+
// Cap iterations defensively — a pathological symlink loop would
|
|
123
|
+
// otherwise spin until the call stack overflows.
|
|
124
|
+
for (let i = 0; i < 64; i += 1) {
|
|
125
|
+
const dir = i === 0 ? dirname(absStart) : dirname(current);
|
|
126
|
+
const candidate = join(dir, 'xpcshell.toml');
|
|
127
|
+
if (await pathExists(candidate))
|
|
128
|
+
return candidate;
|
|
129
|
+
if (dir === engineAbs || dir === dirname(dir))
|
|
130
|
+
return null;
|
|
131
|
+
current = dir;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Reads `<objDir>/mozinfo.json` for the active app name. Returns
|
|
137
|
+
* `"firefox"` when mozinfo cannot be read or the field is missing — that
|
|
138
|
+
* is the safe default because it matches stock Firefox behaviour and
|
|
139
|
+
* means the resolver will not inject anything (the manifest's
|
|
140
|
+
* `firefox-appdir` value WILL be honoured by the upstream harness when
|
|
141
|
+
* appname is firefox).
|
|
142
|
+
*/
|
|
143
|
+
export async function readMozinfoAppname(objDirPath) {
|
|
144
|
+
const mozinfoPath = join(objDirPath, 'mozinfo.json');
|
|
145
|
+
if (!(await pathExists(mozinfoPath)))
|
|
146
|
+
return 'firefox';
|
|
147
|
+
try {
|
|
148
|
+
const data = await readJson(mozinfoPath);
|
|
149
|
+
if (isObject(data) && isString(data['appname'])) {
|
|
150
|
+
return data['appname'];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// Malformed mozinfo is a build-system problem out of scope for the
|
|
155
|
+
// appdir resolver; treat as if appname were missing.
|
|
156
|
+
}
|
|
157
|
+
return 'firefox';
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Probes the obj-dir's `dist/` subtree for the absolute path that the
|
|
161
|
+
* harness would have computed if the manifest key had been honoured.
|
|
162
|
+
* Returns null when no candidate exists — better to skip injection
|
|
163
|
+
* silently than to point the harness at a path that doesn't exist
|
|
164
|
+
* (which fails with a different error than the original `firefox-appdir`
|
|
165
|
+
* symptom and confuses triage).
|
|
166
|
+
*
|
|
167
|
+
* Probe order matches the on-disk layouts FireForge supports today:
|
|
168
|
+
* 1. `<objDir>/dist/bin/<value>` — Linux primary, also macOS via the
|
|
169
|
+
* `dist/bin -> dist/<App>.app/Contents/MacOS/` symlink.
|
|
170
|
+
* 2. `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` — macOS
|
|
171
|
+
* packaged layout, where `dist/bin/` may not exist as a directory.
|
|
172
|
+
*/
|
|
173
|
+
export async function resolveAbsoluteAppPath(objDirAbs, relativeAppdir) {
|
|
174
|
+
const distBinCandidate = join(objDirAbs, 'dist', 'bin', relativeAppdir);
|
|
175
|
+
if (await pathExists(distBinCandidate))
|
|
176
|
+
return distBinCandidate;
|
|
177
|
+
const distDir = join(objDirAbs, 'dist');
|
|
178
|
+
if (!(await pathExists(distDir)))
|
|
179
|
+
return null;
|
|
180
|
+
let entries;
|
|
181
|
+
try {
|
|
182
|
+
entries = await readdir(distDir);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
for (const entry of entries) {
|
|
188
|
+
if (!entry.endsWith('.app'))
|
|
189
|
+
continue;
|
|
190
|
+
const candidate = join(distDir, entry, 'Contents', 'Resources', relativeAppdir);
|
|
191
|
+
if (await pathExists(candidate))
|
|
192
|
+
return candidate;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Top-level resolver. Walks every test path, reads the nearest
|
|
198
|
+
* xpcshell.toml, and returns the single absolute path to inject (or a
|
|
199
|
+
* structured "no injection" outcome). Never throws — every fs / parse
|
|
200
|
+
* error is folded into a `none` outcome so the test command always falls
|
|
201
|
+
* through to the diagnostic hint instead of dying inside a helper.
|
|
202
|
+
*/
|
|
203
|
+
export async function resolveXpcshellAppdirArg(engineDir, testPaths, objDirName) {
|
|
204
|
+
if (testPaths.length === 0)
|
|
205
|
+
return { kind: 'none' };
|
|
206
|
+
const objDirAbs = resolve(engineDir, objDirName);
|
|
207
|
+
const appname = await readMozinfoAppname(objDirAbs);
|
|
208
|
+
// When appname IS "firefox" the upstream harness reads `firefox-appdir`
|
|
209
|
+
// natively. Injecting in that case would be a no-op at best and an
|
|
210
|
+
// override at worst, so bail out before doing any IO per-path.
|
|
211
|
+
if (appname === 'firefox')
|
|
212
|
+
return { kind: 'none' };
|
|
213
|
+
const appnameKey = `${appname}-appdir`;
|
|
214
|
+
const seenInjections = new Map();
|
|
215
|
+
for (const testPath of testPaths) {
|
|
216
|
+
const manifestPath = await findNearestXpcshellManifest(engineDir, testPath);
|
|
217
|
+
if (!manifestPath)
|
|
218
|
+
continue;
|
|
219
|
+
let body;
|
|
220
|
+
try {
|
|
221
|
+
body = await readText(manifestPath);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// Operator already migrated — harness will read the appname-keyed
|
|
227
|
+
// value directly. Nothing to do.
|
|
228
|
+
if (parseAppdirFromToml(body, appnameKey) !== undefined)
|
|
229
|
+
continue;
|
|
230
|
+
const fallback = parseAppdirFromToml(body, 'firefox-appdir');
|
|
231
|
+
if (fallback === undefined)
|
|
232
|
+
continue;
|
|
233
|
+
const absolute = await resolveAbsoluteAppPath(objDirAbs, fallback.value);
|
|
234
|
+
if (!absolute) {
|
|
235
|
+
return {
|
|
236
|
+
kind: 'unresolved',
|
|
237
|
+
relativeAppdir: fallback.value,
|
|
238
|
+
manifestPath,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
seenInjections.set(absolute, {
|
|
242
|
+
appPath: absolute,
|
|
243
|
+
manifestPath,
|
|
244
|
+
key: 'firefox-appdir',
|
|
245
|
+
relativeAppdir: fallback.value,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (seenInjections.size === 0)
|
|
249
|
+
return { kind: 'none' };
|
|
250
|
+
if (seenInjections.size > 1) {
|
|
251
|
+
return { kind: 'mismatch', values: Array.from(seenInjections.keys()) };
|
|
252
|
+
}
|
|
253
|
+
const [result] = seenInjections.values();
|
|
254
|
+
// Map.size === 1 was just checked, so result is defined.
|
|
255
|
+
return { kind: 'injected', result: result };
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Returns true when the operator already passed `--app-path=` (or its
|
|
259
|
+
* `--app-path <value>` two-token form) through `--mach-arg`. Used by the
|
|
260
|
+
* test command to skip auto-injection so the operator override always
|
|
261
|
+
* wins.
|
|
262
|
+
*/
|
|
263
|
+
export function operatorAlreadySetAppPath(extraArgs) {
|
|
264
|
+
for (let i = 0; i < extraArgs.length; i += 1) {
|
|
265
|
+
const arg = extraArgs[i] ?? '';
|
|
266
|
+
if (arg === '--app-path' && i + 1 < extraArgs.length)
|
|
267
|
+
return true;
|
|
268
|
+
if (arg.startsWith('--app-path='))
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=xpcshell-appdir.js.map
|
|
@@ -25,5 +25,18 @@ export declare const ExitCode: {
|
|
|
25
25
|
readonly FURNACE_ERROR: 9;
|
|
26
26
|
/** Patch conflict resolution error */
|
|
27
27
|
readonly RESOLUTION_ERROR: 10;
|
|
28
|
+
/**
|
|
29
|
+
* `fireforge run --smoke-exit` observed one or more unallowed console
|
|
30
|
+
* error lines inside the smoke window. Distinct from BUILD_ERROR so CI
|
|
31
|
+
* can route smoke regressions separately from compile/config failures.
|
|
32
|
+
*/
|
|
33
|
+
readonly SMOKE_EXIT_FAILURE: 12;
|
|
34
|
+
/**
|
|
35
|
+
* `fireforge run --smoke-exit` saw the browser exit with a non-clean
|
|
36
|
+
* status before the smoke window elapsed — a launch-side failure that
|
|
37
|
+
* did NOT surface as a console error line (crash before console wiring,
|
|
38
|
+
* missing profile, etc.).
|
|
39
|
+
*/
|
|
40
|
+
readonly SMOKE_LAUNCH_FAILURE: 13;
|
|
28
41
|
};
|
|
29
42
|
export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
|
package/dist/src/errors/codes.js
CHANGED
|
@@ -26,5 +26,18 @@ export const ExitCode = {
|
|
|
26
26
|
FURNACE_ERROR: 9,
|
|
27
27
|
/** Patch conflict resolution error */
|
|
28
28
|
RESOLUTION_ERROR: 10,
|
|
29
|
+
/**
|
|
30
|
+
* `fireforge run --smoke-exit` observed one or more unallowed console
|
|
31
|
+
* error lines inside the smoke window. Distinct from BUILD_ERROR so CI
|
|
32
|
+
* can route smoke regressions separately from compile/config failures.
|
|
33
|
+
*/
|
|
34
|
+
SMOKE_EXIT_FAILURE: 12,
|
|
35
|
+
/**
|
|
36
|
+
* `fireforge run --smoke-exit` saw the browser exit with a non-clean
|
|
37
|
+
* status before the smoke window elapsed — a launch-side failure that
|
|
38
|
+
* did NOT surface as a console error line (crash before console wiring,
|
|
39
|
+
* missing profile, etc.).
|
|
40
|
+
*/
|
|
41
|
+
SMOKE_LAUNCH_FAILURE: 13,
|
|
29
42
|
};
|
|
30
43
|
//# sourceMappingURL=codes.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FireForgeError } from './base.js';
|
|
2
|
+
import { ExitCode } from './codes.js';
|
|
3
|
+
/**
|
|
4
|
+
* Error raised by `fireforge run --smoke-exit` when the captured console
|
|
5
|
+
* stream produced one or more error lines that did NOT match the
|
|
6
|
+
* configured allowlist.
|
|
7
|
+
*
|
|
8
|
+
* Distinct from `BuildError` so CI pipelines can route smoke failures
|
|
9
|
+
* differently from build failures and so the exit code is the smoke-run
|
|
10
|
+
* contract's `SMOKE_EXIT_FAILURE` rather than the generic `BUILD_ERROR`.
|
|
11
|
+
*/
|
|
12
|
+
export declare class SmokeRunError extends FireForgeError {
|
|
13
|
+
readonly code: ExitCode;
|
|
14
|
+
constructor(message: string, exitCode: ExitCode, cause?: Error);
|
|
15
|
+
get userMessage(): string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { FireForgeError } from './base.js';
|
|
3
|
+
/**
|
|
4
|
+
* Error raised by `fireforge run --smoke-exit` when the captured console
|
|
5
|
+
* stream produced one or more error lines that did NOT match the
|
|
6
|
+
* configured allowlist.
|
|
7
|
+
*
|
|
8
|
+
* Distinct from `BuildError` so CI pipelines can route smoke failures
|
|
9
|
+
* differently from build failures and so the exit code is the smoke-run
|
|
10
|
+
* contract's `SMOKE_EXIT_FAILURE` rather than the generic `BUILD_ERROR`.
|
|
11
|
+
*/
|
|
12
|
+
export class SmokeRunError extends FireForgeError {
|
|
13
|
+
code;
|
|
14
|
+
constructor(message, exitCode, cause) {
|
|
15
|
+
super(message, cause);
|
|
16
|
+
this.code = exitCode;
|
|
17
|
+
}
|
|
18
|
+
get userMessage() {
|
|
19
|
+
return `Smoke run failed: ${this.message}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -155,6 +155,16 @@ export interface ReExportOptions {
|
|
|
155
155
|
yes?: boolean;
|
|
156
156
|
/** Bypass cross-patch lint refusal on projected shrink state */
|
|
157
157
|
forceUnsafe?: boolean;
|
|
158
|
+
/**
|
|
159
|
+
* After every selected patch re-exports cleanly, stamp each re-exported
|
|
160
|
+
* patch's `sourceEsrVersion` in `patches.json` to the current
|
|
161
|
+
* `firefox.version` from `fireforge.json`. Opt-in because the default
|
|
162
|
+
* contract of `re-export` is "refresh the patch body and filesAffected";
|
|
163
|
+
* version stamping is normally a `rebase` responsibility. Use this when
|
|
164
|
+
* re-exporting after a manual Firefox bump that did not go through
|
|
165
|
+
* `rebase`.
|
|
166
|
+
*/
|
|
167
|
+
stamp?: boolean;
|
|
158
168
|
}
|
|
159
169
|
/**
|
|
160
170
|
* Options for the rebase command.
|
|
@@ -177,6 +187,39 @@ export interface RebaseOptions {
|
|
|
177
187
|
export interface RunOptions {
|
|
178
188
|
/** Additional arguments to pass to the browser */
|
|
179
189
|
args?: string[];
|
|
190
|
+
/**
|
|
191
|
+
* Enable smoke-run mode. Launches the browser, streams the console,
|
|
192
|
+
* sends SIGTERM to the whole process group after `smokeExit` seconds,
|
|
193
|
+
* and applies the smoke exit contract:
|
|
194
|
+
* - `0` — clean window (no unallowed error lines).
|
|
195
|
+
* - `ExitCode.SMOKE_EXIT_FAILURE` (12) — one or more console lines
|
|
196
|
+
* matched the error heuristic and were not covered by the allowlist.
|
|
197
|
+
* - `ExitCode.SMOKE_LAUNCH_FAILURE` (13) — the browser exited with a
|
|
198
|
+
* non-clean status before the smoke window elapsed (launch-side
|
|
199
|
+
* failure we cannot observe as a console line — crash before console
|
|
200
|
+
* wiring, missing profile, etc.).
|
|
201
|
+
*
|
|
202
|
+
* POSIX only (process-group semantics do not map cleanly onto Windows);
|
|
203
|
+
* `runSmokeExit` rejects the flag up front on `win32`.
|
|
204
|
+
*/
|
|
205
|
+
smokeExit?: number;
|
|
206
|
+
/**
|
|
207
|
+
* Repeatable regex patterns that mark a matching console line as
|
|
208
|
+
* benign. Matches are still counted for the summary but do not drive
|
|
209
|
+
* the smoke-run exit code.
|
|
210
|
+
*/
|
|
211
|
+
consoleAllow?: string[];
|
|
212
|
+
/**
|
|
213
|
+
* Path to a newline-delimited allowlist regex file. Blank lines and
|
|
214
|
+
* `#` comments are ignored; each remaining line is compiled as a
|
|
215
|
+
* regex and appended to the active allowlist.
|
|
216
|
+
*/
|
|
217
|
+
consoleAllowFile?: string;
|
|
218
|
+
/**
|
|
219
|
+
* Mirror the captured console output to this file path so agents can
|
|
220
|
+
* inspect the raw stream after smoke-exit returns.
|
|
221
|
+
*/
|
|
222
|
+
captureConsole?: string;
|
|
180
223
|
}
|
|
181
224
|
/**
|
|
182
225
|
* Options for the test command.
|
|
@@ -192,6 +235,13 @@ export interface TestOptions {
|
|
|
192
235
|
* spawned. When no paths are supplied, runs the preflight only and exits.
|
|
193
236
|
*/
|
|
194
237
|
doctor?: boolean;
|
|
238
|
+
/**
|
|
239
|
+
* Extra arguments forwarded verbatim to `mach test` (repeatable). Escape
|
|
240
|
+
* valve for upstream xpcshell/mochitest flags that FireForge does not
|
|
241
|
+
* model directly. Order relative to other flags is preserved; passthrough
|
|
242
|
+
* values appear after `--headless` if both are set.
|
|
243
|
+
*/
|
|
244
|
+
machArg?: string[];
|
|
195
245
|
}
|
|
196
246
|
/**
|
|
197
247
|
* Options for the furnace apply command.
|
|
@@ -306,6 +356,14 @@ export interface FurnaceCreateOptions {
|
|
|
306
356
|
testStyle?: 'mochikit' | 'browser-chrome' | 'xpcshell';
|
|
307
357
|
/** Stock component tag names composed internally by this component */
|
|
308
358
|
compose?: string[];
|
|
359
|
+
/**
|
|
360
|
+
* Participate in a pre-existing feature-scoped Fluent bundle at this
|
|
361
|
+
* path (as used by `insertFTLIfNeeded`, e.g. `browser/hominis-dock.ftl`)
|
|
362
|
+
* instead of scaffolding a per-component `.ftl`. Implies `localized`.
|
|
363
|
+
* Persists onto the furnace.json entry so validation and apply skip the
|
|
364
|
+
* per-component paths.
|
|
365
|
+
*/
|
|
366
|
+
sharedFtl?: string;
|
|
309
367
|
/**
|
|
310
368
|
* Show the planned file set and furnace.json changes without writing
|
|
311
369
|
* anything. All validation that does not require disk writes (tag name
|
|
@@ -51,6 +51,28 @@ export interface PatchMetadata {
|
|
|
51
51
|
sourceEsrVersion: string;
|
|
52
52
|
/** Array of file paths affected by this patch */
|
|
53
53
|
filesAffected: string[];
|
|
54
|
+
/**
|
|
55
|
+
* Optional per-patch list of lint check IDs to suppress when this patch
|
|
56
|
+
* is the target of `export`, `export-all`, or `re-export`. Exists for
|
|
57
|
+
* the class of patch that is advisory-noisy by nature — a cohesive
|
|
58
|
+
* branding bundle, a localised-resource pack, an auto-generated
|
|
59
|
+
* manifest — where the generic `large-patch-lines` / `large-patch-files`
|
|
60
|
+
* thresholds do not apply but `--skip-lint` (which silences *all*
|
|
61
|
+
* errors, not just the one that does not apply) is too coarse a hammer.
|
|
62
|
+
*
|
|
63
|
+
* Previously the only escape hatches were `--skip-lint` (blunt) or the
|
|
64
|
+
* full `rebase` flow (refreshes the same patch through a code path that
|
|
65
|
+
* silently skips `runPatchLint` — an asymmetry that forced operators
|
|
66
|
+
* through a multi-minute Firefox source re-download just to refresh
|
|
67
|
+
* one patch body).
|
|
68
|
+
*
|
|
69
|
+
* Values are free-form check IDs (e.g. `"large-patch-lines"`,
|
|
70
|
+
* `"large-patch-files"`). Checks not listed here still run normally.
|
|
71
|
+
* An entry for an unknown check ID is a no-op — the patch metadata
|
|
72
|
+
* documents the *intent* to suppress even if the check is later
|
|
73
|
+
* renamed or removed.
|
|
74
|
+
*/
|
|
75
|
+
lintIgnore?: string[];
|
|
54
76
|
}
|
|
55
77
|
/**
|
|
56
78
|
* Schema for patches/patches.json file.
|
|
@@ -50,6 +50,45 @@ export interface CustomComponentConfig {
|
|
|
50
50
|
localized: boolean;
|
|
51
51
|
/** Stock component tag names composed internally by this component */
|
|
52
52
|
composes?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Opts the component out of the `no-keyboard-handler` accessibility check
|
|
55
|
+
* when it wraps a native-interactive inner element that is not tracked in
|
|
56
|
+
* `composes` (for example a hand-authored `<button>` or a non-stock
|
|
57
|
+
* `moz-*` widget). When `true`, the check is skipped even if the template
|
|
58
|
+
* appears to attach `@click` to synthetic markup.
|
|
59
|
+
*
|
|
60
|
+
* Leave unset for the default behavior: the validator still silences the
|
|
61
|
+
* check automatically when any entry in `composes` matches its native-
|
|
62
|
+
* interactive allowlist (e.g. `moz-button`, `moz-toggle`). This flag is
|
|
63
|
+
* only needed when `composes` does not capture the inner element.
|
|
64
|
+
*
|
|
65
|
+
* Operator-asserted: setting this to `true` does not re-check the
|
|
66
|
+
* component, so it can be used to silence genuine findings. Prefer adding
|
|
67
|
+
* the wrapped element to `composes` when that field applies.
|
|
68
|
+
*/
|
|
69
|
+
keyboardCovered?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Path of a pre-existing feature-scoped Fluent bundle this component
|
|
72
|
+
* participates in, in the same form used by `insertFTLIfNeeded` (for
|
|
73
|
+
* example `browser/hominis-dock.ftl`). When set:
|
|
74
|
+
*
|
|
75
|
+
* - `furnace create --localized` does NOT scaffold a per-component
|
|
76
|
+
* `.ftl` stub — the component shares the feature bundle.
|
|
77
|
+
* - The generated `.mjs` calls `insertFTLIfNeeded("<sharedFtl>")` at
|
|
78
|
+
* the shared path instead of the per-component one.
|
|
79
|
+
* - `furnace validate`'s `missing-ftl` structural rule is skipped for
|
|
80
|
+
* the component (there is no `<tag>.ftl` to require).
|
|
81
|
+
* - `furnace apply` does NOT copy a per-component `.ftl` into the FTL
|
|
82
|
+
* tree nor register a new entry in the locale `jar.mn` — the shared
|
|
83
|
+
* file is owned by whoever authored the feature bundle.
|
|
84
|
+
*
|
|
85
|
+
* Requires `localized: true`. Mutually exclusive with the per-component
|
|
86
|
+
* `.ftl` scaffold. Does NOT auto-migrate previous per-component FTL
|
|
87
|
+
* state: flipping an existing component onto `sharedFtl` leaves the
|
|
88
|
+
* prior per-component entry in the engine tree and the locale `jar.mn`
|
|
89
|
+
* until explicitly cleaned up.
|
|
90
|
+
*/
|
|
91
|
+
sharedFtl?: string;
|
|
53
92
|
}
|
|
54
93
|
/**
|
|
55
94
|
* The furnace.json schema.
|
|
@@ -79,6 +79,69 @@ export declare function execInherit(command: string, args: string[], options?: E
|
|
|
79
79
|
export declare function execInheritCapture(command: string, args: string[], options?: ExecOptions & {
|
|
80
80
|
shutdownGraceMs?: number;
|
|
81
81
|
}): Promise<ExecResult>;
|
|
82
|
+
/** Per-line callback for smoke-run stream dispatch. */
|
|
83
|
+
export type SmokeLineCallback = (line: string) => void;
|
|
84
|
+
/** Options for {@link execSmokeRun}. */
|
|
85
|
+
export interface SmokeRunOptions extends ExecOptions {
|
|
86
|
+
/**
|
|
87
|
+
* Hard deadline in milliseconds. When it elapses the child process
|
|
88
|
+
* group is sent SIGTERM and, after `killGraceMs`, SIGKILL. The returned
|
|
89
|
+
* {@link SmokeRunResult.timedOut} is `true` when the deadline fires —
|
|
90
|
+
* callers treat that as a clean smoke window (no child-driven error),
|
|
91
|
+
* not a failure.
|
|
92
|
+
*/
|
|
93
|
+
smokeTimeoutMs: number;
|
|
94
|
+
/**
|
|
95
|
+
* Grace period between SIGTERM and SIGKILL when the deadline fires.
|
|
96
|
+
* Defaults to 10000 ms because Firefox's AsyncShutdown and
|
|
97
|
+
* profileBeforeChange blockers can take ~5–10 s to flush in-memory
|
|
98
|
+
* state. A shorter grace risks corrupting the dev profile mid-quit.
|
|
99
|
+
*/
|
|
100
|
+
killGraceMs?: number;
|
|
101
|
+
/** Invoked once per complete line of stdout. Final partial line is flushed on close. */
|
|
102
|
+
onStdoutLine?: SmokeLineCallback;
|
|
103
|
+
/** Invoked once per complete line of stderr. Final partial line is flushed on close. */
|
|
104
|
+
onStderrLine?: SmokeLineCallback;
|
|
105
|
+
/**
|
|
106
|
+
* Optional writable stream to mirror captured output to (e.g. an
|
|
107
|
+
* operator-supplied `--capture-console` file). Writes happen inline
|
|
108
|
+
* with line dispatch and the stream is NOT closed here — the caller
|
|
109
|
+
* owns its lifecycle.
|
|
110
|
+
*/
|
|
111
|
+
mirror?: {
|
|
112
|
+
stdout?: NodeJS.WritableStream;
|
|
113
|
+
stderr?: NodeJS.WritableStream;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Result of {@link execSmokeRun}. */
|
|
117
|
+
export interface SmokeRunResult extends ExecResult {
|
|
118
|
+
/**
|
|
119
|
+
* `true` when the smoke deadline fired and we SIGTERMed the child
|
|
120
|
+
* ourselves. Callers that want to distinguish "smoke window elapsed
|
|
121
|
+
* cleanly" from "child exited on its own" check this flag — the
|
|
122
|
+
* `exitCode` in the timedOut path is almost always 143 (SIGTERM) and
|
|
123
|
+
* should NOT be treated as a child-driven failure.
|
|
124
|
+
*/
|
|
125
|
+
timedOut: boolean;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Spawns `command` with `args` in its own process group (POSIX), streams
|
|
129
|
+
* stdout/stderr line-by-line to the caller, enforces a deadline by
|
|
130
|
+
* SIGTERMing the whole group when it elapses, and returns the captured
|
|
131
|
+
* output alongside a `timedOut` flag.
|
|
132
|
+
*
|
|
133
|
+
* Process-group semantics matter here because `mach run` execs a Python
|
|
134
|
+
* wrapper that then forks Firefox, which itself spawns content processes.
|
|
135
|
+
* Sending SIGTERM only to the Python PID leaves an orphan Firefox tree
|
|
136
|
+
* behind. Running the child as a process-group leader (`detached: true`
|
|
137
|
+
* on POSIX) and signalling `-pid` routes the kill to every descendant
|
|
138
|
+
* that inherited the group.
|
|
139
|
+
*
|
|
140
|
+
* Windows fallback: `detached: true` does not create an equivalent group
|
|
141
|
+
* there, so we degrade to `child.kill()` and log a best-effort warning
|
|
142
|
+
* via the `onStderrLine` callback if the caller wired one.
|
|
143
|
+
*/
|
|
144
|
+
export declare function execSmokeRun(command: string, args: string[], options: SmokeRunOptions): Promise<SmokeRunResult>;
|
|
82
145
|
/**
|
|
83
146
|
* Finds an executable in the system PATH.
|
|
84
147
|
* @param name - Name of the executable
|