@hominis/fireforge 0.15.6 → 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 (34) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +68 -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-registration.d.ts +80 -0
  21. package/dist/src/core/build-audit-registration.js +187 -0
  22. package/dist/src/core/build-audit-transforms.d.ts +23 -0
  23. package/dist/src/core/build-audit-transforms.js +94 -0
  24. package/dist/src/core/build-audit.js +107 -7
  25. package/dist/src/core/furnace-validate-registration.d.ts +6 -4
  26. package/dist/src/core/furnace-validate-registration.js +66 -6
  27. package/dist/src/core/mach-build-artifacts.d.ts +44 -0
  28. package/dist/src/core/mach-build-artifacts.js +104 -3
  29. package/dist/src/core/mach.d.ts +1 -1
  30. package/dist/src/core/mach.js +1 -1
  31. package/dist/src/core/test-stale-check.d.ts +42 -0
  32. package/dist/src/core/test-stale-check.js +114 -0
  33. package/dist/src/types/commands/options.d.ts +16 -0
  34. package/package.json +1 -1
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { readdir } from 'node:fs/promises';
3
- import { join, resolve } from 'node:path';
3
+ import { join, relative, resolve, sep } from 'node:path';
4
4
  import { toError } from '../utils/errors.js';
5
- import { pathExists, readJson } from '../utils/fs.js';
5
+ import { pathExists, readJson, writeJson } from '../utils/fs.js';
6
6
  import { verbose } from '../utils/logger.js';
7
7
  import { isObject, isString } from '../utils/validation.js';
8
8
  function validateBuildMozinfo(data) {
@@ -112,6 +112,107 @@ export function buildArtifactMismatchMessage(engineDir, buildCheck, commandName)
112
112
  }
