@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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## 0.15.0
|
|
4
4
|
|
|
5
|
+
### Test — xpcshell appdir auto-injection
|
|
6
|
+
|
|
7
|
+
- `fireforge test` now auto-injects `--app-path=<absolute>` into mach test invocations whose nearest `xpcshell.toml` sets `firefox-appdir = "browser"` on a rebranded fork (appname != `firefox`). Without this, every `resource:///modules/<name>.sys.mjs` import inside the harness throws because the upstream xpcshell harness reads the appdir override under the appname-keyed manifest field (`<appname>-appdir`) — the literal `firefox-appdir = "browser"` directive is silently ignored when `appname` is anything other than `firefox`, so `appPath` falls back to `xrePath` (one level above the real app root). The resolver lives in the new `src/core/xpcshell-appdir.ts`: it walks each test path to the nearest `xpcshell.toml`, reads `mozinfo.json` for the active appname, prefers any `<appname>-appdir` already in the manifest (so an operator who already migrated is not overridden), and otherwise probes `<objDir>/dist/bin/<value>` and `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` for the absolute target. Operator overrides via `--mach-arg=--app-path=…` always win and the resolver is skipped silently when one is detected. Mismatches across multiple test paths (different manifests resolving to different app dirs) and unresolvable manifest values (no candidate under `dist/`) are surfaced as warnings rather than guessed at, so triage can reach the underlying cause instead of debugging a wrong path.
|
|
8
|
+
- The `xpcshell appdir` diagnostic that fires when the injection did not prevent the symptom now branches its "Likely triggers" copy on whether the auto-injection ran. When it ran and the failure persists, the message says so and points at packing or layout mismatches; when it did not run (no manifest found, or appname=`firefox`), the original appname-key explanation is preserved. Adds a fourth troubleshooting option — adding `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the manifest — so operators can adopt the durable fix instead of relying on injection on every run.
|
|
9
|
+
- `fireforge test` was previously diagnostic-only for this class of failure: it detected the symptom and surfaced a hint suggesting `--mach-arg` overrides. The hint remains as a fallback for cases the resolver cannot fix, but no manual flag is needed for the common case.
|
|
10
|
+
|
|
11
|
+
### Run — `--smoke-exit` for unattended chrome smoke checks
|
|
12
|
+
|
|
13
|
+
- New `fireforge run --smoke-exit <seconds>` launches the real built browser with the dev profile, streams the merged console line-by-line, sends `SIGTERM` to the entire child process group at the deadline, and exits non-zero when any `JavaScript error:` / `console.error:` / `[JavaScript Error]` / `###!!! [Parent]` line surfaces inside the smoke window without matching the operator-supplied allowlist. Closes the headless-vs-real-chrome gap that previously forced agents to choose between `fireforge run` (no exit hook, hangs on a human) and `fireforge run --headless` (does not load `browser.xhtml`/`<custom>.xhtml`, so chrome-window constructor errors stay invisible). The motivating case was `TypeError: lazy.HominisEvents.HOMINIS_TOPICS is undefined` thrown from a chrome-document constructor on every window-open, which `--headless` never observed because the main document was never instantiated.
|
|
14
|
+
- Allowlist surface lives in the new `src/core/smoke-patterns.ts`: regex error patterns (`SMOKE_ERROR_PATTERNS`) anchored on the start of each line so embedded mentions of the same prefix inside descriptive prose do not trip the scanner, plus `compileAllowlistFromFile` and `compileAllowlistFromStrings` for `--console-allow-file <path>` and repeatable `--console-allow <regex>` inputs respectively. Allowlisted hits still count toward the summary so operators can see how many lines were suppressed; only unallowed errors drive the exit code. A blank or `#`-prefixed line in an allowlist file is skipped, and a malformed regex throws fast at CLI parse time rather than silently matching nothing.
|
|
15
|
+
- New `--capture-console <file>` mirrors the captured stream to a file path so post-exit inspection has the raw log without re-running. Mirroring is inline with line dispatch and the file is fsync'd on close.
|
|
16
|
+
- Smoke-run exit codes are wired distinctly from `BUILD_ERROR`: `SMOKE_EXIT_FAILURE = 12` (unallowed console errors observed inside the window), `SMOKE_LAUNCH_FAILURE = 13` (browser exited non-clean before the window elapsed — crashes before console wiring, missing profile, etc.). CI pipelines can route these distinctly from compile/config failures and from the existing FireForge exit codes. Carried by the new `SmokeRunError` in `src/errors/run.ts` so the throw site signals both the message and the exit code in one type. The `runMachSmoke()` wrapper in `src/core/mach.ts` handles the deadline-driven SIGTERM/SIGKILL escalation (default 10 s grace between SIGTERM and SIGKILL because Firefox's `AsyncShutdown` and `profileBeforeChange` blockers can take ~5–10 s to flush in-memory state; a shorter grace risks corrupting the dev profile mid-quit). POSIX-only — process-group semantics do not map cleanly onto Windows, so the flag is rejected up-front there. A smoke window shorter than 30 s also surfaces a one-line warning recommending a longer interval, since cold-start time alone can consume that budget on a debug build.
|
|
17
|
+
|
|
18
|
+
### Furnace create — `--shared-ftl` for feature-scoped Fluent bundles
|
|
19
|
+
|
|
20
|
+
- `furnace create <tag> --localized --shared-ftl <chrome-uri>` now scaffolds a component that participates in a pre-existing feature-scoped `.ftl` bundle (e.g. `browser/hominis-dock.ftl`) instead of getting its own per-component stub. Without the flag, every `--localized` create produced a `<tag>.ftl` under `components/custom/<tag>/` plus an `insertFTLIfNeeded("toolkit/global/<tag>.ftl")` call in the `.mjs`; on a feature like a dock with eight components sharing one bundle, this stranded seven empty per-component stubs in the workspace and required hand-editing every `.mjs` to point `insertFTLIfNeeded` at the shared file (and accepting that the seven empty stubs would still get packaged into `Resources/localization/en-US/toolkit/global/`). The flag short-circuits all of that: the `.mjs` is generated with the shared path baked in via `generateMjsContent`, the per-component `.ftl` is not written, and the `furnace.json` `custom` entry carries a new `sharedFtl` field so subsequent runs (and validators) know the component participates in a shared bundle.
|
|
21
|
+
- `--shared-ftl` implies `--localized`; combining `--no-localized` with `--shared-ftl` is rejected fast-fail with an actionable message rather than letting the cross-field check inside the config parser surface the same error later. The interactive features prompt is suppressed when `--shared-ftl` is set so the operator is not asked to flip a flag we are about to enforce.
|
|
22
|
+
- `furnace apply` and `furnace remove` honour `sharedFtl` symmetrically: apply does NOT copy a per-component `.ftl` into the FTL tree nor add a locale `jar.mn` entry (the shared file is registered by whoever owns the feature bundle), and remove early-returns from `removeCustomFtlJarMnEntry` so dropping our component's reference does not orphan every other participant in the shared bundle. The dry-run path mirrors apply, so a `--dry-run` plan accurately previews the actual file set.
|
|
23
|
+
- `furnace validate`'s `missing-ftl` structural check is skipped for `sharedFtl` components — there is no per-component `<tag>.ftl` to require. The remaining error message also points at `sharedFtl` as the third option ("Create the file, set localized: false in furnace.json, or switch to sharedFtl") so an operator who hits the warning sees the structured way out alongside the existing two.
|
|
24
|
+
- The `sharedFtl` value is validated in one place (`src/core/shared-ftl.ts`) by both the CLI flag path and the `furnace.json` parser, so the two entry points cannot drift. Backticks, backslashes, and `${` are rejected because the value is interpolated verbatim into the generated `.mjs` template literal — without that check, an unsafe input would either close the template or open an executable expression. `--no-localized + --shared-ftl` and `--shared-ftl=""` (empty after trim) are rejected with structured reasons. The custom-component schema parser was extracted from `furnace-config.ts` into `src/core/furnace-config-custom.ts` to keep the main config module under the per-file LOC budget as the schema continues to gain opt-in fields (`composes`, `keyboardCovered`, `sharedFtl`).
|
|
25
|
+
|
|
26
|
+
### Furnace validate — `no-keyboard-handler` composition awareness
|
|
27
|
+
|
|
28
|
+
- `no-keyboard-handler` no longer warns when `@click` sits on a custom-element host whose `composes` entry names a native-interactive child (e.g. `moz-button`, `moz-toggle`, `moz-checkbox`, `moz-radio`, `moz-menulist`). The wrapper's `@click` handler catches keyboard activation transitively — `<moz-button>` itself dispatches `click` on Enter/Space via the platform — so a duplicate `@keydown`/`@keypress` handler on the wrapper would either no-op or double-fire alongside the child's built-in keyboard path. Previously a `hominis-pin-section` rendering a row of `<hominis-dock-button @click=…>` instances flagged `[no-keyboard-handler] Interactive element has @click but no keyboard event handler` on every validate, even though the entire row was fully keyboard-accessible.
|
|
29
|
+
- New optional `keyboardCovered: true` field on a custom component's `furnace.json` entry forces the same skip when the wrapped inner element is hand-authored (e.g. a vendored `<button>`) or is a non-stock `moz-*` widget that does not appear in `composes`. Operator-asserted: setting this to `true` does not re-check the component, so it can be used to silence a genuine finding. The check's docstring calls out the preferred path of adding the wrapped tag to `composes` when that field applies, since `composes` carries semantic value beyond a11y.
|
|
30
|
+
- Unchanged: synthetic interactive markup (`<div @click>`) and bare `<a>` without an `href` attribute still fire the warning. Those are the real keyboard-a11y hazards the rule was written for; the new branches are narrowly scoped to the composition cases that previously trained authors to ignore validator output.
|
|
31
|
+
|
|
32
|
+
### Test — diagnostics for harness failure modes
|
|
33
|
+
|
|
34
|
+
- `fireforge test` now detects the `MochitestDesktop.http3Server` AttributeError that crashes the mochitest cleanup path and rewrites it as an actionable hint: branding registration is the lazy-init chain that initialises `http3Server`, so the AttributeError at teardown is almost always a downstream symptom of `chrome://branding` not registering correctly in the fork. The hint enumerates the three concrete checks (branding listed in `browser/branding/moz.build`, `chrome://branding/locale/brand.properties` resolves at runtime, `BROWSER_CHROME_MANIFESTS` registers the chrome.manifest) so operators do not chase the AttributeError as if it were the root cause. Surfaces as a `GeneralError` so the operator sees the diagnostic instead of the generic `BuildError("Tests failed")`. The crash itself remains an upstream Firefox harness bug — FireForge can only diagnose it.
|
|
35
|
+
- New `fireforge test --mach-arg <arg>` (repeatable) forwards a single argument verbatim to `mach test`, after FireForge-managed flags. Escape valve for upstream xpcshell/mochitest flags FireForge does not model directly. Used by the xpcshell appdir hint as the operator's escape valve when auto-injection is the wrong fix; also useful for `--keep-going`, `--verbose`, or `--debugger` while iterating on a flaky test.
|
|
36
|
+
|
|
5
37
|
### Furnace create
|
|
6
38
|
|
|
7
39
|
- `furnace create` now accepts `--dry-run` so operators can preview the planned file set, test scaffold, and `furnace.json` mutation without touching the workspace. Previously the only way to preview a create was `fireforge furnace create --help` followed by running the real command and inspecting the result after the fact — a workflow that stranded a component directory, a furnace.json entry, and (with `--with-tests`) test sources under `engine/` behind every aborted preview. The dry-run path runs every validation the real command would run (name shape, name conflicts, engine pre-existence, `--compose` target existence + cycle detection, prefix warning) BEFORE emitting the plan, so a preview that fails matches a real run that would fail. A new `src/commands/furnace/create-dry-run.ts` owns the plan formatter and the success-note formatter so the two renderings stay in lock-step when the scaffolded layout changes.
|
package/README.md
CHANGED
|
@@ -44,7 +44,7 @@ mkdir mybrowser && cd mybrowser
|
|
|
44
44
|
npm init -y
|
|
45
45
|
npm install --save-dev @hominis/fireforge
|
|
46
46
|
|
|
47
|
-
npx fireforge setup
|
|
47
|
+
npx fireforge setup # interactive project init
|
|
48
48
|
npx fireforge download # fetch Firefox source (~1 GB)
|
|
49
49
|
npx fireforge bootstrap # install build deps (may need sudo)
|
|
50
50
|
npx fireforge import # apply your patches (if any exist)
|
|
@@ -56,22 +56,25 @@ Your project now has `fireforge.json`, an `engine/` directory with Firefox sourc
|
|
|
56
56
|
|
|
57
57
|
### Workflow Overview
|
|
58
58
|
|
|
59
|
+
1. Make changes inside the `engine/` directory.
|
|
60
|
+
2. Export your changes as a patch:
|
|
61
|
+
|
|
59
62
|
```bash
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
npx fireforge export browser/base/content/browser.js --name "custom-toolbar" --category ui
|
|
64
|
+
```
|
|
62
65
|
|
|
63
|
-
|
|
64
|
-
npx fireforge export browser/base/content/browser.js \
|
|
65
|
-
--name "custom-toolbar" --category ui
|
|
66
|
+
3. Your patch is now in `patches/`.
|
|
66
67
|
|
|
67
|
-
#
|
|
68
|
-
# with metadata tracked in patches/patches.json
|
|
68
|
+
# 4. Reset and import to verify everything applies cleanly:
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
```bash
|
|
71
71
|
npx fireforge reset --yes
|
|
72
72
|
npx fireforge import # --dry-run to preview without applying
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
5. When Mozilla releases a new version, update fireforge.json, re-download and rebase:
|
|
73
76
|
|
|
74
|
-
|
|
77
|
+
```bash
|
|
75
78
|
npx fireforge download --force
|
|
76
79
|
npx fireforge rebase
|
|
77
80
|
```
|
|
@@ -536,6 +539,83 @@ The two flags can be combined — `--with-tests --xpcshell` writes both harnesse
|
|
|
536
539
|
|
|
537
540
|
`fireforge test <path>` (without `--build`) now runs a preflight that diffs engine HEAD and the workdir against the last successful `fireforge build` (recorded at `.fireforge/last-build.json`). When packageable engine files have changed since that baseline, the command prints a single up-front warning naming the paths and pointing at `fireforge test --build`. This catches the class of failure where a newly scaffolded chrome resource or pref file is registered correctly but `obj-*/dist/` still holds the pre-edit bundle, so the test reads stale packaged artifacts and errors out with a cryptic `NS_ERROR_FILE_NOT_FOUND` inside xpcshell / mach test. The preflight is warn-only — a fork that rebuilt out-of-band (direct `./mach build`, IDE plugin, separate CI stage) is not blocked. Passing `--build` skips the preflight because the rebuild just refreshed the bundle.
|
|
538
541
|
|
|
542
|
+
### xpcshell appdir auto-injection on rebranded forks
|
|
543
|
+
|
|
544
|
+
`fireforge test` auto-resolves and injects `--app-path=<absolute>` into the underlying `mach test` invocation when the nearest `xpcshell.toml` sets `firefox-appdir = "browser"` and the active build's `appname` is anything other than `firefox`. Without this, every `resource:///modules/<name>.sys.mjs` import inside the harness throws `Failed to load resource:///modules/…` because the upstream xpcshell harness reads the appdir override under the appname-keyed manifest field (`<appname>-appdir`) — the literal `firefox-appdir = "browser"` directive is silently ignored on rebranded forks, `appPath` falls back to `xrePath`, and `resource:///` resolves one level above the real app root. The resolver walks each test path to its nearest manifest, reads `mozinfo.json` for the active appname, prefers any `<appname>-appdir` already in the manifest, and otherwise probes `<objDir>/dist/bin/<value>` and `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` for the absolute target. Operator overrides via `--mach-arg=--app-path=…` always win and skip the resolver silently. Mismatches across multiple test paths and unresolvable manifest values surface as warnings rather than guesses, so triage reaches the underlying cause.
|
|
545
|
+
|
|
546
|
+
The durable fix is to add `<appname>-appdir = "browser"` alongside `firefox-appdir = "browser"` in the manifest — the harness then reads the appname-keyed value directly without auto-injection. The xpcshell appdir hint that fires when the symptom persists despite injection lists this option first.
|
|
547
|
+
|
|
548
|
+
### Smoke-run mode (`fireforge run --smoke-exit`)
|
|
549
|
+
|
|
550
|
+
`fireforge run --smoke-exit <seconds>` launches the real built browser, streams the merged console line-by-line, sends `SIGTERM` to the entire child process group at the deadline, and exits non-zero when any `JavaScript error:` / `console.error:` / `[JavaScript Error]` / `###!!! [Parent]` line surfaces inside the smoke window without matching an allowlist. Closes the headless-vs-real-chrome gap that previously forced agents to choose between `fireforge run` (no exit hook, hangs on a human) and `--headless` (does not load the chrome document, so chrome-window constructor errors stay invisible).
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
# Launch, wait 60s, exit 0 unless an unallowed error fired
|
|
554
|
+
fireforge run --smoke-exit 60
|
|
555
|
+
|
|
556
|
+
# Same, but ignore a known async-shutdown blocker we've already triaged
|
|
557
|
+
fireforge run --smoke-exit 60 --console-allow 'AsyncShutdown blocker timed out'
|
|
558
|
+
|
|
559
|
+
# Allowlist file (one regex per line, # comments and blanks skipped) + capture
|
|
560
|
+
fireforge run --smoke-exit 60 --console-allow-file scripts/smoke-allow.txt --capture-console smoke.log
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
Exit codes are wired distinct from `BUILD_ERROR`:
|
|
564
|
+
|
|
565
|
+
| Code | Meaning |
|
|
566
|
+
| ---- | ---------------------------------------------------------------------- |
|
|
567
|
+
| 0 | Smoke window elapsed cleanly (or only allowlisted errors fired). |
|
|
568
|
+
| 12 | One or more unallowed console errors fired inside the window. |
|
|
569
|
+
| 13 | Browser exited non-clean before the window elapsed (launch-side fail). |
|
|
570
|
+
|
|
571
|
+
POSIX only — process-group semantics do not map cleanly onto Windows. A smoke window shorter than 30 s warns up-front because cold-start time alone can consume that budget on a debug build; `--capture-console <file>` mirrors the captured stream so post-exit inspection has the raw log without re-running.
|
|
572
|
+
|
|
573
|
+
### Furnace `--shared-ftl` for feature-scoped Fluent bundles
|
|
574
|
+
|
|
575
|
+
A feature with multiple components (e.g. an eight-component dock) typically wants one shared `.ftl` per feature rather than eight per-component stubs. `furnace create <tag> --localized --shared-ftl <chrome-uri>` participates in an existing feature-scoped bundle:
|
|
576
|
+
|
|
577
|
+
```bash
|
|
578
|
+
fireforge furnace create hominis-dock-button --localized --shared-ftl browser/hominis-dock.ftl
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
The generated `.mjs` calls `insertFTLIfNeeded("browser/hominis-dock.ftl")` instead of the per-component path. No `<tag>.ftl` stub is written. The `furnace.json` `custom` entry carries a new `sharedFtl` field so apply, validate, and remove all honour the participation:
|
|
582
|
+
|
|
583
|
+
- `furnace apply` does not copy a per-component `.ftl` into the FTL tree nor add a locale `jar.mn` entry — the shared file is registered by whoever owns the feature bundle.
|
|
584
|
+
- `furnace remove` early-returns from the locale `jar.mn` cleanup, so dropping our component's reference does not orphan the bundle for every other participant.
|
|
585
|
+
- `furnace validate`'s `missing-ftl` structural check is skipped — there is no per-component `.ftl` to require.
|
|
586
|
+
|
|
587
|
+
`--shared-ftl` implies `--localized`. `--no-localized + --shared-ftl` is rejected fast-fail. The value is interpolated verbatim into the generated template literal, so backticks, backslashes, and `${` are rejected at parse time. Setting `sharedFtl` does not auto-migrate previous per-component FTL state — flipping an existing component leaves the prior per-component entry in the engine tree and locale `jar.mn` until cleaned up explicitly.
|
|
588
|
+
|
|
589
|
+
### Furnace `keyboardCovered` for composed-button wrappers
|
|
590
|
+
|
|
591
|
+
`furnace validate`'s `no-keyboard-handler` rule is automatically suppressed when `@click` sits on a custom-element host whose `composes` lists a native-interactive child (e.g. `moz-button`, `moz-toggle`). The wrapper's click handler catches keyboard activation transitively because the inner element dispatches `click` on Enter/Space via the platform; a duplicate `@keydown` on the wrapper would either no-op or double-fire alongside the child's built-in path.
|
|
592
|
+
|
|
593
|
+
When the wrapped inner element is hand-authored or is a non-stock `moz-*` widget that does not appear in `composes`, the explicit `keyboardCovered: true` field on the component's `furnace.json` entry forces the same skip:
|
|
594
|
+
|
|
595
|
+
```json
|
|
596
|
+
"hominis-dock-button": {
|
|
597
|
+
"description": "Dock button wrapper",
|
|
598
|
+
"targetPath": "components/custom/hominis-dock-button",
|
|
599
|
+
"register": true,
|
|
600
|
+
"localized": false,
|
|
601
|
+
"composes": ["moz-button"],
|
|
602
|
+
"keyboardCovered": true
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
`keyboardCovered` is operator-asserted — it does not re-check the component, so it can be used to silence a genuine finding. Prefer adding the wrapped tag to `composes` when that field applies (it carries semantic value beyond a11y).
|
|
607
|
+
|
|
608
|
+
### Test escape valves
|
|
609
|
+
|
|
610
|
+
`fireforge test --mach-arg <arg>` (repeatable) forwards a single argument verbatim to `mach test` after FireForge-managed flags. Escape valve for upstream xpcshell/mochitest options FireForge does not model directly:
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
fireforge test browser/base/content/test/foo --mach-arg=--keep-going --mach-arg=--verbose
|
|
614
|
+
fireforge test browser/components/tests/unit/test_x.js --mach-arg=--app-path=/abs/override
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
Operator overrides for `--app-path` always win over the auto-injection described above.
|
|
618
|
+
|
|
539
619
|
## Roadmap
|
|
540
620
|
|
|
541
621
|
Planned but not yet implemented:
|
|
@@ -4,6 +4,13 @@ export interface DryRunPlanInput {
|
|
|
4
4
|
localized: boolean;
|
|
5
5
|
register: boolean;
|
|
6
6
|
composes: string[] | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Feature-scoped Fluent bundle the component participates in (the same
|
|
9
|
+
* value that will be written to `furnace.json`'s `sharedFtl`). When set,
|
|
10
|
+
* the component's own `.ftl` is NOT scaffolded and the plan preview
|
|
11
|
+
* reflects that. Omit the key for the default per-component scaffold.
|
|
12
|
+
*/
|
|
13
|
+
sharedFtl?: string;
|
|
7
14
|
testStyle: ResolvedTestStyle;
|
|
8
15
|
description: string;
|
|
9
16
|
binaryName: string;
|
|
@@ -75,9 +75,11 @@ export function formatSuccessNote(args) {
|
|
|
75
75
|
* `components/custom/` to match the wording of the real success note.
|
|
76
76
|
*/
|
|
77
77
|
export function formatDryRunPlan(args) {
|
|
78
|
-
const { componentName, localized, register, composes, testStyle, description, binaryName } = args;
|
|
78
|
+
const { componentName, localized, register, composes, sharedFtl, testStyle, description, binaryName, } = args;
|
|
79
79
|
const componentFiles = [`${componentName}.mjs`, `${componentName}.css`];
|
|
80
|
-
|
|
80
|
+
// A per-component .ftl is scaffolded only when the component does NOT
|
|
81
|
+
// opt into a shared feature-scoped bundle. Mirrors writeComponentFiles.
|
|
82
|
+
if (localized && !sharedFtl)
|
|
81
83
|
componentFiles.push(`${componentName}.ftl`);
|
|
82
84
|
let plan = `Would create files in components/custom/${componentName}/:\n` +
|
|
83
85
|
componentFiles.map((f) => ` ${f}`).join('\n');
|
|
@@ -90,6 +92,9 @@ export function formatDryRunPlan(args) {
|
|
|
90
92
|
if (composes && composes.length > 0) {
|
|
91
93
|
plan += `\n composes: ${composes.join(', ')}`;
|
|
92
94
|
}
|
|
95
|
+
if (sharedFtl) {
|
|
96
|
+
plan += `\n sharedFtl: ${sharedFtl}`;
|
|
97
|
+
}
|
|
93
98
|
return plan;
|
|
94
99
|
}
|
|
95
100
|
//# sourceMappingURL=create-dry-run.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature-flag resolution for `furnace create`. Extracted from
|
|
3
|
+
* `create.ts` so the authoring command stays under the per-file LOC
|
|
4
|
+
* budget — the flag resolver has grown with each new opt-in (`--xpcshell`,
|
|
5
|
+
* `--test-style`, `--shared-ftl`).
|
|
6
|
+
*/
|
|
7
|
+
import type { FurnaceCreateOptions } from '../../types/commands/index.js';
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the localized and registration feature flags for a new component.
|
|
10
|
+
*
|
|
11
|
+
* `--shared-ftl` implies `localized` and short-circuits the interactive
|
|
12
|
+
* prompt so the operator is not asked to flip a flag we are about to
|
|
13
|
+
* enforce. `--no-localized` combined with `--shared-ftl` is rejected
|
|
14
|
+
* fast-fail; the cross-field check in furnace-config would catch it
|
|
15
|
+
* too, but later and without a clear command-line message.
|
|
16
|
+
*
|
|
17
|
+
* @param isInteractive - Whether interactive prompts are available
|
|
18
|
+
* @param options - CLI-provided feature flags
|
|
19
|
+
* @returns Final feature selections, or null when creation is cancelled
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveCreateFeatures(isInteractive: boolean, options: FurnaceCreateOptions): Promise<{
|
|
22
|
+
localized: boolean;
|
|
23
|
+
register: boolean;
|
|
24
|
+
} | null>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Feature-flag resolution for `furnace create`. Extracted from
|
|
4
|
+
* `create.ts` so the authoring command stays under the per-file LOC
|
|
5
|
+
* budget — the flag resolver has grown with each new opt-in (`--xpcshell`,
|
|
6
|
+
* `--test-style`, `--shared-ftl`).
|
|
7
|
+
*/
|
|
8
|
+
import { multiselect } from '@clack/prompts';
|
|
9
|
+
import { InvalidArgumentError } from '../../errors/base.js';
|
|
10
|
+
import { cancel, isCancel } from '../../utils/logger.js';
|
|
11
|
+
/**
|
|
12
|
+
* Resolves the localized and registration feature flags for a new component.
|
|
13
|
+
*
|
|
14
|
+
* `--shared-ftl` implies `localized` and short-circuits the interactive
|
|
15
|
+
* prompt so the operator is not asked to flip a flag we are about to
|
|
16
|
+
* enforce. `--no-localized` combined with `--shared-ftl` is rejected
|
|
17
|
+
* fast-fail; the cross-field check in furnace-config would catch it
|
|
18
|
+
* too, but later and without a clear command-line message.
|
|
19
|
+
*
|
|
20
|
+
* @param isInteractive - Whether interactive prompts are available
|
|
21
|
+
* @param options - CLI-provided feature flags
|
|
22
|
+
* @returns Final feature selections, or null when creation is cancelled
|
|
23
|
+
*/
|
|
24
|
+
export async function resolveCreateFeatures(isInteractive, options) {
|
|
25
|
+
let localized = options.localized ?? false;
|
|
26
|
+
let register = options.register ?? true;
|
|
27
|
+
if (options.sharedFtl !== undefined) {
|
|
28
|
+
if (options.localized === false) {
|
|
29
|
+
throw new InvalidArgumentError('--shared-ftl requires localization. Remove --no-localized or drop --shared-ftl.', 'sharedFtl');
|
|
30
|
+
}
|
|
31
|
+
localized = true;
|
|
32
|
+
}
|
|
33
|
+
const featuresPromptSuppressed = options.sharedFtl !== undefined;
|
|
34
|
+
if (isInteractive &&
|
|
35
|
+
options.localized === undefined &&
|
|
36
|
+
options.register === undefined &&
|
|
37
|
+
!featuresPromptSuppressed) {
|
|
38
|
+
const features = await multiselect({
|
|
39
|
+
message: 'Component features:',
|
|
40
|
+
options: [
|
|
41
|
+
{ value: 'localized', label: 'Fluent localization (data-l10n-id)' },
|
|
42
|
+
{ value: 'register', label: 'Register in customElements.js' },
|
|
43
|
+
],
|
|
44
|
+
initialValues: ['register'],
|
|
45
|
+
});
|
|
46
|
+
if (isCancel(features)) {
|
|
47
|
+
cancel('Create cancelled');
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const selected = features;
|
|
51
|
+
localized = selected.includes('localized');
|
|
52
|
+
register = selected.includes('register');
|
|
53
|
+
}
|
|
54
|
+
return { localized, register };
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=create-features.js.map
|
|
@@ -14,12 +14,16 @@
|
|
|
14
14
|
* per-instance shadow-DOM Fluent attachment via `l10n.connectRoot`. We mirror
|
|
15
15
|
* that pattern here so `--localized` produces functional code.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Path resolution precedence (when `localized` is true):
|
|
18
|
+
* 1. `sharedFtl` — used verbatim. The caller has resolved it from
|
|
19
|
+
* `--shared-ftl` / `furnace.json`; this template does no rewriting.
|
|
20
|
+
* Use this when the component participates in a feature-scoped
|
|
21
|
+
* bundle that another component owns.
|
|
22
|
+
* 2. `<ftlChromeSubPath>/<name>.ftl` — the default per-component path,
|
|
23
|
+
* matching the locale jar.mn entry that `furnace apply` writes.
|
|
24
|
+
* 3. `<name>.ftl` — fallback when no chrome sub-path was resolvable.
|
|
21
25
|
*/
|
|
22
|
-
export declare function generateMjsContent(name: string, className: string, description: string, localized: boolean, header: string, ftlChromeSubPath: string | undefined): string;
|
|
26
|
+
export declare function generateMjsContent(name: string, className: string, description: string, localized: boolean, header: string, ftlChromeSubPath: string | undefined, sharedFtl: string | undefined): string;
|
|
23
27
|
/** Generates the .css file content for a custom component. */
|
|
24
28
|
export declare function generateCssContent(header: string): string;
|
|
25
29
|
/** Generates the .ftl file content for a custom component. */
|
|
@@ -15,13 +15,21 @@
|
|
|
15
15
|
* per-instance shadow-DOM Fluent attachment via `l10n.connectRoot`. We mirror
|
|
16
16
|
* that pattern here so `--localized` produces functional code.
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
18
|
+
* Path resolution precedence (when `localized` is true):
|
|
19
|
+
* 1. `sharedFtl` — used verbatim. The caller has resolved it from
|
|
20
|
+
* `--shared-ftl` / `furnace.json`; this template does no rewriting.
|
|
21
|
+
* Use this when the component participates in a feature-scoped
|
|
22
|
+
* bundle that another component owns.
|
|
23
|
+
* 2. `<ftlChromeSubPath>/<name>.ftl` — the default per-component path,
|
|
24
|
+
* matching the locale jar.mn entry that `furnace apply` writes.
|
|
25
|
+
* 3. `<name>.ftl` — fallback when no chrome sub-path was resolvable.
|
|
22
26
|
*/
|
|
23
|
-
export function generateMjsContent(name, className, description, localized, header, ftlChromeSubPath) {
|
|
24
|
-
const ftlPath =
|
|
27
|
+
export function generateMjsContent(name, className, description, localized, header, ftlChromeSubPath, sharedFtl) {
|
|
28
|
+
const ftlPath = sharedFtl !== undefined
|
|
29
|
+
? sharedFtl
|
|
30
|
+
: ftlChromeSubPath !== undefined
|
|
31
|
+
? `${ftlChromeSubPath}/${name}.ftl`
|
|
32
|
+
: `${name}.ftl`;
|
|
25
33
|
const ftlModulePreamble = localized
|
|
26
34
|
? `
|
|
27
35
|
window.MozXULElement?.insertFTLIfNeeded("${ftlPath}");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { text } from '@clack/prompts';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
5
5
|
import { createDefaultFurnaceConfig, detectComposesCycles, furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
6
|
import { resolveFtlChromeSubPath, tagNameToClassName } from '../../core/furnace-constants.js';
|
|
@@ -10,12 +10,14 @@ import { createRollbackJournal, recordCreatedDir, restoreRollbackJournalOrThrow,
|
|
|
10
10
|
import { isComponentInEngine } from '../../core/furnace-scanner.js';
|
|
11
11
|
import { DEFAULT_LICENSE, getLicenseHeader } from '../../core/license-headers.js';
|
|
12
12
|
import { registerTestManifest } from '../../core/manifest-register.js';
|
|
13
|
+
import { validateSharedFtl } from '../../core/shared-ftl.js';
|
|
13
14
|
import { InvalidArgumentError } from '../../errors/base.js';
|
|
14
15
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
15
16
|
import { toError } from '../../utils/errors.js';
|
|
16
17
|
import { ensureDir, pathExists, readText, writeText } from '../../utils/fs.js';
|
|
17
18
|
import { cancel, intro, isCancel, note, outro, success, warn } from '../../utils/logger.js';
|
|
18
19
|
import { formatDryRunPlan, formatSuccessNote } from './create-dry-run.js';
|
|
20
|
+
import { resolveCreateFeatures } from './create-features.js';
|
|
19
21
|
import { scaffoldMochikitTestFiles } from './create-mochikit.js';
|
|
20
22
|
import { generateCssContent, generateFtlContent, generateMjsContent } from './create-templates.js';
|
|
21
23
|
import { scaffoldXpcshellTestFiles } from './create-xpcshell.js';
|
|
@@ -158,40 +160,6 @@ add_task(async function test_${underscored}_defined() {
|
|
|
158
160
|
}
|
|
159
161
|
return testFiles;
|
|
160
162
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Resolves the localized and registration feature flags for a new component.
|
|
163
|
-
* @param isInteractive - Whether interactive prompts are available
|
|
164
|
-
* @param options - CLI-provided feature flags
|
|
165
|
-
* @returns Final feature selections, or null when creation is cancelled
|
|
166
|
-
*/
|
|
167
|
-
async function resolveCreateFeatures(isInteractive, options) {
|
|
168
|
-
let localized = options.localized ?? false;
|
|
169
|
-
let register = options.register ?? true;
|
|
170
|
-
if (isInteractive && options.localized === undefined && options.register === undefined) {
|
|
171
|
-
const features = await multiselect({
|
|
172
|
-
message: 'Component features:',
|
|
173
|
-
options: [
|
|
174
|
-
{
|
|
175
|
-
value: 'localized',
|
|
176
|
-
label: 'Fluent localization (data-l10n-id)',
|
|
177
|
-
},
|
|
178
|
-
{
|
|
179
|
-
value: 'register',
|
|
180
|
-
label: 'Register in customElements.js',
|
|
181
|
-
},
|
|
182
|
-
],
|
|
183
|
-
initialValues: ['register'],
|
|
184
|
-
});
|
|
185
|
-
if (isCancel(features)) {
|
|
186
|
-
cancel('Create cancelled');
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
const selected = features;
|
|
190
|
-
localized = selected.includes('localized');
|
|
191
|
-
register = selected.includes('register');
|
|
192
|
-
}
|
|
193
|
-
return { localized, register };
|
|
194
|
-
}
|
|
195
163
|
/**
|
|
196
164
|
* Writes the scaffolded component source files to disk.
|
|
197
165
|
* @param componentDir - Destination component directory
|
|
@@ -203,20 +171,25 @@ async function resolveCreateFeatures(isInteractive, options) {
|
|
|
203
171
|
* @param journal - Optional rollback journal that snapshots files before writes
|
|
204
172
|
* @returns Relative filenames written for the component
|
|
205
173
|
*/
|
|
206
|
-
async function writeComponentFiles(componentDir, componentName, className, description, localized, license, ftlChromeSubPath, journal) {
|
|
174
|
+
async function writeComponentFiles(componentDir, componentName, className, description, localized, license, ftlChromeSubPath, sharedFtl, journal) {
|
|
207
175
|
await ensureDir(componentDir);
|
|
208
176
|
const files = [`${componentName}.mjs`, `${componentName}.css`];
|
|
209
177
|
const mjsPath = join(componentDir, `${componentName}.mjs`);
|
|
210
178
|
if (journal)
|
|
211
179
|
await snapshotFile(journal, mjsPath);
|
|
212
|
-
const mjsContent = generateMjsContent(componentName, className, description, localized, getLicenseHeader(license, 'js'), ftlChromeSubPath);
|
|
180
|
+
const mjsContent = generateMjsContent(componentName, className, description, localized, getLicenseHeader(license, 'js'), ftlChromeSubPath, sharedFtl);
|
|
213
181
|
await writeText(mjsPath, mjsContent);
|
|
214
182
|
const cssPath = join(componentDir, `${componentName}.css`);
|
|
215
183
|
if (journal)
|
|
216
184
|
await snapshotFile(journal, cssPath);
|
|
217
185
|
const cssContent = generateCssContent(getLicenseHeader(license, 'css'));
|
|
218
186
|
await writeText(cssPath, cssContent);
|
|
219
|
-
|
|
187
|
+
// Skip the per-component .ftl stub when the component participates in a
|
|
188
|
+
// pre-existing feature-scoped bundle. The shared bundle is owned
|
|
189
|
+
// elsewhere; dropping a stub here would clutter the workspace with
|
|
190
|
+
// empty files that never get packaged (furnace apply also skips copying
|
|
191
|
+
// them in this mode).
|
|
192
|
+
if (localized && !sharedFtl) {
|
|
220
193
|
const ftlPath = join(componentDir, `${componentName}.ftl`);
|
|
221
194
|
if (journal)
|
|
222
195
|
await snapshotFile(journal, ftlPath);
|
|
@@ -277,7 +250,7 @@ async function performCreateMutations(args) {
|
|
|
277
250
|
// so signal-driven rollback can clean it up even if writeComponentFiles
|
|
278
251
|
// is interrupted mid-ensureDir.
|
|
279
252
|
recordCreatedDir(journal, args.componentDir);
|
|
280
|
-
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, args.ftlChromeSubPath, journal);
|
|
253
|
+
files = await writeComponentFiles(args.componentDir, args.componentName, args.className, args.description, args.localized, args.license, args.ftlChromeSubPath, args.sharedFtl, journal);
|
|
281
254
|
const customEntry = {
|
|
282
255
|
description: args.description,
|
|
283
256
|
targetPath: `toolkit/content/widgets/${args.componentName}`,
|
|
@@ -287,6 +260,9 @@ async function performCreateMutations(args) {
|
|
|
287
260
|
if (args.composes && args.composes.length > 0) {
|
|
288
261
|
customEntry.composes = args.composes;
|
|
289
262
|
}
|
|
263
|
+
if (args.sharedFtl) {
|
|
264
|
+
customEntry.sharedFtl = args.sharedFtl;
|
|
265
|
+
}
|
|
290
266
|
args.config.custom[args.componentName] = customEntry;
|
|
291
267
|
await snapshotFile(journal, args.furnacePaths.furnaceConfig);
|
|
292
268
|
await writeFurnaceConfig(args.projectRoot, args.config);
|
|
@@ -455,6 +431,21 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
455
431
|
// does not strand component files behind.
|
|
456
432
|
const composes = options.compose;
|
|
457
433
|
validateComposesTargets(config, componentName, composes);
|
|
434
|
+
// --- Normalize and validate --shared-ftl ahead of any writes. Shares the
|
|
435
|
+
// structural rules with furnace-config.ts so the command and the on-disk
|
|
436
|
+
// schema cannot diverge. Pass the resolved `localized` rather than a
|
|
437
|
+
// `true` literal so the validator's cross-field check stays anchored to
|
|
438
|
+
// the real feature selection — `resolveCreateFeatures` promotes localized
|
|
439
|
+
// upstream, but hard-coding `true` here would hide a regression if that
|
|
440
|
+
// promotion ever moved or dropped.
|
|
441
|
+
let sharedFtl;
|
|
442
|
+
if (options.sharedFtl !== undefined) {
|
|
443
|
+
const result = validateSharedFtl(options.sharedFtl, { localized });
|
|
444
|
+
if (!result.ok) {
|
|
445
|
+
throw new InvalidArgumentError(`--shared-ftl ${result.reason}.`, 'sharedFtl');
|
|
446
|
+
}
|
|
447
|
+
sharedFtl = result.value;
|
|
448
|
+
}
|
|
458
449
|
// Dry-run exits here — every validation that does not need a write has
|
|
459
450
|
// already run, so the plan we render reflects exactly what the real
|
|
460
451
|
// command would do. The mutation phase and its rollback journal are
|
|
@@ -465,6 +456,9 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
465
456
|
localized,
|
|
466
457
|
register,
|
|
467
458
|
composes,
|
|
459
|
+
// Spread rather than assign so the key is absent when sharedFtl is
|
|
460
|
+
// undefined — the DryRunPlanInput type uses strict-optional shape.
|
|
461
|
+
...(sharedFtl !== undefined ? { sharedFtl } : {}),
|
|
468
462
|
testStyle,
|
|
469
463
|
description,
|
|
470
464
|
binaryName: forgeConfig.binaryName,
|
|
@@ -490,6 +484,7 @@ export async function furnaceCreateCommand(projectRoot, name, options = {}) {
|
|
|
490
484
|
localized,
|
|
491
485
|
register,
|
|
492
486
|
composes,
|
|
487
|
+
sharedFtl,
|
|
493
488
|
componentDir,
|
|
494
489
|
furnacePaths,
|
|
495
490
|
config,
|
|
@@ -81,6 +81,7 @@ function registerFurnaceInfoCommands(furnace, context) {
|
|
|
81
81
|
return value;
|
|
82
82
|
})
|
|
83
83
|
.option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
|
|
84
|
+
.option('--shared-ftl <path>', 'Participate in an existing feature-scoped .ftl at this path (e.g. "browser/hominis-dock.ftl"); skips the per-component .ftl scaffold (implies --localized)')
|
|
84
85
|
.option('--dry-run', 'Show the planned file set and furnace.json changes without writing')
|
|
85
86
|
.action(withErrorHandling(async (name, options) => {
|
|
86
87
|
await furnaceCreateCommand(getProjectRoot(), name, options);
|
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import type { CommandContext } from '../types/cli.js';
|
|
3
|
+
import type { RunOptions } from '../types/commands/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Exit code returned by smoke-run mode when the captured console stream
|
|
6
|
+
* produced one or more error lines that did NOT match the operator's
|
|
7
|
+
* allowlist.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SMOKE_EXIT_FAILURE: 12;
|
|
10
|
+
/**
|
|
11
|
+
* Exit code returned by smoke-run mode when the browser itself exited
|
|
12
|
+
* with a non-clean status before the smoke window elapsed — i.e. a
|
|
13
|
+
* launch-side failure we could NOT observe as a console error line
|
|
14
|
+
* (crash before console wiring, missing profile, etc.).
|
|
15
|
+
*/
|
|
16
|
+
export declare const SMOKE_LAUNCH_FAILURE: 13;
|
|
3
17
|
/**
|
|
4
18
|
* Runs the run command to launch the built browser.
|
|
5
19
|
* @param projectRoot - Root directory of the project
|
|
6
20
|
*/
|
|
7
|
-
export declare function runCommand(projectRoot: string): Promise<void>;
|
|
21
|
+
export declare function runCommand(projectRoot: string, options?: RunOptions): Promise<void>;
|
|
8
22
|
/** Registers the run command on the CLI program. */
|
|
9
23
|
export declare function registerRun(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
|