@hominis/fireforge 0.15.9 → 0.16.0

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 +73 -0
  2. package/README.md +2 -0
  3. package/dist/src/cli.d.ts +4 -1
  4. package/dist/src/cli.js +6 -3
  5. package/dist/src/commands/download.js +9 -0
  6. package/dist/src/commands/export-all.js +46 -0
  7. package/dist/src/commands/export.js +10 -1
  8. package/dist/src/commands/furnace/diff.js +22 -2
  9. package/dist/src/commands/furnace/override.js +35 -12
  10. package/dist/src/commands/furnace/preview.js +33 -1
  11. package/dist/src/commands/furnace/rename.js +14 -3
  12. package/dist/src/commands/lint.js +10 -1
  13. package/dist/src/commands/package.js +16 -5
  14. package/dist/src/commands/re-export.js +25 -0
  15. package/dist/src/commands/register.js +2 -18
  16. package/dist/src/commands/run.js +23 -2
  17. package/dist/src/commands/status.js +25 -3
  18. package/dist/src/commands/test.js +6 -24
  19. package/dist/src/commands/token.js +14 -1
  20. package/dist/src/commands/watch.js +14 -2
  21. package/dist/src/core/branding.d.ts +23 -0
  22. package/dist/src/core/branding.js +39 -0
  23. package/dist/src/core/browser-wire.js +68 -23
  24. package/dist/src/core/mach-build-artifacts.d.ts +41 -0
  25. package/dist/src/core/mach-build-artifacts.js +70 -0
  26. package/dist/src/core/mach-error-hints.js +15 -0
  27. package/dist/src/core/mach-mozconfig.d.ts +25 -0
  28. package/dist/src/core/mach-mozconfig.js +66 -0
  29. package/dist/src/core/mach.d.ts +12 -1
  30. package/dist/src/core/mach.js +14 -1
  31. package/dist/src/core/manifest-rules.js +22 -1
  32. package/dist/src/utils/fs.d.ts +12 -0
  33. package/dist/src/utils/fs.js +12 -0
  34. package/dist/src/utils/paths.d.ts +19 -0
  35. package/dist/src/utils/paths.js +33 -0
  36. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { MozconfigError } from '../errors/build.js';
4
4
  import { pathExists, readText, writeText } from '../utils/fs.js';
5
5
  import { getPlatform } from '../utils/platform.js';
