@hominis/fireforge 0.15.3 → 0.15.5
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 +13 -2
- package/README.md +8 -1
- package/dist/src/commands/doctor-furnace.js +8 -1
- package/dist/src/commands/wire.js +59 -6
- package/dist/src/core/browser-wire.d.ts +8 -0
- package/dist/src/core/browser-wire.js +2 -2
- package/dist/src/core/build-audit-platform.d.ts +36 -0
- package/dist/src/core/build-audit-platform.js +170 -0
- package/dist/src/core/build-audit-resolve.d.ts +66 -0
- package/dist/src/core/build-audit-resolve.js +209 -0
- package/dist/src/core/build-audit.d.ts +4 -1
- package/dist/src/core/build-audit.js +139 -85
- package/dist/src/core/wire-dom-fragment.d.ts +16 -4
- package/dist/src/core/wire-dom-fragment.js +32 -17
- package/dist/src/types/commands/options.d.ts +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -38,7 +38,11 @@
|
|
|
38
38
|
|
|
39
39
|
### Build audit
|
|
40
40
|
|
|
41
|
-
- `fireforge build` now runs a warn-only post-build dist-tree audit after a successful mach build. The audit diffs engine-relative paths touched since the last successful build (stored as `.fireforge/last-build.json`) against the dist bundle, and warns per file that is packageable-by-convention (`.js`/`.mjs`/`.css`/`.ftl`/`.xhtml`/`app/profile/…`) but has no matching artifact, or whose dist mtime is older than the source. Surfaces the class of bug where a new pref file or widget is edited but never registered in `moz.build` / `jar.mn` / `package-manifest.in
|
|
41
|
+
- `fireforge build` now runs a warn-only post-build dist-tree audit after a successful mach build. The audit diffs engine-relative paths touched since the last successful build (stored as `.fireforge/last-build.json`) against the dist bundle, and warns per file that is packageable-by-convention (`.js`/`.mjs`/`.css`/`.ftl`/`.xhtml`/`app/profile/…`) but has no matching artifact, or whose dist mtime is older than the source. Surfaces the class of bug where a new pref file or widget is edited but never registered in `moz.build` / `jar.mn` / `package-manifest.in`.
|
|
42
|
+
- Build-system inputs (`jar.mn`, `moz.build`, `moz.configure`, `Makefile.in`, `mozbuild.in`) are now excluded from the audit's "must appear in dist/" check. They are consumed by the build to produce chrome registrations / make targets but never themselves ship, so every edit produced a guaranteed false positive — and worse, when an unrelated upstream `moz.build` coincidentally existed elsewhere in the bundle (e.g. `<App>.app/Contents/moz.build`) the audit reported "source is newer than packaged artifact" against two completely unrelated files. The exclusion list lives next to `PACKAGEABLE_EXTENSIONS` in `src/core/build-audit.ts`.
|
|
43
|
+
- Same-basename collisions in `dist/` are now disambiguated by trailing-segment overlap: a branding override at `engine/browser/branding/<name>/content/aboutDialog.css` (which ships at `chrome/<area>/content/branding/aboutDialog.css`) no longer gets matched against the unrelated upstream `chrome/<area>/content/browser/aboutDialog.css`. The scorer rewards candidates whose path contains meaningful intermediate segments from the source (e.g. the `branding` segment) so re-rooted artifacts win over coincidentally-named ones. Generic segments like `content` / `chrome` / `bin` do not count toward the bonus to avoid breaking ties on noise.
|
|
44
|
+
- Test sources (anything under `/test(s)/`, plus `browser_*.js` / `test_*.js` / `xpcshell.toml` / `browser.ini`) are now resolved against the `_tests/` tree under the active `obj-*` directory instead of `dist/`. Mochitest and xpcshell harnesses copy registered tests under `_tests/testing/...`, never into the packaged bundle, so the previous dist-only walk false-flagged every registered test as "missing packaged artifact". Misses still warn — but they now point at `_tests/`, directing the operator to `BROWSER_CHROME_MANIFESTS` / `XPCSHELL_TESTS_MANIFESTS` instead of `package-manifest.in`.
|
|
45
|
+
- Files inside an `if CONFIG[…]:` block in their owning `moz.build` are now skipped on hosts where the gate evaluates off (Windows-only stubinstaller CSS on a macOS build, Darwin-only artwork on Linux, etc.). The detection walks up from the source file to the closest `moz.build`, scans for the basename inside a Python-style indented `if CONFIG[…]:` block, and matches the gate against the host platform via `getPlatform()`. Negation expressions (`!= "WINNT"`, `not CONFIG[…]`) are conservatively NOT treated as single-OS gates, so we never wrongly suppress a warning for a file that should ship on the current host. Lives in the new `src/core/build-audit-platform.ts`.
|
|
42
46
|
- Ends every build with a `Packaged: N updated, M stale, K missing, S skipped` summary so operators can distinguish a fast-because-incremental build from a fast-because-silently-skipped one without a post-build `find`.
|
|
43
47
|
- `fireforge build` auto-runs `mach configure` before the mach build step when any `moz.build`, `moz.configure`, or `Makefile.in` changed since the last successful build. Prevents the stale-backend trap where an incremental build skips work against a recursive-make backend that no longer matches the source tree. Emits a `Backend config changed; running mach configure first...` banner so the extra step is visible, and continues the build even if configure exits non-zero.
|
|
44
48
|
- Mach build failures with known-cryptic mozbuild errors now print actionable hints. First entry in the table: `mozbuild.preprocessor.Preprocessor.Error: no preprocessor directives found` prints `Use JS_PREFERENCE_FILES instead, or add at least one #filter / #expand directive to the file.` The hint table lives in `src/core/mach-error-hints.ts` and is extensible — new cryptic errors we diagnose get one-line hints added without touching the build wrapper.
|
|
@@ -47,6 +51,11 @@
|
|
|
47
51
|
|
|
48
52
|
- `fireforge lint --since <git-rev>` tags each lint issue as `[introduced]` (file touched in the diff since `<git-rev>`) or `[cumulative]` (pre-existing patch-state drift). Output gains the tag prefix and the summary line splits counts (e.g. `Lint: 2 introduced error(s), 0 introduced warning(s); 5 cumulative error(s), 1 cumulative warning(s)`). Exit code semantics are unchanged — an introduced OR cumulative error still fails lint — but triage of "is the diff I just produced clean?" no longer requires mentally subtracting pre-existing noise from every report. Without `--since` the output is unchanged.
|
|
49
53
|
|
|
54
|
+
### Fork chrome-doc assumptions
|
|
55
|
+
|
|
56
|
+
- `fireforge doctor`'s `Furnace engine paths` check now reads `furnace.json.tokenHostDocuments` instead of hardcoding `browser/base/content/browser.xhtml`. Forks that replaced the stock chrome document were emitting a permanent "browser.xhtml missing" warning every doctor run; reusing the same field the `missing-token-link` validator already consumes means a fork configures chrome-doc paths once and both checks agree. Defaults to `['browser/base/content/browser.xhtml']` when unset — behaviour is unchanged for forks that ship the stock chrome document.
|
|
57
|
+
- `fireforge wire --dom` no longer hardcodes `browser/base/content/browser.xhtml` as the target chrome document. The target now resolves in this order: explicit `--target <path>` flag → first entry of `furnace.json.tokenHostDocuments` → upstream `browser/base/content/browser.xhtml`. Forks that replaced browser.xhtml were getting a cryptic "could not find insertion point in browser.xhtml" error that read like a FireForge bug; the resolved path now propagates into the dry-run plan, the success message, and the insertion-failure error so the actual target is surfaced. When the resolved target does not exist on disk, wire fails up-front with a pointer to `tokenHostDocuments` / `--target` rather than blowing up in the AST pass. The `addDomFragment` core now computes the `#include` directive relative to the target's own directory instead of a hardcoded `browser/base/content/`, so a target that lives elsewhere in the engine tree gets a correctly resolved include path.
|
|
58
|
+
|
|
50
59
|
### Furnace chrome-doc
|
|
51
60
|
|
|
52
61
|
- New `furnace chrome-doc create <name>` subcommand scaffolds a top-level chrome document (xhtml + js + css + ftl) plus the three jar.mn registrations (`browser/base/jar.mn`, `browser/themes/shared/jar.inc.mn`, `browser/locales/jar.mn`). Default emits titlebar-buttonbox markup and a `windowtype="navigator:browser"` shell; `--no-titlebar` produces a frameless overlay with the macOS `.titlebar-button { display: none }` carve-out. Mirrors the workflow for custom elements (`furnace create`) so hand-authoring mistakes — the `*` preprocessor flag, the startup-topic observer, the platform titlebar inheritance — are eliminated. All writes go through a rollback journal under the signal-handler pathway: a Ctrl+C mid-scaffold restores every touched file.
|
|
@@ -65,7 +74,9 @@
|
|
|
65
74
|
- New `src/core/marionette-preflight.ts` owns the `--doctor` probe and its teardown semantics.
|
|
66
75
|
- 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.
|
|
67
76
|
- 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. Closes a v0.15.0 slip-through (one `@hominis/fireforge` reference remained in `src/core/furnace-operation.ts` as a generic example alongside the npm-identity occurrences; the code example is now fork-neutral).
|
|
68
|
-
-
|
|
77
|
+
- Second pass on the same scrub: residual `hominis.xhtml` test fixtures in `wire.test.ts`, `browser-wire.test.ts`, and `doctor.test.ts` are now `mybrowser-shell.xhtml`; the `hominis.js` reference in the build-audit changelog motivating-case description is now generic. Tests retain a distinct override-target (`mybrowser.xhtml`) to preserve the configured-vs-overridden semantic of the wire `--target` precedence test.
|
|
78
|
+
- New modules landed under coverage-gate protection: `src/core/mach-error-hints.ts`, `src/core/build-audit.ts`, `src/core/build-audit-resolve.ts`, `src/core/build-audit-platform.ts`, `src/core/build-baseline.ts`, `src/core/patch-lint-diff-tag.ts`, `src/commands/furnace/chrome-doc.ts`, `src/commands/furnace/chrome-doc-templates.ts`, `src/commands/furnace/create-mochikit.ts`. Per-module thresholds added to `scripts/check-coverage-thresholds.mjs`.
|
|
79
|
+
- Path resolution and Python-style moz.build gate detection extracted from `build-audit.ts` into the new `build-audit-resolve.ts` (basename collisions, `_tests/` routing, trailing-segment scoring) and `build-audit-platform.ts` (`if CONFIG[…]:` gate parser keyed on host platform). Keeps the orchestrator under the per-file LOC budget after the false-positive fixes landed.
|
|
69
80
|
|
|
70
81
|
## 0.14.0
|
|
71
82
|
|
package/README.md
CHANGED
|
@@ -398,6 +398,13 @@ The summary line splits counts — e.g. `Lint: 2 introduced error(s), 0 introduc
|
|
|
398
398
|
|
|
399
399
|
`fireforge build` is a transactional step: after a successful mach build it audits the dist bundle against engine-relative paths touched since the last successful build, and warns per file that is packageable-by-convention (`.js`/`.mjs`/`.css`/`.ftl`/`.xhtml`/`app/profile/…`) but has no matching artifact or whose dist mtime is older than the source. Ends every build with a `Packaged: N updated, M stale, K missing, S skipped` summary. The audit is warn-only — it never fails a build that mach reported green.
|
|
400
400
|
|
|
401
|
+
The audit applies four routing rules to suppress false positives that previously trained operators to ignore its warnings:
|
|
402
|
+
|
|
403
|
+
- **Build inputs are excluded.** `jar.mn`, `moz.build`, `moz.configure`, `Makefile.in`, and `mozbuild.in` are consumed by the build to produce chrome registrations / make targets but never themselves ship. They are skipped before the dist lookup, so editing them no longer fires a "missing packaged artifact" warning.
|
|
404
|
+
- **Same-basename collisions in `dist/` are disambiguated by trailing-segment overlap.** A branding override at `engine/browser/branding/<name>/content/aboutDialog.css` ships at `chrome/<area>/content/branding/aboutDialog.css`. A naive basename match would tie that against the unrelated upstream `chrome/<area>/content/browser/aboutDialog.css`; the audit now scores candidates by trailing path-segment match plus a small bonus for non-generic source segments (`branding`, the branding directory name) appearing in the candidate path, so re-rooted artifacts win over coincidentally-named ones.
|
|
405
|
+
- **Test sources are looked up under `_tests/`, not `dist/`.** Anything under `/test(s)/` directories, plus `browser_*.js` / `test_*.js` / `xpcshell.toml` / `browser.ini`, is resolved against the `_tests/` tree under the active `obj-*` directory. Mochitest and xpcshell harnesses copy registered tests there, never into the packaged bundle. Misses still warn — but they point at `_tests/`, directing the operator to `BROWSER_CHROME_MANIFESTS` / `XPCSHELL_TESTS_MANIFESTS` instead of `package-manifest.in`.
|
|
406
|
+
- **Files inside an `if CONFIG[…]:` block in their owning `moz.build` are skipped on hosts where the gate is off.** Windows-only stubinstaller CSS on a macOS build, Darwin-only artwork on Linux, etc. The detection walks up to the closest `moz.build`, scans for the basename inside a Python-style indented `if CONFIG[…]:` block, and matches the gate against the host platform. Negation expressions are conservatively NOT treated as single-OS gates so a warning is never wrongly suppressed for a file that should ship on the current host.
|
|
407
|
+
|
|
401
408
|
The build also auto-runs `mach configure` before the mach build step when any `moz.build`, `moz.configure`, or `Makefile.in` changed since the last successful build. Prevents incremental builds from silently skipping work against a stale recursive-make backend. Emits a `Backend config changed; running mach configure first...` banner when it fires.
|
|
402
409
|
|
|
403
410
|
Mach build failures with known-cryptic mozbuild errors now print actionable hints. Example: a `JS_PREFERENCE_PP_FILES` entry with no `#filter` / `#expand` directives now prints `Hint: ...use JS_PREFERENCE_FILES instead, or add at least one #filter / #expand directive to the file.` alongside the raw mach traceback.
|
|
@@ -429,7 +436,7 @@ Mach build failures with known-cryptic mozbuild errors now print actionable hint
|
|
|
429
436
|
|
|
430
437
|
**`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.
|
|
431
438
|
|
|
432
|
-
**`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.
|
|
439
|
+
**`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. `fireforge doctor`'s engine-paths probe reads the same field when confirming the chrome document exists on disk, and `fireforge wire --dom` uses the first entry as the default target for its `#include` directive (override per-invocation with `--target <path>`). Forks that fully replaced `browser.xhtml` with a custom top-level chrome document configure this field once and both checks agree.
|
|
433
440
|
|
|
434
441
|
### `furnace create --localized` for `MozLitElement`
|
|
435
442
|
|
|
@@ -319,12 +319,19 @@ const furnaceEnginePathsCheck = {
|
|
|
319
319
|
dependsOn: ['Furnace configuration'],
|
|
320
320
|
skipIf: (ctx) => !ctx.furnaceConfigExists || !ctx.furnaceConfig || !ctx.engineExists,
|
|
321
321
|
run: async (ctx) => {
|
|
322
|
+
// Forks that replace browser.xhtml with a custom top-level chrome
|
|
323
|
+
// document enumerate their chrome docs in furnace.json.tokenHostDocuments.
|
|
324
|
+
// Reuse the same field for the engine-path probe so those forks stop
|
|
325
|
+
// seeing a permanent "browser.xhtml missing" warning.
|
|
326
|
+
const hostDocs = ctx.furnaceConfig?.tokenHostDocuments && ctx.furnaceConfig.tokenHostDocuments.length > 0
|
|
327
|
+
? ctx.furnaceConfig.tokenHostDocuments
|
|
328
|
+
: ['browser/base/content/browser.xhtml'];
|
|
322
329
|
const expectedPaths = [
|
|
323
330
|
CUSTOM_ELEMENTS_JS,
|
|
324
331
|
JAR_MN,
|
|
325
332
|
'toolkit/content/widgets',
|
|
326
333
|
resolveFtlDir(ctx.furnaceConfig?.ftlBasePath),
|
|
327
|
-
|
|
334
|
+
...hostDocs,
|
|
328
335
|
];
|
|
329
336
|
const missing = [];
|
|
330
337
|
for (const relative of expectedPaths) {
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
3
|
import { DEFAULT_BROWSER_SUBSCRIPT_DIR, wireSubscript } from '../core/browser-wire.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
+
import { furnaceConfigExists as checkFurnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
|
|
5
6
|
import { consumeParserFallbackEvents } from '../core/parser-fallback.js';
|
|
7
|
+
import { DEFAULT_DOM_TARGET } from '../core/wire-dom-fragment.js';
|
|
6
8
|
import { InvalidArgumentError } from '../errors/base.js';
|
|
7
9
|
import { toError } from '../utils/errors.js';
|
|
8
10
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -10,7 +12,7 @@ import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
|
10
12
|
import { pickDefined } from '../utils/options.js';
|
|
11
13
|
import { isContainedRelativePath, isPathInsideRoot, toRootRelativePath } from '../utils/paths.js';
|
|
12
14
|
const BROWSER_BASE_DIR = 'browser/base';
|
|
13
|
-
function printWireDryRun(engineDir, name, subscriptDir, domFilePath, options) {
|
|
15
|
+
function printWireDryRun(engineDir, name, subscriptDir, domFilePath, domTargetPath, options) {
|
|
14
16
|
info('[dry-run] Would wire subscript:');
|
|
15
17
|
info(` source: ${subscriptDir}/${name}.js`);
|
|
16
18
|
info(` browser-main.js: loadSubScript("chrome://browser/content/${name}.js")`);
|
|
@@ -22,12 +24,45 @@ function printWireDryRun(engineDir, name, subscriptDir, domFilePath, options) {
|
|
|
22
24
|
}
|
|
23
25
|
if (domFilePath) {
|
|
24
26
|
const includePath = relative(join(engineDir, subscriptDir), join(engineDir, domFilePath)).replace(/\\/g, '/');
|
|
25
|
-
info(`
|
|
27
|
+
info(` ${domTargetPath}: #include ${includePath}`);
|
|
26
28
|
}
|
|
27
29
|
const relPath = relative(join(engineDir, BROWSER_BASE_DIR), join(engineDir, subscriptDir)).replace(/\\/g, '/');
|
|
28
30
|
info(` jar.mn: content/browser/${name}.js (${relPath}/${name}.js)`);
|
|
29
31
|
outro('Dry run complete');
|
|
30
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the chrome document the `#include` directive is inserted into.
|
|
35
|
+
*
|
|
36
|
+
* Preference order:
|
|
37
|
+
* 1. `--target <path>` CLI flag (explicit caller intent)
|
|
38
|
+
* 2. First entry of `furnace.json.tokenHostDocuments` (fork-configured
|
|
39
|
+
* chrome doc list; already consumed by the missing-token-link
|
|
40
|
+
* validator and the doctor check)
|
|
41
|
+
* 3. `browser/base/content/browser.xhtml` (upstream default)
|
|
42
|
+
*
|
|
43
|
+
* Step 2 is silent — a missing / invalid furnace.json falls through to the
|
|
44
|
+
* upstream default rather than surfacing a warning, because forks that don't
|
|
45
|
+
* use furnace shouldn't have to configure anything.
|
|
46
|
+
*/
|
|
47
|
+
async function resolveDomTargetPath(projectRoot, explicit) {
|
|
48
|
+
if (explicit !== undefined) {
|
|
49
|
+
return explicit;
|
|
50
|
+
}
|
|
51
|
+
if (await checkFurnaceConfigExists(projectRoot)) {
|
|
52
|
+
try {
|
|
53
|
+
const furnaceConfig = await loadFurnaceConfig(projectRoot);
|
|
54
|
+
const first = furnaceConfig.tokenHostDocuments?.[0];
|
|
55
|
+
if (first !== undefined && first.length > 0) {
|
|
56
|
+
return first;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Fall through to default — a broken furnace.json should not block
|
|
61
|
+
// the wire command. The doctor surfaces that issue separately.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return DEFAULT_DOM_TARGET;
|
|
65
|
+
}
|
|
31
66
|
/**
|
|
32
67
|
* Validates a subscript name supplied on the command line. Subscripts are
|
|
33
68
|
* resolved into filenames under the subscript directory and registered in
|
|
@@ -90,6 +125,21 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
90
125
|
}
|
|
91
126
|
domFilePath = toRootRelativePath(paths.engine, options.dom);
|
|
92
127
|
}
|
|
128
|
+
// Resolve the chrome document the `#include` directive will land in.
|
|
129
|
+
// Only consulted when `--dom` is supplied — we still resolve it here so
|
|
130
|
+
// the dry-run plan can print the target accurately.
|
|
131
|
+
if (options.target !== undefined && !isContainedRelativePath(options.target)) {
|
|
132
|
+
throw new InvalidArgumentError(`Target chrome document must stay within engine/: ${options.target}`, 'target');
|
|
133
|
+
}
|
|
134
|
+
const domTargetPath = await resolveDomTargetPath(projectRoot, options.target);
|
|
135
|
+
if (domFilePath) {
|
|
136
|
+
const paths = getProjectPaths(projectRoot);
|
|
137
|
+
if (!options.dryRun && !(await pathExists(join(paths.engine, domTargetPath)))) {
|
|
138
|
+
throw new InvalidArgumentError(`Chrome document not found in engine: ${domTargetPath}\n` +
|
|
139
|
+
'Set "tokenHostDocuments" in furnace.json (first entry is used by wire) ' +
|
|
140
|
+
'or pass --target <path>.', 'target');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
93
143
|
// Verify the subscript file exists in engine/ (skip for dry-run)
|
|
94
144
|
if (!options.dryRun) {
|
|
95
145
|
const paths = getProjectPaths(projectRoot);
|
|
@@ -100,13 +150,14 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
100
150
|
}
|
|
101
151
|
}
|
|
102
152
|
if (options.dryRun) {
|
|
103
|
-
printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, options);
|
|
153
|
+
printWireDryRun(getProjectPaths(projectRoot).engine, name, subscriptDir, domFilePath, domTargetPath, options);
|
|
104
154
|
return;
|
|
105
155
|
}
|
|
106
156
|
const result = await wireSubscript(projectRoot, name, {
|
|
107
157
|
...(options.init !== undefined ? { init: options.init } : {}),
|
|
108
158
|
...(options.destroy !== undefined ? { destroy: options.destroy } : {}),
|
|
109
159
|
...(domFilePath !== undefined ? { domFilePath } : {}),
|
|
160
|
+
...(domFilePath !== undefined && domTargetPath !== DEFAULT_DOM_TARGET ? { domTargetPath } : {}),
|
|
110
161
|
...(options.after !== undefined ? { after: options.after } : {}),
|
|
111
162
|
...(subscriptDir !== DEFAULT_BROWSER_SUBSCRIPT_DIR ? { subscriptDir } : {}),
|
|
112
163
|
dryRun: false,
|
|
@@ -140,10 +191,10 @@ export async function wireCommand(projectRoot, name, options = {}) {
|
|
|
140
191
|
}
|
|
141
192
|
if (domFilePath) {
|
|
142
193
|
if (result.domInserted) {
|
|
143
|
-
success(`Inserted #include directive into
|
|
194
|
+
success(`Inserted #include directive into ${domTargetPath}`);
|
|
144
195
|
}
|
|
145
196
|
else {
|
|
146
|
-
info(`#include directive already present in
|
|
197
|
+
info(`#include directive already present in ${domTargetPath} (skipped)`);
|
|
147
198
|
}
|
|
148
199
|
}
|
|
149
200
|
if (result.jarMnResult.skipped) {
|
|
@@ -161,10 +212,12 @@ export function registerWire(program, { getProjectRoot, withErrorHandling }) {
|
|
|
161
212
|
.description('Wire a chrome subscript into the browser')
|
|
162
213
|
.option('--init <expression>', 'Init expression for browser-init.js onLoad()')
|
|
163
214
|
.option('--destroy <expression>', 'Destroy expression for browser-init.js onUnload()')
|
|
164
|
-
.option('--dom <file>', 'XHTML fragment file to insert into
|
|
215
|
+
.option('--dom <file>', 'XHTML fragment file to insert into the chrome document')
|
|
165
216
|
.option('--dry-run', 'Show what would be changed without writing')
|
|
166
217
|
.option('--after <name>', 'Insert init block after the block for this name')
|
|
167
218
|
.option('--subscript-dir <dir>', 'Subscript directory relative to engine/ (default: browser/base/content)')
|
|
219
|
+
.option('--target <path>', 'Chrome document to insert --dom into, relative to engine/ ' +
|
|
220
|
+
'(default: first entry of furnace.json tokenHostDocuments, else browser/base/content/browser.xhtml)')
|
|
168
221
|
.action(withErrorHandling(async (name, options) => {
|
|
169
222
|
await wireCommand(getProjectRoot(), name, pickDefined(options));
|
|
170
223
|
}));
|
|
@@ -22,6 +22,14 @@ export interface WireOptions {
|
|
|
22
22
|
destroy?: string | undefined;
|
|
23
23
|
/** Path to `.inc.xhtml` file relative to engine root */
|
|
24
24
|
domFilePath?: string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Top-level chrome document the DOM fragment's `#include` directive is
|
|
27
|
+
* inserted into, relative to engine/. Defaults to
|
|
28
|
+
* `browser/base/content/browser.xhtml`. Forks that replace browser.xhtml
|
|
29
|
+
* with a custom chrome document (e.g. `mybrowser.xhtml`) pass the
|
|
30
|
+
* replacement path here.
|
|
31
|
+
*/
|
|
32
|
+
domTargetPath?: string | undefined;
|
|
25
33
|
/** Dry run — don't write any files */
|
|
26
34
|
dryRun?: boolean | undefined;
|
|
27
35
|
/** Insert init block after the block containing this name */
|
|
@@ -48,10 +48,10 @@ export async function wireSubscript(root, name, options = {}) {
|
|
|
48
48
|
if (options.destroy) {
|
|
49
49
|
destroyAdded = await addDestroyToBrowserInit(engineDir, options.destroy);
|
|
50
50
|
}
|
|
51
|
-
// 4. Add #include directive to
|
|
51
|
+
// 4. Add #include directive to the top-level chrome document (if provided)
|
|
52
52
|
let domInserted = false;
|
|
53
53
|
if (options.domFilePath) {
|
|
54
|
-
domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath));
|
|
54
|
+
domInserted = await addDomFragment(engineDir, toRootRelativePath(engineDir, options.domFilePath), options.domTargetPath);
|
|
55
55
|
}
|
|
56
56
|
// 5. Register in jar.mn
|
|
57
57
|
const jarMnResult = await registerBrowserContent(engineDir, `${name}.js`, undefined, jarMnSourcePath);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Outcome of a moz.build platform-gate lookup for a single source file. */
|
|
2
|
+
export interface PlatformGateResult {
|
|
3
|
+
/** True when the file is gated off on the current host. */
|
|
4
|
+
gatedOff: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* The gate expression that excluded the file, if any. Surfaced in
|
|
7
|
+
* verbose output so an operator can confirm the audit's reasoning.
|
|
8
|
+
*/
|
|
9
|
+
gateExpression?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Scans a moz.build file for a basename match inside a conditional
|
|
13
|
+
* block, returning the gate expression when one encloses the match.
|
|
14
|
+
*
|
|
15
|
+
* The scanner uses indentation to track block scope (Python-style):
|
|
16
|
+
* an `if CONFIG[…]:` line opens a scope at indent N+1, and dedenting
|
|
17
|
+
* back to indent N closes it. A basename match inside that scope
|
|
18
|
+
* inherits the gate expression.
|
|
19
|
+
*
|
|
20
|
+
* @param content moz.build file content
|
|
21
|
+
* @param basename Basename of the source file we are auditing
|
|
22
|
+
* @returns The enclosing gate expression, or undefined if none
|
|
23
|
+
*/
|
|
24
|
+
export declare function findEnclosingGate(content: string, basename: string): string | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Determines whether the given source file is gated off on the current
|
|
27
|
+
* host by an enclosing `if CONFIG[...]:` block in its owning moz.build.
|
|
28
|
+
* Returns `gatedOff: false` and no expression when no gate is found —
|
|
29
|
+
* the file is not platform-restricted, so the caller should audit it
|
|
30
|
+
* normally.
|
|
31
|
+
*
|
|
32
|
+
* @param engineDir Absolute path to the engine root
|
|
33
|
+
* @param sourcePath Engine-relative POSIX path of the source file
|
|
34
|
+
* @returns Detection result
|
|
35
|
+
*/
|
|
36
|
+
export declare function detectPlatformGate(engineDir: string, sourcePath: string): Promise<PlatformGateResult>;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/*
|
|
3
|
+
* Platform-gate detection for the post-build dist-tree audit.
|
|
4
|
+
*
|
|
5
|
+
* `moz.build` files commonly wrap entries in conditional blocks like
|
|
6
|
+
* `if CONFIG["MAKENSISU"]:` (Windows-only stubinstaller) or
|
|
7
|
+
* `if CONFIG["OS_TARGET"] == "Darwin":` (macOS-only artwork). On a host
|
|
8
|
+
* that does not match the gate, the wrapped files are never processed
|
|
9
|
+
* by the build, so they cannot appear under `dist/`. Without this
|
|
10
|
+
* detection, the audit fires a "missing packaged artifact" warning for
|
|
11
|
+
* every gated file on every off-platform build — pure noise.
|
|
12
|
+
*
|
|
13
|
+
* The detection is intentionally lightweight: we walk up from the
|
|
14
|
+
* source file looking for the closest `moz.build`, scan it for an
|
|
15
|
+
* occurrence of the source basename inside an `if CONFIG[...]:` block,
|
|
16
|
+
* and check whether the gate expression matches the host platform.
|
|
17
|
+
*
|
|
18
|
+
* This is best-effort. False negatives (we miss a gate and warn anyway)
|
|
19
|
+
* are tolerable — the audit is warn-only. False positives (we wrongly
|
|
20
|
+
* skip a gated file that should ship on this host) are not, so the
|
|
21
|
+
* detection errs toward NOT skipping when uncertain.
|
|
22
|
+
*/
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
24
|
+
import { pathExists, readText } from '../utils/fs.js';
|
|
25
|
+
import { getPlatform } from '../utils/platform.js';
|
|
26
|
+
/**
|
|
27
|
+
* Tokens that uniquely identify a Windows-only `if CONFIG[...]:` block.
|
|
28
|
+
* `MAKENSISU` is the Windows stubinstaller compiler; `OS_TARGET ==
|
|
29
|
+
* "WINNT"` and `MOZ_WIDGET_TOOLKIT == "windows"` are the conventional
|
|
30
|
+
* platform discriminators.
|
|
31
|
+
*/
|
|
32
|
+
const WINDOWS_ONLY_GATE_TOKENS = ['MAKENSISU', '"WINNT"', "'WINNT'", '"windows"', "'windows'"];
|
|
33
|
+
/** Tokens that mark a macOS-only `if CONFIG[...]:` block. */
|
|
34
|
+
const DARWIN_ONLY_GATE_TOKENS = ['"Darwin"', "'Darwin'", '"cocoa"', "'cocoa'"];
|
|
35
|
+
/** Tokens that mark a Linux-only `if CONFIG[...]:` block. */
|
|
36
|
+
const LINUX_ONLY_GATE_TOKENS = ['"Linux"', "'Linux'", '"gtk"', "'gtk'"];
|
|
37
|
+
/**
|
|
38
|
+
* Returns true when the platform-gate expression includes one of the
|
|
39
|
+
* tokens characteristic of a single OS that is not the current host.
|
|
40
|
+
*/
|
|
41
|
+
function isGateOffHost(expression, host) {
|
|
42
|
+
const matchesWindows = WINDOWS_ONLY_GATE_TOKENS.some((t) => expression.includes(t));
|
|
43
|
+
const matchesDarwin = DARWIN_ONLY_GATE_TOKENS.some((t) => expression.includes(t));
|
|
44
|
+
const matchesLinux = LINUX_ONLY_GATE_TOKENS.some((t) => expression.includes(t));
|
|
45
|
+
// Negation gates (`!= "WINNT"`, `not CONFIG["MAKENSISU"]`) flip the
|
|
46
|
+
// semantics. Keep this conservative — if we cannot confidently parse
|
|
47
|
+
// a negation, return false so we don't wrongly suppress a warning.
|
|
48
|
+
const negated = /\bnot\b|!=/.test(expression);
|
|
49
|
+
if (negated)
|
|
50
|
+
return false;
|
|
51
|
+
if (matchesWindows && host !== 'win32')
|
|
52
|
+
return true;
|
|
53
|
+
if (matchesDarwin && host !== 'darwin')
|
|
54
|
+
return true;
|
|
55
|
+
if (matchesLinux && host !== 'linux')
|
|
56
|
+
return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Walks from a starting directory up to (but not above) the engine root,
|
|
61
|
+
* yielding the first `moz.build` encountered. Returns undefined when no
|
|
62
|
+
* ancestor has one — typically only for files that live above any
|
|
63
|
+
* moz.build entry point, which would not be packageable anyway.
|
|
64
|
+
*/
|
|
65
|
+
async function findOwningMozBuild(engineDir, sourceDir) {
|
|
66
|
+
let current = sourceDir;
|
|
67
|
+
const root = engineDir.replace(/\/+$/, '');
|
|
68
|
+
while (current.startsWith(root)) {
|
|
69
|
+
const candidate = join(current, 'moz.build');
|
|
70
|
+
if (await pathExists(candidate))
|
|
71
|
+
return candidate;
|
|
72
|
+
const parent = dirname(current);
|
|
73
|
+
if (parent === current)
|
|
74
|
+
break;
|
|
75
|
+
current = parent;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Matches a `basename` appearing at the tail of a quoted path literal
|
|
81
|
+
* on a single moz.build line. Catches both the bare entry
|
|
82
|
+
* `"installing_page.css"` and the path-prefixed entry
|
|
83
|
+
* `"stubinstaller/installing_page.css"`.
|
|
84
|
+
*/
|
|
85
|
+
function matchesQuotedBasename(line, basename) {
|
|
86
|
+
const escaped = basename.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
87
|
+
return new RegExp(`["'](?:[^"']*\\/)?${escaped}["']`).test(line);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Scans a moz.build file for a basename match inside a conditional
|
|
91
|
+
* block, returning the gate expression when one encloses the match.
|
|
92
|
+
*
|
|
93
|
+
* The scanner uses indentation to track block scope (Python-style):
|
|
94
|
+
* an `if CONFIG[…]:` line opens a scope at indent N+1, and dedenting
|
|
95
|
+
* back to indent N closes it. A basename match inside that scope
|
|
96
|
+
* inherits the gate expression.
|
|
97
|
+
*
|
|
98
|
+
* @param content moz.build file content
|
|
99
|
+
* @param basename Basename of the source file we are auditing
|
|
100
|
+
* @returns The enclosing gate expression, or undefined if none
|
|
101
|
+
*/
|
|
102
|
+
export function findEnclosingGate(content, basename) {
|
|
103
|
+
const lines = content.split('\n');
|
|
104
|
+
const stack = [];
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
if (line.trim() === '')
|
|
107
|
+
continue;
|
|
108
|
+
const indent = (/^(\s*)/.exec(line)?.[1] ?? '').length;
|
|
109
|
+
while (stack.length > 0) {
|
|
110
|
+
const top = stack[stack.length - 1];
|
|
111
|
+
if (top && indent <= top.indent) {
|
|
112
|
+
stack.pop();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
const ifMatch = /^\s*if\s+(.+?):\s*(?:#.*)?$/.exec(line);
|
|
118
|
+
if (ifMatch?.[1]) {
|
|
119
|
+
stack.push({ indent, expression: ifMatch[1] });
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (matchesQuotedBasename(line, basename)) {
|
|
123
|
+
const top = stack[stack.length - 1];
|
|
124
|
+
if (top)
|
|
125
|
+
return top.expression;
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Determines whether the given source file is gated off on the current
|
|
133
|
+
* host by an enclosing `if CONFIG[...]:` block in its owning moz.build.
|
|
134
|
+
* Returns `gatedOff: false` and no expression when no gate is found —
|
|
135
|
+
* the file is not platform-restricted, so the caller should audit it
|
|
136
|
+
* normally.
|
|
137
|
+
*
|
|
138
|
+
* @param engineDir Absolute path to the engine root
|
|
139
|
+
* @param sourcePath Engine-relative POSIX path of the source file
|
|
140
|
+
* @returns Detection result
|
|
141
|
+
*/
|
|
142
|
+
export async function detectPlatformGate(engineDir, sourcePath) {
|
|
143
|
+
const sourceDir = dirname(join(engineDir, sourcePath));
|
|
144
|
+
const mozBuild = await findOwningMozBuild(engineDir, sourceDir);
|
|
145
|
+
if (!mozBuild)
|
|
146
|
+
return { gatedOff: false };
|
|
147
|
+
let content;
|
|
148
|
+
try {
|
|
149
|
+
content = await readText(mozBuild);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return { gatedOff: false };
|
|
153
|
+
}
|
|
154
|
+
const sourceBasename = sourcePath.split('/').pop() ?? '';
|
|
155
|
+
const expression = findEnclosingGate(content, sourceBasename);
|
|
156
|
+
if (!expression)
|
|
157
|
+
return { gatedOff: false };
|
|
158
|
+
let host;
|
|
159
|
+
try {
|
|
160
|
+
host = getPlatform();
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return { gatedOff: false };
|
|
164
|
+
}
|
|
165
|
+
if (isGateOffHost(expression, host)) {
|
|
166
|
+
return { gatedOff: true, gateExpression: expression };
|
|
167
|
+
}
|
|
168
|
+
return { gatedOff: false, gateExpression: expression };
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=build-audit-platform.js.map
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristic test for "this looks like a packaged-test source file" — the
|
|
3
|
+
* audit routes such paths to `_tests/` instead of `dist/`. Matches
|
|
4
|
+
* mochitest / xpcshell / browser-chrome conventions: any source under a
|
|
5
|
+
* `/test/` or `/tests/` directory, or with a `browser_` / `test_` prefix
|
|
6
|
+
* on a `.js`/`.toml` basename. Test manifests (`*.toml`, `*.list`,
|
|
7
|
+
* `*.ini`) under those directories also qualify.
|
|
8
|
+
*
|
|
9
|
+
* @param sourcePath Engine-relative POSIX path
|
|
10
|
+
* @returns True when the file belongs to the test tree, not the bundle
|
|
11
|
+
*/
|
|
12
|
+
export declare function isTestPath(sourcePath: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Counts how many trailing segments two paths share. Used to score
|
|
15
|
+
* candidate dist artifacts against a source path so that
|
|
16
|
+
* `branding/<name>/content/aboutDialog.css` prefers a candidate at
|
|
17
|
+
* `…/content/branding/aboutDialog.css` over one at
|
|
18
|
+
* `…/content/browser/aboutDialog.css`.
|
|
19
|
+
*
|
|
20
|
+
* @param a First path
|
|
21
|
+
* @param b Second path
|
|
22
|
+
* @returns Count of matching trailing segments (basename always counts as 1)
|
|
23
|
+
*/
|
|
24
|
+
export declare function countTrailingSegmentMatches(a: string, b: string): number;
|
|
25
|
+
/**
|
|
26
|
+
* Computes a score for `candidatePath` relative to `sourcePath`. Higher
|
|
27
|
+
* scores win. Score = trailing-segment match count, with a bonus when
|
|
28
|
+
* the candidate's path contains a meaningful intermediate segment from
|
|
29
|
+
* the source (e.g. `branding`, the branding dir name itself, etc.).
|
|
30
|
+
*
|
|
31
|
+
* The bonus exists because Firefox packaging often re-roots files: a
|
|
32
|
+
* source `branding/<name>/content/aboutDialog.css` lands at
|
|
33
|
+
* `chrome/browser/content/branding/aboutDialog.css` — only the basename
|
|
34
|
+
* trails-match, but the `branding` segment moved into the middle of
|
|
35
|
+
* the candidate path. Without the bonus, that candidate would tie with
|
|
36
|
+
* the unrelated `chrome/browser/content/browser/aboutDialog.css` and
|
|
37
|
+
* the audit would pick whichever the directory walk hit first.
|
|
38
|
+
*
|
|
39
|
+
* @param sourcePath Engine-relative POSIX path
|
|
40
|
+
* @param candidatePath Absolute path under the dist tree
|
|
41
|
+
* @returns Numeric score; higher means better match
|
|
42
|
+
*/
|
|
43
|
+
export declare function scoreCandidate(sourcePath: string, candidatePath: string): number;
|
|
44
|
+
/**
|
|
45
|
+
* Walks a tree under `root` and returns every file whose basename equals
|
|
46
|
+
* `name`. Skips dotfile / hidden directories so the symlinked
|
|
47
|
+
* `.mozbuild` cache (a full upstream copy) does not dominate the scan
|
|
48
|
+
* on macOS.
|
|
49
|
+
*
|
|
50
|
+
* @param root Tree root to search
|
|
51
|
+
* @param name Basename to match
|
|
52
|
+
* @param maxDepth Optional traversal cap (default 12)
|
|
53
|
+
* @returns All matching absolute paths
|
|
54
|
+
*/
|
|
55
|
+
export declare function findAllByBasename(root: string, name: string, maxDepth?: number): Promise<string[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the best-matching artifact for a source path under one or
|
|
58
|
+
* more search roots. Returns the highest-scoring candidate by trailing
|
|
59
|
+
* segment overlap; ties go to the first-found path (deterministic via
|
|
60
|
+
* the directory-walk order). Returns undefined when no candidate exists.
|
|
61
|
+
*
|
|
62
|
+
* @param sourcePath Engine-relative POSIX source path
|
|
63
|
+
* @param searchRoots Absolute roots to scan (e.g. dist/, _tests/)
|
|
64
|
+
* @returns Best-matching artifact path, or undefined
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolveBestArtifact(sourcePath: string, searchRoots: readonly string[]): Promise<string | undefined>;
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/*
|
|
3
|
+
* Path-resolution helpers for the post-build dist-tree audit.
|
|
4
|
+
*
|
|
5
|
+
* Resolves the expected on-disk artifact location for a given engine
|
|
6
|
+
* source path. The previous implementation matched purely by basename and
|
|
7
|
+
* suffered three classes of false positive:
|
|
8
|
+
*
|
|
9
|
+
* 1. A branding override (e.g. `engine/browser/branding/<name>/content/aboutDialog.css`)
|
|
10
|
+
* shipped at `chrome/browser/content/branding/aboutDialog.css` would
|
|
11
|
+
* get matched against the unrelated upstream
|
|
12
|
+
* `chrome/browser/content/browser/aboutDialog.css`.
|
|
13
|
+
* 2. Test files (`browser_*.js`, `test_*.js`) live under `_tests/`, not
|
|
14
|
+
* `dist/`, so every registered test was reported as missing.
|
|
15
|
+
* 3. Build inputs (`jar.mn`, `moz.build`, `Makefile.in`, `moz.configure`)
|
|
16
|
+
* never appear under `dist/` — they are consumed, not packaged.
|
|
17
|
+
*
|
|
18
|
+
* The helpers below address (1) by ranking same-basename candidates by
|
|
19
|
+
* how many trailing path segments they share with the source, and (2) by
|
|
20
|
+
* routing test paths to a separate `_tests/`-aware resolver.
|
|
21
|
+
*
|
|
22
|
+
* (3) is handled in `build-audit.ts` via `isPackageablePath`.
|
|
23
|
+
*/
|
|
24
|
+
import { readdir } from 'node:fs/promises';
|
|
25
|
+
import { basename, join } from 'node:path';
|
|
26
|
+
import { pathExists } from '../utils/fs.js';
|
|
27
|
+
/** Maximum directory depth to traverse when scanning a tree root. */
|
|
28
|
+
const MAX_SCAN_DEPTH = 12;
|
|
29
|
+
/**
|
|
30
|
+
* Heuristic test for "this looks like a packaged-test source file" — the
|
|
31
|
+
* audit routes such paths to `_tests/` instead of `dist/`. Matches
|
|
32
|
+
* mochitest / xpcshell / browser-chrome conventions: any source under a
|
|
33
|
+
* `/test/` or `/tests/` directory, or with a `browser_` / `test_` prefix
|
|
34
|
+
* on a `.js`/`.toml` basename. Test manifests (`*.toml`, `*.list`,
|
|
35
|
+
* `*.ini`) under those directories also qualify.
|
|
36
|
+
*
|
|
37
|
+
* @param sourcePath Engine-relative POSIX path
|
|
38
|
+
* @returns True when the file belongs to the test tree, not the bundle
|
|
39
|
+
*/
|
|
40
|
+
export function isTestPath(sourcePath) {
|
|
41
|
+
if (sourcePath.includes('/test/') || sourcePath.includes('/tests/')) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
const name = basename(sourcePath);
|
|
45
|
+
if (/^browser_.+\.(js|toml|ini)$/.test(name))
|
|
46
|
+
return true;
|
|
47
|
+
if (/^test_.+\.(js|toml|ini)$/.test(name))
|
|
48
|
+
return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Splits a POSIX path into segments, dropping empties.
|
|
53
|
+
* @param path POSIX-separated path
|
|
54
|
+
*/
|
|
55
|
+
function pathSegments(path) {
|
|
56
|
+
return path.split('/').filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Counts how many trailing segments two paths share. Used to score
|
|
60
|
+
* candidate dist artifacts against a source path so that
|
|
61
|
+
* `branding/<name>/content/aboutDialog.css` prefers a candidate at
|
|
62
|
+
* `…/content/branding/aboutDialog.css` over one at
|
|
63
|
+
* `…/content/browser/aboutDialog.css`.
|
|
64
|
+
*
|
|
65
|
+
* @param a First path
|
|
66
|
+
* @param b Second path
|
|
67
|
+
* @returns Count of matching trailing segments (basename always counts as 1)
|
|
68
|
+
*/
|
|
69
|
+
export function countTrailingSegmentMatches(a, b) {
|
|
70
|
+
const aSegs = pathSegments(a);
|
|
71
|
+
const bSegs = pathSegments(b);
|
|
72
|
+
let matches = 0;
|
|
73
|
+
while (matches < aSegs.length &&
|
|
74
|
+
matches < bSegs.length &&
|
|
75
|
+
aSegs[aSegs.length - 1 - matches] === bSegs[bSegs.length - 1 - matches]) {
|
|
76
|
+
matches += 1;
|
|
77
|
+
}
|
|
78
|
+
return matches;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Computes a score for `candidatePath` relative to `sourcePath`. Higher
|
|
82
|
+
* scores win. Score = trailing-segment match count, with a bonus when
|
|
83
|
+
* the candidate's path contains a meaningful intermediate segment from
|
|
84
|
+
* the source (e.g. `branding`, the branding dir name itself, etc.).
|
|
85
|
+
*
|
|
86
|
+
* The bonus exists because Firefox packaging often re-roots files: a
|
|
87
|
+
* source `branding/<name>/content/aboutDialog.css` lands at
|
|
88
|
+
* `chrome/browser/content/branding/aboutDialog.css` — only the basename
|
|
89
|
+
* trails-match, but the `branding` segment moved into the middle of
|
|
90
|
+
* the candidate path. Without the bonus, that candidate would tie with
|
|
91
|
+
* the unrelated `chrome/browser/content/browser/aboutDialog.css` and
|
|
92
|
+
* the audit would pick whichever the directory walk hit first.
|
|
93
|
+
*
|
|
94
|
+
* @param sourcePath Engine-relative POSIX path
|
|
95
|
+
* @param candidatePath Absolute path under the dist tree
|
|
96
|
+
* @returns Numeric score; higher means better match
|
|
97
|
+
*/
|
|
98
|
+
export function scoreCandidate(sourcePath, candidatePath) {
|
|
99
|
+
const trailing = countTrailingSegmentMatches(sourcePath, candidatePath);
|
|
100
|
+
const sourceSegs = pathSegments(sourcePath);
|
|
101
|
+
const candSegs = pathSegments(candidatePath);
|
|
102
|
+
// Look for source segments that appear anywhere in the candidate path
|
|
103
|
+
// but are not part of the trailing match. Each unique mid-path hit on
|
|
104
|
+
// a meaningful (>2-char, not generic like 'content'/'chrome'/'bin')
|
|
105
|
+
// segment adds 1 to the score.
|
|
106
|
+
const generic = new Set([
|
|
107
|
+
'content',
|
|
108
|
+
'chrome',
|
|
109
|
+
'bin',
|
|
110
|
+
'browser',
|
|
111
|
+
'toolkit',
|
|
112
|
+
'modules',
|
|
113
|
+
'base',
|
|
114
|
+
'app',
|
|
115
|
+
'profile',
|
|
116
|
+
'shared',
|
|
117
|
+
'themes',
|
|
118
|
+
]);
|
|
119
|
+
const trailingSet = new Set(sourceSegs.slice(sourceSegs.length - trailing));
|
|
120
|
+
let bonus = 0;
|
|
121
|
+
for (const seg of sourceSegs) {
|
|
122
|
+
if (seg.length <= 2)
|
|
123
|
+
continue;
|
|
124
|
+
if (generic.has(seg))
|
|
125
|
+
continue;
|
|
126
|
+
if (trailingSet.has(seg))
|
|
127
|
+
continue;
|
|
128
|
+
if (candSegs.includes(seg))
|
|
129
|
+
bonus += 1;
|
|
130
|
+
}
|
|
131
|
+
return trailing * 10 + bonus;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Walks a tree under `root` and returns every file whose basename equals
|
|
135
|
+
* `name`. Skips dotfile / hidden directories so the symlinked
|
|
136
|
+
* `.mozbuild` cache (a full upstream copy) does not dominate the scan
|
|
137
|
+
* on macOS.
|
|
138
|
+
*
|
|
139
|
+
* @param root Tree root to search
|
|
140
|
+
* @param name Basename to match
|
|
141
|
+
* @param maxDepth Optional traversal cap (default 12)
|
|
142
|
+
* @returns All matching absolute paths
|
|
143
|
+
*/
|
|
144
|
+
export async function findAllByBasename(root, name, maxDepth = MAX_SCAN_DEPTH) {
|
|
145
|
+
const results = [];
|
|
146
|
+
if (!(await pathExists(root)))
|
|
147
|
+
return results;
|
|
148
|
+
const stack = [{ dir: root, depth: 0 }];
|
|
149
|
+
while (stack.length > 0) {
|
|
150
|
+
const entry = stack.pop();
|
|
151
|
+
if (!entry)
|
|
152
|
+
break;
|
|
153
|
+
if (entry.depth > maxDepth)
|
|
154
|
+
continue;
|
|
155
|
+
let children;
|
|
156
|
+
try {
|
|
157
|
+
children = await readdir(entry.dir, { withFileTypes: true });
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const child of children) {
|
|
163
|
+
const fullPath = join(entry.dir, child.name);
|
|
164
|
+
if (child.isDirectory()) {
|
|
165
|
+
if (child.name.startsWith('.'))
|
|
166
|
+
continue;
|
|
167
|
+
stack.push({ dir: fullPath, depth: entry.depth + 1 });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (child.name === name) {
|
|
171
|
+
results.push(fullPath);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the best-matching artifact for a source path under one or
|
|
179
|
+
* more search roots. Returns the highest-scoring candidate by trailing
|
|
180
|
+
* segment overlap; ties go to the first-found path (deterministic via
|
|
181
|
+
* the directory-walk order). Returns undefined when no candidate exists.
|
|
182
|
+
*
|
|
183
|
+
* @param sourcePath Engine-relative POSIX source path
|
|
184
|
+
* @param searchRoots Absolute roots to scan (e.g. dist/, _tests/)
|
|
185
|
+
* @returns Best-matching artifact path, or undefined
|
|
186
|
+
*/
|
|
187
|
+
export async function resolveBestArtifact(sourcePath, searchRoots) {
|
|
188
|
+
const name = basename(sourcePath);
|
|
189
|
+
const allCandidates = [];
|
|
190
|
+
for (const root of searchRoots) {
|
|
191
|
+
const found = await findAllByBasename(root, name);
|
|
192
|
+
allCandidates.push(...found);
|
|
193
|
+
}
|
|
194
|
+
if (allCandidates.length === 0)
|
|
195
|
+
return undefined;
|
|
196
|
+
if (allCandidates.length === 1)
|
|
197
|
+
return allCandidates[0];
|
|
198
|
+
let bestScore = -1;
|
|
199
|
+
let best;
|
|
200
|
+
for (const candidate of allCandidates) {
|
|
201
|
+
const score = scoreCandidate(sourcePath, candidate);
|
|
202
|
+
if (score > bestScore) {
|
|
203
|
+
bestScore = score;
|
|
204
|
+
best = candidate;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return best;
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=build-audit-resolve.js.map
|
|
@@ -28,7 +28,10 @@ export interface AuditSummary {
|
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
30
|
* Decides whether a source path should be packaged. Returns true for paths
|
|
31
|
-
* whose extension or directory fragment matches a known-packaged pattern
|
|
31
|
+
* whose extension or directory fragment matches a known-packaged pattern,
|
|
32
|
+
* after excluding build inputs (`jar.mn`, `moz.build`, etc.) which are
|
|
33
|
+
* consumed by the build but never themselves packaged.
|
|
34
|
+
*
|
|
32
35
|
* @param sourcePath Engine-relative POSIX path (for example browser/app/profile/pref.js).
|
|
33
36
|
* @returns True when the path implies packaging.
|
|
34
37
|
*/
|
|
@@ -19,12 +19,27 @@
|
|
|
19
19
|
* - False positives are acceptable at this stage: fork-specific packaging
|
|
20
20
|
* tricks FireForge doesn't know about will surface as warnings an
|
|
21
21
|
* operator can investigate. The audit never fails the build.
|
|
22
|
+
*
|
|
23
|
+
* Routing rules:
|
|
24
|
+
* - Build inputs (jar.mn, moz.build, Makefile.in, moz.configure) are
|
|
25
|
+
* skipped; they are consumed, not packaged.
|
|
26
|
+
* - Test sources (anything under /test(s)/, browser_*.js, test_*.js)
|
|
27
|
+
* are looked up under _tests/, not dist/ — that's where mach copies
|
|
28
|
+
* them.
|
|
29
|
+
* - Files inside an `if CONFIG[...]:` block in moz.build that gates
|
|
30
|
+
* off on the current host are skipped (Windows stubinstaller CSS on
|
|
31
|
+
* a macOS build, etc.).
|
|
32
|
+
* - Same-basename collisions in dist/ are disambiguated by trailing-
|
|
33
|
+
* segment overlap so a branding override does not get matched
|
|
34
|
+
* against an unrelated upstream file with the same basename.
|
|
22
35
|
*/
|
|
23
36
|
import { stat } from 'node:fs/promises';
|
|
24
37
|
import { basename, join } from 'node:path';
|
|
25
38
|
import { toError } from '../utils/errors.js';
|
|
26
39
|
import { pathExists } from '../utils/fs.js';
|
|
27
40
|
import { info, verbose, warn } from '../utils/logger.js';
|
|
41
|
+
import { detectPlatformGate } from './build-audit-platform.js';
|
|
42
|
+
import { isTestPath, resolveBestArtifact } from './build-audit-resolve.js';
|
|
28
43
|
import { hasChanges, isMissingHeadError } from './git.js';
|
|
29
44
|
import { git } from './git-base.js';
|
|
30
45
|
import { getUntrackedFiles } from './git-status.js';
|
|
@@ -44,48 +59,29 @@ const PACKAGEABLE_EXTENSIONS = [
|
|
|
44
59
|
const PACKAGEABLE_PATH_FRAGMENTS = ['/app/profile/', '/chrome/', '/locales/'];
|
|
45
60
|
/** Directories that are build artifacts, not source — never audited. */
|
|
46
61
|
const IGNORE_PATH_FRAGMENTS = ['obj-', 'node_modules/', '.git/', '.cargo/', '.mozbuild/'];
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
62
|
+
/**
|
|
63
|
+
* Basenames that are build-system inputs, not packaged artifacts.
|
|
64
|
+
* `jar.mn` is consumed to produce chrome registrations; `moz.build` /
|
|
65
|
+
* `moz.configure` / `Makefile.in` feed the build backend; none of them
|
|
66
|
+
* ship in the bundle. Auditing them produced a guaranteed false
|
|
67
|
+
* positive on every edit (nothing under dist/ ever has these names),
|
|
68
|
+
* and a worse failure mode when an unrelated upstream `moz.build`
|
|
69
|
+
* coincidentally exists at e.g. `MyBrowser.app/Contents/moz.build` and
|
|
70
|
+
* gets matched as a "stale artifact" of an entirely different file.
|
|
52
71
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (entry.depth > maxDepth)
|
|
61
|
-
continue;
|
|
62
|
-
let children;
|
|
63
|
-
try {
|
|
64
|
-
children = await readdir(entry.dir, { withFileTypes: true });
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
for (const child of children) {
|
|
70
|
-
const fullPath = join(entry.dir, child.name);
|
|
71
|
-
if (child.isDirectory()) {
|
|
72
|
-
// Skip the symlinked mozbuild cache tree which contains full copies
|
|
73
|
-
// and would dominate the scan on macOS.
|
|
74
|
-
if (child.name.startsWith('.'))
|
|
75
|
-
continue;
|
|
76
|
-
stack.push({ dir: fullPath, depth: entry.depth + 1 });
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (child.name === name) {
|
|
80
|
-
return fullPath;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return undefined;
|
|
85
|
-
}
|
|
72
|
+
const BUILD_INPUT_BASENAMES = new Set([
|
|
73
|
+
'jar.mn',
|
|
74
|
+
'moz.build',
|
|
75
|
+
'moz.configure',
|
|
76
|
+
'Makefile.in',
|
|
77
|
+
'mozbuild.in',
|
|
78
|
+
]);
|
|
86
79
|
/**
|
|
87
80
|
* Decides whether a source path should be packaged. Returns true for paths
|
|
88
|
-
* whose extension or directory fragment matches a known-packaged pattern
|
|
81
|
+
* whose extension or directory fragment matches a known-packaged pattern,
|
|
82
|
+
* after excluding build inputs (`jar.mn`, `moz.build`, etc.) which are
|
|
83
|
+
* consumed by the build but never themselves packaged.
|
|
84
|
+
*
|
|
89
85
|
* @param sourcePath Engine-relative POSIX path (for example browser/app/profile/pref.js).
|
|
90
86
|
* @returns True when the path implies packaging.
|
|
91
87
|
*/
|
|
@@ -94,6 +90,8 @@ export function isPackageablePath(sourcePath) {
|
|
|
94
90
|
if (sourcePath.includes(fragment))
|
|
95
91
|
return false;
|
|
96
92
|
}
|
|
93
|
+
if (BUILD_INPUT_BASENAMES.has(basename(sourcePath)))
|
|
94
|
+
return false;
|
|
97
95
|
for (const ext of PACKAGEABLE_EXTENSIONS) {
|
|
98
96
|
if (sourcePath.endsWith(ext))
|
|
99
97
|
return true;
|
|
@@ -169,6 +167,97 @@ async function resolveDistRoot(engineDir) {
|
|
|
169
167
|
}
|
|
170
168
|
return undefined;
|
|
171
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Resolves the `_tests/` tree under the active obj-* directory, used as
|
|
172
|
+
* a secondary search root for sources that look like packaged tests.
|
|
173
|
+
* Returns undefined when no obj dir exists yet.
|
|
174
|
+
*/
|
|
175
|
+
async function resolveTestsRoot(engineDir) {
|
|
176
|
+
const { readdir } = await import('node:fs/promises');
|
|
177
|
+
let entries;
|
|
178
|
+
try {
|
|
179
|
+
entries = await readdir(engineDir);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
const objDirs = entries.filter((e) => e.startsWith('obj-'));
|
|
185
|
+
for (const objDir of objDirs) {
|
|
186
|
+
const testsPath = join(engineDir, objDir, '_tests');
|
|
187
|
+
if (await pathExists(testsPath)) {
|
|
188
|
+
return testsPath;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Resolves the search roots an individual source path should be looked
|
|
195
|
+
* up under. Test-shaped paths get `_tests/`; everything else gets `dist/`.
|
|
196
|
+
*/
|
|
197
|
+
function searchRootsFor(source, distRoot, testsRoot) {
|
|
198
|
+
if (isTestPath(source)) {
|
|
199
|
+
return testsRoot ? [testsRoot] : [];
|
|
200
|
+
}
|
|
201
|
+
return [distRoot];
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Audits one engine source path and returns its entry. Pure orchestration
|
|
205
|
+
* helper kept separate so `auditBuildArtifacts` stays under the per-function
|
|
206
|
+
* line budget.
|
|
207
|
+
*/
|
|
208
|
+
async function auditSinglePath(source, ctx) {
|
|
209
|
+
if (!isPackageablePath(source)) {
|
|
210
|
+
return { source, artifact: undefined, status: 'skipped' };
|
|
211
|
+
}
|
|
212
|
+
const gate = await detectPlatformGate(ctx.engineDir, source);
|
|
213
|
+
if (gate.gatedOff) {
|
|
214
|
+
verbose(`Audit: skipping engine/${source} — gated off by moz.build "${gate.gateExpression ?? '?'}".`);
|
|
215
|
+
return { source, artifact: undefined, status: 'skipped' };
|
|
216
|
+
}
|
|
217
|
+
const sourcePath = join(ctx.engineDir, source);
|
|
218
|
+
let sourceMtime;
|
|
219
|
+
try {
|
|
220
|
+
const sourceStat = await stat(sourcePath);
|
|
221
|
+
sourceMtime = sourceStat.mtimeMs;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Deletion that didn't propagate — distinct class of bug, not audited yet.
|
|
225
|
+
return { source, artifact: undefined, status: 'skipped' };
|
|
226
|
+
}
|
|
227
|
+
const roots = searchRootsFor(source, ctx.distRoot, ctx.testsRoot);
|
|
228
|
+
const artifact = await resolveBestArtifact(source, roots);
|
|
229
|
+
if (!artifact) {
|
|
230
|
+
const where = isTestPath(source) ? '_tests/' : 'dist/';
|
|
231
|
+
return {
|
|
232
|
+
source,
|
|
233
|
+
artifact: undefined,
|
|
234
|
+
status: 'missing',
|
|
235
|
+
warning: `Audit: engine/${source} was touched but no packaged artifact with basename "${basename(source)}" was found under ${where}. Missing moz.build / jar.mn / package-manifest.in registration?`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
let artifactMtime;
|
|
239
|
+
try {
|
|
240
|
+
const artifactStat = await stat(artifact);
|
|
241
|
+
artifactMtime = artifactStat.mtimeMs;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return {
|
|
245
|
+
source,
|
|
246
|
+
artifact,
|
|
247
|
+
status: 'missing',
|
|
248
|
+
warning: `Audit: engine/${source} has no readable packaged artifact at ${artifact} (disappeared during audit).`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (artifactMtime + 1 < sourceMtime) {
|
|
252
|
+
return {
|
|
253
|
+
source,
|
|
254
|
+
artifact,
|
|
255
|
+
status: 'stale',
|
|
256
|
+
warning: `Audit: engine/${source} is newer than its packaged artifact ${artifact}. Build reported success but the file's path may not flow through packaging — check moz.build / jar.mn entries.`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return { source, artifact, status: 'updated' };
|
|
260
|
+
}
|
|
172
261
|
/**
|
|
173
262
|
* Runs the post-build audit. Emits per-file warnings for missing or
|
|
174
263
|
* stale artifacts and a summary info line at the end. Always returns
|
|
@@ -193,57 +282,22 @@ export async function auditBuildArtifacts(projectRoot, engineDir, baseline) {
|
|
|
193
282
|
verbose('Audit skipped: no dist tree found under obj-*/dist/.');
|
|
194
283
|
return summary;
|
|
195
284
|
}
|
|
285
|
+
const testsRoot = await resolveTestsRoot(engineDir);
|
|
196
286
|
const changed = await collectChangedFiles(engineDir, baseline);
|
|
197
287
|
if (changed.length === 0) {
|
|
198
288
|
return summary;
|
|
199
289
|
}
|
|
290
|
+
const ctx = { engineDir, distRoot, testsRoot };
|
|
200
291
|
for (const source of changed) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
sourceMtime = sourceStat.mtimeMs;
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// File was deleted since the diff was computed. Skip — a deletion
|
|
214
|
-
// that didn't propagate to the dist tree is a distinct class of bug
|
|
215
|
-
// we don't audit yet.
|
|
216
|
-
summary.skipped += 1;
|
|
217
|
-
summary.entries.push({ source, artifact: undefined, status: 'skipped' });
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
const artifact = await findArtifactByBasename(distRoot, basename(source));
|
|
221
|
-
if (!artifact) {
|
|
222
|
-
summary.missing += 1;
|
|
223
|
-
summary.entries.push({ source, artifact: undefined, status: 'missing' });
|
|
224
|
-
warn(`Audit: engine/${source} was touched but no packaged artifact with basename "${basename(source)}" was found under ${distRoot}. Missing moz.build / jar.mn / package-manifest.in registration?`);
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
let artifactMtime;
|
|
228
|
-
try {
|
|
229
|
-
const artifactStat = await stat(artifact);
|
|
230
|
-
artifactMtime = artifactStat.mtimeMs;
|
|
231
|
-
}
|
|
232
|
-
catch {
|
|
233
|
-
// Disappeared after the directory scan; treat as missing.
|
|
234
|
-
summary.missing += 1;
|
|
235
|
-
summary.entries.push({ source, artifact, status: 'missing' });
|
|
236
|
-
warn(`Audit: engine/${source} has no readable packaged artifact at ${artifact} (disappeared during audit).`);
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (artifactMtime + 1 < sourceMtime) {
|
|
240
|
-
summary.stale += 1;
|
|
241
|
-
summary.entries.push({ source, artifact, status: 'stale' });
|
|
242
|
-
warn(`Audit: engine/${source} is newer than its packaged artifact ${artifact}. Build reported success but the file's path may not flow through packaging — check moz.build / jar.mn entries.`);
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
summary.updated += 1;
|
|
246
|
-
summary.entries.push({ source, artifact, status: 'updated' });
|
|
292
|
+
const result = await auditSinglePath(source, ctx);
|
|
293
|
+
summary[result.status] += 1;
|
|
294
|
+
summary.entries.push({
|
|
295
|
+
source: result.source,
|
|
296
|
+
artifact: result.artifact,
|
|
297
|
+
status: result.status,
|
|
298
|
+
});
|
|
299
|
+
if (result.warning)
|
|
300
|
+
warn(result.warning);
|
|
247
301
|
}
|
|
248
302
|
info(`Packaged: ${summary.updated} updated, ${summary.stale} stale, ${summary.missing} missing, ${summary.skipped} skipped`);
|
|
249
303
|
return summary;
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Top-level chrome document — DOM fragment insertion.
|
|
3
|
+
*
|
|
4
|
+
* Default target is `browser/base/content/browser.xhtml`. Forks that replace
|
|
5
|
+
* browser.xhtml with a custom top-level chrome document pass the replacement
|
|
6
|
+
* path in via `targetPath`; the insertion logic is shape-agnostic (looks for
|
|
7
|
+
* `#include browser-sets.inc`, then falls back to `<html:body>`), so any
|
|
8
|
+
* browser.xhtml-shaped xhtml works.
|
|
3
9
|
*/
|
|
10
|
+
export declare const DEFAULT_DOM_TARGET = "browser/base/content/browser.xhtml";
|
|
4
11
|
/**
|
|
5
12
|
* Tokenizer-based implementation for DOM fragment insertion.
|
|
6
13
|
*/
|
|
@@ -10,14 +17,19 @@ export declare function addDomFragmentTokenized(content: string, includeDirectiv
|
|
|
10
17
|
*/
|
|
11
18
|
export declare function legacyAddDomFragment(content: string, includeDirective: string): string;
|
|
12
19
|
/**
|
|
13
|
-
* Inserts a `#include` directive for an `.inc.xhtml` file into
|
|
14
|
-
*
|
|
20
|
+
* Inserts a `#include` directive for an `.inc.xhtml` file into the top-level
|
|
21
|
+
* chrome document (default: `browser/base/content/browser.xhtml`), before
|
|
22
|
+
* `#include browser-sets.inc`.
|
|
15
23
|
*
|
|
16
24
|
* If the file's content was previously inlined (detected by root element id=),
|
|
17
25
|
* the inlined block is automatically replaced with the `#include` directive.
|
|
18
26
|
*
|
|
19
27
|
* @param engineDir - Engine source root
|
|
20
28
|
* @param domFilePath - Path to the `.inc.xhtml` file relative to engine root
|
|
29
|
+
* @param targetPath - Chrome document to insert into, relative to engine
|
|
30
|
+
* root. Defaults to {@link DEFAULT_DOM_TARGET}. Forks that replace
|
|
31
|
+
* browser.xhtml with a custom top-level chrome document pass the
|
|
32
|
+
* replacement path here.
|
|
21
33
|
* @returns true if inserted, false if already present
|
|
22
34
|
*/
|
|
23
|
-
export declare function addDomFragment(engineDir: string, domFilePath: string): Promise<boolean>;
|
|
35
|
+
export declare function addDomFragment(engineDir: string, domFilePath: string, targetPath?: string): Promise<boolean>;
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Top-level chrome document — DOM fragment insertion.
|
|
4
|
+
*
|
|
5
|
+
* Default target is `browser/base/content/browser.xhtml`. Forks that replace
|
|
6
|
+
* browser.xhtml with a custom top-level chrome document pass the replacement
|
|
7
|
+
* path in via `targetPath`; the insertion logic is shape-agnostic (looks for
|
|
8
|
+
* `#include browser-sets.inc`, then falls back to `<html:body>`), so any
|
|
9
|
+
* browser.xhtml-shaped xhtml works.
|
|
4
10
|
*/
|
|
5
|
-
import { join, relative } from 'node:path';
|
|
11
|
+
import { dirname, join, relative } from 'node:path';
|
|
6
12
|
import { GeneralError } from '../errors/base.js';
|
|
7
13
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
8
14
|
import { toRootRelativePath } from '../utils/paths.js';
|
|
9
15
|
import { escapeRegex } from '../utils/regex.js';
|
|
10
16
|
import { withParserFallback } from './parser-fallback.js';
|
|
11
17
|
import { tokenizeXhtml } from './wire-utils.js';
|
|
12
|
-
const
|
|
18
|
+
export const DEFAULT_DOM_TARGET = 'browser/base/content/browser.xhtml';
|
|
13
19
|
/**
|
|
14
20
|
* Tokenizer-based implementation for DOM fragment insertion.
|
|
15
21
|
*/
|
|
@@ -36,7 +42,7 @@ export function addDomFragmentTokenized(content, includeDirective) {
|
|
|
36
42
|
}
|
|
37
43
|
}
|
|
38
44
|
if (insertIndex === -1) {
|
|
39
|
-
throw new GeneralError('Could not find insertion point in
|
|
45
|
+
throw new GeneralError('Could not find insertion point in chrome document');
|
|
40
46
|
}
|
|
41
47
|
lines.splice(insertIndex, 0, includeDirective);
|
|
42
48
|
return lines.join('\n');
|
|
@@ -64,32 +70,41 @@ export function legacyAddDomFragment(content, includeDirective) {
|
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
if (insertIndex === -1) {
|
|
67
|
-
throw new GeneralError('Could not find insertion point in
|
|
73
|
+
throw new GeneralError('Could not find insertion point in chrome document');
|
|
68
74
|
}
|
|
69
75
|
lines.splice(insertIndex, 0, includeDirective);
|
|
70
76
|
return lines.join('\n');
|
|
71
77
|
}
|
|
72
78
|
/**
|
|
73
|
-
* Inserts a `#include` directive for an `.inc.xhtml` file into
|
|
74
|
-
*
|
|
79
|
+
* Inserts a `#include` directive for an `.inc.xhtml` file into the top-level
|
|
80
|
+
* chrome document (default: `browser/base/content/browser.xhtml`), before
|
|
81
|
+
* `#include browser-sets.inc`.
|
|
75
82
|
*
|
|
76
83
|
* If the file's content was previously inlined (detected by root element id=),
|
|
77
84
|
* the inlined block is automatically replaced with the `#include` directive.
|
|
78
85
|
*
|
|
79
86
|
* @param engineDir - Engine source root
|
|
80
87
|
* @param domFilePath - Path to the `.inc.xhtml` file relative to engine root
|
|
88
|
+
* @param targetPath - Chrome document to insert into, relative to engine
|
|
89
|
+
* root. Defaults to {@link DEFAULT_DOM_TARGET}. Forks that replace
|
|
90
|
+
* browser.xhtml with a custom top-level chrome document pass the
|
|
91
|
+
* replacement path here.
|
|
81
92
|
* @returns true if inserted, false if already present
|
|
82
93
|
*/
|
|
83
|
-
export async function addDomFragment(engineDir, domFilePath) {
|
|
84
|
-
const
|
|
94
|
+
export async function addDomFragment(engineDir, domFilePath, targetPath = DEFAULT_DOM_TARGET) {
|
|
95
|
+
const targetAbsPath = join(engineDir, targetPath);
|
|
85
96
|
const safeDomFilePath = toRootRelativePath(engineDir, domFilePath);
|
|
86
|
-
if (!(await pathExists(
|
|
87
|
-
throw new GeneralError(`${
|
|
97
|
+
if (!(await pathExists(targetAbsPath))) {
|
|
98
|
+
throw new GeneralError(`${targetPath} not found in engine`);
|
|
88
99
|
}
|
|
89
|
-
// Compute include path relative to
|
|
90
|
-
|
|
100
|
+
// Compute include path relative to the target's directory — the `#include`
|
|
101
|
+
// directive is resolved by the preprocessor relative to the file that
|
|
102
|
+
// contains it, so this must track the chrome doc's location, not a
|
|
103
|
+
// hardcoded `browser/base/content/`.
|
|
104
|
+
const targetDir = dirname(targetPath);
|
|
105
|
+
const includePath = relative(targetDir, safeDomFilePath).replace(/\\/g, '/');
|
|
91
106
|
const includeDirective = `#include ${includePath}`;
|
|
92
|
-
let content = await readText(
|
|
107
|
+
let content = await readText(targetAbsPath);
|
|
93
108
|
// Idempotency: check if the #include directive already exists (line-anchored to avoid substring matches)
|
|
94
109
|
if (new RegExp(`^${escapeRegex(includeDirective)}$`, 'm').test(content)) {
|
|
95
110
|
return false;
|
|
@@ -116,14 +131,14 @@ export async function addDomFragment(engineDir, domFilePath) {
|
|
|
116
131
|
}
|
|
117
132
|
lines.splice(startIdx, endIdx - startIdx, includeDirective);
|
|
118
133
|
content = lines.join('\n');
|
|
119
|
-
await writeText(
|
|
134
|
+
await writeText(targetAbsPath, content);
|
|
120
135
|
return true;
|
|
121
136
|
}
|
|
122
137
|
}
|
|
123
138
|
}
|
|
124
139
|
// Normal insertion
|
|
125
|
-
const { value } = withParserFallback(() => addDomFragmentTokenized(content, includeDirective), () => legacyAddDomFragment(content, includeDirective),
|
|
126
|
-
await writeText(
|
|
140
|
+
const { value } = withParserFallback(() => addDomFragmentTokenized(content, includeDirective), () => legacyAddDomFragment(content, includeDirective), targetPath);
|
|
141
|
+
await writeText(targetAbsPath, value);
|
|
127
142
|
return true;
|
|
128
143
|
}
|
|
129
144
|
//# sourceMappingURL=wire-dom-fragment.js.map
|
|
@@ -309,6 +309,13 @@ export interface WireOptions {
|
|
|
309
309
|
dryRun?: boolean;
|
|
310
310
|
after?: string;
|
|
311
311
|
subscriptDir?: string;
|
|
312
|
+
/**
|
|
313
|
+
* Chrome document the DOM fragment's `#include` is inserted into, relative
|
|
314
|
+
* to engine/. Defaults to the first entry of
|
|
315
|
+
* `furnace.json.tokenHostDocuments` when set, otherwise
|
|
316
|
+
* `browser/base/content/browser.xhtml`.
|
|
317
|
+
*/
|
|
318
|
+
target?: string;
|
|
312
319
|
}
|
|
313
320
|
/**
|
|
314
321
|
* Options for the register command.
|