@hominis/fireforge 0.15.5 → 0.15.7

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +70 -5
  3. package/dist/src/commands/build.js +60 -3
  4. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +17 -0
  5. package/dist/src/commands/furnace/chrome-doc-templates.js +18 -0
  6. package/dist/src/commands/furnace/chrome-doc-tests.d.ts +23 -0
  7. package/dist/src/commands/furnace/chrome-doc-tests.js +120 -0
  8. package/dist/src/commands/furnace/chrome-doc.d.ts +11 -0
  9. package/dist/src/commands/furnace/chrome-doc.js +37 -4
  10. package/dist/src/commands/furnace/create-dry-run.d.ts +31 -0
  11. package/dist/src/commands/furnace/create-dry-run.js +95 -0
  12. package/dist/src/commands/furnace/create-templates.js +14 -0
  13. package/dist/src/commands/furnace/create.js +28 -24
  14. package/dist/src/commands/furnace/index.js +3 -1
  15. package/dist/src/commands/lint.d.ts +17 -2
  16. package/dist/src/commands/lint.js +25 -2
  17. package/dist/src/commands/register.d.ts +1 -1
  18. package/dist/src/commands/register.js +30 -7
  19. package/dist/src/commands/test.js +16 -1
  20. package/dist/src/core/build-audit-platform.d.ts +3 -1
  21. package/dist/src/core/build-audit-platform.js +87 -20
  22. package/dist/src/core/build-audit-registration.d.ts +80 -0
  23. package/dist/src/core/build-audit-registration.js +187 -0
  24. package/dist/src/core/build-audit-transforms.d.ts +23 -0
  25. package/dist/src/core/build-audit-transforms.js +94 -0
  26. package/dist/src/core/build-audit.js +210 -3
  27. package/dist/src/core/furnace-validate-registration.d.ts +6 -4
  28. package/dist/src/core/furnace-validate-registration.js +66 -6
  29. package/dist/src/core/mach-build-artifacts.d.ts +44 -0
  30. package/dist/src/core/mach-build-artifacts.js +104 -3
  31. package/dist/src/core/mach.d.ts +1 -1
  32. package/dist/src/core/mach.js +1 -1
  33. package/dist/src/core/test-stale-check.d.ts +42 -0
  34. package/dist/src/core/test-stale-check.js +114 -0
  35. package/dist/src/types/commands/options.d.ts +16 -0
  36. package/package.json +1 -1
@@ -10,10 +10,23 @@
10
10
  * detection, the audit fires a "missing packaged artifact" warning for
11
11
  * every gated file on every off-platform build — pure noise.
12
12
  *
13
+ * Two gate sources are consulted, in order:
14
+ * 1. Python-style `if CONFIG[...]:` blocks in the owning `moz.build`.
15
+ * 2. Path-convention gates — certain directory fragments are packaged
16
+ * by platform-specific Makefile.in / NSIS recipes that FireForge
17
+ * does not parse, so a file living under `browser/installer/windows/`
18
+ * or any `/stubinstaller/` subtree is Windows-only regardless of
19
+ * what its nearest moz.build says. (The branding stubinstaller CSS
20
+ * is the motivating case: referenced from
21
+ * `browser/installer/windows/Makefile.in` / `nsis/stub.nsh` with no
22
+ * `if CONFIG[…]:` in any moz.build ancestor.)
23
+ *
13
24
  * The detection is intentionally lightweight: we walk up from the
14
25
  * source file looking for the closest `moz.build`, scan it for an
15
26
  * occurrence of the source basename inside an `if CONFIG[...]:` block,
16
27
  * and check whether the gate expression matches the host platform.
28
+ * The path-convention pass kicks in only when no moz.build gate is
29
+ * found, so an explicit moz.build gate always wins.
17
30
  *
18
31
  * This is best-effort. False negatives (we miss a gate and warn anyway)