6
+ import { BrandingMozconfigMismatchError } from './branding.js';
6
7
  /**
7
8
  * Replaces template variables in a string.
8
9
  * @param content - Content with ${variable} placeholders
@@ -16,6 +17,66 @@ function replaceVariables(content, variables) {
16
17
  .replace(/\$\{appId\}/g, variables.appId)
17
18
  .replace(/\$\{binaryName\}/g, variables.binaryName);
18
19
  }
20
+ /**
21
+ * Matches an `--with-branding=<path>` directive anywhere in a rendered
22
+ * mozconfig. The directive form is the one mach reads; an optional
23
+ * `ac_add_options` prefix is the on-disk convention. `m` flag anchors the
24
+ * search per-line so a multi-line mozconfig with older directives earlier
25
+ * in the file doesn't confuse the extractor. We pick the LAST match
26
+ * because mach itself takes the last-write-wins semantics of shell
27
+ * configuration for overlapping `ac_add_options` calls.
28
+ */
29
+ const WITH_BRANDING_PATTERN = /^\s*(?:ac_add_options\s+)?--with-branding\s*=\s*(\S+)/gm;
30
+ /**
31
+ * Extracts the `--with-branding=<path>` value from a rendered mozconfig
32
+ * body. Returns `undefined` when no directive is present — callers treat
33
+ * that as "mozconfig is missing branding", which is itself an actionable
34
+ * configuration error.
35
+ *
36
+ * Exported for testing.
37
+ */
38
+ export function extractWithBrandingPath(mozconfigContent) {
39
+ const matches = [...mozconfigContent.matchAll(WITH_BRANDING_PATTERN)];
40
+ const last = matches.at(-1);
41
+ return last?.[1];
42
+ }
43
+ /**
44
+ * Preflights the just-written mozconfig against the branding tree FireForge
45
+ * set up. A drift between the two is silent-corruption territory — the
46
+ * build runs, `mach configure` reads the stale directory name out of
47
+ * mozconfig, and then the recursive make backend errors out with a "path
48
+ * does not exist" message that names the branding dir the mozconfig
49
+ * referenced. By parsing the mozconfig here and comparing to
50
+ * `config.binaryName`, we turn that into a single-line actionable error
51
+ * before `mach` runs.
52
+ *
53
+ * @param engineDir Path to the engine directory (the branding tree lives here)
54
+ * @param mozconfigPath Path to the mozconfig just written
55
+ * @param config FireForge configuration (reads `binaryName`)
56
+ * @throws BrandingMozconfigMismatchError on drift or missing directive
57
+ */
58
+ export async function assertBrandingMozconfigAgreement(engineDir, mozconfigPath, config) {
59
+ const mozconfigContent = await readText(mozconfigPath);
60
+ const found = extractWithBrandingPath(mozconfigContent);
61
+ const expected = `browser/branding/${config.binaryName}`;
62
+ if (!found) {
63
+ throw new BrandingMozconfigMismatchError(expected, '(no --with-branding directive)', 'mozconfig-missing-branding');
64
+ }
65
+ // Normalise both sides to forward slashes before compare — Windows-edited
66
+ // configs can carry backslash path separators that the build would treat
67
+ // as literal characters in a repo-relative path.
68
+ const normalizedFound = found.replace(/\\/g, '/');
69
+ if (normalizedFound !== expected) {
70
+ throw new BrandingMozconfigMismatchError(expected, found, 'name-mismatch');
71
+ }
72
+ // Last line of defence: even with matching names, a missing branding tree
73
+ // means the scaffold step hasn't run. Preflight here so the operator
74
+ // doesn't pay for a configure-through-build cycle to discover it.
75
+ const brandingMozBuild = join(engineDir, expected, 'moz.build');
76
+ if (!(await pathExists(brandingMozBuild))) {
77
+ throw new BrandingMozconfigMismatchError(expected, found, 'branding-dir-missing');
78
+ }
79
+ }
19
80
  /**
20
81
  * Generates a mozconfig file from templates.
21
82
  * @param configsDir - Path to the configs directory
@@ -46,5 +107,10 @@ export async function generateMozconfig(configsDir, engineDir, config) {
46
107
  const platformContent = await readText(platformPath);
47
108
  content += `# Platform configuration (${platform})\n${replaceVariables(platformContent, variables)}`;
48
109
  await writeText(outputPath, content);
110
+ // Preflight: the mozconfig we just wrote must reference the branding
111
+ // directory FireForge actually set up. Catching the drift here (after the
112
+ // write, before anything consumes mozconfig) keeps `generateMozconfig`
113
+ // the single source of truth for both the render and the sanity-check.
114
+ await assertBrandingMozconfigAgreement(engineDir, outputPath, config);
49
115
  }
50
116
  //# sourceMappingURL=mach-mozconfig.js.map
@@ -1,5 +1,5 @@
1
1
  import { type SmokeLineCallback, type SmokeRunResult } from '../utils/process.js';
2
- export { attemptMozinfoRewrite, type BuildArtifactCheck, buildArtifactMismatchMessage, hasBuildArtifacts, type MozinfoRewriteResult, } from './mach-build-artifacts.js';
2
+ export { attemptMozinfoRewrite, type BuildArtifactCheck, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, type MozinfoRewriteResult, type RunnableBundleCheck, } from './mach-build-artifacts.js';
3
3
  export { generateMozconfig, type MozconfigVariables } from './mach-mozconfig.js';
4
4
  export { ensurePython, resetResolvedPython } from './mach-python.js';
5
5
  /**
@@ -111,6 +111,17 @@ export declare function runMachSmoke(args: string[], engineDir: string, options:
111
111
  * @returns Exit code
112
112
  */
