@hominis/fireforge 0.33.0 → 0.34.1

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 (55) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/src/commands/build.js +9 -1
  3. package/dist/src/commands/doctor-furnace-jar.d.ts +16 -0
  4. package/dist/src/commands/doctor-furnace-jar.js +47 -0
  5. package/dist/src/commands/doctor-furnace.js +2 -0
  6. package/dist/src/commands/export-flow.js +4 -1
  7. package/dist/src/commands/export-placement-gate.js +4 -1
  8. package/dist/src/commands/export.js +61 -4
  9. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +20 -0
  10. package/dist/src/commands/furnace/chrome-doc-templates.js +52 -0
  11. package/dist/src/commands/furnace/chrome-doc.d.ts +10 -0
  12. package/dist/src/commands/furnace/chrome-doc.js +31 -4
  13. package/dist/src/commands/furnace/create-browser-test.d.ts +30 -0
  14. package/dist/src/commands/furnace/create-browser-test.js +180 -0
  15. package/dist/src/commands/furnace/create-mochikit.js +11 -4
  16. package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
  17. package/dist/src/commands/furnace/create-xpcshell.js +38 -12
  18. package/dist/src/commands/furnace/create.js +7 -114
  19. package/dist/src/commands/furnace/index.js +6 -1
  20. package/dist/src/commands/furnace/scan.d.ts +1 -0
  21. package/dist/src/commands/furnace/scan.js +57 -27
  22. package/dist/src/commands/furnace/validate.js +17 -1
  23. package/dist/src/commands/re-export-options.d.ts +26 -0
  24. package/dist/src/commands/re-export-options.js +48 -1
  25. package/dist/src/commands/re-export.js +4 -1
  26. package/dist/src/commands/register.js +10 -1
  27. package/dist/src/commands/test-diagnose.js +12 -0
  28. package/dist/src/commands/test.js +29 -22
  29. package/dist/src/core/furnace-registration.d.ts +24 -0
  30. package/dist/src/core/furnace-registration.js +56 -3
  31. package/dist/src/core/furnace-validate-registration.js +18 -0
  32. package/dist/src/core/mach-resource-shim.d.ts +70 -16
  33. package/dist/src/core/mach-resource-shim.js +310 -52
  34. package/dist/src/core/mach.d.ts +51 -10
  35. package/dist/src/core/mach.js +76 -32
  36. package/dist/src/core/manifest-helpers.d.ts +13 -0
  37. package/dist/src/core/manifest-helpers.js +1 -1
  38. package/dist/src/core/manifest-rules.d.ts +13 -3
  39. package/dist/src/core/manifest-rules.js +44 -17
  40. package/dist/src/core/patch-export.d.ts +10 -0
  41. package/dist/src/core/patch-export.js +23 -2
  42. package/dist/src/core/register-module.d.ts +1 -1
  43. package/dist/src/core/register-module.js +15 -2
  44. package/dist/src/core/register-result.d.ts +9 -0
  45. package/dist/src/core/register-scaffold.d.ts +55 -0
  46. package/dist/src/core/register-scaffold.js +146 -0
  47. package/dist/src/core/register-test-manifest.js +3 -1
  48. package/dist/src/core/register-xpcshell-test.d.ts +30 -0
  49. package/dist/src/core/register-xpcshell-test.js +96 -0
  50. package/dist/src/core/test-harness-crash.d.ts +26 -1
  51. package/dist/src/core/test-harness-crash.js +149 -11
  52. package/dist/src/core/xpcshell-appdir.d.ts +7 -0
  53. package/dist/src/core/xpcshell-appdir.js +12 -3
  54. package/dist/src/types/commands/options.d.ts +12 -0
  55. package/package.json +3 -3
@@ -10,7 +10,7 @@
10
10
  * Ties on the lower-cased key fall back to byte order so the comparison is
11
11
  * total and stable.
12
12
  */