19
32
  * are tolerable — the audit is warn-only. False positives (we wrongly
@@ -128,9 +141,58 @@ export function findEnclosingGate(content, basename) {
128
141
  }
129
142
  return undefined;
130
143
  }
144
+ /**
145
+ * Path-convention gates: directories whose files are packaged by
146
+ * platform-specific build recipes (NSIS stub installer, DMG creation,
147
+ * Linux installer scripts) that live outside the moz.build graph. A
148
+ * file under any of these fragments is platform-restricted regardless
149
+ * of what its nearest `moz.build` says.
150
+ *
151
+ * `stubinstaller/` is the Windows NSIS stub installer asset tree. It
152
+ * is referenced from `browser/installer/windows/Makefile.in` (via
153
+ * `FILES` / `_WIDGET_FILES` lists) and `nsis/stub.nsh`, never through
154
+ * an `if CONFIG[…]:` block an ancestor moz.build exposes. Without
155
+ * this path-level gate, the audit warns on every touched branding
156
+ * stubinstaller CSS on every non-Windows build.
157
+ */
158
+ const PATH_GATES = [
159
+ { fragment: '/stubinstaller/', platform: 'win32', label: 'path convention: /stubinstaller/' },
160
+ {
161
+ fragment: '/browser/installer/windows/',
162
+ platform: 'win32',
163
+ label: 'path convention: browser/installer/windows/',
164
+ },
165
+ {
166
+ fragment: '/browser/installer/macosx/',
167
+ platform: 'darwin',
168
+ label: 'path convention: browser/installer/macosx/',
169
+ },
170
+ {
171
+ fragment: '/browser/installer/linux/',
172
+ platform: 'linux',
173
+ label: 'path convention: browser/installer/linux/',
174
+ },
175
+ ];
176
+ /**
177
+ * Returns a path-convention gate for `sourcePath` when one applies.
178
+ * Leading slash added so `startsWith`-style prefix traps
179
+ * (`browser/installer/windows/…`) match whether or not the input
180
+ * starts with a separator.
181
+ */
182
+ function findPathConventionGate(sourcePath) {
183
+ const normalised = `/${sourcePath}`.replace(/\/+/g, '/');
184
+ for (const entry of PATH_GATES) {
185
+ if (normalised.includes(entry.fragment)) {
186
+ return { platform: entry.platform, label: entry.label };
187
+ }
188
+ }
189
+ return undefined;
190
+ }
131
191
  /**
132
192
  * 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.
193
+ * host by an enclosing `if CONFIG[...]:` block in its owning moz.build,
194
+ * OR by a path-convention rule for installer-tree subdirectories that
195
+ * are packaged via Makefile.in recipes the audit does not parse.
134
196
  * Returns `gatedOff: false` and no expression when no gate is found —
135
197
  * the file is not platform-restricted, so the caller should audit it
136
198
  * normally.
@@ -140,31 +202,36 @@ export function findEnclosingGate(content, basename) {
140
202
  * @returns Detection result
141
203
  */
142
204
  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
205
  let host;
159
206
  try {
160
207
  host = getPlatform();
161
208
  }
162
209
  catch {
163
- return { gatedOff: false };
210
+ host = undefined;
211
+ }
212
+ const sourceDir = dirname(join(engineDir, sourcePath));
213
+ const mozBuild = await findOwningMozBuild(engineDir, sourceDir);
214
+ if (mozBuild) {
215
+ let content;
216
+ try {
217
+ content = await readText(mozBuild);
218
+ }
219
+ catch {
220
+ content = '';
221
+ }
222
+ const sourceBasename = sourcePath.split('/').pop() ?? '';
223
+ const expression = findEnclosingGate(content, sourceBasename);
224
+ if (expression) {
225
+ if (host && isGateOffHost(expression, host)) {
226
+ return { gatedOff: true, gateExpression: expression };
227
+ }
228
+ return { gatedOff: false, gateExpression: expression };
229
+ }
164
230
  }