113
113
  export declare function machPackage(engineDir: string): Promise<number>;
114
+ /**
115
+ * Creates a distribution package while streaming output to the terminal
116
+ * and capturing the stderr tail for post-run diagnostics. Callers that
117
+ * want to consult {@link explainMachError} on failure should use this
118
+ * variant; the inherit-only `machPackage` above remains for callers that
119
+ * just need an exit code.
120
+ *
121
+ * @param engineDir - Path to the engine directory
122
+ * @returns Captured mach result (stdout tail, stderr tail, exit code)
123
+ */
124
+ export declare function machPackageCapture(engineDir: string): Promise<MachCommandResult>;
114
125
  /**
115
126
  * Runs mach watch for auto-rebuilding.
116
127
  * @param engineDir - Path to the engine directory
@@ -7,7 +7,7 @@ import { exec, execInherit, execInheritCapture, execSmokeRun, execStream, } from
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 { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, } from './mach-build-artifacts.js';
10
+ export { attemptMozinfoRewrite, buildArtifactMismatchMessage, hasBuildArtifacts, hasRunnableBundle, } from './mach-build-artifacts.js';
11
11
  export { generateMozconfig } from './mach-mozconfig.js';
12
12
  export { ensurePython, resetResolvedPython } from './mach-python.js';
13
13
  /**
@@ -197,6 +197,19 @@ export async function runMachSmoke(args, engineDir, options) {
197
197
  export async function machPackage(engineDir) {
198
198
  return runMach(['package'], engineDir, { inherit: true });
199
199
  }
200
+ /**
201
+ * Creates a distribution package while streaming output to the terminal
202
+ * and capturing the stderr tail for post-run diagnostics. Callers that
203
+ * want to consult {@link explainMachError} on failure should use this
204
+ * variant; the inherit-only `machPackage` above remains for callers that
205
+ * just need an exit code.
206
+ *
207
+ * @param engineDir - Path to the engine directory
208
+ * @returns Captured mach result (stdout tail, stderr tail, exit code)
209
+ */
210
+ export async function machPackageCapture(engineDir) {
211
+ return runMachCapture(['package'], engineDir);
212
+ }
200
213
  /**
201
214
  * Runs mach watch for auto-rebuilding.
202
215
  * @param engineDir - Path to the engine directory
@@ -16,7 +16,15 @@ export function getRules(binaryName) {
16
16
  extractArgs: (m) => [m[1] ?? ''],
17
17
  },
18
18
  {
19
- pattern: /^browser\/base\/content\/(.+\.(?:js|mjs|xhtml|css))$/,
19
+ // `.inc.xhtml` fragments under browser/base/content/ are deliberately
20
+ // excluded: they are consumed via `#include` from a registered chrome
21
+ // document (typically browser.xhtml) and do not get their own
22
+ // packaged chrome URI. Before this carve-out, `status` flagged every
23
+ // wired fragment as "potentially unregistered" and `register --dry-run`
24
+ // proposed a bogus jar.mn entry. The lookahead blocks the match so
25
+ // `getUnregistrableAdvice` gets a chance to emit the correct
26
+ // guidance for the `.inc.xhtml` case.
27
+ pattern: /^browser\/base\/content\/(?!.+\.inc\.xhtml$)(.+\.(?:js|mjs|xhtml|css))$/,
20
28
  isRegistered: (engineDir, fileName) => isBrowserContentRegistered(engineDir, fileName),
21
29
  register: (engineDir, after, dryRun, fileName) => registerBrowserContent(engineDir, fileName, after, undefined, dryRun),
22
30
  extractArgs: (m) => [m[1] ?? ''],
@@ -98,6 +106,19 @@ function getUnregistrableAdvice(filePath) {
98
106
  if (filePath.endsWith('.ftl')) {
99
107
  return "FTL locale files are auto-discovered via jar.mn glob patterns and don't need manual registration.";
100
108
  }
109
+ // `.inc.xhtml` fragments live under browser/base/content/ but are
110
+ // consumed via `#include` from a registered chrome document (browser.xhtml
111
+ // by default; a fork's custom top-level doc when `wire --dom-target` is
112
+ // set). The preprocessor resolves the include at packaging time, so the
113
+ // fragment never needs its own chrome URI entry in jar.mn. Give the
114
+ // operator the actionable `wire` path instead of letting the generic
115
+ // "unknown file pattern" message above fire.
116
+ if (/^browser\/base\/content\/.+\.inc\.xhtml$/.test(filePath)) {
117
+ return ('`.inc.xhtml` fragments are consumed via `#include` from a registered chrome document ' +
118
+ '(e.g. browser.xhtml). They do not need an independent jar.mn entry — run ' +
119
+ '"fireforge wire <name> --dom <path>" to insert the #include, or add the directive manually ' +
120
+ 'in the top-level chrome document.');
121
+ }
101
122
  const testMatch = filePath.match(/^browser\/base\/content\/test\/([^/]+)\/(?!browser\.toml$).+$/);
102
123
  if (testMatch) {
103
124
  const dir = testMatch[1];
@@ -78,6 +78,18 @@ export declare function writeFileAtomic(path: string, content: string | Buffer):
78
78
  * @param dest - Destination directory path
79
79
  */