113
113
  return (`${commandName} cannot use copied or relocated build artifacts whose metadata still points at a different Firefox workspace.\n\n` +
114
114
  `${details.join('\n')}\n\n` +
115
- 'Delete the stale obj-* directory in this workspace and run "fireforge build" again so mach regenerates build metadata for the current checkout.');
115
+ 'Delete the stale obj-* directory in this workspace and run "fireforge build" again so mach regenerates build metadata for the current checkout.\n' +
116
+ 'If the workspace was simply moved (same tree, different prefix), "fireforge build --rewrite-mozinfo" will patch mozinfo.json paths in place and run mach configure instead of scrubbing the whole tree.');
117
+ }
118
+ /**
119
+ * Safe-relocation rewriter for mozinfo.json under the active obj-* tree.
120
+ *
121
+ * Firefox build artefacts bake the topsrcdir into many generated files
122
+ * (Makefiles, config.status, backend.mk, .deps dependency files — anything
123
+ * produced by `mach configure`). A fresh `mach configure` rebuilds those
124
+ * from the top, so the rewriter only needs to patch the one file `mach`
125
+ * reads to learn where its checkout actually lives. Once mozinfo.json
126
+ * agrees with the on-disk layout, `mach configure` regenerates the rest.
127
+ *
128
+ * Safety rules — the rewrite is refused when any of them are violated:
129
+ * - `topsrcdir` and `topobjdir` must both be present and non-empty.
130
+ * - `topobjdir` must resolve to `<topsrcdir>/<objDir>`; a non-in-tree
131
+ * objdir means the previous workspace was configured differently,
132
+ * so a blind prefix-rewrite could point mach at the wrong tree.
133
+ * - The computed new `topobjdir` must be `<engineDir>/<objDir>`; if it
134
+ * is not, the objDir name itself changed and we cannot prove safety.
135
+ *
136
+ * When any rule trips, the caller should fall back to the clean-rebuild
137
+ * instruction — that's always a correct (if expensive) recovery path.
138
+ *
139
+ * @param engineDir Absolute path to the current engine checkout.
140
+ * @param objDir Name of the obj-* directory to rewrite against.
141
+ * @returns Result object; callers inspect `rewritten` and surface `reason`.
142
+ */
143
+ export async function attemptMozinfoRewrite(engineDir, objDir) {
144
+ const mozinfoPath = join(engineDir, objDir, 'mozinfo.json');
145
+ if (!(await pathExists(mozinfoPath))) {
146
+ return { rewritten: false, reason: 'mozinfo.json not found in obj directory' };
147
+ }
148
+ let raw;
149
+ try {
150
+ raw = await readJson(mozinfoPath);
151
+ }
152
+ catch (error) {
153
+ return { rewritten: false, reason: `mozinfo.json is unreadable: ${toError(error).message}` };
154
+ }
155
+ if (!isObject(raw)) {
156
+ return { rewritten: false, reason: 'mozinfo.json is not a JSON object' };
157
+ }
158
+ let mozinfo;
159
+ try {
160
+ mozinfo = validateBuildMozinfo(raw);
161
+ }
162
+ catch (error) {
163
+ return { rewritten: false, reason: toError(error).message };
164
+ }
165
+ const oldSrc = mozinfo.topsrcdir;
166
+ const oldObj = mozinfo.topobjdir;
167
+ if (!oldSrc || !oldObj) {
168
+ return {
169
+ rewritten: false,
170
+ reason: 'mozinfo.json is missing topsrcdir or topobjdir; cannot rewrite safely',
171
+ };
172
+ }
173
+ const oldSrcResolved = resolve(oldSrc);
174
+ const oldObjResolved = resolve(oldObj);
175
+ const insideTree = oldObjResolved === oldSrcResolved ||
176
+ oldObjResolved.startsWith(oldSrcResolved + sep) ||
177
+ oldObjResolved.startsWith(oldSrcResolved + '/');
178
+ if (!insideTree) {
179
+ return {
180
+ rewritten: false,
181
+ reason: `topobjdir (${oldObjResolved}) is not inside topsrcdir (${oldSrcResolved}) — rewrite would change workspace layout`,
182
+ };
183
+ }
184
+ const relativeObj = relative(oldSrcResolved, oldObjResolved).split(sep).join('/');
185
+ if (relativeObj !== objDir) {
186
+ return {
187
+ rewritten: false,
188
+ reason: `mozinfo objdir "${relativeObj}" does not match detected objdir "${objDir}" — rewrite would change the obj directory name`,
189
+ };
190
+ }
191
+ const newSrc = resolve(engineDir);
192
+ const newObj = resolve(engineDir, objDir);
193
+ const patched = { ...raw, topsrcdir: newSrc, topobjdir: newObj };
194
+ let newMozconfig;
195
+ if (mozinfo.mozconfig) {
196
+ const oldMozconfigResolved = resolve(mozinfo.mozconfig);
197
+ if (oldMozconfigResolved === oldSrcResolved ||
198
+ oldMozconfigResolved.startsWith(oldSrcResolved + sep) ||
199
+ oldMozconfigResolved.startsWith(oldSrcResolved + '/')) {
200
+ const rel = relative(oldSrcResolved, oldMozconfigResolved);
201
+ newMozconfig = resolve(newSrc, rel);
202
+ patched['mozconfig'] = newMozconfig;
203
+ }
204
+ // A mozconfig living outside the old topsrcdir is left as-is — it
205
+ // probably points at a shared configuration file the user kept in
206
+ // place across the relocation. A relocated checkout that also moved
207
+ // its mozconfig will still fail configure; operator can re-point
208
+ // with `MOZCONFIG=…` or run a full clean rebuild.
209
+ }
210
+ await writeJson(mozinfoPath, patched);
211
+ return {
212
+ rewritten: true,
213
+ newTopsrcdir: newSrc,
214
+ newTopobjdir: newObj,
215
+ ...(newMozconfig ? { newMozconfig } : {}),
216
+ };
116
217
  }
117
218
  //# sourceMappingURL=mach-build-artifacts.js.map
@@ -1,4 +1,4 @@
1
- export { type BuildArtifactCheck, buildArtifactMismatchMessage, hasBuildArtifacts, } from './mach-build-artifacts.js';
1
+ export { attemptMozinfoRewrite, type BuildArtifactCheck, buildArtifactMismatchMessage, hasBuildArtifacts, type MozinfoRewriteResult, } from './mach-build-artifacts.js';
2
2
  export { generateMozconfig, type MozconfigVariables } from './mach-mozconfig.js';
3
3
  export { ensurePython, resetResolvedPython } from './mach-python.js';