165
- if (isGateOffHost(expression, host)) {
166
- return { gatedOff: true, gateExpression: expression };
231
+ const pathGate = findPathConventionGate(sourcePath);
232
+ if (pathGate && host && host !== pathGate.platform) {
233
+ return { gatedOff: true, gateExpression: pathGate.label };
167
234
  }
168
- return { gatedOff: false, gateExpression: expression };
235
+ return { gatedOff: false };
169
236
  }
170
237
  //# sourceMappingURL=build-audit-platform.js.map
@@ -0,0 +1,80 @@
1
+ /** Parsed jar.mn registration anchored to a specific engine source path. */
2
+ export interface RegistrationHit {
3
+ /** Target path extracted from the entry (POSIX). */
4
+ target: string;
5
+ /** Source path from the entry (POSIX, relative to the jar.mn directory). */
6
+ source: string;
7
+ /** Absolute path of the jar.mn that owns the registration. */
8
+ jarManifest: string;
9
+ }
10
+ /** Result of a registration-aware dist probe. */
11
+ export interface RegistrationProbeResult {
12
+ /** Absolute path of the packaged artifact matching the registration target. */
13
+ artifact: string;
14
+ /** The registration entry that anchored the match. */
15
+ hit: RegistrationHit;
16
+ }
17
+ /**
18
+ * Parses a single jar.mn line into `{ target, source }` when the line is a
19
+ * content entry with an explicit `(source)` reference. Returns undefined
20
+ * for comments, headers (`browser.jar:`), `%` manifest directives, blank
21
+ * lines, and entries without a source reference.
22
+ *
23
+ * Accepted entry shapes:
24
+ * ` content/browser/foo.js (content/foo.js)` bare
25
+ * `* content/browser/foo.js (content/foo.js)` `*` = preprocessed
26
+ * `en-US.jar: content/foo.js (content/foo.js)` locale-prefixed
27
+ */
28
+ export declare function parseJarMnEntry(line: string): {
29
+ target: string;
30
+ source: string;
31
+ } | undefined;
32
+ /**
33
+ * Scans a jar.mn file's contents for an entry whose source reference
34
+ * matches `relativeSource` (POSIX, relative to the jar.mn directory).
35
+ * Returns the first match; jar.mn enforces uniqueness of `(source)` in
36
+ * practice, so a first-match wins behaviour is adequate.
37
+ */
38
+ export declare function findJarMnEntryForSource(content: string, relativeSource: string): {
39
+ target: string;
40
+ source: string;
41
+ } | undefined;
42
+ /**
43
+ * Walks from the source's directory upward to the engine root, returning
44
+ * the first jar.mn entry that registers the given source. Returns undefined
45
+ * when no ancestor jar.mn claims the source.
46
+ *
47
+ * @param engineDir Absolute engine root; walk halts here.
48
+ * @param source Engine-relative POSIX source path.
49
+ */
50
+ export declare function findRegisteredTarget(engineDir: string, source: string): Promise<RegistrationHit | undefined>;
51
+ /**
52
+ * Probes the dist tree for the artifact registered against the given
53
+ * source. Returns the matched candidate and the registration hit that
54
+ * anchored it; undefined when the source has no owning jar.mn or when
55
+ * no same-basename candidate under the search roots ends with the
56
+ * registered target path.
57
+ *
58
+ * Suffix-matching against the target path is intentional: jar.mn targets
59
+ * are relative to the jar root (`browser.jar:`, `toolkit.jar:`), but the
60
+ * dist tree prefixes every entry with a jar-specific directory
61
+ * (`.../chrome/browser/content/browser/…`). The source basename plus the
62
+ * target suffix are unambiguous across every packaging convention we
63
+ * care about.
64
+ *
65
+ * @param engineDir Absolute engine root.
66
+ * @param source Engine-relative POSIX source path.
67
+ * @param searchRoots Absolute roots to probe (dist/, _tests/).
68
+ */
69
+ export declare function resolveArtifactByRegistration(engineDir: string, source: string, searchRoots: readonly string[]): Promise<RegistrationProbeResult | undefined>;
70
+ /**
71
+ * Returns the absolute paths of every same-basename candidate under the
72
+ * given search roots. Used by the audit to enumerate ALL false-match
73
+ * candidates when the heuristic fallback downgrades to "missing" — the
74
+ * operator needs to see the full set, not just the scorer's pick, to
75
+ * distinguish a registration bug from a genuine packaging drop.
76
+ *
77
+ * @param source Engine-relative POSIX source path.
78
+ * @param searchRoots Absolute roots to scan.
79
+ */
80
+ export declare function collectSameBasenameCandidates(source: string, searchRoots: readonly string[]): Promise<string[]>;
@@ -0,0 +1,187 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /*
3
+ * Registration-aware artifact resolution for the post-build dist-tree audit.
4
+ *
5
+ * The basename-plus-similarity heuristic in `build-audit-resolve.ts` cannot
6
+ * distinguish two unrelated files that share a source basename. Motivating
7
+ * case: one fork registers `content/mybrowser.js` in `browser/base/jar.mn`
8
+ * (packaged under `chrome/browser/content/browser/mybrowser.js`) while an
9
+ * unrelated patch registers a `mybrowser.js` pref file under
10
+ * `browser/defaults/preferences/`. The basename walker surfaces both
11
+ * candidates, `scoreCandidate` awards them an equal trailing-overlap score
12
+ * (basename only, no meaningful mid-path match), and whichever the
13
+ * directory walk hits first wins — frequently the wrong one.
14
+ *
15
+ * This module anchors resolution to the `(source)` reference inside
16
+ * `jar.mn`. For a source under audit we walk its ancestor directories for
17
+ * an owning `jar.mn`, find the entry whose `(source)` resolves to our
18
+ * path, and expose both the target path recorded in the entry and the
19
+ * manifest that owns it. Callers in `build-audit.ts` prefer candidates
20
+ * whose absolute dist-tree path ends with the registered target, and
21
+ * report an unambiguous "registered but not packaged" miss when no such
22
+ * candidate exists — rather than falling through to the heuristic which
23
+ * would pick an unrelated same-basename file.
24
+ *
25
+ * Fallback semantics: when no jar.mn registration is found, the caller
26
+ * is expected to use the similarity heuristic. Registration wins when it
27
+ * exists; the heuristic fills the gap for sources registered through
28
+ * moz.build (`FINAL_TARGET_FILES`, `JS_PREFERENCE_FILES`, etc.) or
29
+ * `package-manifest.in` entries, which this module intentionally does
30
+ * not parse — supporting every Firefox registration surface would bloat
31
+ * the audit, and the heuristic's remaining weak case (unrelated same-
32
+ * basename hits) is surfaced to the operator in the warning copy.
33
+ */
34
+ import { basename, dirname, join, relative, sep } from 'node:path';
35
+ import { pathExists, readText } from '../utils/fs.js';
36
+ import { findAllByBasename } from './build-audit-resolve.js';
37
+ /** Ceiling on ancestor-directory hops when searching for an owning jar.mn. */
38
+ const MAX_JAR_MN_SCAN_DEPTH = 8;
39
+ /**
40
+ * Parses a single jar.mn line into `{ target, source }` when the line is a
41
+ * content entry with an explicit `(source)` reference. Returns undefined
42
+ * for comments, headers (`browser.jar:`), `%` manifest directives, blank
43
+ * lines, and entries without a source reference.
44
+ *
45
+ * Accepted entry shapes:
46
+ * ` content/browser/foo.js (content/foo.js)` bare
47
+ * `* content/browser/foo.js (content/foo.js)` `*` = preprocessed
48
+ * `en-US.jar: content/foo.js (content/foo.js)` locale-prefixed
49
+ */
50
+ export function parseJarMnEntry(line) {
51
+ if (!line)
52
+ return undefined;
53
+ const trimmed = line.trim();
54
+ if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('%'))
55
+ return undefined;
56
+ // Leading `*` (preprocessed) and optional `locale.jar:` prefix are dropped
57
+ // before matching the target/(source) pair. Both are whitespace-separated
58
+ // from the target entry.
59
+ const stripped = trimmed.replace(/^\*\s+/, '').replace(/^[A-Za-z0-9.\-_]+\.jar:\s+/, '');
60
+ const match = /^(\S+)\s+\(([^)]+)\)\s*$/.exec(stripped);
61
+ if (!match)
62
+ return undefined;
63
+ const target = (match[1] ?? '').trim();
64
+ const source = (match[2] ?? '').trim();
65
+ if (!target || !source)
66
+ return undefined;
67
+ return { target, source };
68
+ }
69
+ /**
70
+ * Scans a jar.mn file's contents for an entry whose source reference
71
+ * matches `relativeSource` (POSIX, relative to the jar.mn directory).
72
+ * Returns the first match; jar.mn enforces uniqueness of `(source)` in
73
+ * practice, so a first-match wins behaviour is adequate.
74
+ */
75
+ export function findJarMnEntryForSource(content, relativeSource) {
76
+ const normalized = relativeSource.replace(/\\/g, '/');
77
+ for (const line of content.split('\n')) {
78
+ const parsed = parseJarMnEntry(line);
79
+ if (!parsed)
80
+ continue;
81
+ if (parsed.source === normalized)
82
+ return parsed;
83
+ }
84
+ return undefined;
85
+ }
86
+ /**
87
+ * Walks from the source's directory upward to the engine root, returning
88
+ * the first jar.mn entry that registers the given source. Returns undefined
89
+ * when no ancestor jar.mn claims the source.
90
+ *
91
+ * @param engineDir Absolute engine root; walk halts here.
92
+ * @param source Engine-relative POSIX source path.
93
+ */
94
+ export async function findRegisteredTarget(engineDir, source) {
95
+ const sourceAbs = join(engineDir, source);
96
+ const root = engineDir.replace(/[/\\]+$/, '');
97
+ let current = dirname(sourceAbs);
98
+ let depth = 0;
99
+ while (depth <= MAX_JAR_MN_SCAN_DEPTH &&
100
+ (current === root || current.startsWith(`${root}/`) || current.startsWith(`${root}\\`))) {
101
+ const jarMn = join(current, 'jar.mn');
102
+ if (await pathExists(jarMn)) {
103
+ let content;
104
+ try {
105
+ content = await readText(jarMn);
106
+ }
107
+ catch {
108
+ content = '';
109
+ }
110
+ const rel = relative(current, sourceAbs).split(sep).join('/');
111
+ const entry = findJarMnEntryForSource(content, rel);
112
+ if (entry) {
113
+ return { target: entry.target, source: entry.source, jarManifest: jarMn };
114
+ }
115
+ }
116
+ const parent = dirname(current);
117
+ if (parent === current)
118
+ break;
119
+ current = parent;
120
+ depth += 1;
121
+ }
122
+ return undefined;
123
+ }
124
+ /**
125
+ * Normalises an absolute filesystem path to POSIX separators so the
126
+ * suffix comparison against a jar.mn target (always POSIX) is platform-
127
+ * independent.
128
+ */
129
+ function toPosix(path) {
130
+ return path.split(sep).join('/');
131
+ }
132
+ /**
133
+ * Probes the dist tree for the artifact registered against the given
134
+ * source. Returns the matched candidate and the registration hit that
135
+ * anchored it; undefined when the source has no owning jar.mn or when
136
+ * no same-basename candidate under the search roots ends with the
137
+ * registered target path.
138
+ *
139
+ * Suffix-matching against the target path is intentional: jar.mn targets
140
+ * are relative to the jar root (`browser.jar:`, `toolkit.jar:`), but the
141
+ * dist tree prefixes every entry with a jar-specific directory
142
+ * (`.../chrome/browser/content/browser/…`). The source basename plus the
143
+ * target suffix are unambiguous across every packaging convention we
144
+ * care about.
145
+ *
146
+ * @param engineDir Absolute engine root.
147
+ * @param source Engine-relative POSIX source path.
148
+ * @param searchRoots Absolute roots to probe (dist/, _tests/).
149
+ */
150
+ export async function resolveArtifactByRegistration(engineDir, source, searchRoots) {
151
+ const hit = await findRegisteredTarget(engineDir, source);
152
+ if (!hit)
153
+ return undefined;
154
+ const name = basename(source);
155
+ const targetSuffix = `/${hit.target.replace(/^\/+/, '')}`;
156
+ const candidates = [];
157
+ for (const root of searchRoots) {
158
+ const found = await findAllByBasename(root, name);
159
+ candidates.push(...found);
160
+ }
161
+ for (const candidate of candidates) {
162
+ if (toPosix(candidate).endsWith(targetSuffix)) {
163
+ return { artifact: candidate, hit };
164
+ }
165
+ }
166
+ return undefined;
167
+ }
168
+ /**
169
+ * Returns the absolute paths of every same-basename candidate under the
170
+ * given search roots. Used by the audit to enumerate ALL false-match
171
+ * candidates when the heuristic fallback downgrades to "missing" — the
172
+ * operator needs to see the full set, not just the scorer's pick, to
173
+ * distinguish a registration bug from a genuine packaging drop.
174
+ *
175
+ * @param source Engine-relative POSIX source path.
176
+ * @param searchRoots Absolute roots to scan.
177
+ */
178
+ export async function collectSameBasenameCandidates(source, searchRoots) {
179
+ const name = basename(source);
180
+ const out = [];
181
+ for (const root of searchRoots) {
182
+ const found = await findAllByBasename(root, name);
183
+ out.push(...found);
184
+ }
185
+ return out;
186
+ }
187
+ //# sourceMappingURL=build-audit-registration.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Returns the expected chrome-tree suffix for an engine-relative POSIX
3
+ * source path when the path falls under a known transform prefix;
4
+ * undefined otherwise.
5
+ *
6
+ * @param source Engine-relative POSIX source path.
7
+ */
8
+ export declare function expectedChromeSuffix(source: string): string | undefined;
9
+ /**
10
+ * Probes the dist tree for the artifact implied by a known
11
+ * source→chrome transform. Returns the first absolute candidate whose
12
+ * POSIX path ends with the expected chrome suffix, or undefined when
13
+ * no transform applies or no candidate matches.
14
+ *
15
+ * The transform check is treated as high-confidence by `build-audit.ts`
16
+ * (callers pass `{ registered: true }` to `evaluateArtifactMtime`), so
17
+ * a match bypasses the structural-relation check that rejects generic
18
+ * basename collisions.
19
+ *
20
+ * @param source Engine-relative POSIX source path.
21
+ * @param searchRoots Absolute roots to probe (dist/, _tests/).
22
+ */
23
+ export declare function resolveArtifactByKnownTransform(source: string, searchRoots: readonly string[]): Promise<string | undefined>;
@@ -0,0 +1,94 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /*
3
+ * Known source→packaging path transforms for the post-build dist-tree audit.
4
+ *
5
+ * Motivating case: a source at `engine/browser/base/content/foo.js` ships
6
+ * under `chrome/browser/content/browser/foo.js`. If an unrelated patch
7
+ * registers a different `foo.js` elsewhere in the tree (e.g. a pref file
8
+ * under `browser/defaults/preferences/foo.js`), the basename walker
9
+ * surfaces both candidates, `scoreCandidate` awards them an identical
10
+ * trailing-overlap score (basename only; every intermediate segment is
11
+ * in the generic list), and whichever the directory walk hits first wins.
12
+ * The heuristic then declares the chosen candidate "not structurally
13
+ * related" and reports the correctly-packaged chrome resource as missing.
14
+ *
15
+ * The transforms below anchor resolution to the well-known subtree→chrome
16
+ * conventions used by upstream mozilla-central jar.mn. When a source
17
+ * matches one of these prefixes, a candidate whose absolute path ends
18
+ * with the implied chrome suffix is treated as a confident match — the
19
+ * scorer never runs and the structural-relation check is bypassed.
20
+ *
21
+ * Scope is intentionally narrow: only subtrees whose packaging target is
22
+ * stable across every fork we know about. A fork that reroutes a known
23
+ * subtree can still win by adding `(source)` annotations in its own
24
+ * `jar.mn`, which `resolveArtifactByRegistration` consults first.
25
+ */
26
+ import { basename, sep } from 'node:path';
27
+ import { findAllByBasename } from './build-audit-resolve.js';
28
+ /**
29
+ * Table of `prefix → chrome-suffix` transforms. Each rule names an
30
+ * engine-relative subtree prefix and produces the expected dist-tree
31
+ * suffix for a file under it. Rules are evaluated in array order and
32
+ * the first-matching prefix wins, so `toolkit/content/widgets/` must
33
+ * precede the looser `toolkit/content/`.
34
+ */
35
+ const KNOWN_TRANSFORMS = [
36
+ {
37
+ prefix: 'browser/base/content/',
38
+ build: (rest) => `chrome/browser/content/browser/${rest}`,
39
+ },
40
+ {
41
+ prefix: 'toolkit/content/widgets/',
42
+ build: (rest) => `chrome/toolkit/content/global/elements/${rest}`,
43
+ },
44
+ {
45
+ prefix: 'toolkit/content/',
46
+ build: (rest) => `chrome/toolkit/content/global/${rest}`,
47
+ },
48
+ ];
49
+ /**
50
+ * Returns the expected chrome-tree suffix for an engine-relative POSIX
51
+ * source path when the path falls under a known transform prefix;
52
+ * undefined otherwise.
53
+ *
54
+ * @param source Engine-relative POSIX source path.
55
+ */
56
+ export function expectedChromeSuffix(source) {
57
+ for (const rule of KNOWN_TRANSFORMS) {
58
+ if (source.startsWith(rule.prefix)) {
59
+ return rule.build(source.slice(rule.prefix.length));
60
+ }
61
+ }
62
+ return undefined;
63
+ }
64
+ /**
65
+ * Probes the dist tree for the artifact implied by a known
66
+ * source→chrome transform. Returns the first absolute candidate whose
67
+ * POSIX path ends with the expected chrome suffix, or undefined when
68
+ * no transform applies or no candidate matches.
69
+ *
70
+ * The transform check is treated as high-confidence by `build-audit.ts`
71
+ * (callers pass `{ registered: true }` to `evaluateArtifactMtime`), so
72
+ * a match bypasses the structural-relation check that rejects generic
73
+ * basename collisions.
74
+ *
75
+ * @param source Engine-relative POSIX source path.
76
+ * @param searchRoots Absolute roots to probe (dist/, _tests/).
77
+ */
78
+ export async function resolveArtifactByKnownTransform(source, searchRoots) {
79
+ const suffix = expectedChromeSuffix(source);
80
+ if (!suffix)
81
+ return undefined;
82
+ const name = basename(source);
83
+ const suffixWithSlash = `/${suffix}`;
84
+ for (const root of searchRoots) {
85
+ const candidates = await findAllByBasename(root, name);
86
+ for (const candidate of candidates) {
87
+ if (candidate.split(sep).join('/').endsWith(suffixWithSlash)) {
88
+ return candidate;
89
+ }
90
+ }
91
+ }
92
+ return undefined;
93
+ }
94
+ //# sourceMappingURL=build-audit-transforms.js.map