80
80
  export declare function copyDir(src: string, dest: string): Promise<void>;
81
+ /**
82
+ * Matches the atomic-temp-file shape emitted by `createAtomicTempPath`
83
+ * anywhere in a normalised (forward-slash) path. The `.fireforge-tmp-`
84
+ * marker plus a PID/UUID tail is unique to our own rename-based atomic
85
+ * writes, so callers (notably `status`) can filter these mid-flight
86
+ * artefacts out of their listings without racing the rename.
87
+ *
88
+ * Intentionally anchored so a legitimately-named backup file like
89
+ * `.notes.fireforge-tmp-backup` (no PID+UUID continuation) is NOT treated
90
+ * as one of our temps. The full shape is `.<filename>.fireforge-tmp-<pid>-<uuid>`.
91
+ */
92
+ export declare const FIREFORGE_TMP_PATH_PATTERN: RegExp;
81
93
  /**
82
94
  * Checks available disk space at a path and warns via the provided
83
95
  * callback when it falls below `minBytes`.
@@ -180,6 +180,18 @@ export async function copyDir(src, dest) {
180
180
  }
181
181
  }
182
182
  }
183
+ /**
184
+ * Matches the atomic-temp-file shape emitted by `createAtomicTempPath`
185
+ * anywhere in a normalised (forward-slash) path. The `.fireforge-tmp-`
186
+ * marker plus a PID/UUID tail is unique to our own rename-based atomic
187
+ * writes, so callers (notably `status`) can filter these mid-flight
188
+ * artefacts out of their listings without racing the rename.
189
+ *
190
+ * Intentionally anchored so a legitimately-named backup file like
191
+ * `.notes.fireforge-tmp-backup` (no PID+UUID continuation) is NOT treated
192
+ * as one of our temps. The full shape is `.<filename>.fireforge-tmp-<pid>-<uuid>`.
193
+ */
194
+ export const FIREFORGE_TMP_PATH_PATTERN = /(^|\/)\.[^/]+\.fireforge-tmp-\d+-[0-9a-f-]{36}$/i;
183
195
  /**
184
196
  * Generates a unique temp file path for atomic writes.
185
197
  *
@@ -1,5 +1,24 @@
1
1
  /** Converts Windows path separators to forward slashes for stable comparisons. */
2
2
  export declare function normalizePathSlashes(path: string): string;