4
4
  /**
@@ -7,7 +7,7 @@ import { exec, execInherit, execInheritCapture, execStream } from '../utils/proc
7
7
  import { explainMachError } from './mach-error-hints.js';
8
8
  import { getPython } from './mach-python.js';
9
9
  // Re-export sub-modules so existing `from './mach.js'` imports keep working.
10
- export { buildArtifactMismatchMessage, hasBuildArtifacts, } from './mach-build-artifacts.js';
10
+ export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, } from './mach-build-artifacts.js';
11
11
  export { generateMozconfig } from './mach-mozconfig.js';
12
12
  export { ensurePython, resetResolvedPython } from './mach-python.js';
13
13
  /**
@@ -0,0 +1,42 @@
1
+ import type { BuildBaseline } from './build-baseline.js';
2
+ /** Result of the stale-build preflight probe. */
3
+ export interface StaleBuildResult {
4
+ /** True when at least one packageable engine file changed since the baseline. */
5
+ stale: boolean;
6
+ /**
7
+ * Engine-relative paths that would have been packaged but appear to have
8
+ * changed since the baseline. Sorted and deduplicated. Truncated at
9
+ * {@link STALE_PATHS_LIMIT} entries for rendering; consult
10
+ * {@link StaleBuildResult.truncated} to know when to append a `(+N more)`
11
+ * tail to the warning.
12
+ */
13
+ changedPaths: string[];
14
+ /**
15
+ * How many paths were dropped from `changedPaths` due to the render cap.
16
+ * Callers render this as `(+N more)` in the warning body.
17
+ */
18
+ truncated: number;
19
+ /**
20
+ * The baseline that anchored the diff, or undefined when no previous
21
+ * successful build exists. A missing baseline is treated as "not stale"
22
+ * — we have nothing to compare against and a warning would mislead.
23
+ */
24
+ baseline: BuildBaseline | undefined;
25
+ }
26
+ /**
27
+ * Probes the engine tree for packageable changes since the last successful
28
+ * `fireforge build`. Returns a summary the `fireforge test` handler renders
29
+ * as an up-front warning when `--build` was NOT passed. The probe never
30
+ * throws; git failures and a missing baseline both degrade to `stale: false`
31
+ * so a broken probe cannot block a test run.
32
+ *
33
+ * @param projectRoot Root directory of the project.
34
+ * @param engineDir Path to the engine directory.
35
+ */
36
+ export declare function checkStaleBuildForTest(projectRoot: string, engineDir: string): Promise<StaleBuildResult>;
37
+ /**
38
+ * Formats a human-readable warning body from a {@link StaleBuildResult}.
39
+ * Kept separate from the probe so test code can assert on the structured
40
+ * result without matching the rendered copy.
41
+ */
42
+ export declare function formatStaleBuildWarning(result: StaleBuildResult): string;
@@ -0,0 +1,114 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /*
3
+ * Stale-build preflight for `fireforge test`.
4
+ *
5
+ * Without this preflight, an operator who edits engine chrome / packaged
6
+ * resources (`jar.mn` entries, `.xhtml`/`.mjs`/`.css` under chrome trees,
7
+ * pref files) and then runs `fireforge test <path>` only discovers the
8
+ * build is stale AFTER xpcshell / mach test starts and errors out with
9
+ * `NS_ERROR_FILE_NOT_FOUND` against a `chrome://browser/content/…` URI
10
+ * — which reads as a test bug, not a rebuild prompt. The motivating case
11
+ * was scaffolding a new top-level chrome document + BrowserGlue-style
12
+ * xpcshell test: the test file existed, the manifests were registered,
13
+ * but `dist/` still held the pre-edit bundle and chrome URIs resolved
14
+ * to nothing.
15
+ *
16
+ * This preflight diffs engine HEAD (or workdir) against the last-build
17
+ * baseline (`.fireforge/last-build.json`), filters to paths that imply
18
+ * packaging, and returns a compact summary. `fireforge test` prints a
19
+ * warning up-front so the operator sees "you edited X, Y, Z since the
20
+ * last build — rerun with `--build` to refresh" BEFORE mach test
21
+ * launches. Detection stays advisory (warn-only) because a fork that
22
+ * rebuilds out-of-band (a separate `./mach build` invocation, an IDE
23
+ * plugin, etc.) can legitimately have a fresh `dist/` with no
24
+ * FireForge-recorded baseline update.
25
+ */
26
+ import { toError } from '../utils/errors.js';
27
+ import { verbose } from '../utils/logger.js';
28
+ import { isPackageablePath } from './build-audit.js';
29
+ import { readBuildBaseline } from './build-baseline.js';
30
+ import { hasChanges, isMissingHeadError } from './git.js';
31
+ import { git } from './git-base.js';
32
+ import { getUntrackedFiles } from './git-status.js';
33
+ /** Cap on the number of changed paths rendered inline. */
34
+ const STALE_PATHS_LIMIT = 10;
35
+ /**
36
+ * Collects engine paths that changed since the baseline SHA plus any
37
+ * workdir modifications. Mirrors the helper inside `build-prepare.ts` but
38
+ * is kept separate so the test-side preflight does not need to pull in
39
+ * the full build-prepare dependency graph (mozconfig generation, furnace
40
+ * apply hooks, …).
41
+ */
42
+ async function collectChangedEnginePaths(engineDir, baseline) {
43
+ const collected = new Set();
44
+ if (baseline.engineHeadSha) {
45
+ try {
46
+ const diff = await git(['diff', '--name-only', `${baseline.engineHeadSha}..HEAD`], engineDir);
47
+ for (const line of diff.split('\n')) {
48
+ const trimmed = line.trim();
49
+ if (trimmed)
50
+ collected.add(trimmed);
51
+ }
52
+ }
53
+ catch (error) {
54
+ if (!isMissingHeadError(error)) {
55
+ verbose(`Stale-build preflight: could not diff engine against baseline — ${toError(error).message}`);
56
+ }
57
+ }
58
+ }
59
+ try {
60
+ if (await hasChanges(engineDir)) {
61
+ const worktreeDiff = await git(['diff', '--name-only', 'HEAD'], engineDir);
62
+ for (const line of worktreeDiff.split('\n')) {
63
+ const trimmed = line.trim();
64
+ if (trimmed)
65
+ collected.add(trimmed);
66
+ }
67
+ for (const untracked of await getUntrackedFiles(engineDir)) {
68
+ collected.add(untracked);
69
+ }
70
+ }
71
+ }
72
+ catch (error) {
73
+ verbose(`Stale-build preflight: could not enumerate workdir changes — ${toError(error).message}`);
74
+ }
75
+ return [...collected];
76
+ }
77
+ /**
78
+ * Probes the engine tree for packageable changes since the last successful
79
+ * `fireforge build`. Returns a summary the `fireforge test` handler renders
80
+ * as an up-front warning when `--build` was NOT passed. The probe never
81
+ * throws; git failures and a missing baseline both degrade to `stale: false`
82
+ * so a broken probe cannot block a test run.
83
+ *
84
+ * @param projectRoot Root directory of the project.
85
+ * @param engineDir Path to the engine directory.
86
+ */
87
+ export async function checkStaleBuildForTest(projectRoot, engineDir) {
88
+ const baseline = await readBuildBaseline(projectRoot);
89
+ if (!baseline) {
90
+ return { stale: false, changedPaths: [], truncated: 0, baseline: undefined };
91
+ }
92
+ const changed = await collectChangedEnginePaths(engineDir, baseline);
93
+ const packageable = changed.filter((path) => isPackageablePath(path)).sort();
94
+ if (packageable.length === 0) {
95
+ return { stale: false, changedPaths: [], truncated: 0, baseline };
96
+ }
97
+ const head = packageable.slice(0, STALE_PATHS_LIMIT);
98
+ const truncated = Math.max(0, packageable.length - head.length);
99
+ return { stale: true, changedPaths: head, truncated, baseline };
100
+ }
101
+ /**
102
+ * Formats a human-readable warning body from a {@link StaleBuildResult}.
103
+ * Kept separate from the probe so test code can assert on the structured
104
+ * result without matching the rendered copy.
105
+ */
106
+ export function formatStaleBuildWarning(result) {
107
+ const tail = result.truncated > 0 ? `, … (+${result.truncated} more)` : '';
108
+ const list = result.changedPaths.join(', ') + tail;
109
+ return (`Engine tree has changed since the last successful fireforge build (${list}).\n` +
110
+ 'The current obj-*/dist/ bundle may not reflect those edits. If your test reads ' +
111
+ 'packaged chrome / jar.mn resources, rerun with "fireforge test --build" (or ' +
112
+ '"fireforge build --ui") first. Passing --build skips this check.');
113
+ }
114
+ //# sourceMappingURL=test-stale-check.js.map
@@ -41,6 +41,14 @@ export interface BuildOptions {
41
41
  jobs?: number;
42
42
  /** Brand to build (stable, esr, etc.) */
43
43
  brand?: string;
44
+ /**
45
+ * When a mozinfo mismatch is detected that looks like a safe path
46
+ * relocation (same structure, different prefix), patch mozinfo paths
47
+ * in place and run `mach configure` rather than aborting with a
48
+ * full-rebuild instruction. Falls back to the original abort message
49
+ * for any mismatch the rewriter cannot prove safe.
50
+ */
51
+ rewriteMozinfo?: boolean;
44
52
  }
45
53
  /**
46
54
  * Options for the export command.
@@ -298,6 +306,14 @@ export interface FurnaceCreateOptions {
298
306
  testStyle?: 'mochikit' | 'browser-chrome' | 'xpcshell';
299
307
  /** Stock component tag names composed internally by this component */
300
308
  compose?: string[];
309
+ /**
310
+ * Show the planned file set and furnace.json changes without writing
311
+ * anything. All validation that does not require disk writes (tag name
312
+ * shape, name conflicts, engine pre-existence, `--compose` targets, cycle
313
+ * detection, prefix warning) runs before the plan is emitted, so a
314
+ * dry-run faithfully previews the real command's outcome.
315
+ */
316
+ dryRun?: boolean;
301
317
  }
302
318
  /**
303
319
  * Options for the wire command.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.15.6",
3
+ "version": "0.15.7",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",