13
- function mozbuildSortCompare(a, b) {
13
+ export function mozbuildSortCompare(a, b) {
14
14
  const la = a.toLowerCase();
15
15
  const lb = b.toLowerCase();
16
16
  if (la < lb)
@@ -10,8 +10,14 @@ export interface PatternRule {
10
10
  /** Extract arguments from the regex match */
11
11
  extractArgs: (match: RegExpMatchArray) => string[];
12
12
  }
13
- /** Returns manifest registration rules for the supported engine file patterns. */
14
- export declare function getRules(binaryName: string): PatternRule[];
13
+ /**
14
+ * Returns manifest registration rules for the supported engine file
15
+ * patterns. `createManifest` (from `register --create-manifest`) lets the
16
+ * module and xpcshell-test rules scaffold a missing manifest (directory
17
+ * moz.build / xpcshell.toml) and wire the parent chain instead of failing
18
+ * with "Manifest not found".
19
+ */
20
+ export declare function getRules(binaryName: string, createManifest?: boolean): PatternRule[];
15
21
  /**
16
22
  * Checks if a file path matches any known registrable pattern.
17
23
  * @param filePath - Path relative to engine/
@@ -34,6 +40,10 @@ export declare function isFileRegistered(root: string, filePath: string): Promis
34
40
  * @param filePath - Path relative to engine/
35
41
  * @param dryRun - If true, return what would be done without writing
36
42
  * @param after - Optional substring to place entry after (instead of alphabetical)
43
+ * @param options - `createManifest` scaffolds a missing manifest (directory
44
+ * moz.build / xpcshell.toml) and wires the parent chain instead of failing
37
45
  * @returns Registration result
38
46
  */
39
- export declare function registerFile(root: string, filePath: string, dryRun?: boolean, after?: string): Promise<RegisterResult>;
47
+ export declare function registerFile(root: string, filePath: string, dryRun?: boolean, after?: string, options?: {
48
+ createManifest?: boolean;
49
+ }): Promise<RegisterResult>;
@@ -5,8 +5,15 @@ import { pathExists, readText } from '../utils/fs.js';
5
5
  import { escapeRegex } from '../utils/regex.js';
6
6
  import { getProjectPaths, loadConfig } from './config.js';
7
7
  import { registerBrowserContent, registerFireForgeModule, registerSharedCSS, registerTestManifest, registerToolkitWidget, } from './manifest-register.js';
8
- /** Returns manifest registration rules for the supported engine file patterns. */
9
- export function getRules(binaryName) {
8
+ import { isXpcshellTestRegistered, registerXpcshellTest } from './register-xpcshell-test.js';
9
+ /**
10
+ * Returns manifest registration rules for the supported engine file
11
+ * patterns. `createManifest` (from `register --create-manifest`) lets the
12
+ * module and xpcshell-test rules scaffold a missing manifest (directory
13
+ * moz.build / xpcshell.toml) and wire the parent chain instead of failing
14
+ * with "Manifest not found".
15
+ */
16
+ export function getRules(binaryName, createManifest = false) {
10
17
  const moduleDir = `browser/modules/${binaryName}`;
11
18
  return [
12
19
  {
@@ -39,7 +46,10 @@ export function getRules(binaryName) {
39
46
  extractArgs: (m) => [m[1] ?? ''],
40
47
  },
41
48
  {
42
- pattern: /^browser\/base\/content\/test\/([^/]+)\/browser\.toml$/,
49
+ // Fork-owned browser.toml manifests at ARBITRARY depth under
50
+ // browser/base/content/test/ (0.34.0 field report: only one level
51
+ // was supported, rejecting nested browser-chrome manifests).
52
+ pattern: /^browser\/base\/content\/test\/((?:[^/]+\/)*[^/]+)\/browser\.toml$/,
43
53
  isRegistered: (_engineDir, testDir) => isTestManifestRegistered(_engineDir, testDir),
44
54
  register: (_engineDir, _after, dryRun, testDir) => registerTestManifest(_engineDir, testDir, dryRun),
45
55
  extractArgs: (m) => [m[1] ?? ''],
@@ -47,7 +57,7 @@ export function getRules(binaryName) {
47
57
  {
48
58
  pattern: new RegExp(`^browser/modules/${escapeRegex(binaryName)}/(.+\\.sys\\.mjs)$`),
49
59
  isRegistered: (_engineDir, fileName) => isFireForgeModuleRegistered(_engineDir, fileName, moduleDir),
50
- register: (_engineDir, _after, dryRun, fileName) => registerFireForgeModule(_engineDir, fileName, moduleDir, dryRun),
60
+ register: (_engineDir, _after, dryRun, fileName) => registerFireForgeModule(_engineDir, fileName, moduleDir, dryRun, createManifest),
51
61
  extractArgs: (m) => [m[1] ?? ''],
52
62
  },
53
63
  {
@@ -56,6 +66,20 @@ export function getRules(binaryName) {
56
66
  register: (_engineDir, _after, dryRun, tagName, fileName) => registerToolkitWidget(_engineDir, tagName, fileName, dryRun),
57
67
  extractArgs: (m) => [m[1] ?? '', m[2] ?? ''],
58
68
  },
69
+ {
70
+ // xpcshell test files (0.34.0 field report: rejected as "Unknown
71
+ // file pattern"). Any `test_*.js` outside browser/base/content/test/
72
+ // (which stays browser-chrome territory with its browser.toml
73
+ // guidance) registers into the directory's xpcshell.toml; with
74
+ // --create-manifest a missing manifest is scaffolded and wired via
75
+ // XPCSHELL_TESTS_MANIFESTS.
76
+ pattern: /^(?!browser\/base\/content\/test\/)(.+)\/(test_[^/]+\.js)$/,
77
+ isRegistered: (engineDir, dirRel, fileName) => isXpcshellTestRegistered(engineDir, dirRel, fileName),
78
+ register: (engineDir, _after, dryRun, dirRel, fileName) => registerXpcshellTest(engineDir, dirRel, fileName, dryRun, createManifest),
79
+ // Both groups are guaranteed by the pattern; slice avoids dead
80
+ // fallback branches.
81
+ extractArgs: (m) => m.slice(1, 3),
82
+ },
59
83
  ];
60
84
  }
61
85
  async function isSharedCSSRegistered(engineDir, fileName) {
@@ -149,7 +173,8 @@ function getUnregistrableAdvice(filePath) {
149
173
  return (`xpcshell.toml manifests are not auto-registered by "fireforge register". ` +
150
174
  `Add "XPCSHELL_TESTS_MANIFESTS += ['${basename(filePath)}']" to the ` +
151
175
  `moz.build that covers ${filePath.replace(/\/xpcshell\.toml$/, '/')}, ` +
152
- 'or let `furnace create --test-style xpcshell` print the warning that names the canonical wiring location. ' +
176
+ 'or register one of its test files with "fireforge register <path>/test_*.js --create-manifest", ' +
177
+ 'which scaffolds the manifest and wires XPCSHELL_TESTS_MANIFESTS automatically. ' +
153
178
  'Browser-chrome tests (browser.toml) are auto-registered via "fireforge register <path>/browser.toml".');
154
179
  }
155
180
  const testMatch = filePath.match(/^browser\/base\/content\/test\/([^/]+)\/(?!browser\.toml$).+$/);
@@ -183,12 +208,17 @@ export async function isFileRegistered(root, filePath) {
183
208
  if (advice) {
184
209
  throw new InvalidArgumentError(advice, 'path');
185
210
  }
186
- throw new InvalidArgumentError(`Unknown file pattern: "${normalizedPath}". Supported patterns:\n` +
211
+ throw new InvalidArgumentError(buildUnknownPatternMessage(normalizedPath, config.binaryName), 'path');
212
+ }
213
+ /** Shared "Unknown file pattern" message naming every supported pattern. */
214
+ function buildUnknownPatternMessage(normalizedPath, binaryName) {
215
+ return (`Unknown file pattern: "${normalizedPath}". Supported patterns:\n` +
187
216
  ' browser/themes/shared/*.css\n' +
188
217
  ' browser/base/content/*.{js,mjs,xhtml,css}\n' +
189
- ' browser/base/content/test/*/browser.toml\n' +
190
- ` browser/modules/${config.binaryName}/*.sys.mjs\n` +
191
- ' toolkit/content/widgets/*/*.{mjs,css}', 'path');
218
+ ' browser/base/content/test/**/browser.toml\n' +
219
+ ` browser/modules/${binaryName}/*.sys.mjs\n` +
220
+ ' toolkit/content/widgets/*/*.{mjs,css}\n' +
221
+ ' **/test_*.js (xpcshell test files; registers into the nearest xpcshell.toml)');
192
222
  }
193
223
  /**
194
224
  * Registers a file in the appropriate build manifest.
@@ -197,12 +227,14 @@ export async function isFileRegistered(root, filePath) {
197
227
  * @param filePath - Path relative to engine/
198
228
  * @param dryRun - If true, return what would be done without writing
199
229
  * @param after - Optional substring to place entry after (instead of alphabetical)
230
+ * @param options - `createManifest` scaffolds a missing manifest (directory
231
+ * moz.build / xpcshell.toml) and wires the parent chain instead of failing
200
232
  * @returns Registration result
201
233
  */
202
- export async function registerFile(root, filePath, dryRun = false, after) {
234
+ export async function registerFile(root, filePath, dryRun = false, after, options = {}) {
203
235
  const { engine: engineDir } = getProjectPaths(root);
204
236
  const config = await loadConfig(root);
205
- const rules = getRules(config.binaryName);
237
+ const rules = getRules(config.binaryName, options.createManifest === true);
206
238
  // Normalize path separators
207
239
  const normalizedPath = filePath.replace(/\\/g, '/');
208
240
  for (const rule of rules) {
@@ -216,11 +248,6 @@ export async function registerFile(root, filePath, dryRun = false, after) {
216
248
  if (advice) {
217
249
  throw new InvalidArgumentError(advice, 'path');
218
250
  }
219
- throw new InvalidArgumentError(`Unknown file pattern: "${normalizedPath}". Supported patterns:\n` +
220
- ' browser/themes/shared/*.css\n' +
221
- ' browser/base/content/*.{js,mjs,xhtml,css}\n' +
222
- ' browser/base/content/test/*/browser.toml\n' +
223
- ` browser/modules/${config.binaryName}/*.sys.mjs\n` +
224
- ' toolkit/content/widgets/*/*.{mjs,css}', 'path');
251
+ throw new InvalidArgumentError(buildUnknownPatternMessage(normalizedPath, config.binaryName), 'path');
225
252
  }
226
253
  //# sourceMappingURL=manifest-rules.js.map
@@ -20,6 +20,16 @@ export declare function getNextPatchNumber(patchesDir: string): Promise<string>;
20
20
  * to a slug `export` could never reach.
21
21
  */
22
22
  export declare function sanitizeName(name: string): string;
23
+ /**
24
+ * Strips a leading `NNN-<category>-` prefix from a sanitized name slug.
25
+ * Operators frequently pass the DESIRED FILENAME stem to `--name`
26
+ * (`--name 203-ui-foo --category ui`), which 0.33.0 double-prefixed into
27
+ * `203-ui-203-ui-foo.patch`. The filename builders prepend the order and
28
+ * category themselves, so a matching prefix in the name is always
29
+ * redundant. Applied repeatedly so a twice-prefixed slug (from a previous
30
+ * double-prefix incident) also collapses. Exported for direct testing.
31
+ */
32
+ export declare function stripRedundantCategoryPrefix(sanitizedName: string, category: string): string;
23
33
  /**
24
34
  * Generates the next patch filename with category.
25
35
  * @param patchesDir - Path to the patches directory
@@ -46,6 +46,27 @@ export function sanitizeName(name) {
46
46
  .replace(/^-+|-+$/g, '')
47
47
  .slice(0, 50);
48
48
  }
49
+ /**
50
+ * Strips a leading `NNN-<category>-` prefix from a sanitized name slug.
51
+ * Operators frequently pass the DESIRED FILENAME stem to `--name`
52
+ * (`--name 203-ui-foo --category ui`), which 0.33.0 double-prefixed into
53
+ * `203-ui-203-ui-foo.patch`. The filename builders prepend the order and
54
+ * category themselves, so a matching prefix in the name is always
55
+ * redundant. Applied repeatedly so a twice-prefixed slug (from a previous
56
+ * double-prefix incident) also collapses. Exported for direct testing.
57
+ */
58
+ export function stripRedundantCategoryPrefix(sanitizedName, category) {
59
+ const prefix = new RegExp(`^\\d+-${category}-`);
60
+ let stripped = sanitizedName;
61
+ while (prefix.test(stripped)) {
62
+ stripped = stripped.replace(prefix, '');
63
+ }
64
+ return stripped.length > 0 ? stripped : sanitizedName;
65
+ }
66
+ /** Sanitizes a patch name and drops a redundant `NNN-<category>-` prefix. */
67
+ function patchNameSlug(name, category) {
68
+ return stripRedundantCategoryPrefix(sanitizeName(name), category);
69
+ }
49
70
  /**
50
71
  * Generates the next patch filename with category.
51
72
  * @param patchesDir - Path to the patches directory
@@ -55,7 +76,7 @@ export function sanitizeName(name) {
55
76
  */
56
77
  export async function getNextPatchFilename(patchesDir, category, name) {
57
78
  const patchNumber = await getNextPatchNumber(patchesDir);
58
- const sanitizedName = sanitizeName(name);
79
+ const sanitizedName = patchNameSlug(name, category);
59
80
  return `${patchNumber}-${category}-${sanitizedName}.patch`;
60
81
  }
61
82
  /**
@@ -253,7 +274,7 @@ async function computeExportPlanUnderLock(input) {
253
274
  ? allocatePolicyOrder(input.config, manifestBefore?.patches ?? [], input.category)
254
275
  : null;
255
276
  const patchFilename = policyOrder !== null
256
- ? `${String(policyOrder).padStart(3, '0')}-${input.category}-${sanitizeName(input.name)}.patch`
277
+ ? `${String(policyOrder).padStart(3, '0')}-${input.category}-${patchNameSlug(input.name, input.category)}.patch`
257
278
  : await getNextPatchFilename(input.patchesDir, input.category, input.name);
258
279
  const patchPath = join(input.patchesDir, patchFilename);
259
280
  const metadata = {
@@ -8,4 +8,4 @@ import type { RegisterResult } from './register-result.js';
8
8
  * Entry format:
9
9
  * "{name}.sys.mjs",
10
10
  */
11
- export declare function registerFireForgeModule(engineDir: string, fileName: string, moduleDir: string, dryRun?: boolean): Promise<RegisterResult>;
11
+ export declare function registerFireForgeModule(engineDir: string, fileName: string, moduleDir: string, dryRun?: boolean, createManifest?: boolean): Promise<RegisterResult>;
@@ -8,6 +8,7 @@ import { pathExists, readText, writeText } from '../utils/fs.js';
8
8
  import { findAlphabeticalMozBuildPosition, findAlphabeticalPosition } from './manifest-helpers.js';
9
9
  import { tokenizeMozBuildList } from './manifest-tokenizers.js';
10
10
  import { withParserFallback } from './parser-fallback.js';
11
+ import { scaffoldModuleMozBuild } from './register-scaffold.js';
11
12
  /**
12
13
  * Tokenizer-based implementation for module registration.
13
14
  */
@@ -55,11 +56,23 @@ function legacyRegisterFireForgeModule(content, fileName, entry, moduleDir) {
55
56
  * Entry format:
56
57
  * "{name}.sys.mjs",
57
58
  */
58
- export async function registerFireForgeModule(engineDir, fileName, moduleDir, dryRun = false) {
59
+ export async function registerFireForgeModule(engineDir, fileName, moduleDir, dryRun = false, createManifest = false) {
59
60
  const manifest = `${moduleDir}/moz.build`;
60
61
  const manifestPath = join(engineDir, manifest);
61
62
  if (!(await pathExists(manifestPath))) {
62
- throw new GeneralError(`Manifest not found: ${manifest}`);
63
+ if (createManifest) {
64
+ // 0.34.0 field report: registering a module under a directory with
65
+ // no moz.build failed with "Manifest not found". Scaffold the
66
+ // directory manifest and wire the parent DIRS chain.
67
+ const scaffoldActions = await scaffoldModuleMozBuild(engineDir, moduleDir, fileName, dryRun);
68
+ return {
69
+ manifest,
70
+ entry: ` "${fileName}",`,
71
+ skipped: false,
72
+ scaffoldActions,
73
+ };
74
+ }
75
+ throw new GeneralError(`Manifest not found: ${manifest}. Pass --create-manifest to scaffold it and wire the parent DIRS entry.`);
63
76
  }
64
77
  const entry = ` "${fileName}",`.replace(/\\/g, '/');
65
78
  const content = await readText(manifestPath);
@@ -18,4 +18,13 @@ export interface RegisterResult {
18
18
  skipped: boolean;
19
19
  /** Whether --after target was not found and fell back to alphabetical */
20
20
  afterFallback?: boolean | undefined;
21
+ /**
22
+ * Manifests created or wired by `--create-manifest` scaffolding
23
+ * (directory moz.build, parent DIRS chain, XPCSHELL_TESTS_MANIFESTS),
24
+ * for user display.
25
+ */
26
+ scaffoldActions?: {
27
+ manifest: string;
28
+ change: string;
29
+ }[] | undefined;
21
30
  }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Manifest scaffolding for `fireforge register --create-manifest`
3
+ * (0.34.0 field report): registering a module under a directory with no
4
+ * moz.build used to fail with "Manifest not found" because `register`
5
+ * only inserts into EXISTING manifests. This module owns the two
6
+ * scaffolding primitives:
7
+ *
8
+ * - creating a directory `moz.build` (license header + the requested
9
+ * list directive) and wiring the parent chain's `DIRS` entries up to
10
+ * the nearest existing moz.build;
11
+ * - wiring an `XPCSHELL_TESTS_MANIFESTS` entry for a (possibly freshly
12
+ * created) `xpcshell.toml` into the nearest existing moz.build.
13
+ *
14
+ * All writers reuse the tokenizer + alphabetical-position helpers the
15
+ * existing register writers use, so inserted entries respect mozbuild's
16
+ * case-insensitive StrictOrderingOnAppendList rule.
17
+ */
18
+ /** One manifest mutation performed (or planned, on dry-run) by a scaffold. */
19
+ export interface ScaffoldAction {
20
+ /** Engine-relative manifest path that was created or modified. */
21
+ manifest: string;
22
+ /** Human-readable description of the change. */
23
+ change: string;
24
+ }
25
+ /**
26
+ * Inserts `value` into the named moz.build list directive, creating the
27
+ * `NAME += [ ... ]` block at the end of the file when the directive does
28
+ * not exist yet. Returns the updated content, or null when the value is
29
+ * already present.
30
+ */
31
+ export declare function upsertMozBuildListEntry(content: string, directive: string, value: string): string | null;
32
+ /**
33
+ * Ensures every parent directory between `childRelDir` and the nearest
34
+ * existing moz.build carries a `DIRS` entry pointing at the next segment
35
+ * down, creating intermediate moz.build files as needed. Walks upward
36
+ * from the child's parent; throws when no moz.build exists anywhere up
37
+ * to the engine root (an engine checkout always has one at the root, so
38
+ * this indicates a bogus path).
39
+ */
40
+ export declare function ensureParentDirsWiring(engineDir: string, childRelDir: string, dryRun: boolean): Promise<ScaffoldAction[]>;
41
+ /**
42
+ * Creates `<moduleDir>/moz.build` with an `EXTRA_JS_MODULES.<namespace>`
43
+ * list containing `fileName`, and wires the parent DIRS chain. The
44
+ * namespace is the module directory's basename (the fork's binary name
45
+ * for `browser/modules/<binaryName>/`), matching Firefox's convention of
46
+ * mapping `EXTRA_JS_MODULES.<ns>` into `resource:///modules/<ns>/`.
47
+ */
48
+ export declare function scaffoldModuleMozBuild(engineDir: string, moduleDir: string, fileName: string, dryRun: boolean): Promise<ScaffoldAction[]>;
49
+ /**
50
+ * Wires an `XPCSHELL_TESTS_MANIFESTS` entry for `manifestRelPath` (an
51
+ * engine-relative `.../xpcshell.toml`) into the nearest EXISTING
52
+ * moz.build above it. Returns the performed actions (empty when the
53
+ * entry already exists).
54
+ */
55
+ export declare function ensureXpcshellManifestWiring(engineDir: string, manifestRelPath: string, dryRun: boolean): Promise<ScaffoldAction[]>;
@@ -0,0 +1,146 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Manifest scaffolding for `fireforge register --create-manifest`
4
+ * (0.34.0 field report): registering a module under a directory with no
5
+ * moz.build used to fail with "Manifest not found" because `register`
6
+ * only inserts into EXISTING manifests. This module owns the two
7
+ * scaffolding primitives:
8
+ *
9
+ * - creating a directory `moz.build` (license header + the requested
10
+ * list directive) and wiring the parent chain's `DIRS` entries up to
11
+ * the nearest existing moz.build;
12
+ * - wiring an `XPCSHELL_TESTS_MANIFESTS` entry for a (possibly freshly
13
+ * created) `xpcshell.toml` into the nearest existing moz.build.
14
+ *
15
+ * All writers reuse the tokenizer + alphabetical-position helpers the
16
+ * existing register writers use, so inserted entries respect mozbuild's
17
+ * case-insensitive StrictOrderingOnAppendList rule.
18
+ */
19
+ import { dirname, join } from 'node:path';
20
+ import { GeneralError } from '../errors/base.js';
21
+ import { pathExists, readText, writeText } from '../utils/fs.js';
22
+ import { getLicenseHeader } from './license-headers.js';
23
+ import { findAlphabeticalMozBuildPosition } from './manifest-helpers.js';
24
+ import { tokenizeMozBuildList } from './manifest-tokenizers.js';
25
+ /**
26
+ * moz.build files use Firefox's canonical MPL-2.0 hash-comment header
27
+ * regardless of the fork's own project license — they live in the engine
28
+ * tree among upstream MPL files.
29
+ */
30
+ function mozBuildHeader() {
31
+ return getLicenseHeader('MPL-2.0', 'hash');
32
+ }
33
+ /**
34
+ * Inserts `value` into the named moz.build list directive, creating the
35
+ * `NAME += [ ... ]` block at the end of the file when the directive does
36
+ * not exist yet. Returns the updated content, or null when the value is
37
+ * already present.
38
+ */
39
+ export function upsertMozBuildListEntry(content, directive, value) {
40
+ if (content.includes(`"${value}"`))
41
+ return null;
42
+ const lines = content.split('\n');
43
+ const listResult = tokenizeMozBuildList(lines, new RegExp(`^${directive}\\b`));
44
+ if (listResult) {
45
+ const { insertIndex } = findAlphabeticalMozBuildPosition(listResult.tokens, value);
46
+ lines.splice(insertIndex, 0, ` "${value}",`);
47
+ return lines.join('\n');
48
+ }
49
+ // No existing list: append a fresh block, keeping exactly one blank
50
+ // line between the previous content and the new directive.
51
+ while (lines.length > 0 && (lines.at(-1) ?? '').trim() === '')
52
+ lines.pop();
53
+ const block = [`${directive} += [`, ` "${value}",`, `]`];
54
+ return [...lines, ...(lines.length > 0 ? [''] : []), ...block, ''].join('\n');
55
+ }
56
+ /**
57
+ * Ensures every parent directory between `childRelDir` and the nearest
58
+ * existing moz.build carries a `DIRS` entry pointing at the next segment
59
+ * down, creating intermediate moz.build files as needed. Walks upward
60
+ * from the child's parent; throws when no moz.build exists anywhere up
61
+ * to the engine root (an engine checkout always has one at the root, so
62
+ * this indicates a bogus path).
63
+ */
64
+ export async function ensureParentDirsWiring(engineDir, childRelDir, dryRun) {
65
+ const actions = [];
66
+ let childName = childRelDir.split('/').at(-1) ?? childRelDir;
67
+ let parentRel = dirname(childRelDir).replace(/\\/g, '/');
68
+ for (;;) {
69
+ if (parentRel === '.' || parentRel === '' || parentRel === '/') {
70
+ throw new GeneralError(`No moz.build found in any parent of ${childRelDir} up to the engine root; ` +
71
+ 'cannot wire the DIRS chain. Check the path.');
72
+ }
73
+ const parentManifestRel = `${parentRel}/moz.build`;
74
+ const parentManifestPath = join(engineDir, parentManifestRel);
75
+ if (await pathExists(parentManifestPath)) {
76
+ const content = await readText(parentManifestPath);
77
+ const updated = upsertMozBuildListEntry(content, 'DIRS', childName);
78
+ if (updated !== null) {
79
+ if (!dryRun)
80
+ await writeText(parentManifestPath, updated);
81
+ actions.push({ manifest: parentManifestRel, change: `DIRS += ["${childName}"]` });
82
+ }
83
+ return actions;
84
+ }
85
+ // Parent has no moz.build either: scaffold one carrying the DIRS
86
+ // entry and keep walking up until an existing manifest anchors the
87
+ // chain.
88
+ const scaffold = `${mozBuildHeader()}\n\nDIRS += [\n "${childName}",\n]\n`;
89
+ if (!dryRun)
90
+ await writeText(parentManifestPath, scaffold);
91
+ actions.push({ manifest: parentManifestRel, change: `created with DIRS += ["${childName}"]` });
92
+ childName = parentRel.split('/').at(-1) ?? parentRel;
93
+ parentRel = dirname(parentRel).replace(/\\/g, '/');
94
+ }
95
+ }
96
+ /**
97
+ * Creates `<moduleDir>/moz.build` with an `EXTRA_JS_MODULES.<namespace>`
98
+ * list containing `fileName`, and wires the parent DIRS chain. The
99
+ * namespace is the module directory's basename (the fork's binary name
100
+ * for `browser/modules/<binaryName>/`), matching Firefox's convention of
101
+ * mapping `EXTRA_JS_MODULES.<ns>` into `resource:///modules/<ns>/`.
102
+ */
103
+ export async function scaffoldModuleMozBuild(engineDir, moduleDir, fileName, dryRun) {
104
+ const namespace = moduleDir.split('/').at(-1) ?? moduleDir;
105
+ const manifestRel = `${moduleDir}/moz.build`;
106
+ const content = `${mozBuildHeader()}\n\nEXTRA_JS_MODULES.${namespace} += [\n "${fileName}",\n]\n`;
107
+ if (!dryRun)
108
+ await writeText(join(engineDir, manifestRel), content);
109
+ const actions = [
110
+ {
111
+ manifest: manifestRel,
112
+ change: `created with EXTRA_JS_MODULES.${namespace} += ["${fileName}"]`,
113
+ },
114
+ ];
115
+ actions.push(...(await ensureParentDirsWiring(engineDir, moduleDir, dryRun)));
116
+ return actions;
117
+ }
118
+ /**
119
+ * Wires an `XPCSHELL_TESTS_MANIFESTS` entry for `manifestRelPath` (an
120
+ * engine-relative `.../xpcshell.toml`) into the nearest EXISTING
121
+ * moz.build above it. Returns the performed actions (empty when the
122
+ * entry already exists).
123
+ */
124
+ export async function ensureXpcshellManifestWiring(engineDir, manifestRelPath, dryRun) {
125
+ let dirRel = dirname(manifestRelPath).replace(/\\/g, '/');
126
+ for (;;) {
127
+ if (dirRel === '.' || dirRel === '' || dirRel === '/') {
128
+ throw new GeneralError(`No moz.build found in any parent of ${manifestRelPath} up to the engine root; ` +
129
+ 'cannot wire XPCSHELL_TESTS_MANIFESTS. Check the path.');
130
+ }
131
+ const mozBuildRel = `${dirRel}/moz.build`;
132
+ const mozBuildPath = join(engineDir, mozBuildRel);
133
+ if (await pathExists(mozBuildPath)) {
134
+ const relEntry = manifestRelPath.slice(dirRel.length + 1);
135
+ const content = await readText(mozBuildPath);
136
+ const updated = upsertMozBuildListEntry(content, 'XPCSHELL_TESTS_MANIFESTS', relEntry);
137
+ if (updated === null)
138
+ return [];
139
+ if (!dryRun)
140
+ await writeText(mozBuildPath, updated);
141
+ return [{ manifest: mozBuildRel, change: `XPCSHELL_TESTS_MANIFESTS += ["${relEntry}"]` }];
142
+ }
143
+ dirRel = dirname(dirRel).replace(/\\/g, '/');
144
+ }
145
+ }
146
+ //# sourceMappingURL=register-scaffold.js.map
@@ -27,7 +27,9 @@ function registerTestManifestTokenized(content, testDir, entry) {
27
27
  function legacyRegisterTestManifest(content, testDir, entry) {
28
28
  const lines = content.split('\n');
29
29
  const extractKey = (line) => {
30
- const match = /"content\/test\/([^/]+)\/browser\.toml"/.exec(line);
30
+ // Nested manifests (`content/test/a/b/browser.toml`) are supported —
31
+ // capture the whole directory chain, not just one segment.
32
+ const match = /"content\/test\/(.+)\/browser\.toml"/.exec(line);
31
33
  return match?.[1];
32
34
  };
33
35
  let sectionStart = -1;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * xpcshell test-file registration (0.34.0 field report): xpcshell test
3
+ * files (e.g. `<module-dir>/test/unit/test_*.js`) were rejected by
4
+ * `fireforge register` as "Unknown file pattern". This writer inserts the
5
+ * `["test_*.js"]` section into the directory's `xpcshell.toml`
6
+ * (alphabetically, idempotently) and — with `--create-manifest` — creates
7
+ * the manifest and wires `XPCSHELL_TESTS_MANIFESTS` into the nearest
8
+ * moz.build.
9
+ */
10
+ import type { RegisterResult } from './register-result.js';
11
+ /**
12
+ * Checks whether the test file is listed in its directory's
13
+ * `xpcshell.toml`. Throws a "Manifest not found" error when the manifest
14
+ * does not exist (status catches that prefix and reports the manifest as
15
+ * missing rather than the file as unregistered).
16
+ */
17
+ export declare function isXpcshellTestRegistered(engineDir: string, dirRel: string, fileName: string): Promise<boolean>;
18
+ /**
19
+ * Inserts the `["<fileName>"]` section into the manifest content in
20
+ * mozbuild's case-insensitive sort order relative to the other test
21
+ * sections, keeping one blank line between sections. Exported for direct
22
+ * unit testing.
23
+ */
24
+ export declare function insertXpcshellManifestSection(content: string, fileName: string): string;
25
+ /**
26
+ * Registers an xpcshell test file in its directory's `xpcshell.toml`.
27
+ * With `createManifest`, a missing manifest is scaffolded and wired into
28
+ * the nearest moz.build via `XPCSHELL_TESTS_MANIFESTS`.
29
+ */
30
+ export declare function registerXpcshellTest(engineDir: string, dirRel: string, fileName: string, dryRun?: boolean, createManifest?: boolean): Promise<RegisterResult>;
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * xpcshell test-file registration (0.34.0 field report): xpcshell test
4
+ * files (e.g. `<module-dir>/test/unit/test_*.js`) were rejected by
5
+ * `fireforge register` as "Unknown file pattern". This writer inserts the
6
+ * `["test_*.js"]` section into the directory's `xpcshell.toml`
7
+ * (alphabetically, idempotently) and — with `--create-manifest` — creates
8
+ * the manifest and wires `XPCSHELL_TESTS_MANIFESTS` into the nearest
9
+ * moz.build.
10
+ */
11
+ import { join } from 'node:path';
12
+ import { GeneralError } from '../errors/base.js';
13
+ import { pathExists, readText, writeText } from '../utils/fs.js';
14
+ import { mozbuildSortCompare } from './manifest-helpers.js';
15
+ import { ensureXpcshellManifestWiring } from './register-scaffold.js';
16
+ function manifestMissingError(manifestRel) {
17
+ return new GeneralError(`Manifest not found: ${manifestRel}. ` +
18
+ 'Pass --create-manifest to scaffold the xpcshell.toml and wire XPCSHELL_TESTS_MANIFESTS ' +
19
+ 'into the nearest moz.build.');
20
+ }
21
+ /**
22
+ * Checks whether the test file is listed in its directory's
23
+ * `xpcshell.toml`. Throws a "Manifest not found" error when the manifest
24
+ * does not exist (status catches that prefix and reports the manifest as
25
+ * missing rather than the file as unregistered).
26
+ */
27
+ export async function isXpcshellTestRegistered(engineDir, dirRel, fileName) {
28
+ const manifestRel = `${dirRel}/xpcshell.toml`;
29
+ const manifestPath = join(engineDir, manifestRel);
30
+ if (!(await pathExists(manifestPath))) {
31
+ throw manifestMissingError(manifestRel);
32
+ }
33
+ const content = await readText(manifestPath);
34
+ return content.includes(`["${fileName}"]`);
35
+ }
36
+ /**
37
+ * Inserts the `["<fileName>"]` section into the manifest content in
38
+ * mozbuild's case-insensitive sort order relative to the other test
39
+ * sections, keeping one blank line between sections. Exported for direct
40
+ * unit testing.
41
+ */
42
+ export function insertXpcshellManifestSection(content, fileName) {
43
+ const lines = content.split('\n');
44
+ const sectionHeaderPattern = /^\["([^"]+)"\]\s*$/;
45
+ let insertIndex = -1;
46
+ for (let i = 0; i < lines.length; i += 1) {
47
+ const match = sectionHeaderPattern.exec(lines[i] ?? '');
48
+ if (!match)
49
+ continue;
50
+ if (insertIndex === -1 && mozbuildSortCompare(fileName, match[1] ?? '') < 0) {
51
+ insertIndex = i;
52
+ }
53
+ }
54
+ if (insertIndex !== -1) {
55
+ lines.splice(insertIndex, 0, `["${fileName}"]`, '');
56
+ return lines.join('\n');
57
+ }
58
+ // Append after the last section (or at end of file when there are no
59
+ // sections yet), keeping exactly one trailing newline.
60
+ while (lines.length > 0 && (lines.at(-1) ?? '').trim() === '')
61
+ lines.pop();
62
+ return [...lines, '', `["${fileName}"]`, ''].join('\n');
63
+ }
64
+ /**
65
+ * Registers an xpcshell test file in its directory's `xpcshell.toml`.
66
+ * With `createManifest`, a missing manifest is scaffolded and wired into
67
+ * the nearest moz.build via `XPCSHELL_TESTS_MANIFESTS`.
68
+ */
69
+ export async function registerXpcshellTest(engineDir, dirRel, fileName, dryRun = false, createManifest = false) {
70
+ const manifestRel = `${dirRel}/xpcshell.toml`;
71
+ const manifestPath = join(engineDir, manifestRel);
72
+ const entry = `["${fileName}"]`;
73
+ if (!(await pathExists(manifestPath))) {
74
+ if (!createManifest) {
75
+ throw manifestMissingError(manifestRel);
76
+ }
77
+ const scaffoldActions = [
78
+ { manifest: manifestRel, change: `created with ${entry}` },
79
+ ];
80
+ if (!dryRun) {
81
+ await writeText(manifestPath, `[DEFAULT]\n\n${entry}\n`);
82
+ }
83
+ scaffoldActions.push(...(await ensureXpcshellManifestWiring(engineDir, manifestRel, dryRun)));
84
+ return { manifest: manifestRel, entry, skipped: false, scaffoldActions };
85
+ }
86
+ const content = await readText(manifestPath);
87
+ if (content.includes(entry)) {
88
+ return { manifest: manifestRel, entry, skipped: true };
89
+ }
90
+ const updated = insertXpcshellManifestSection(content, fileName);
91
+ if (!dryRun) {
92
+ await writeText(manifestPath, updated);
93
+ }
94
+ return { manifest: manifestRel, entry, skipped: false };
95
+ }
96
+ //# sourceMappingURL=register-xpcshell-test.js.map