3
+ /**
4
+ * Strips a leading `engine/` (or `engine\\`) segment from a user-supplied
5
+ * path so the same command invocation accepts both repo-root-relative paths
6
+ * (`engine/browser/base/content/foo.js`) and engine-relative paths
7
+ * (`browser/base/content/foo.js`). The match is case-insensitive because
8
+ * default macOS and Windows filesystems treat `Engine/` and `engine/` as
9
+ * the same directory; a literal lowercase-only check previously left `mach`
10
+ * / the manifest writers resolving against a wrongly-cased prefix. Leading
11
+ * whitespace is ignored so tab-completed inputs don't slip past the strip.
12
+ *
13
+ * The return value is trimmed of the same leading whitespace when the
14
+ * prefix matched, and otherwise passed through verbatim — callers that
15
+ * care about internal whitespace can trim on their side.
16
+ *
17
+ * @param filePath Path as provided by the user
18
+ * @returns Path relative to the engine directory (or the original when the
19
+ * prefix was absent)
20
+ */
21
+ export declare function stripEnginePrefix(filePath: string): string;
3
22
  /** Checks whether a path is explicitly absolute on either POSIX or Windows. */
4
23
  export declare function isExplicitAbsolutePath(path: string): boolean;
5
24
  /** Resolves a candidate path and returns whether it stays within the given root. */
@@ -2,10 +2,43 @@
2
2
  import { isAbsolute, relative, resolve } from 'node:path';
3
3
  const WINDOWS_ABSOLUTE_PATH = /^[a-zA-Z]:[\\/]/;
4
4
  const RELATIVE_PATH_ROOT = resolve('/__fireforge_path_root__');
5
+ /**
6
+ * Matches a leading `engine/` or `engine\\` segment (case-insensitive,
7
+ * tolerates leading whitespace). Shared between `register`, `test`, `lint`,
8
+ * and `export` so every command that takes an engine-relative path accepts
9
+ * both the repo-root form (`engine/browser/...`) and the engine-relative
10
+ * form (`browser/...`) without diverging.
11
+ */
12
+ const ENGINE_PREFIX_PATTERN = /^\s*engine[/\\]/i;
5
13
  /** Converts Windows path separators to forward slashes for stable comparisons. */
6
14
  export function normalizePathSlashes(path) {
7
15
  return path.replace(/\\/g, '/');
8
16
  }
17
+ /**
18
+ * Strips a leading `engine/` (or `engine\\`) segment from a user-supplied
19
+ * path so the same command invocation accepts both repo-root-relative paths
20
+ * (`engine/browser/base/content/foo.js`) and engine-relative paths
21
+ * (`browser/base/content/foo.js`). The match is case-insensitive because
22
+ * default macOS and Windows filesystems treat `Engine/` and `engine/` as
23
+ * the same directory; a literal lowercase-only check previously left `mach`
24
+ * / the manifest writers resolving against a wrongly-cased prefix. Leading
25
+ * whitespace is ignored so tab-completed inputs don't slip past the strip.
26
+ *
27
+ * The return value is trimmed of the same leading whitespace when the
28
+ * prefix matched, and otherwise passed through verbatim — callers that
29
+ * care about internal whitespace can trim on their side.
30
+ *
31
+ * @param filePath Path as provided by the user
32
+ * @returns Path relative to the engine directory (or the original when the
33
+ * prefix was absent)
34
+ */
35
+ export function stripEnginePrefix(filePath) {
36
+ const match = ENGINE_PREFIX_PATTERN.exec(filePath);
37
+ if (match) {
38
+ return filePath.slice(match[0].length);
39
+ }
40
+ return filePath;
41
+ }
9
42
  /** Checks whether a path is explicitly absolute on either POSIX or Windows. */
10
43
  export function isExplicitAbsolutePath(path) {
11
44
  return isAbsolute(path) || WINDOWS_ABSOLUTE_PATH.test(path);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.15.9",
3
+ "version": "0.16.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",