@hominis/fireforge 0.15.1 → 0.15.2
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 +15 -3
- package/README.md +22 -3
- package/dist/src/commands/build.js +12 -1
- package/dist/src/commands/furnace/create-templates.d.ts +21 -0
- package/dist/src/commands/furnace/create-templates.js +49 -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.js +17 -8
- package/dist/src/commands/furnace/index.js +1 -0
- package/dist/src/commands/setup.d.ts +1 -1
- package/dist/src/commands/setup.js +3 -2
- package/dist/src/core/build-prepare.js +6 -1
- 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-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/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.js +29 -9
- package/dist/src/types/commands/options.d.ts +10 -0
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +12 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
### Furnace registration
|
|
6
6
|
|
|
7
7
|
- `furnace apply` idempotency check is marker-comment-tolerant. Previously the single-line substring match (`content.includes('["tag",')`) missed multi-line entries, and the standalone-line regex anchored on `\s*$`, which did not allow trailing `// <marker>:` comments an operator may have appended to a previously-written entry. A duplicate tag was then inserted on every re-apply, and the second `setElementCreationCallback` invocation threw `NotSupportedError: Operation is not supported` at every window-load. The idempotency check now matches on tag-name column 0 (both single- and multi-line array shapes) and tolerates trailing `//` comments on the line.
|
|
8
|
-
- New optional fireforge.json field `markerComment` (e.g. `"
|
|
8
|
+
- New optional fireforge.json field `markerComment` (e.g. `"MYBROWSER"`) is appended as a ` // MYBROWSER:` suffix to every line FireForge writes into `customElements.js`. Keeps fork modifications discoverable and re-applies idempotent without hand-tagging after each apply. The field is threaded through `applyCustomComponent` and `furnace deploy`, not just `furnace create`.
|
|
9
9
|
- `addCustomElementRegistration` and its regex fallback both accept the new marker as an optional parameter; the AST idempotency check and the regex-fallback idempotency check share a single helper (`isTagAlreadyRegistered`).
|
|
10
10
|
|
|
11
11
|
### Furnace `--localized`
|
|
@@ -17,19 +17,31 @@
|
|
|
17
17
|
|
|
18
18
|
### Furnace validate
|
|
19
19
|
|
|
20
|
-
- `missing-token-link` now reads `tokenHostDocuments` from furnace.json and scans every configured chrome document for the tokens CSS link. Warning fires only when NONE of them link the tokens CSS; the warning enumerates the documents it actually checked. Previously the check was hardcoded to `browser/base/content/browser.xhtml`, which false-positived on forks that mount components in a different chrome document (e.g. `
|
|
20
|
+
- `missing-token-link` now reads `tokenHostDocuments` from furnace.json and scans every configured chrome document for the tokens CSS link. Warning fires only when NONE of them link the tokens CSS; the warning enumerates the documents it actually checked. Previously the check was hardcoded to `browser/base/content/browser.xhtml`, which false-positived on forks that mount components in a different chrome document (e.g. `mybrowser.xhtml`). Defaults to `["browser/base/content/browser.xhtml"]` when omitted — behaviour is unchanged for projects that never set the field.
|
|
21
21
|
- `no-keyboard-handler` no longer warns when `@click` sits on a native interactive element (`<button>`, `<a href>`, `<input>`, `<select>`, `<textarea>`, `<summary>`, `<details>`, or the Firefox `moz-button`/`moz-toggle`/`moz-checkbox`/`moz-radio`/`moz-menulist` widgets). Those elements dispatch `click` on Enter and Space via the platform, so a duplicate `@keydown`/`@keypress` handler would double-fire. The rule still fires for synthetic interactive markup (e.g. `<div @click>`) and for bare `<a>` without an `href` attribute, which are the real keyboard-a11y hazards.
|
|
22
|
+
- `token-prefix-violation` stops flagging component-owned runtime CSS custom properties. Previously every `var(--foo)` that did not match `tokenPrefix` was rejected as a design-token escape, even when the property was a per-frame state channel (`--cam-x`, `--tile-z`) both written and read by the component. Two relaxations ship together: (a) a new optional `runtimeVariables: string[]` field in `furnace.json` explicitly allowlists cross-component runtime channels (e.g. set in JS, read in the child's CSS); (b) variables that are both declared (`--foo: value;`) and consumed (`var(--foo)`) inside the same CSS file are auto-exempted — no config entry required. The same relaxations apply to the patch-stack lint. The violation message now calls out `runtimeVariables` as the third escape hatch alongside `tokenPrefix` and `tokenAllowlist`.
|
|
23
|
+
- `hardcoded-text` narrowed from a bare `>…<` scan to three context-aware probes: text inside Lit `` html`…` `` template literals, string literals assigned via `.textContent = "…"` / `.innerHTML = "…"`, and XUL-attribute values set via `setAttribute("label"|"title"|"tooltiptext", "…")`. Previously the rule also matched JS comparisons (`if (x > 0 && y < 100)`), long diagnostic strings (`console.error("…")`), and identifier literals passed to `querySelector`, producing noise that trained authors to ignore validator warnings. The file-wide `// furnace-ignore: hardcoded-text` escape hatch is preserved.
|
|
22
24
|
|
|
23
25
|
### Run / test
|
|
24
26
|
|
|
25
27
|
- `fireforge run`, `fireforge watch`, `fireforge build`, and every other `mach` invocation launched with inherited stdio now forward parent `SIGINT`/`SIGTERM` to the child as `SIGTERM` and wait ~1.5 s before escalating to `SIGKILL`. A second Ctrl-C during the grace window escalates immediately (matches the usual "hit Ctrl-C twice to force-quit" UX). Previously the parent could exit before Gecko's `AsyncShutdown` / `profileBeforeChange` blockers finished flushing in-memory state, losing the last few seconds of edits. The grace window is configurable via a new `shutdownGraceMs` option on `execInherit` / `execInheritCapture`.
|
|
26
28
|
- New `fireforge test --doctor` runs a short marionette handshake preflight before (optionally) invoking `mach test`. Spawns the built browser headless, opens a TCP socket to `127.0.0.1:2828`, waits for the handshake bytes, and reports PASS/FAIL with the tail of stderr on FAIL. When `--doctor` is supplied with no test paths, it exits after the preflight — a sub-minute way to tell "marionette wedged" apart from "test failed to discover" when `mach test` hangs for the full 360 s marionette timeout. When supplied with test paths, a FAIL preflight short-circuits before `mach test` runs.
|
|
29
|
+
- `fireforge test --doctor` is now a cascade of six layered probes (engine-present → mach-available → python-available → profile-creatable → browser-spawns → marionette-handshake) with tight per-layer budgets. Previously the single 30 s socket poll gave the same generic "socket did not respond" diagnostic whether mach was missing, Python was unavailable, `/tmp` was not writable, or the browser binary crashed at startup — so operators had no lead on where to start debugging. Each layer now short-circuits with a `[layer N/6: <name>]`-prefixed detail message so the first broken layer is surfaced immediately, and a crashing browser is caught by a short settle window at layer 5 instead of wasting the full budget waiting for bytes that never come.
|
|
30
|
+
|
|
31
|
+
### Furnace build
|
|
32
|
+
|
|
33
|
+
- `fireforge build` already auto-applies Furnace components (via `prepareBuildEnvironment`) before the mach build step, but the behaviour was undocumented and silent — operators who edited `components/custom/` and then ran `fireforge build` could not distinguish "auto-apply wrote files" from "nothing changed". A loud `Furnace: source → engine sync wrote N component(s) before build (...)` banner now fires whenever apply wrote files, naming every component that was synced. The `fireforge build --help` description and help footer now call out that apply runs before the build step.
|
|
34
|
+
|
|
35
|
+
### Furnace create
|
|
36
|
+
|
|
37
|
+
- New `furnace create --xpcshell` flag scaffolds an xpcshell test harness alongside (or instead of) the browser-chrome mochitest that `--with-tests` already produces. xpcshell runs headless without a `tabbrowser`, so storage-layer / observer-driven / module-loading code on forks that do not mount the upstream browser chrome (no `openLinkIn` → `URILoadingHelper`) can be covered without the harness complaining about a missing tab strip. Writes `test_<name>_module_loads.js` and an `xpcshell.toml` manifest into `engine/browser/base/content/test/<binary-name>-xpcshell/<component-name>/`. Registration in `XPCSHELL_TESTS_MANIFESTS` is left to the operator — the moz.build that should own the entry depends on where the component lives.
|
|
27
38
|
|
|
28
39
|
### Internal
|
|
29
40
|
|
|
30
41
|
- Extracted `furnace-apply-ftl.ts`, `furnace-config-tokens.ts`, and `create-templates.ts` to keep apply / config / scaffolding files under the per-file LOC budget after the new features landed. `parseStringArray` is now exported from `furnace-config.ts` for cross-module reuse.
|
|
31
42
|
- New `src/core/marionette-preflight.ts` owns the `--doctor` probe and its teardown semantics.
|
|
32
43
|
- Test mocks for `furnace-registration.js` now cover the new `addLocaleFtlJarMnEntry` / `removeLocaleFtlJarMnEntry` exports; `config.js` mocks in apply-batch tests now cover `loadConfig` because the apply path reads `markerComment` from fireforge.json.
|
|
44
|
+
- Repo-wide scrub of fork-example mentions (`hominis.xhtml`, `HOMINIS` marker-comment examples, fixture tag names) in favour of a generic `mybrowser` / `MYBROWSER` placeholder. FireForge reads as fork-agnostic in docs and fixtures; the npm identity (`@hominis/fireforge`) is unchanged.
|
|
33
45
|
|
|
34
46
|
## 0.14.0
|
|
35
47
|
|
|
@@ -249,7 +261,7 @@
|
|
|
249
261
|
### General improvements
|
|
250
262
|
|
|
251
263
|
- getPackageRoot up to this point expected hardcoded `@hominis/fireforge`, was changed to just the package name for potential forks and more flexibility when changing project name.
|
|
252
|
-
- Some test generators were derived from early
|
|
264
|
+
- Some test generators were derived from an early downstream fork; the fork-specific names have been replaced with generic naming so the templates apply to any Firefox fork.
|
|
253
265
|
|
|
254
266
|
### Build and Git reliability
|
|
255
267
|
|
package/README.md
CHANGED
|
@@ -298,7 +298,7 @@ fireforge furnace status # workspace vs engine drift
|
|
|
298
298
|
fireforge furnace diff moz-button # unified diff against baseline
|
|
299
299
|
```
|
|
300
300
|
|
|
301
|
-
`furnace deploy` validates components before applying. As always, errors block, warnings are advisory. `fireforge build` and `fireforge test --build` run apply automatically. Use `fireforge doctor --repair-furnace` if the engine gets out of sync.
|
|
301
|
+
`furnace deploy` validates components before applying. As always, errors block, warnings are advisory. `fireforge build` and `fireforge test --build` run apply automatically — when apply wrote files during a build, the build prints a `Furnace: source → engine sync wrote N component(s) …` banner naming every component that was synced, so it is obvious whether engine/ was freshly updated. Use `fireforge doctor --repair-furnace` if the engine gets out of sync.
|
|
302
302
|
|
|
303
303
|
## Additional Commands
|
|
304
304
|
|
|
@@ -367,7 +367,7 @@ fireforge token --name "--my-color" --value "light-dark(#fff, #000)"
|
|
|
367
367
|
"wire": { "subscriptDir": "browser/components/mybrowser" },
|
|
368
368
|
"patchLint": {
|
|
369
369
|
"checkJs": true,
|
|
370
|
-
"rawColorAllowlist": ["
|
|
370
|
+
"rawColorAllowlist": ["mybrowser-tokens.css"]
|
|
371
371
|
},
|
|
372
372
|
"markerComment": "MYBROWSER"
|
|
373
373
|
}
|
|
@@ -375,7 +375,7 @@ fireforge token --name "--my-color" --value "light-dark(#fff, #000)"
|
|
|
375
375
|
|
|
376
376
|
**`markerComment`** (optional). Appended as a ` // <marker>:` suffix to every line FireForge writes into upstream Firefox source files (starting with `customElements.js`). Keeps fork modifications discoverable and makes re-apply idempotent without hand-tagging entries after each `furnace apply`. Reject list: empty strings, leading/trailing whitespace, newlines, `*/` (would close an enclosing block comment), control characters.
|
|
377
377
|
|
|
378
|
-
**`furnace.json.tokenHostDocuments`** (optional). List of chrome XHTML documents the `missing-token-link` validator scans for the tokens CSS link. Forks with a second chrome host (e.g. `
|
|
378
|
+
**`furnace.json.tokenHostDocuments`** (optional). List of chrome XHTML documents the `missing-token-link` validator scans for the tokens CSS link. Forks with a second chrome host (e.g. `mybrowser.xhtml` alongside `browser.xhtml`) should list every document that may own the link — the rule fires only when NONE of them link the tokens CSS. Defaults to `["browser/base/content/browser.xhtml"]` when omitted.
|
|
379
379
|
|
|
380
380
|
### `furnace create --localized` for `MozLitElement`
|
|
381
381
|
|
|
@@ -391,6 +391,25 @@ fireforge test --doctor browser/base/content/test/foo/browser_bar.js
|
|
|
391
391
|
|
|
392
392
|
Spawns the built browser headless, waits for a marionette handshake on `127.0.0.1:2828`, and reports PASS/FAIL with the tail of the browser's stderr on FAIL. Distinguishes "marionette wedged" (socket silent) from "mach test discovery failed" — both otherwise surface as a silent 360-second hang followed by `Passed: 0, Failed: 0`. Useful as a prefix on routine `fireforge test` invocations when marionette has been flaky.
|
|
393
393
|
|
|
394
|
+
The probe is a cascade of six layered checks — engine-present → mach-available → python-available → profile-creatable → browser-spawns → marionette-handshake. Each failure is tagged `[layer N/6: <name>]` so the first broken layer is surfaced immediately instead of the whole cascade blocking on the final socket poll. When the browser binary crashes at startup (missing dylib, wrong CPU arch, corrupt profile) the cascade fails at layer 5 within the settle window, not after the full socket timeout.
|
|
395
|
+
|
|
396
|
+
### Runtime CSS variables in Furnace
|
|
397
|
+
|
|
398
|
+
Design tokens imported from the fork's palette are enforced by `tokenPrefix`, but some components write and read CSS custom properties as runtime state channels (`--cam-x` per frame, `--tile-z` from a hit-test observer). Two escape hatches exist:
|
|
399
|
+
|
|
400
|
+
- **Auto-exempt** — a variable that is both declared (`--foo: 0;`) and consumed (`var(--foo)`) inside the same component's CSS file is recognised as a component-local runtime channel. No config entry required.
|
|
401
|
+
- **`furnace.json.runtimeVariables`** — explicit allowlist for names that are _written_ in JS and _read_ in a different file's CSS (cross-component runtime channels that the CSS-only auto-exempt cannot see). Entries must start with `--`.
|
|
402
|
+
|
|
403
|
+
Both rules compose with the existing `tokenPrefix` / `tokenAllowlist` checks and apply to both component validation and patch-stack lint.
|
|
404
|
+
|
|
405
|
+
### Test harness options
|
|
406
|
+
|
|
407
|
+
`fireforge furnace create --with-tests` scaffolds a **browser-chrome mochitest**. Use this when the component renders UI that depends on the tab strip (`openLinkIn` → `URILoadingHelper`, `gBrowser`, etc.).
|
|
408
|
+
|
|
409
|
+
`fireforge furnace create --xpcshell` scaffolds an **xpcshell test harness** instead. Use this when the component's code path is storage-only, observer-driven, or module-loading logic that does not touch a `tabbrowser`. xpcshell runs headless without browser chrome, so forks without an upstream tab strip can still cover these paths. The scaffolder writes `test_<name>_module_loads.js` + `xpcshell.toml` into `engine/browser/base/content/test/<binary-name>-xpcshell/<component-name>/` and prints a note: registration in `XPCSHELL_TESTS_MANIFESTS` is the operator's call (the moz.build that should own the entry depends on where the component actually lives).
|
|
410
|
+
|
|
411
|
+
The two flags can be combined — `--with-tests --xpcshell` writes both harnesses.
|
|
412
|
+
|
|
394
413
|
## Roadmap
|
|
395
414
|
|
|
396
415
|
Planned but not yet implemented:
|
|
@@ -96,10 +96,21 @@ export async function buildCommand(projectRoot, options) {
|
|
|
96
96
|
export function registerBuild(program, { getProjectRoot, withErrorHandling }) {
|
|
97
97
|
program
|
|
98
98
|
.command('build')
|
|
99
|
-
.description('Build the browser')
|
|
99
|
+
.description('Build the browser (auto-applies Furnace components first)')
|
|
100
100
|
.option('--ui', 'Fast UI-only rebuild')
|
|
101
101
|
.option('-j, --jobs <n>', 'Number of parallel jobs', parseJobCount)
|
|
102
102
|
.option('--brand <name>', 'Build specific brand')
|
|
103
|
+
.addHelpText('after', [
|
|
104
|
+
'',
|
|
105
|
+
'Furnace apply runs automatically before the build step, so edits in',
|
|
106
|
+
'components/custom/ and components/overrides/ are propagated to the',
|
|
107
|
+
'engine/ tree every time. The command prints a banner listing the',
|
|
108
|
+
'components synced during the current invocation.',
|
|
109
|
+
'',
|
|
110
|
+
'If you want to preview the engine state without triggering a build,',
|
|
111
|
+
'run `fireforge furnace apply` directly. For source-change-driven',
|
|
112
|
+
'rebuild loops during development, use `fireforge watch`.',
|
|
113
|
+
].join('\n'))
|
|
103
114
|
.action(withErrorHandling(async (options) => {
|
|
104
115
|
await buildCommand(getProjectRoot(), pickDefined(options));
|
|
105
116
|
}));
|
|
@@ -24,3 +24,24 @@ export declare function generateMjsContent(name: string, className: string, desc
|
|
|
24
24
|
export declare function generateCssContent(header: string): string;
|
|
25
25
|
/** Generates the .ftl file content for a custom component. */
|
|
26
26
|
export declare function generateFtlContent(name: string, header: string): string;
|
|
27
|
+
/** Returns the canonical xpcshell test file basename for a component. */
|
|
28
|
+
export declare function xpcshellTestFileName(name: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Generates an xpcshell test file for a custom component.
|
|
31
|
+
*
|
|
32
|
+
* xpcshell tests run headless without a `tabbrowser`, so they suit
|
|
33
|
+
* storage/observer/module-loading code in forks that do not mount the
|
|
34
|
+
* upstream browser chrome (and therefore lack `openLinkIn` →
|
|
35
|
+
* `URILoadingHelper`). The scaffold imports the component module via
|
|
36
|
+
* `ChromeUtils.importESModule` and asserts the module resolves — enough
|
|
37
|
+
* to catch registration regressions without touching DOM rendering paths
|
|
38
|
+
* that xpcshell cannot execute.
|
|
39
|
+
*/
|
|
40
|
+
export declare function generateXpcshellTestContent(name: string, header: string): string;
|
|
41
|
+
/**
|
|
42
|
+
* Generates an `xpcshell.toml` manifest for a custom component's test
|
|
43
|
+
* directory. Kept minimal — adding prefs, head.js, and support-files is
|
|
44
|
+
* left to the operator because those decisions depend on what the
|
|
45
|
+
* component actually touches (Services.storage, observer topics, etc.).
|
|
46
|
+
*/
|
|
47
|
+
export declare function generateXpcshellManifestContent(name: string, header: string): string;
|
|
@@ -83,4 +83,53 @@ export function generateFtlContent(name, header) {
|
|
|
83
83
|
## Strings for the ${name} component
|
|
84
84
|
`;
|
|
85
85
|
}
|
|
86
|
+
/** Returns the canonical xpcshell test file basename for a component. */
|
|
87
|
+
export function xpcshellTestFileName(name) {
|
|
88
|
+
return `test_${name.replace(/-/g, '_')}_module_loads.js`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Generates an xpcshell test file for a custom component.
|
|
92
|
+
*
|
|
93
|
+
* xpcshell tests run headless without a `tabbrowser`, so they suit
|
|
94
|
+
* storage/observer/module-loading code in forks that do not mount the
|
|
95
|
+
* upstream browser chrome (and therefore lack `openLinkIn` →
|
|
96
|
+
* `URILoadingHelper`). The scaffold imports the component module via
|
|
97
|
+
* `ChromeUtils.importESModule` and asserts the module resolves — enough
|
|
98
|
+
* to catch registration regressions without touching DOM rendering paths
|
|
99
|
+
* that xpcshell cannot execute.
|
|
100
|
+
*/
|
|
101
|
+
export function generateXpcshellTestContent(name, header) {
|
|
102
|
+
return `${header}
|
|
103
|
+
|
|
104
|
+
"use strict";
|
|
105
|
+
|
|
106
|
+
add_task(async function test_${name.replace(/-/g, '_')}_module_loads() {
|
|
107
|
+
// Module-load smoke check: resolves the ESM at its registered chrome URI.
|
|
108
|
+
// Replace or extend with storage-layer assertions as the component grows
|
|
109
|
+
// (Services.storage, observer topics, JSONFile, etc. are all available
|
|
110
|
+
// here without a tabbrowser).
|
|
111
|
+
const moduleUri = "chrome://global/content/elements/${name}.mjs";
|
|
112
|
+
const module = await ChromeUtils.importESModule(moduleUri);
|
|
113
|
+
Assert.ok(
|
|
114
|
+
module,
|
|
115
|
+
"${name}.mjs should load under xpcshell (storage-layer code path).",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
`;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Generates an `xpcshell.toml` manifest for a custom component's test
|
|
122
|
+
* directory. Kept minimal — adding prefs, head.js, and support-files is
|
|
123
|
+
* left to the operator because those decisions depend on what the
|
|
124
|
+
* component actually touches (Services.storage, observer topics, etc.).
|
|
125
|
+
*/
|
|
126
|
+
export function generateXpcshellManifestContent(name, header) {
|
|
127
|
+
return `${header}
|
|
128
|
+
|
|
129
|
+
[DEFAULT]
|
|
130
|
+
head = ""
|
|
131
|
+
|
|
132
|
+
["${xpcshellTestFileName(name)}"]
|
|
133
|
+
`;
|
|
134
|
+
}
|
|
86
135
|
//# sourceMappingURL=create-templates.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xpcshell test-harness scaffolder for `fireforge furnace create --xpcshell`.
|
|
3
|
+
* Extracted from `create.ts` so the command entrypoint stays under the
|
|
4
|
+
* per-file LOC budget and the scaffolder is unit-testable in isolation.
|
|
5
|
+
*/
|
|
6
|
+
import { type RollbackJournal } from '../../core/furnace-rollback.js';
|
|
7
|
+
import type { ProjectLicense } from '../../types/config.js';
|
|
8
|
+
/**
|
|
9
|
+
* Scaffolds an xpcshell test harness for a newly created custom component.
|
|
10
|
+
*
|
|
11
|
+
* xpcshell is the appropriate harness for storage-layer code on forks
|
|
12
|
+
* without a `tabbrowser` (no `openLinkIn` → `URILoadingHelper`). Browser
|
|
13
|
+
* chrome mochitests require tabbrowser; xpcshell does not, so storage,
|
|
14
|
+
* observers, and ESM-loading logic can be covered headless.
|
|
15
|
+
*
|
|
16
|
+
* Writes `test_<name>_module_loads.js` and an `xpcshell.toml` manifest
|
|
17
|
+
* into `engine/browser/base/content/test/<binary-name>-xpcshell/
|
|
18
|
+
* <component-name>/`. moz.build registration is intentionally left to the
|
|
19
|
+
* operator — wiring an `XPCSHELL_TESTS_MANIFESTS` entry requires a
|
|
20
|
+
* deliberate choice about which moz.build should own it, and an
|
|
21
|
+
* auto-insertion that guessed wrong would be worse than a note.
|
|
22
|
+
*/
|
|
23
|
+
export declare function scaffoldXpcshellTestFiles(componentName: string, license: ProjectLicense, forgeConfig: {
|
|
24
|
+
binaryName: string;
|
|
25
|
+
}, paths: {
|
|
26
|
+
engine: string;
|
|
27
|
+
}, journal?: RollbackJournal): Promise<string[]>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* xpcshell test-harness scaffolder for `fireforge furnace create --xpcshell`.
|
|
4
|
+
* Extracted from `create.ts` so the command entrypoint stays under the
|
|
5
|
+
* per-file LOC budget and the scaffolder is unit-testable in isolation.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { recordCreatedDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
9
|
+
import { getLicenseHeader } from '../../core/license-headers.js';
|
|
10
|
+
import { ensureDir, pathExists, writeText } from '../../utils/fs.js';
|
|
11
|
+
import { warn } from '../../utils/logger.js';
|
|
12
|
+
import { generateXpcshellManifestContent, generateXpcshellTestContent, xpcshellTestFileName, } from './create-templates.js';
|
|
13
|
+
/**
|
|
14
|
+
* Scaffolds an xpcshell test harness for a newly created custom component.
|
|
15
|
+
*
|
|
16
|
+
* xpcshell is the appropriate harness for storage-layer code on forks
|
|
17
|
+
* without a `tabbrowser` (no `openLinkIn` → `URILoadingHelper`). Browser
|
|
18
|
+
* chrome mochitests require tabbrowser; xpcshell does not, so storage,
|
|
19
|
+
* observers, and ESM-loading logic can be covered headless.
|
|
20
|
+
*
|
|
21
|
+
* Writes `test_<name>_module_loads.js` and an `xpcshell.toml` manifest
|
|
22
|
+
* into `engine/browser/base/content/test/<binary-name>-xpcshell/
|
|
23
|
+
* <component-name>/`. moz.build registration is intentionally left to the
|
|
24
|
+
* operator — wiring an `XPCSHELL_TESTS_MANIFESTS` entry requires a
|
|
25
|
+
* deliberate choice about which moz.build should own it, and an
|
|
26
|
+
* auto-insertion that guessed wrong would be worse than a note.
|
|
27
|
+
*/
|
|
28
|
+
export async function scaffoldXpcshellTestFiles(componentName, license, forgeConfig, paths, journal) {
|
|
29
|
+
const parentDirName = `${forgeConfig.binaryName}-xpcshell`;
|
|
30
|
+
const testDir = join(paths.engine, 'browser/base/content/test', parentDirName, componentName);
|
|
31
|
+
if (journal && !(await pathExists(testDir))) {
|
|
32
|
+
recordCreatedDir(journal, testDir);
|
|
33
|
+
}
|
|
34
|
+
await ensureDir(testDir);
|
|
35
|
+
const jsHeader = getLicenseHeader(license, 'js');
|
|
36
|
+
const hashHeader = getLicenseHeader(license, 'hash');
|
|
37
|
+
const testFiles = [];
|
|
38
|
+
const testFileName = xpcshellTestFileName(componentName);
|
|
39
|
+
const testFilePath = join(testDir, testFileName);
|
|
40
|
+
if (journal)
|
|
41
|
+
await snapshotFile(journal, testFilePath);
|
|
42
|
+
await writeText(testFilePath, generateXpcshellTestContent(componentName, jsHeader));
|
|
43
|
+
testFiles.push(testFileName);
|
|
44
|
+
const manifestPath = join(testDir, 'xpcshell.toml');
|
|
45
|
+
if (journal)
|
|
46
|
+
await snapshotFile(journal, manifestPath);
|
|
47
|
+
await writeText(manifestPath, generateXpcshellManifestContent(componentName, hashHeader));
|
|
48
|
+
testFiles.push('xpcshell.toml');
|
|
49
|
+
warn(`xpcshell scaffold written under browser/base/content/test/${parentDirName}/${componentName}/. ` +
|
|
50
|
+
'Add the directory to XPCSHELL_TESTS_MANIFESTS in the nearest moz.build to run it via "fireforge test".');
|
|
51
|
+
return testFiles;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=create-xpcshell.js.map
|
|
@@ -16,6 +16,7 @@ import { toError } from '../../utils/errors.js';
|
|
|
16
16
|
import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
|
|
17
17
|
import { cancel, intro, isCancel, note, outro, success, warn } from '../../utils/logger.js';
|
|
18
18
|
import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
19
|
+
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
19
20
|
async function loadAuthoringFurnaceConfig(projectRoot) {
|
|
20
21
|
if (await furnaceConfigExists(projectRoot)) {
|
|
21
22
|
return loadFurnaceConfig(projectRoot);
|
|
@@ -262,6 +263,10 @@ async function performCreateMutations(args) {
|
|
|
262
263
|
const scafFiles = await scaffoldTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
263
264
|
testFiles.push(...scafFiles);
|
|
264
265
|
}
|
|
266
|
+
if (args.xpcshellTests) {
|
|
267
|
+
const xpcshellFiles = await scaffoldXpcshellTestFiles(args.componentName, args.license, args.forgeConfig, args.paths, journal);
|
|
268
|
+
testFiles.push(...xpcshellFiles);
|
|
269
|
+
}
|
|
265
270
|
}
|
|
266
271
|
catch (error) {
|
|
267
272
|
try {
|
|
@@ -396,12 +401,14 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
396
401
|
}
|
|
397
402
|
const { localized, register } = featureSelection;
|
|
398
403
|
// --with-tests writes files under engine/browser/base/content/test/ and
|
|
399
|
-
// registers them in moz.build.
|
|
400
|
-
//
|
|
401
|
-
//
|
|
404
|
+
// registers them in moz.build. --xpcshell is the equivalent for forks
|
|
405
|
+
// without a tabbrowser (storage-layer code). Guard against a missing
|
|
406
|
+
// engine now rather than letting the scaffolders fabricate a partial
|
|
407
|
+
// engine tree with ensureDir.
|
|
402
408
|
const withTests = options.withTests ?? false;
|
|
403
|
-
|
|
404
|
-
|
|
409
|
+
const xpcshellTests = options.xpcshell ?? false;
|
|
410
|
+
if ((withTests || xpcshellTests) && !(await pathExists(paths.engine))) {
|
|
411
|
+
throw new FurnaceError('Engine directory not found. Run "fireforge download" first to use --with-tests or --xpcshell.', componentName);
|
|
405
412
|
}
|
|
406
413
|
// --- Generate component files ---
|
|
407
414
|
const className = tagNameToClassName(componentName);
|
|
@@ -438,6 +445,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
438
445
|
paths,
|
|
439
446
|
license,
|
|
440
447
|
withTests,
|
|
448
|
+
xpcshellTests,
|
|
441
449
|
ftlChromeSubPath,
|
|
442
450
|
operationContext: ctx,
|
|
443
451
|
}));
|
|
@@ -445,9 +453,10 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
445
453
|
let noteParts = `Files created in components/custom/${componentName}/:\n` +
|
|
446
454
|
files.map((f) => ` ${f}`).join('\n');
|
|
447
455
|
if (testFiles.length > 0) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
456
|
+
const testRoot = xpcshellTests
|
|
457
|
+
? `engine/browser/base/content/test/${forgeConfig.binaryName}-xpcshell/${componentName}/`
|
|
458
|
+
: `engine/browser/base/content/test/${forgeConfig.binaryName}/`;
|
|
459
|
+
noteParts += `\n\nTest files in ${testRoot}:\n` + testFiles.map((f) => ` ${f}`).join('\n');
|
|
451
460
|
}
|
|
452
461
|
noteParts +=
|
|
453
462
|
'\n\n' +
|
|
@@ -72,6 +72,7 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
72
72
|
.option('--localized', 'Include Fluent l10n support')
|
|
73
73
|
.option('--no-register', 'Skip customElements.js registration')
|
|
74
74
|
.option('--with-tests', 'Scaffold Mochitest directory and register in moz.build')
|
|
75
|
+
.option('--xpcshell', 'Scaffold an xpcshell test harness (for storage-layer code on forks without tabbrowser)')
|
|
75
76
|
.option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
|
|
76
77
|
.action(withErrorHandling(async (name, options) => {
|
|
77
78
|
await furnaceCreateCommand(getProjectRoot(), name, options);
|
|
@@ -8,4 +8,4 @@ import type { SetupOptions } from '../types/commands/index.js';
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function setupCommand(projectRoot: string, options?: SetupOptions): Promise<void>;
|
|
10
10
|
/** Registers the setup command on the CLI program. */
|
|
11
|
-
export declare function registerSetup(program: Command, {
|
|
11
|
+
export declare function registerSetup(program: Command, { withErrorHandling }: CommandContext): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { resolve } from 'node:path';
|
|
2
3
|
import { confirm } from '@clack/prompts';
|
|
3
4
|
import { Option } from 'commander';
|
|
4
5
|
import { configExists } from '../core/config.js';
|
|
@@ -57,7 +58,7 @@ export async function setupCommand(projectRoot, options = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
/** Registers the setup command on the CLI program. */
|
|
60
|
-
export function registerSetup(program, {
|
|
61
|
+
export function registerSetup(program, { withErrorHandling }) {
|
|
61
62
|
program
|
|
62
63
|
.command('setup')
|
|
63
64
|
.description('Initialize a new FireForge project')
|
|
@@ -88,7 +89,7 @@ export function registerSetup(program, { getProjectRoot, withErrorHandling }) {
|
|
|
88
89
|
setupOptions.license = parsedLicense;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
|
-
await setupCommand(
|
|
92
|
+
await setupCommand(resolve(process.cwd()), setupOptions);
|
|
92
93
|
}));
|
|
93
94
|
}
|
|
94
95
|
//# sourceMappingURL=setup.js.map
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { FurnaceError } from '../errors/furnace.js';
|
|
7
7
|
import { pathExists } from '../utils/fs.js';
|
|
8
|
-
import { spinner, warn } from '../utils/logger.js';
|
|
8
|
+
import { info, spinner, warn } from '../utils/logger.js';
|
|
9
9
|
import { isBrandingSetup, setupBranding } from './branding.js';
|
|
10
10
|
import { applyAllComponents } from './furnace-apply.js';
|
|
11
11
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
@@ -95,7 +95,12 @@ export async function prepareBuildEnvironment(projectRoot, paths, config) {
|
|
|
95
95
|
throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
|
|
96
96
|
}
|
|
97
97
|
if (furnaceApplied > 0) {
|
|
98
|
+
const appliedNames = result.applied.map((entry) => entry.name).join(', ');
|
|
98
99
|
furnaceSpinner.stop(`Applied ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'}`);
|
|
100
|
+
// Loud banner: the build operator needs to see that engine/ was
|
|
101
|
+
// updated before this build, otherwise a silent re-apply is
|
|
102
|
+
// indistinguishable from a build that shipped stale components.
|
|
103
|
+
info(`Furnace: source → engine sync wrote ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'} before build (${appliedNames}). engine/ now matches components/.`);
|
|
99
104
|
}
|
|
100
105
|
else {
|
|
101
106
|
furnaceSpinner.stop('Components up to date');
|
|
@@ -94,7 +94,7 @@ export declare function hasCustomEngineDrift(root: string, name: string, compone
|
|
|
94
94
|
export interface CustomApplyOptions {
|
|
95
95
|
/**
|
|
96
96
|
* Trailing project marker appended to inserted `customElements.js` entries
|
|
97
|
-
* (e.g. `"
|
|
97
|
+
* (e.g. `"MYBROWSER"` emits ` // MYBROWSER:` on each line). Mirrors the
|
|
98
98
|
* `markerComment` field in fireforge.json.
|
|
99
99
|
*/
|
|
100
100
|
markerComment?: string;
|
|
@@ -9,3 +9,9 @@
|
|
|
9
9
|
* violation; does nothing for `undefined` (field is optional).
|
|
10
10
|
*/
|
|
11
11
|
export declare function validateTokenHostDocuments(raw: unknown): void;
|
|
12
|
+
/**
|
|
13
|
+
* Validates a `runtimeVariables` raw value. Each entry must start with `--`
|
|
14
|
+
* (it is a CSS custom property name). Throws `FurnaceError` on violation;
|
|
15
|
+
* does nothing for `undefined` (field is optional).
|
|
16
|
+
*/
|
|
17
|
+
export declare function validateRuntimeVariables(raw: unknown): void;
|
|
@@ -25,4 +25,19 @@ export function validateTokenHostDocuments(raw) {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Validates a `runtimeVariables` raw value. Each entry must start with `--`
|
|
30
|
+
* (it is a CSS custom property name). Throws `FurnaceError` on violation;
|
|
31
|
+
* does nothing for `undefined` (field is optional).
|
|
32
|
+
*/
|
|
33
|
+
export function validateRuntimeVariables(raw) {
|
|
34
|
+
if (raw === undefined)
|
|
35
|
+
return;
|
|
36
|
+
const vars = parseStringArray(raw, 'runtimeVariables');
|
|
37
|
+
for (const name of vars) {
|
|
38
|
+
if (!name.startsWith('--')) {
|
|
39
|
+
throw new FurnaceError(`Furnace config: "runtimeVariables" entries must start with "--" (got ${JSON.stringify(name)})`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
28
43
|
//# sourceMappingURL=furnace-config-tokens.js.map
|
|
@@ -7,7 +7,7 @@ import { warn } from '../utils/logger.js';
|
|
|
7
7
|
import { isExplicitAbsolutePath } from '../utils/paths.js';
|
|
8
8
|
import { isArray, isBoolean, isObject, isString } from '../utils/validation.js';
|
|
9
9
|
import { FIREFORGE_DIR } from './config.js';
|
|
10
|
-
import { validateTokenHostDocuments } from './furnace-config-tokens.js';
|
|
10
|
+
import { validateRuntimeVariables, validateTokenHostDocuments } from './furnace-config-tokens.js';
|
|
11
11
|
import { resolveFtlDir } from './furnace-constants.js';
|
|
12
12
|
import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
|
|
13
13
|
import { quarantineStateFile, withStateFileLock } from './state-file.js';
|
|
@@ -183,6 +183,9 @@ export function validateFurnaceConfig(data) {
|
|
|
183
183
|
if (migrated['tokenAllowlist'] !== undefined) {
|
|
184
184
|
parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
185
185
|
}
|
|
186
|
+
// Validate optional runtimeVariables — CSS runtime state channels
|
|
187
|
+
// (e.g. `--cam-x`) that are exempt from `token-prefix-violation`.
|
|
188
|
+
validateRuntimeVariables(migrated['runtimeVariables']);
|
|
186
189
|
// Validate optional tokenHostDocuments — list of chrome XHTMLs that the
|
|
187
190
|
// `missing-token-link` validator scans for the tokens CSS link.
|
|
188
191
|
validateTokenHostDocuments(migrated['tokenHostDocuments']);
|
|
@@ -236,14 +239,17 @@ export function validateFurnaceConfig(data) {
|
|
|
236
239
|
overrides,
|
|
237
240
|
custom,
|
|
238
241
|
};
|
|
239
|
-
if (migrated['tokenPrefix'] !== undefined)
|
|
242
|
+
if (migrated['tokenPrefix'] !== undefined)
|
|
240
243
|
config.tokenPrefix = migrated['tokenPrefix'];
|
|
241
|
-
}
|
|
242
244
|
if (migrated['tokenAllowlist'] !== undefined) {
|
|
243
245
|
config.tokenAllowlist = parseStringArray(migrated['tokenAllowlist'], 'tokenAllowlist');
|
|
244
246
|
}
|
|
247
|
+
if (migrated['runtimeVariables'] !== undefined) {
|
|
248
|
+
config.runtimeVariables = parseStringArray(migrated['runtimeVariables'], 'runtimeVariables');
|
|
249
|
+
}
|
|
245
250
|
if (migrated['tokenHostDocuments'] !== undefined) {
|
|
246
|
-
|
|
251
|
+
const docs = parseStringArray(migrated['tokenHostDocuments'], 'tokenHostDocuments');
|
|
252
|
+
config.tokenHostDocuments = docs;
|
|
247
253
|
}
|
|
248
254
|
// Validate optional ftlBasePath
|
|
249
255
|
if (migrated['ftlBasePath'] !== undefined) {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export interface RegistrationWriteOptions {
|
|
10
10
|
/**
|
|
11
|
-
* Trailing project marker appended to every inserted line (e.g. `"
|
|
12
|
-
* produces ` //
|
|
11
|
+
* Trailing project marker appended to every inserted line (e.g. `"MYBROWSER"`
|
|
12
|
+
* produces ` // MYBROWSER:` at end-of-line). Keeps modifications discoverable
|
|
13
13
|
* without requiring the operator to hand-tag them post-apply.
|
|
14
14
|
*/
|
|
15
15
|
markerComment?: string;
|
|
@@ -16,7 +16,7 @@ import { validateRegistrationPlacement, validateTagName } from './furnace-regist
|
|
|
16
16
|
/**
|
|
17
17
|
* Returns true when `content` already contains a registration for `tagName`.
|
|
18
18
|
*
|
|
19
|
-
* Tolerates trailing line comments (e.g. a project marker like `//
|
|
19
|
+
* Tolerates trailing line comments (e.g. a project marker like `// MYBROWSER:`)
|
|
20
20
|
* that an operator may have appended to entries written by a previous apply.
|
|
21
21
|
* Without this, a re-apply would insert a duplicate entry, and the second
|
|
22
22
|
* `setElementCreationCallback` at window-load would throw `NotSupportedError`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
4
|
import { hasRawCssColors } from '../utils/regex.js';
|
|
5
|
-
import { classExtendsMozLitElement, collectCssVariableReferences, createIssue, getTokenPrefixContext, hasCustomElementDefineCall, hasRelativeModuleImport, stripCssBlockComments, } from './furnace-validate-helpers.js';
|
|
5
|
+
import { classExtendsMozLitElement, collectCssVariableDeclarations, collectCssVariableReferences, createIssue, getTokenPrefixContext, hasCustomElementDefineCall, hasRelativeModuleImport, stripCssBlockComments, } from './furnace-validate-helpers.js';
|
|
6
6
|
async function validateMjsCompatibility(mjsPath, tagName) {
|
|
7
7
|
if (!(await pathExists(mjsPath)))
|
|
8
8
|
return [];
|
|
@@ -29,13 +29,24 @@ async function validateCssCompatibility(cssPath, tagName, type, config, root) {
|
|
|
29
29
|
issues.push(createIssue(tagName, 'error', 'raw-color-value', 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.'));
|
|
30
30
|
}
|
|
31
31
|
if (config?.tokenPrefix) {
|
|
32
|
-
const { allowlist, inheritedOverrideVars } = await getTokenPrefixContext(tagName, type, config, root);
|
|
32
|
+
const { allowlist, inheritedOverrideVars, runtimeVariables } = await getTokenPrefixContext(tagName, type, config, root);
|
|
33
|
+
// Auto-exempt component-local runtime channels: a CSS custom property
|
|
34
|
+
// both declared and consumed in the same file is a runtime state
|
|
35
|
+
// channel (e.g. `--cam-x`), not a design-token reference. See
|
|
36
|
+
// `runtimeVariables` in furnace.json for cross-component cases.
|
|
37
|
+
const localDeclarations = collectCssVariableDeclarations(cssContent);
|
|
33
38
|
for (const prop of collectCssVariableReferences(cssContent)) {
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
if (prop.startsWith(config.tokenPrefix))
|
|
40
|
+
continue;
|
|
41
|
+
if (allowlist.has(prop))
|
|
42
|
+
continue;
|
|
43
|
+
if (inheritedOverrideVars.has(prop))
|
|
44
|
+
continue;
|
|
45
|
+
if (runtimeVariables.has(prop))
|
|
46
|
+
continue;
|
|
47
|
+
if (localDeclarations.has(prop))
|
|
48
|
+
continue;
|
|
49
|
+
issues.push(createIssue(tagName, 'error', 'token-prefix-violation', `CSS references var(${prop}) which does not match the required token prefix "${config.tokenPrefix}". Use a design token, add to tokenAllowlist, or (for runtime state channels) list the variable in runtimeVariables.`));
|
|
39
50
|
}
|
|
40
51
|
}
|
|
41
52
|
// Flag excessive !important usage
|
|
@@ -21,7 +21,25 @@ export declare function hasTemplateKeyboardHandler(content: string): boolean;
|
|
|
21
21
|
* interactive element — those already fire `click` on keyboard activation.
|
|
22
22
|
*/
|
|
23
23
|
export declare function hasTemplateClickOnSyntheticInteractive(content: string): boolean;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Detects hardcoded user-visible template text that should usually be
|
|
26
|
+
* localized.
|
|
27
|
+
*
|
|
28
|
+
* Scoped to three positive contexts rather than scanning the whole file,
|
|
29
|
+
* because a bare `>…<` regex catches JS comparisons (`if (x > 0 && y <
|
|
30
|
+
* 100)`), diagnostic strings (`console.error("Failed <id> lookup")`), and
|
|
31
|
+
* identifier literals that are never shown to a user. Only matches that
|
|
32
|
+
* actually enter a UI render path count:
|
|
33
|
+
*
|
|
34
|
+
* 1. Content inside a Lit `` html`…` `` tagged template literal.
|
|
35
|
+
* 2. The string literal on the RHS of `.textContent = "…"` or
|
|
36
|
+
* `.innerHTML = "…"`.
|
|
37
|
+
* 3. The string literal assigned to an XUL-widget `label=`,
|
|
38
|
+
* `title=`, or `tooltiptext=` attribute when constructing DOM in JS.
|
|
39
|
+
*
|
|
40
|
+
* A file-wide `// furnace-ignore: hardcoded-text` comment suppresses all
|
|
41
|
+
* findings (matches the pre-existing escape hatch).
|
|
42
|
+
*/
|
|
25
43
|
export declare function containsHardcodedTemplateText(content: string): boolean;
|
|
26
44
|
/** Detects whether a component opts into shadow-root focus delegation. */
|
|
27
45
|
export declare function hasDelegatesFocusEnabled(content: string): boolean;
|
|
@@ -35,8 +53,20 @@ export declare function hasCustomElementDefineCall(mjsContent: string): boolean;
|
|
|
35
53
|
export declare function classExtendsMozLitElement(mjsContent: string): boolean;
|
|
36
54
|
/** Collects CSS custom property references used via var(--token-name). */
|
|
37
55
|
export declare function collectCssVariableReferences(cssContent: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Collects CSS custom property *declarations* — names appearing on the
|
|
58
|
+
* left-hand side of a `--name:` declaration. Used to auto-exempt
|
|
59
|
+
* component-local runtime variables from the token-prefix check: if the
|
|
60
|
+
* component both declares and consumes a variable in its own CSS file, it
|
|
61
|
+
* is a local runtime channel, not a design-token reference.
|
|
62
|
+
*
|
|
63
|
+
* The anchor `(?:^|[{;,\s])` rules out `var(--name)` occurrences (which are
|
|
64
|
+
* always preceded by `(`), so references are not mistaken for declarations.
|
|
65
|
+
*/
|
|
66
|
+
export declare function collectCssVariableDeclarations(cssContent: string): Set<string>;
|
|
38
67
|
/** Builds token-validation context from the config allowlist and inherited override CSS. */
|
|
39
68
|
export declare function getTokenPrefixContext(tagName: string, type: ComponentType, config: FurnaceConfig, root: string | undefined): Promise<{
|
|
40
69
|
allowlist: Set<string>;
|
|
41
70
|
inheritedOverrideVars: Set<string>;
|
|
71
|
+
runtimeVariables: Set<string>;
|
|
42
72
|
}>;
|
|
@@ -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.
|
|
@@ -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
|
*
|
|
@@ -110,12 +110,14 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config
|
|
|
110
110
|
// Load furnace config gracefully — skip token-prefix check if unavailable
|
|
111
111
|
let tokenPrefix;
|
|
112
112
|
let tokenAllowlist;
|
|
113
|
+
let runtimeVariables;
|
|
113
114
|
try {
|
|
114
115
|
const root = join(repoDir, '..');
|
|
115
116
|
const furnaceConfig = await loadFurnaceConfig(root);
|
|
116
117
|
if (furnaceConfig.tokenPrefix) {
|
|
117
118
|
tokenPrefix = furnaceConfig.tokenPrefix;
|
|
118
119
|
tokenAllowlist = new Set(furnaceConfig.tokenAllowlist ?? []);
|
|
120
|
+
runtimeVariables = new Set(furnaceConfig.runtimeVariables ?? []);
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
catch (error) {
|
|
@@ -154,20 +156,38 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config
|
|
|
154
156
|
});
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
|
-
// Check for non-tokenized custom properties
|
|
159
|
+
// Check for non-tokenized custom properties. A variable that is both
|
|
160
|
+
// declared and consumed inside the same file is auto-exempted as a
|
|
161
|
+
// runtime state channel (see furnace.json → runtimeVariables).
|
|
158
162
|
if (tokenPrefix) {
|
|
163
|
+
const declarationPattern = /(?:^|[{;,\s])(--[\w-]+)\s*:/g;
|
|
164
|
+
const localDeclarations = new Set();
|
|
165
|
+
let declMatch;
|
|
166
|
+
while ((declMatch = declarationPattern.exec(cssContent)) !== null) {
|
|
167
|
+
const name = declMatch[1];
|
|
168
|
+
if (name)
|
|
169
|
+
localDeclarations.add(name);
|
|
170
|
+
}
|
|
159
171
|
const varPattern = /var\(\s*(--[\w-]+)/g;
|
|
160
172
|
let match;
|
|
161
173
|
while ((match = varPattern.exec(cssContent)) !== null) {
|
|
162
174
|
const prop = match[1];
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
if (!prop)
|
|
176
|
+
continue;
|
|
177
|
+
if (prop.startsWith(tokenPrefix))
|
|
178
|
+
continue;
|
|
179
|
+
if (tokenAllowlist?.has(prop))
|
|
180
|
+
continue;
|
|
181
|
+
if (runtimeVariables?.has(prop))
|
|
182
|
+
continue;
|
|
183
|
+
if (localDeclarations.has(prop))
|
|
184
|
+
continue;
|
|
185
|
+
issues.push({
|
|
186
|
+
file,
|
|
187
|
+
check: 'token-prefix-violation',
|
|
188
|
+
message: `CSS references var(${prop}) which does not match the required token prefix "${tokenPrefix}". Use a design token, add to tokenAllowlist, or (for runtime state channels) list the variable in runtimeVariables.`,
|
|
189
|
+
severity: 'error',
|
|
190
|
+
});
|
|
171
191
|
}
|
|
172
192
|
}
|
|
173
193
|
}
|
|
@@ -271,6 +271,16 @@ export interface FurnaceCreateOptions {
|
|
|
271
271
|
register?: boolean;
|
|
272
272
|
/** Scaffold Mochitest directory and register in moz.build */
|
|
273
273
|
withTests?: boolean;
|
|
274
|
+
/**
|
|
275
|
+
* Scaffold an xpcshell test harness (headless, no tabbrowser) instead of
|
|
276
|
+
* browser-chrome. Required for forks without a `tabbrowser` (storage-only
|
|
277
|
+
* code, observer-driven modules). Implies `withTests` when set. Writes an
|
|
278
|
+
* `xpcshell.toml` + `test_<name>.js` under
|
|
279
|
+
* `engine/browser/base/content/test/<binary-name>-xpcshell/` and leaves
|
|
280
|
+
* moz.build registration to the operator (add the directory to
|
|
281
|
+
* `XPCSHELL_TESTS_MANIFESTS`).
|
|
282
|
+
*/
|
|
283
|
+
xpcshell?: boolean;
|
|
274
284
|
/** Stock component tag names composed internally by this component */
|
|
275
285
|
compose?: string[];
|
|
276
286
|
}
|
|
@@ -47,7 +47,7 @@ export interface FireForgeConfig {
|
|
|
47
47
|
/**
|
|
48
48
|
* Project marker prefix appended to lines FireForge writes into
|
|
49
49
|
* upstream Firefox source files (e.g. the `customElements.js` tag list).
|
|
50
|
-
* `"
|
|
50
|
+
* `"MYBROWSER"` emits a trailing ` // MYBROWSER:` on each inserted line.
|
|
51
51
|
* Keeps modifications discoverable and re-applies idempotent.
|
|
52
52
|
*/
|
|
53
53
|
markerComment?: string;
|
|
@@ -63,10 +63,21 @@ export interface FurnaceConfig {
|
|
|
63
63
|
tokenPrefix?: string;
|
|
64
64
|
/** Custom properties allowed even though they don't match tokenPrefix (e.g. ["--background-color-box"]) */
|
|
65
65
|
tokenAllowlist?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* Custom properties used as runtime state channels — written and read by the
|
|
68
|
+
* component itself (e.g. per-frame camera/tile positions) rather than
|
|
69
|
+
* consumed as design tokens. Listed names are exempt from the
|
|
70
|
+
* `token-prefix-violation` check even when they do not match `tokenPrefix`
|
|
71
|
+
* and are not in `tokenAllowlist`. Use this for cross-component runtime
|
|
72
|
+
* variables (e.g. set in JS, read in CSS of a child). Component-local
|
|
73
|
+
* variables that are both declared and consumed inside the same component's
|
|
74
|
+
* own CSS file are auto-exempted and do not need an entry here.
|
|
75
|
+
*/
|
|
76
|
+
runtimeVariables?: string[];
|
|
66
77
|
/**
|
|
67
78
|
* Chrome documents scanned by the `missing-token-link` validator to confirm
|
|
68
79
|
* the tokens CSS file is `<link>`ed. Forks with multiple chrome host
|
|
69
|
-
* documents (e.g. `
|
|
80
|
+
* documents (e.g. `mybrowser.xhtml` beside the stock `browser.xhtml`) should
|
|
70
81
|
* list every document that may own the link. When omitted, defaults to
|
|
71
82
|
* `['browser/base/content/browser.xhtml']` — the upstream Firefox path.
|
|
72
83
|
*/
|