@hominis/fireforge 0.15.7 → 0.15.8
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 +32 -0
- package/README.md +90 -10
- 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/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/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 +48 -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
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
|
-
import { containsHardcodedTemplateText, createIssue, hasAriaRole, hasDelegatesFocusEnabled, hasGenericInteractiveElement, hasPositiveTabindex, hasTemplateClickHandler, hasTemplateClickOnSyntheticInteractive, hasTemplateKeyboardHandler, hasUnlabelledFormInput, } from './furnace-validate-helpers.js';
|
|
4
|
+
import { containsHardcodedTemplateText, createIssue, hasAriaRole, hasDelegatesFocusEnabled, hasGenericInteractiveElement, hasPositiveTabindex, hasTemplateClickHandler, hasTemplateClickOnSyntheticInteractive, hasTemplateKeyboardHandler, hasUnlabelledFormInput, isKeyboardCoveredByComposition, } from './furnace-validate-helpers.js';
|
|
5
5
|
/**
|
|
6
6
|
* Validates accessibility patterns in a component's .mjs file.
|
|
7
7
|
* Checks for ARIA roles, keyboard handlers, l10n, and focus delegation.
|
|
8
|
+
*
|
|
9
|
+
* @param customConfig - When the component is custom, its matching entry
|
|
10
|
+
* from `furnace.json`. Used to skip the `no-keyboard-handler` warning
|
|
11
|
+
* when the component declares keyboard coverage either explicitly
|
|
12
|
+
* (`keyboardCovered: true`) or via `composes` naming a native-interactive
|
|
13
|
+
* inner element. Optional so stock/override callers and test fixtures
|
|
14
|
+
* without config in scope can continue to call without changes.
|
|
8
15
|
*/
|
|
9
|
-
export async function validateAccessibility(componentDir, tagName) {
|
|
16
|
+
export async function validateAccessibility(componentDir, tagName, customConfig) {
|
|
10
17
|
const mjsPath = join(componentDir, `${tagName}.mjs`);
|
|
11
18
|
if (!(await pathExists(mjsPath)))
|
|
12
19
|
return [];
|
|
@@ -22,8 +29,15 @@ export async function validateAccessibility(componentDir, tagName) {
|
|
|
22
29
|
// platform, so a duplicate keyboard handler would usually double-fire.
|
|
23
30
|
// Only flag synthetic markup (e.g. `<div @click>`) where the activation
|
|
24
31
|
// path has to be wired manually.
|
|
32
|
+
//
|
|
33
|
+
// A wrapper component whose `composes` entry lists a native-interactive
|
|
34
|
+
// tag (or that sets `keyboardCovered: true`) is treated the same way:
|
|
35
|
+
// activation flows through the inner element and a duplicate handler on
|
|
36
|
+
// the wrapper would either no-op or fire twice alongside the child's
|
|
37
|
+
// built-in keyboard path.
|
|
25
38
|
const hasClickOnSynthetic = hasTemplateClickOnSyntheticInteractive(content);
|
|
26
|
-
|
|
39
|
+
const keyboardCovered = isKeyboardCoveredByComposition(customConfig);
|
|
40
|
+
if (hasClickOnSynthetic && !hasKeyboardHandler && !keyboardCovered) {
|
|
27
41
|
issues.push(createIssue(tagName, 'warning', 'no-keyboard-handler', 'Interactive element has @click but no keyboard event handler (@keydown/@keypress/@keyup).'));
|
|
28
42
|
}
|
|
29
43
|
if (containsHardcodedTemplateText(content)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentType, FurnaceConfig, ValidationIssue } from '../types/furnace.js';
|
|
1
|
+
import type { ComponentType, CustomComponentConfig, FurnaceConfig, ValidationIssue } from '../types/furnace.js';
|
|
2
2
|
/** Creates a normalized validation issue object. */
|
|
3
3
|
export declare function createIssue(component: string, severity: ValidationIssue['severity'], check: ValidationIssue['check'], message: string): ValidationIssue;
|
|
4
4
|
/** Detects whether template or script content assigns an ARIA role. */
|
|
@@ -13,6 +13,18 @@ export declare function hasUnlabelledFormInput(content: string): boolean;
|
|
|
13
13
|
export declare function hasTemplateClickHandler(content: string): boolean;
|
|
14
14
|
/** Detects Lit-style template keyboard handlers. */
|
|
15
15
|
export declare function hasTemplateKeyboardHandler(content: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Returns true when `customConfig` declares that the component's keyboard
|
|
18
|
+
* activation path is covered by a wrapped native-interactive inner element,
|
|
19
|
+
* either through an explicit opt-out (`keyboardCovered: true`) or by
|
|
20
|
+
* composing a tag that lives in {@link NATIVE_CLICK_INTERACTIVE_TAGS}.
|
|
21
|
+
*
|
|
22
|
+
* Uses `.some` rather than `.every` so that a wrapper composing e.g.
|
|
23
|
+
* `['moz-button', 'my-tooltip']` still skips the warning: the keyboard
|
|
24
|
+
* activation path flows through the button, even if other composed
|
|
25
|
+
* children are synthetic.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isKeyboardCoveredByComposition(customConfig: CustomComponentConfig | undefined): boolean;
|
|
16
28
|
/**
|
|
17
29
|
* Returns true when `content` has at least one `@click=${...}` handler on a
|
|
18
30
|
* *synthetic* interactive element (e.g. `<div @click>`), which lacks native
|
|
@@ -76,6 +76,25 @@ const NATIVE_CLICK_INTERACTIVE_TAGS = new Set([
|
|
|
76
76
|
'moz-radio-group',
|
|
77
77
|
'moz-menulist',
|
|
78
78
|
]);
|
|
79
|
+
/**
|
|
80
|
+
* Returns true when `customConfig` declares that the component's keyboard
|
|
81
|
+
* activation path is covered by a wrapped native-interactive inner element,
|
|
82
|
+
* either through an explicit opt-out (`keyboardCovered: true`) or by
|
|
83
|
+
* composing a tag that lives in {@link NATIVE_CLICK_INTERACTIVE_TAGS}.
|
|
84
|
+
*
|
|
85
|
+
* Uses `.some` rather than `.every` so that a wrapper composing e.g.
|
|
86
|
+
* `['moz-button', 'my-tooltip']` still skips the warning: the keyboard
|
|
87
|
+
* activation path flows through the button, even if other composed
|
|
88
|
+
* children are synthetic.
|
|
89
|
+
*/
|
|
90
|
+
export function isKeyboardCoveredByComposition(customConfig) {
|
|
91
|
+
if (!customConfig)
|
|
92
|
+
return false;
|
|
93
|
+
if (customConfig.keyboardCovered === true)
|
|
94
|
+
return true;
|
|
95
|
+
const composes = customConfig.composes ?? [];
|
|
96
|
+
return composes.some((tag) => NATIVE_CLICK_INTERACTIVE_TAGS.has(tag));
|
|
97
|
+
}
|
|
79
98
|
/**
|
|
80
99
|
* Returns true when `content` has at least one `@click=${...}` handler on a
|
|
81
100
|
* *synthetic* interactive element (e.g. `<div @click>`), which lacks native
|
|
@@ -39,14 +39,18 @@ export async function validateStructure(componentDir, tagName, type, customConfi
|
|
|
39
39
|
// Localized custom components must have a {tag}.ftl file. Without one,
|
|
40
40
|
// apply silently deploys nothing for the locale and the runtime
|
|
41
41
|
// localization payload is empty, which is hard to spot in review.
|
|
42
|
-
|
|
42
|
+
//
|
|
43
|
+
// Components that declare `sharedFtl` participate in a pre-existing
|
|
44
|
+
// feature-scoped bundle, so there is no per-component .ftl to require —
|
|
45
|
+
// the shared file is owned by whoever authored the feature bundle.
|
|
46
|
+
if (type === 'custom' && customConfig?.localized && !customConfig.sharedFtl) {
|
|
43
47
|
const ftlPath = join(componentDir, `${tagName}.ftl`);
|
|
44
48
|
if (!(await pathExists(ftlPath))) {
|
|
45
49
|
issues.push({
|
|
46
50
|
component: tagName,
|
|
47
51
|
severity: 'error',
|
|
48
52
|
check: 'missing-ftl',
|
|
49
|
-
message: `Component is marked localized: true but ${tagName}.ftl is missing. Create the file
|
|
53
|
+
message: `Component is marked localized: true but ${tagName}.ftl is missing. Create the file, set localized: false in furnace.json, or switch to sharedFtl.`,
|
|
50
54
|
});
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -37,10 +37,13 @@ function buildOverrideVersionDriftIssues(config, currentVersion, tagName) {
|
|
|
37
37
|
export async function validateComponent(componentDir, tagName, type, config, root, options) {
|
|
38
38
|
const issues = [];
|
|
39
39
|
// Pass the matching custom config so structure validation can enforce
|
|
40
|
-
// the .ftl-when-localized invariant
|
|
40
|
+
// the .ftl-when-localized invariant and accessibility validation can
|
|
41
|
+
// recognize wrapper-over-native components (via `composes` or an
|
|
42
|
+
// explicit `keyboardCovered` opt-out). Non-custom validations ignore the
|
|
41
43
|
// parameter, so this is a no-op for stock and override components.
|
|
42
|
-
|
|
43
|
-
issues.push(...(await
|
|
44
|
+
const customConfigForTag = type === 'custom' ? config?.custom[tagName] : undefined;
|
|
45
|
+
issues.push(...(await validateStructure(componentDir, tagName, type, customConfigForTag)));
|
|
46
|
+
issues.push(...(await validateAccessibility(componentDir, tagName, customConfigForTag)));
|
|
44
47
|
issues.push(...(await validateCompatibility(componentDir, tagName, type, config, root)));
|
|
45
48
|
if (root && config && type === 'override') {
|
|
46
49
|
const forgeConfig = await loadConfig(root);
|
package/dist/src/core/mach.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SmokeLineCallback, type SmokeRunResult } from '../utils/process.js';
|
|
1
2
|
export { attemptMozinfoRewrite, type BuildArtifactCheck, buildArtifactMismatchMessage, hasBuildArtifacts, type MozinfoRewriteResult, } from './mach-build-artifacts.js';
|
|
2
3
|
export { generateMozconfig, type MozconfigVariables } from './mach-mozconfig.js';
|
|
3
4
|
export { ensurePython, resetResolvedPython } from './mach-python.js';
|
|
@@ -79,6 +80,31 @@ export declare function buildUI(engineDir: string): Promise<number>;
|
|
|
79
80
|
* @returns Exit code
|
|
80
81
|
*/
|
|
81
82
|
export declare function run(engineDir: string, args?: string[]): Promise<number>;
|
|
83
|
+
/**
|
|
84
|
+
* Options for {@link runMachSmoke}.
|
|
85
|
+
*/
|
|
86
|
+
export interface RunMachSmokeOptions {
|
|
87
|
+
env?: Record<string, string>;
|
|
88
|
+
smokeTimeoutMs: number;
|
|
89
|
+
killGraceMs?: number;
|
|
90
|
+
onStdoutLine?: SmokeLineCallback;
|
|
91
|
+
onStderrLine?: SmokeLineCallback;
|
|
92
|
+
mirror?: {
|
|
93
|
+
stdout?: NodeJS.WritableStream;
|
|
94
|
+
stderr?: NodeJS.WritableStream;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Launches `mach run` under the smoke-run wrapper: streams line-by-line,
|
|
99
|
+
* enforces a deadline by SIGTERMing the whole process group, and returns
|
|
100
|
+
* the captured output alongside a `timedOut` flag.
|
|
101
|
+
*
|
|
102
|
+
* Unlike {@link run}, this variant does NOT inherit stdio. The child
|
|
103
|
+
* stdout/stderr are piped back through the line callbacks so the caller
|
|
104
|
+
* can scan for `JavaScript error:` / `console.error:` without coupling
|
|
105
|
+
* the runner to chrome-specific pattern logic.
|
|
106
|
+
*/
|
|
107
|
+
export declare function runMachSmoke(args: string[], engineDir: string, options: RunMachSmokeOptions): Promise<SmokeRunResult>;
|
|
82
108
|
/**
|
|
83
109
|
* Creates a distribution package.
|
|
84
110
|
* @param engineDir - Path to the engine directory
|
package/dist/src/core/mach.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { MachNotFoundError } from '../errors/build.js';
|
|
4
4
|
import { pathExists } from '../utils/fs.js';
|
|
5
5
|
import { warn } from '../utils/logger.js';
|
|
6
|
-
import { exec, execInherit, execInheritCapture, execStream } from '../utils/process.js';
|
|
6
|
+
import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from '../utils/process.js';
|
|
7
7
|
import { explainMachError } from './mach-error-hints.js';
|
|
8
8
|
import { getPython } from './mach-python.js';
|
|
9
9
|
// Re-export sub-modules so existing `from './mach.js'` imports keep working.
|
|
@@ -165,6 +165,30 @@ export async function buildUI(engineDir) {
|
|
|
165
165
|
export async function run(engineDir, args = []) {
|
|
166
166
|
return runMach(['run', ...args], engineDir, { inherit: true });
|
|
167
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Launches `mach run` under the smoke-run wrapper: streams line-by-line,
|
|
170
|
+
* enforces a deadline by SIGTERMing the whole process group, and returns
|
|
171
|
+
* the captured output alongside a `timedOut` flag.
|
|
172
|
+
*
|
|
173
|
+
* Unlike {@link run}, this variant does NOT inherit stdio. The child
|
|
174
|
+
* stdout/stderr are piped back through the line callbacks so the caller
|
|
175
|
+
* can scan for `JavaScript error:` / `console.error:` without coupling
|
|
176
|
+
* the runner to chrome-specific pattern logic.
|
|
177
|
+
*/
|
|
178
|
+
export async function runMachSmoke(args, engineDir, options) {
|
|
179
|
+
const python = await getPython(engineDir);
|
|
180
|
+
await ensureMach(engineDir);
|
|
181
|
+
const machPath = join(engineDir, 'mach');
|
|
182
|
+
return execSmokeRun(python, [machPath, ...args], {
|
|
183
|
+
cwd: engineDir,
|
|
184
|
+
...(options.env ? { env: options.env } : {}),
|
|
185
|
+
smokeTimeoutMs: options.smokeTimeoutMs,
|
|
186
|
+
...(options.killGraceMs !== undefined ? { killGraceMs: options.killGraceMs } : {}),
|
|
187
|
+
...(options.onStdoutLine ? { onStdoutLine: options.onStdoutLine } : {}),
|
|
188
|
+
...(options.onStderrLine ? { onStderrLine: options.onStderrLine } : {}),
|
|
189
|
+
...(options.mirror ? { mirror: options.mirror } : {}),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
168
192
|
/**
|
|
169
193
|
* Creates a distribution package.
|
|
170
194
|
* @param engineDir - Path to the engine directory
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural rules for the `sharedFtl` field on a custom component.
|
|
3
|
+
*
|
|
4
|
+
* Both the `--shared-ftl` CLI flag path (`furnace create`) and the
|
|
5
|
+
* furnace.json parser apply the same rules — extracting them here
|
|
6
|
+
* avoids drift between the two entry points and lets the `.mjs`
|
|
7
|
+
* template generator assume the value is safe to interpolate verbatim.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Outcome of {@link validateSharedFtl}. `ok: true` carries the trimmed
|
|
11
|
+
* (operator-safe) value; `ok: false` carries a human-readable message
|
|
12
|
+
* suitable for throwing as a `FurnaceError` or `InvalidArgumentError`.
|
|
13
|
+
*/
|
|
14
|
+
export type SharedFtlValidation = {
|
|
15
|
+
ok: true;
|
|
16
|
+
value: string;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
reason: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Validates a candidate `sharedFtl` value. Returns the trimmed value
|
|
23
|
+
* when well-formed, or a structured reason when not. Callers throw the
|
|
24
|
+
* error type appropriate to their context (CLI vs config parser).
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateSharedFtl(raw: unknown, context: {
|
|
27
|
+
localized: boolean;
|
|
28
|
+
}): SharedFtlValidation;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Structural rules for the `sharedFtl` field on a custom component.
|
|
4
|
+
*
|
|
5
|
+
* Both the `--shared-ftl` CLI flag path (`furnace create`) and the
|
|
6
|
+
* furnace.json parser apply the same rules — extracting them here
|
|
7
|
+
* avoids drift between the two entry points and lets the `.mjs`
|
|
8
|
+
* template generator assume the value is safe to interpolate verbatim.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Characters that must not appear in `sharedFtl`:
|
|
12
|
+
* - Backticks close the MJS template literal the scaffold writes.
|
|
13
|
+
* - `\` is a path-escape we do not want to interpret.
|
|
14
|
+
* - `${` opens a template expression and turns the FTL path into
|
|
15
|
+
* executable code.
|
|
16
|
+
*/
|
|
17
|
+
const UNSAFE_CHARS = /[`\\]|\$\{/;
|
|
18
|
+
/**
|
|
19
|
+
* Validates a candidate `sharedFtl` value. Returns the trimmed value
|
|
20
|
+
* when well-formed, or a structured reason when not. Callers throw the
|
|
21
|
+
* error type appropriate to their context (CLI vs config parser).
|
|
22
|
+
*/
|
|
23
|
+
export function validateSharedFtl(raw, context) {
|
|
24
|
+
if (typeof raw !== 'string') {
|
|
25
|
+
return { ok: false, reason: 'must be a string when set' };
|
|
26
|
+
}
|
|
27
|
+
const value = raw.trim();
|
|
28
|
+
if (value.length === 0) {
|
|
29
|
+
return { ok: false, reason: 'must not be empty' };
|
|
30
|
+
}
|
|
31
|
+
if (UNSAFE_CHARS.test(value)) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: 'must not contain backticks, backslashes, or ${ (would break the generated .mjs)',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (!context.localized) {
|
|
38
|
+
return { ok: false, reason: 'requires localized to be true' };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, value };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=shared-ftl.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared regex library for `fireforge run --smoke-exit`. Used by the smoke
|
|
3
|
+
* runner to decide whether a console line from the real chrome counts as a
|
|
4
|
+
* runtime error. Kept separate from the runner so the patterns can be
|
|
5
|
+
* exercised in isolation and amended without touching process-group logic.
|
|
6
|
+
*
|
|
7
|
+
* Matching is anchored at the start of the line (`^`) on purpose: a runtime
|
|
8
|
+
* error leaves a canonical prefix that Firefox's logging layer prints at
|
|
9
|
+
* column zero. Embedded mentions of the same string inside an unrelated
|
|
10
|
+
* warning (e.g. `"pending JavaScript error cleanup"`) do not start with
|
|
11
|
+
* the prefix and should not trip the scanner.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Line prefixes that signal an actual runtime error in a Firefox chrome
|
|
15
|
+
* process. A hit on any of these patterns is a smoke failure unless the
|
|
16
|
+
* line also matches the caller-supplied allowlist.
|
|
17
|
+
*
|
|
18
|
+
* Additions here should be conservative — false positives turn every
|
|
19
|
+
* smoke run into noise for operators and every CI run into flake.
|
|
20
|
+
*/
|
|
21
|
+
export declare const SMOKE_ERROR_PATTERNS: readonly RegExp[];
|
|
22
|
+
/**
|
|
23
|
+
* Returns `true` when `line` matches any pattern in
|
|
24
|
+
* {@link SMOKE_ERROR_PATTERNS}. Does not consult the allowlist — that step
|
|
25
|
+
* lives in {@link matchesAllowlist}, so the smoke runner can count
|
|
26
|
+
* allowlisted hits separately from raw error matches for its summary.
|
|
27
|
+
*/
|
|
28
|
+
export declare function matchesSmokeError(line: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Returns `true` when `line` matches any regex in `allow`. Safe to call
|
|
31
|
+
* with an empty allowlist (always returns `false`).
|
|
32
|
+
*/
|
|
33
|
+
export declare function matchesAllowlist(line: string, allow: readonly RegExp[]): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Parses a newline-delimited allowlist file body. Lines are trimmed; blank
|
|
36
|
+
* lines and `#`-prefixed comments are skipped. Each remaining line is
|
|
37
|
+
* compiled as a RegExp. A bad pattern throws immediately — better to fail
|
|
38
|
+
* fast at CLI parse time than to silently let a typo match nothing.
|
|
39
|
+
*/
|
|
40
|
+
export declare function compileAllowlistFromFile(body: string, sourcePath: string): RegExp[];
|
|
41
|
+
/**
|
|
42
|
+
* Compiles an array of regex-string inputs (e.g. repeated `--console-allow`
|
|
43
|
+
* flag values). Same fail-fast semantics as {@link compileAllowlistFromFile}.
|
|
44
|
+
*/
|
|
45
|
+
export declare function compileAllowlistFromStrings(sources: readonly string[]): RegExp[];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Shared regex library for `fireforge run --smoke-exit`. Used by the smoke
|
|
4
|
+
* runner to decide whether a console line from the real chrome counts as a
|
|
5
|
+
* runtime error. Kept separate from the runner so the patterns can be
|
|
6
|
+
* exercised in isolation and amended without touching process-group logic.
|
|
7
|
+
*
|
|
8
|
+
* Matching is anchored at the start of the line (`^`) on purpose: a runtime
|
|
9
|
+
* error leaves a canonical prefix that Firefox's logging layer prints at
|
|
10
|
+
* column zero. Embedded mentions of the same string inside an unrelated
|
|
11
|
+
* warning (e.g. `"pending JavaScript error cleanup"`) do not start with
|
|
12
|
+
* the prefix and should not trip the scanner.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Line prefixes that signal an actual runtime error in a Firefox chrome
|
|
16
|
+
* process. A hit on any of these patterns is a smoke failure unless the
|
|
17
|
+
* line also matches the caller-supplied allowlist.
|
|
18
|
+
*
|
|
19
|
+
* Additions here should be conservative — false positives turn every
|
|
20
|
+
* smoke run into noise for operators and every CI run into flake.
|
|
21
|
+
*/
|
|
22
|
+
export const SMOKE_ERROR_PATTERNS = [
|
|
23
|
+
// Firefox chrome error lines — `JavaScript error: chrome://…, line N: TypeError: …`.
|
|
24
|
+
/^\s*JavaScript error:/i,
|
|
25
|
+
// Some log paths prefix browser-console `console.error(...)` with the literal label below.
|
|
26
|
+
/^\s*console\.error:/i,
|
|
27
|
+
// Older bracketed-prefix variant still seen in some chrome logs / test runs.
|
|
28
|
+
/^\s*\[JavaScript (Error|Warning)\]/i,
|
|
29
|
+
// IPC-layer fatal assertions — Firefox prints `###!!! [Parent] Error: …` on content-process crashes.
|
|
30
|
+
/^\s*###!!! \[Parent\]/,
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Returns `true` when `line` matches any pattern in
|
|
34
|
+
* {@link SMOKE_ERROR_PATTERNS}. Does not consult the allowlist — that step
|
|
35
|
+
* lives in {@link matchesAllowlist}, so the smoke runner can count
|
|
36
|
+
* allowlisted hits separately from raw error matches for its summary.
|
|
37
|
+
*/
|
|
38
|
+
export function matchesSmokeError(line) {
|
|
39
|
+
for (const pattern of SMOKE_ERROR_PATTERNS) {
|
|
40
|
+
if (pattern.test(line)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Returns `true` when `line` matches any regex in `allow`. Safe to call
|
|
48
|
+
* with an empty allowlist (always returns `false`).
|
|
49
|
+
*/
|
|
50
|
+
export function matchesAllowlist(line, allow) {
|
|
51
|
+
for (const pattern of allow) {
|
|
52
|
+
if (pattern.test(line)) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parses a newline-delimited allowlist file body. Lines are trimmed; blank
|
|
60
|
+
* lines and `#`-prefixed comments are skipped. Each remaining line is
|
|
61
|
+
* compiled as a RegExp. A bad pattern throws immediately — better to fail
|
|
62
|
+
* fast at CLI parse time than to silently let a typo match nothing.
|
|
63
|
+
*/
|
|
64
|
+
export function compileAllowlistFromFile(body, sourcePath) {
|
|
65
|
+
const lines = body.split(/\r?\n/);
|
|
66
|
+
const compiled = [];
|
|
67
|
+
lines.forEach((raw, index) => {
|
|
68
|
+
const line = raw.trim();
|
|
69
|
+
if (!line || line.startsWith('#'))
|
|
70
|
+
return;
|
|
71
|
+
try {
|
|
72
|
+
compiled.push(new RegExp(line));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
throw new Error(`Invalid allowlist regex at ${sourcePath}:${String(index + 1)}: ${message}`, {
|
|
77
|
+
cause: error,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return compiled;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compiles an array of regex-string inputs (e.g. repeated `--console-allow`
|
|
85
|
+
* flag values). Same fail-fast semantics as {@link compileAllowlistFromFile}.
|
|
86
|
+
*/
|
|
87
|
+
export function compileAllowlistFromStrings(sources) {
|
|
88
|
+
const compiled = [];
|
|
89
|
+
sources.forEach((source, index) => {
|
|
90
|
+
try {
|
|
91
|
+
compiled.push(new RegExp(source));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
throw new Error(`Invalid --console-allow regex at position ${String(index + 1)} ("${source}"): ${message}`, { cause: error });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return compiled;
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=smoke-patterns.js.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-injects `--app-path=<abs>` into `mach test` invocations whose nearest
|
|
3
|
+
* xpcshell.toml sets `firefox-appdir = "browser"` (or `<appname>-appdir = …`)
|
|
4
|
+
* but whose `appname` is not `firefox`.
|
|
5
|
+
*
|
|
6
|
+
* ## Why this exists
|
|
7
|
+
*
|
|
8
|
+
* The upstream xpcshell harness computes the manifest key for the appdir
|
|
9
|
+
* override as `mozInfo["appname"] + "-appdir"`. On a stock Firefox build the
|
|
10
|
+
* key is `firefox-appdir`, so the very common `firefox-appdir = "browser"`
|
|
11
|
+
* directive is honoured. On a rebranded fork (appname=`hominis`,
|
|
12
|
+
* `mybrowser`, …) the harness looks for `hominis-appdir` / `mybrowser-appdir`
|
|
13
|
+
* — the literal `firefox-appdir` line is silently ignored, `appPath` falls
|
|
14
|
+
* back to `xrePath`, and every `resource:///modules/…` import throws
|
|
15
|
+
* `Failed to load resource:///modules/<name>.sys.mjs` because xpcshell now
|
|
16
|
+
* resolves the `resource:///` prefix one level above the real app root.
|
|
17
|
+
*
|
|
18
|
+
* ## Strategy
|
|
19
|
+
*
|
|
20
|
+
* 1. For each test path the operator handed us, find the nearest
|
|
21
|
+
* `xpcshell.toml`. If none exists, the test is not an xpcshell test and
|
|
22
|
+
* nothing to inject.
|
|
23
|
+
* 2. Read the manifest's `[DEFAULT]` section. Look for `<appname>-appdir`
|
|
24
|
+
* first — if present, the harness already finds it and there's nothing to
|
|
25
|
+
* do. Fall back to `firefox-appdir`. This ordering matches upstream
|
|
26
|
+
* precedence and avoids overriding an operator who already migrated.
|
|
27
|
+
* 3. If only `firefox-appdir` is present and `appname != "firefox"`, compute
|
|
28
|
+
* the absolute app dir path against the active `obj-X/dist` tree
|
|
29
|
+
* (probing `dist/bin/<value>` first, then any `dist/<bundle>.app/Contents/
|
|
30
|
+
* Resources/<value>` for the macOS packaged layout) and return it as
|
|
31
|
+
* the value to pass to `--app-path`.
|
|
32
|
+
* 4. If multiple test paths disagree on the resolved value (e.g. one
|
|
33
|
+
* manifest sets `browser`, another sets `xulrunner`), refuse injection
|
|
34
|
+
* and return null — the operator can drop down to `--mach-arg`.
|
|
35
|
+
*
|
|
36
|
+
* Operator escape hatches: `--mach-arg=--app-path=…` always wins (handled in
|
|
37
|
+
* test.ts; we skip injection when `--app-path=` already appears in the
|
|
38
|
+
* forwarded args).
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Result of attempting to resolve the auto-injected `--app-path` value.
|
|
42
|
+
* Carries enough context for the caller to log a useful info line and for
|
|
43
|
+
* the diagnostic hint to know whether an injection was attempted.
|
|
44
|
+
*/
|
|
45
|
+
export interface AppdirResolveResult {
|
|
46
|
+
/** Absolute path to the app dir. Pass as `--app-path=<value>`. */
|
|
47
|
+
appPath: string;
|
|
48
|
+
/** Manifest the value was sourced from. Used for the info log. */
|
|
49
|
+
manifestPath: string;
|
|
50
|
+
/** Manifest key (e.g. `firefox-appdir`) that triggered the injection. */
|
|
51
|
+
key: string;
|
|
52
|
+
/** Relative appdir from the manifest (e.g. `browser`). */
|
|
53
|
+
relativeAppdir: string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `[DEFAULT]` section parser shaped to the narrow case we need: pull a
|
|
57
|
+
* single key/value out without depending on a real TOML parser. Avoids
|
|
58
|
+
* pulling a TOML dep into the test path for a one-shot lookup.
|
|
59
|
+
*
|
|
60
|
+
* Accepts:
|
|
61
|
+
* - Single- or double-quoted values
|
|
62
|
+
* - Whitespace either side of `=`
|
|
63
|
+
* - Continuation comments (`#` or `;`) at the end of the line
|
|
64
|
+
* - Bare unquoted bareword values (e.g. `firefox-appdir = browser`) — some
|
|
65
|
+
* operators omit the quotes and the harness honours either form.
|
|
66
|
+
*
|
|
67
|
+
* Returns `undefined` when the key is absent or sits outside `[DEFAULT]`.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseAppdirFromToml(tomlText: string, key: string): {
|
|
70
|
+
value: string;
|
|
71
|
+
lineIndex: number;
|
|
72
|
+
} | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Walks up from `startPath` (a file or directory under `engineDir`) and
|
|
75
|
+
* returns the absolute path of the first sibling `xpcshell.toml` found.
|
|
76
|
+
* Stops at `engineDir` (inclusive) and returns null on miss.
|
|
77
|
+
*
|
|
78
|
+
* Special-cases `startPath` itself when it already ends with
|
|
79
|
+
* `xpcshell.toml` — operators sometimes pass a manifest path directly.
|
|
80
|
+
*/
|
|
81
|
+
export declare function findNearestXpcshellManifest(engineDir: string, startPath: string): Promise<string | null>;
|
|
82
|
+
/**
|
|
83
|
+
* Reads `<objDir>/mozinfo.json` for the active app name. Returns
|
|
84
|
+
* `"firefox"` when mozinfo cannot be read or the field is missing — that
|
|
85
|
+
* is the safe default because it matches stock Firefox behaviour and
|
|
86
|
+
* means the resolver will not inject anything (the manifest's
|
|
87
|
+
* `firefox-appdir` value WILL be honoured by the upstream harness when
|
|
88
|
+
* appname is firefox).
|
|
89
|
+
*/
|
|
90
|
+
export declare function readMozinfoAppname(objDirPath: string): Promise<string>;
|
|
91
|
+
/**
|
|
92
|
+
* Probes the obj-dir's `dist/` subtree for the absolute path that the
|
|
93
|
+
* harness would have computed if the manifest key had been honoured.
|
|
94
|
+
* Returns null when no candidate exists — better to skip injection
|
|
95
|
+
* silently than to point the harness at a path that doesn't exist
|
|
96
|
+
* (which fails with a different error than the original `firefox-appdir`
|
|
97
|
+
* symptom and confuses triage).
|
|
98
|
+
*
|
|
99
|
+
* Probe order matches the on-disk layouts FireForge supports today:
|
|
100
|
+
* 1. `<objDir>/dist/bin/<value>` — Linux primary, also macOS via the
|
|
101
|
+
* `dist/bin -> dist/<App>.app/Contents/MacOS/` symlink.
|
|
102
|
+
* 2. `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` — macOS
|
|
103
|
+
* packaged layout, where `dist/bin/` may not exist as a directory.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveAbsoluteAppPath(objDirAbs: string, relativeAppdir: string): Promise<string | null>;
|
|
106
|
+
/**
|
|
107
|
+
* Outcome carrier for {@link resolveXpcshellAppdirArg}. Distinguishes the
|
|
108
|
+
* three "did nothing" cases so callers can shape diagnostics:
|
|
109
|
+
* - `none`: no manifest under any test path needs injection.
|
|
110
|
+
* - `mismatch`: at least two manifests resolved to different values; we
|
|
111
|
+
* refuse to guess which one the operator meant.
|
|
112
|
+
* - `unresolved`: the manifest asks for `firefox-appdir = "<value>"` but
|
|
113
|
+
* no `dist/` candidate exists for that value.
|
|
114
|
+
* - `injected`: the absolute path to pass via `--app-path=`.
|
|
115
|
+
*/
|
|
116
|
+
export type XpcshellAppdirOutcome = {
|
|
117
|
+
kind: 'none';
|
|
118
|
+
} | {
|
|
119
|
+
kind: 'mismatch';
|
|
120
|
+
values: string[];
|
|
121
|
+
} | {
|
|
122
|
+
kind: 'unresolved';
|
|
123
|
+
relativeAppdir: string;
|
|
124
|
+
manifestPath: string;
|
|
125
|
+
} | {
|
|
126
|
+
kind: 'injected';
|
|
127
|
+
result: AppdirResolveResult;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Top-level resolver. Walks every test path, reads the nearest
|
|
131
|
+
* xpcshell.toml, and returns the single absolute path to inject (or a
|
|
132
|
+
* structured "no injection" outcome). Never throws — every fs / parse
|
|
133
|
+
* error is folded into a `none` outcome so the test command always falls
|
|
134
|
+
* through to the diagnostic hint instead of dying inside a helper.
|
|
135
|
+
*/
|
|
136
|
+
export declare function resolveXpcshellAppdirArg(engineDir: string, testPaths: readonly string[], objDirName: string): Promise<XpcshellAppdirOutcome>;
|
|
137
|
+
/**
|
|
138
|
+
* Returns true when the operator already passed `--app-path=` (or its
|
|
139
|
+
* `--app-path <value>` two-token form) through `--mach-arg`. Used by the
|
|
140
|
+
* test command to skip auto-injection so the operator override always
|
|
141
|
+
* wins.
|
|
142
|
+
*/
|
|
143
|
+
export declare function operatorAlreadySetAppPath(extraArgs: readonly string[]): boolean;
|