@hominis/fireforge 0.18.8 → 0.18.9

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.
@@ -3,7 +3,7 @@ import { join } from 'node:path';
3
3
  import { prepareBuildEnvironment } from '../core/build-prepare.js';
4
4
  import { getProjectPaths, loadConfig } from '../core/config.js';
5
5
  import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, testWithOutput, } from '../core/mach.js';
6
- import { assertMarionettePortAvailable } from '../core/marionette-port.js';
6
+ import { assertMarionettePortAvailable, extractForwardedMarionettePort, isMarionetteFlavor, } from '../core/marionette-port.js';
7
7
  import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
8
8
  import { checkStaleBuildForTest, formatStaleBuildWarning } from '../core/test-stale-check.js';
9
9
  import { operatorAlreadySetAppPath, resolveXpcshellAppdirArg, } from '../core/xpcshell-appdir.js';
@@ -238,6 +238,20 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
238
238
  warn(formatStaleBuildWarning(stale));
239
239
  }
240
240
  }
241
+ // Resolve the effective Marionette port. Operator precedence:
242
+ // 1. `--marionette-port` (first-class option, parsed at the CLI layer)
243
+ // 2. forwarded `--mach-arg --marionette-port=NNNN` /
244
+ // `--mach-arg --setpref=marionette.port=NNNN`
245
+ // 3. fall back to `DEFAULT_MARIONETTE_PORT` semantics inside the probes
246
+ // (passed as `undefined`).
247
+ // Without (2), an operator working around a stale listener via the
248
+ // documented `--mach-arg --marionette-port=NNNN` workaround would still
249
+ // hit the wrapper preflight refusing on 2828 before the forwarded arg
250
+ // ever reached mach.
251
+ const forwardedPort = options.machArg
252
+ ? extractForwardedMarionettePort(options.machArg)
253
+ : undefined;
254
+ const effectivePort = options.marionettePort ?? forwardedPort;
241
255
  // Stale-browser probe: an interrupted earlier test run can leave a
242
256
  // Firefox/ForgeFresh/Hominis instance listening on the Marionette
243
257
  // control port, which breaks the next mach test launch with a
@@ -246,7 +260,7 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
246
260
  // generic bind failure. 2026-04-21 eval (Finding #20): a stale
247
261
  // `-marionette` process from `fresh/` poisoned a later test run in
248
262
  // the sibling `hominis/` workspace.
249
- await assertMarionettePortAvailable(undefined, { binaryName: projectConfig.binaryName });
263
+ await assertMarionettePortAvailable(effectivePort, { binaryName: projectConfig.binaryName });
250
264
  // `--doctor` runs a short marionette handshake probe. When test paths are
251
265
  // supplied the probe gates the mach test invocation (a FAIL bails out). When
252
266
  // no paths are supplied this is the only step — it's the fastest way to tell
@@ -259,7 +273,9 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
259
273
  // clack box-drawing framing.
260
274
  process.stdout.write('Running marionette preflight...\n');
261
275
  info('Running marionette preflight...');
262
- const preflight = await runMarionettePreflight(paths.engine);
276
+ const preflight = effectivePort !== undefined
277
+ ? await runMarionettePreflight(paths.engine, { port: effectivePort })
278
+ : await runMarionettePreflight(paths.engine);
263
279
  // 2026-04-24 eval Finding 7: the pre-0.18.1 code used
264
280
  // `success()` + `outro()` + a direct `process.stdout.write` as a
265
281
  // belt-and-suspenders but still reproducibly dropped the PASS summary
@@ -303,6 +319,30 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
303
319
  if (options.machArg && options.machArg.length > 0) {
304
320
  extraArgs.push(...options.machArg);
305
321
  }
322
+ // Auto-forward the Marionette port to mach when `--marionette-port` is
323
+ // set. We use `--setpref=marionette.port=<n>` because the marionette
324
+ // listener reads that pref before binding (browser-chrome / mochitest
325
+ // path); xpcshell never reads it, so the pref is a no-op there.
326
+ //
327
+ // Skip forwarding when the operator already supplied an equivalent arg
328
+ // via `--mach-arg` — duplicates would be confusing without changing
329
+ // semantics. Skip with a notice for clearly-non-marionette flavours
330
+ // (xpcshell, or paths that don't look browser-chrome/mochitest) so the
331
+ // operator knows the preflight took the override but mach was not
332
+ // auto-configured. Same escape valve applies: any mach arg can still
333
+ // be supplied via `--mach-arg`.
334
+ if (options.marionettePort !== undefined) {
335
+ const operatorAlreadyForwarded = forwardedPort !== undefined;
336
+ if (operatorAlreadyForwarded) {
337
+ info(`--marionette-port=${options.marionettePort} set, but the same port is already forwarded via --mach-arg; skipping auto-forward.`);
338
+ }
339
+ else if (isMarionetteFlavor(normalizedPaths, options.machArg ?? [])) {
340
+ extraArgs.push(`--setpref=marionette.port=${options.marionettePort}`);
341
+ }
342
+ else {
343
+ info(`--marionette-port=${options.marionettePort} applied to the preflight probe, but the test paths do not look browser-chrome/mochitest — mach is not auto-configured. Pass --mach-arg --setpref=marionette.port=${options.marionettePort} explicitly if mach should also use this port.`);
344
+ }
345
+ }
306
346
  // xpcshell appdir auto-injection — see src/core/xpcshell-appdir.ts for the
307
347
  // full motivation. On rebranded forks (appname != "firefox") the upstream
308
348
  // harness silently ignores `firefox-appdir = "browser"` directives in the
@@ -368,6 +408,13 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
368
408
  acc.push(value);
369
409
  return acc;
370
410
  }, [])
411
+ .option('--marionette-port <port>', 'Override the Marionette control port (default 2828) for the stale-browser probe, the --doctor preflight, and the auto-forwarded --setpref=marionette.port=<n> arg passed to mach. Use this when a stale process holds 2828 or a CI runner reserves a different port.', (raw) => {
412
+ const n = Number.parseInt(raw, 10);
413
+ if (!Number.isFinite(n) || n < 1 || n > 65535) {
414
+ throw new GeneralError(`--marionette-port must be an integer in 1..65535 (got "${raw}")`);
415
+ }
416
+ return n;
417
+ })
371
418
  .action(withErrorHandling(async (paths, options) => {
372
419
  await testCommand(getProjectRoot(), paths, pickDefined(options));
373
420
  }));
@@ -15,8 +15,12 @@ export type AcornESTreeNode<T extends estree.Node = estree.Node> = T & {
15
15
  * Parse JavaScript source as a **script** (not an ES module).
16
16
  * All Mozilla chrome JS files (`browser-main.js`, `browser-init.js`,
17
17
  * `customElements.js`, etc.) are scripts that run in a privileged scope.
18
+ *
19
+ * @param content - Source text to parse
20
+ * @param onComment - Optional array that acorn fills with comment nodes
21
+ * @returns Parsed program AST with character-offset positions
18
22
  */
19
- export declare function parseScript(content: string): AcornESTreeNode<estree.Program>;
23
+ export declare function parseScript(content: string, onComment?: acorn.Comment[]): AcornESTreeNode<estree.Program>;
20
24
  /**
21
25
  * Parse JavaScript source as an **ES module**.
22
26
  * Used for `.sys.mjs` files which use static import/export syntax.
@@ -5,12 +5,19 @@ import { walk } from 'estree-walker';
5
5
  * Parse JavaScript source as a **script** (not an ES module).
6
6
  * All Mozilla chrome JS files (`browser-main.js`, `browser-init.js`,
7
7
  * `customElements.js`, etc.) are scripts that run in a privileged scope.
8
+ *
9
+ * @param content - Source text to parse
10
+ * @param onComment - Optional array that acorn fills with comment nodes
11
+ * @returns Parsed program AST with character-offset positions
8
12
  */
9
- export function parseScript(content) {
10
- return acorn.parse(content, {
13
+ export function parseScript(content, onComment) {
14
+ const opts = {
11
15
  sourceType: 'script',
12
16
  ecmaVersion: 'latest',
13
- });
17
+ };
18
+ if (onComment)
19
+ opts.onComment = onComment;
20
+ return acorn.parse(content, opts);
14
21
  }
15
22
  /**
16
23
  * Parse JavaScript source as an **ES module**.
@@ -2,9 +2,11 @@
2
2
  * Heuristic test for "this looks like a packaged-test source file" — the
3
3
  * audit routes such paths to `_tests/` instead of `dist/`. Matches
4
4
  * mochitest / xpcshell / browser-chrome conventions: any source under a
5
- * `/test/` or `/tests/` directory, or with a `browser_` / `test_` prefix
6
- * on a `.js`/`.toml` basename. Test manifests (`*.toml`, `*.list`,
7
- * `*.ini`) under those directories also qualify.
5
+ * `/test/` or `/tests/` directory, anywhere under a `testing/` subtree
6
+ * (which holds mochitest / marionette / xpcshell harness sources), or
7
+ * with a `browser_` / `test_` prefix on a `.js`/`.toml` basename. Test
8
+ * manifests (`*.toml`, `*.list`, `*.ini`) under those directories also
9
+ * qualify.
8
10
  *
9
11
  * @param sourcePath Engine-relative POSIX path
10
12
  * @returns True when the file belongs to the test tree, not the bundle
@@ -30,9 +30,11 @@ const MAX_SCAN_DEPTH = 12;
30
30
  * Heuristic test for "this looks like a packaged-test source file" — the
31
31
  * audit routes such paths to `_tests/` instead of `dist/`. Matches
32
32
  * mochitest / xpcshell / browser-chrome conventions: any source under a
33
- * `/test/` or `/tests/` directory, or with a `browser_` / `test_` prefix
34
- * on a `.js`/`.toml` basename. Test manifests (`*.toml`, `*.list`,
35
- * `*.ini`) under those directories also qualify.
33
+ * `/test/` or `/tests/` directory, anywhere under a `testing/` subtree
34
+ * (which holds mochitest / marionette / xpcshell harness sources), or
35
+ * with a `browser_` / `test_` prefix on a `.js`/`.toml` basename. Test
36
+ * manifests (`*.toml`, `*.list`, `*.ini`) under those directories also
37
+ * qualify.
36
38
  *
37
39
  * @param sourcePath Engine-relative POSIX path
38
40
  * @returns True when the file belongs to the test tree, not the bundle
@@ -41,6 +43,13 @@ export function isTestPath(sourcePath) {
41
43
  if (sourcePath.includes('/test/') || sourcePath.includes('/tests/')) {
42
44
  return true;
43
45
  }
46
+ // `testing/{mochitest,marionette,xpcshell,...}` are test-infrastructure
47
+ // trees that ship under `_tests/`, not `dist/`. Match both as a root
48
+ // segment (e.g. `testing/mochitest/api.js`) and as an interior segment
49
+ // (e.g. a vendored harness under `third_party/.../testing/...`).
50
+ if (sourcePath.startsWith('testing/') || sourcePath.includes('/testing/')) {
51
+ return true;
52
+ }
44
53
  const name = basename(sourcePath);
45
54
  if (/^browser_.+\.(js|toml|ini)$/.test(name))
46
55
  return true;
@@ -19,7 +19,7 @@ export declare const SRC_DIR = "src";
19
19
  /** Supported top-level fireforge.json keys backed by the current schema. */
20
20
  export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint", "markerComment"];
21
21
  /** Supported config paths that can be read or set without --force. */
22
- export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "markerComment"];
22
+ export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "markerComment"];
23
23
  /**
24
24
  * Gets all project paths based on a root directory.
25
25
  * @param root - Root directory of the project
@@ -49,6 +49,7 @@ export const SUPPORTED_CONFIG_PATHS = [
49
49
  'patchLint.rawColorAllowlist',
50
50
  'patchLint.jsdocClassMethods',
51
51
  'patchLint.testAssertionFloor',
52
+ 'patchLint.chromeScriptJsDoc',
52
53
  'markerComment',
53
54
  ];
54
55
  /**
@@ -229,6 +229,10 @@ function parsePatchLintBlock(rec) {
229
229
  if (testAssertionFloor !== undefined) {
230
230
  out.testAssertionFloor = testAssertionFloor;
231
231
  }
232
+ const chromeScriptJsDoc = parseSeverityGate(rec.raw('chromeScriptJsDoc'), 'patchLint.chromeScriptJsDoc');
233
+ if (chromeScriptJsDoc !== undefined) {
234
+ out.chromeScriptJsDoc = chromeScriptJsDoc;
235
+ }
232
236
  return out;
233
237
  }
234
238
  //# sourceMappingURL=config-validate.js.map
@@ -48,3 +48,32 @@ export declare function probeMarionettePort(port?: number): Promise<MarionettePo
48
48
  export declare function assertMarionettePortAvailable(port?: number, options?: {
49
49
  binaryName?: string;
50
50
  }): Promise<void>;
51
+ /**
52
+ * Extracts a `--marionette-port=N` (or `--marionette-port N`) argument from
53
+ * a list of forwarded mach args, if present. Used so an operator passing the
54
+ * port via `--mach-arg --marionette-port=NNNN` gets the same preflight
55
+ * override they would from a first-class `--marionette-port` option, rather
56
+ * than the wrapper probing the default port and refusing.
57
+ *
58
+ * Also recognises `--setpref=marionette.port=NNNN` since that is the path
59
+ * the test command auto-forwards to mach.
60
+ *
61
+ * @param machArgs - Forwarded mach args as they would appear on the command
62
+ * line (one element per token; `--foo=bar` and `--foo bar` both supported).
63
+ * @returns The integer port if a recognised arg is present and parses; else
64
+ * `undefined`.
65
+ */
66
+ export declare function extractForwardedMarionettePort(machArgs: string[]): number | undefined;
67
+ /**
68
+ * Heuristic: do the test paths or forwarded mach args indicate a flavour
69
+ * that actually launches a Marionette-driven browser? Browser-chrome and
70
+ * mochitest do; xpcshell does not. Used to decide whether to auto-forward
71
+ * `--setpref=marionette.port=<n>` to mach when the operator passed
72
+ * `--marionette-port`. A no-paths invocation (the default "run all tests"
73
+ * shape) is treated as marionette-relevant since it includes browser-chrome.
74
+ *
75
+ * @param testPaths - Engine-relative paths after `stripEnginePrefix`.
76
+ * @param machArgs - Forwarded mach args (post-`--mach-arg`).
77
+ * @returns `true` when the run is likely to bind a Marionette listener.
78
+ */
79
+ export declare function isMarionetteFlavor(testPaths: string[], machArgs: string[]): boolean;
@@ -212,4 +212,86 @@ export async function assertMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
212
212
  throw new GeneralError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
213
213
  `This is not a FireForge-launched browser; stop the holder process or free the port before rerunning.`);
214
214
  }
215
+ /**
216
+ * Extracts a `--marionette-port=N` (or `--marionette-port N`) argument from
217
+ * a list of forwarded mach args, if present. Used so an operator passing the
218
+ * port via `--mach-arg --marionette-port=NNNN` gets the same preflight
219
+ * override they would from a first-class `--marionette-port` option, rather
220
+ * than the wrapper probing the default port and refusing.
221
+ *
222
+ * Also recognises `--setpref=marionette.port=NNNN` since that is the path
223
+ * the test command auto-forwards to mach.
224
+ *
225
+ * @param machArgs - Forwarded mach args as they would appear on the command
226
+ * line (one element per token; `--foo=bar` and `--foo bar` both supported).
227
+ * @returns The integer port if a recognised arg is present and parses; else
228
+ * `undefined`.
229
+ */
230
+ export function extractForwardedMarionettePort(machArgs) {
231
+ for (let i = 0; i < machArgs.length; i++) {
232
+ const arg = machArgs[i];
233
+ if (arg === undefined)
234
+ continue;
235
+ // `--marionette-port=NNNN`
236
+ let match = /^--marionette-port=(\d+)$/.exec(arg);
237
+ if (match?.[1]) {
238
+ const n = Number.parseInt(match[1], 10);
239
+ if (Number.isFinite(n))
240
+ return n;
241
+ }
242
+ // `--marionette-port NNNN` (two tokens)
243
+ if (arg === '--marionette-port') {
244
+ const next = machArgs[i + 1];
245
+ if (next !== undefined) {
246
+ const n = Number.parseInt(next, 10);
247
+ if (Number.isFinite(n))
248
+ return n;
249
+ }
250
+ }
251
+ // `--setpref=marionette.port=NNNN` — the auto-forward shape; recognised
252
+ // here so a duplicate check at the call site can spot operator-supplied
253
+ // setprefs without re-implementing the parse.
254
+ match = /^--setpref=marionette\.port=(\d+)$/.exec(arg);
255
+ if (match?.[1]) {
256
+ const n = Number.parseInt(match[1], 10);
257
+ if (Number.isFinite(n))
258
+ return n;
259
+ }
260
+ }
261
+ return undefined;
262
+ }
263
+ /**
264
+ * Heuristic: do the test paths or forwarded mach args indicate a flavour
265
+ * that actually launches a Marionette-driven browser? Browser-chrome and
266
+ * mochitest do; xpcshell does not. Used to decide whether to auto-forward
267
+ * `--setpref=marionette.port=<n>` to mach when the operator passed
268
+ * `--marionette-port`. A no-paths invocation (the default "run all tests"
269
+ * shape) is treated as marionette-relevant since it includes browser-chrome.
270
+ *
271
+ * @param testPaths - Engine-relative paths after `stripEnginePrefix`.
272
+ * @param machArgs - Forwarded mach args (post-`--mach-arg`).
273
+ * @returns `true` when the run is likely to bind a Marionette listener.
274
+ */
275
+ export function isMarionetteFlavor(testPaths, machArgs) {
276
+ for (const arg of machArgs) {
277
+ if (/^--flavor=xpcshell\b/.test(arg) || arg === '--flavor=xpcshell-tests')
278
+ return false;
279
+ }
280
+ for (const arg of machArgs) {
281
+ if (/^--flavor=(browser-chrome|mochitest|chrome|a11y)\b/.test(arg))
282
+ return true;
283
+ }
284
+ if (testPaths.length === 0)
285
+ return true;
286
+ for (const path of testPaths) {
287
+ const base = path.split('/').pop() ?? path;
288
+ if (/^browser_.+\.(js|ini|toml)$/.test(base))
289
+ return true;
290
+ if (path.includes('/mochitest/') || path.startsWith('mochitest/'))
291
+ return true;
292
+ if (path.includes('/browser-chrome/') || path.startsWith('browser-chrome/'))
293
+ return true;
294
+ }
295
+ return false;
296
+ }
215
297
  //# sourceMappingURL=marionette-port.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * AST-based JSDoc validation for top-level declarations in patch-owned
3
+ * chrome subscripts (`browser/base/content/<binaryName>*.js` and similar
4
+ * `.js` files loaded via `Services.scriptloader.loadSubScript`).
5
+ *
6
+ * Why a separate module from {@link ./patch-lint-jsdoc.ts}: chrome
7
+ * subscripts are NOT ES modules. They are parsed as scripts (no static
8
+ * `export`) and their top-level `class`/`function` declarations are
9
+ * exposed to the loading window as globals rather than declared exports.
10
+ * The export-walker in `patch-lint-jsdoc.ts` would never visit them.
11
+ *
12
+ * The rule shape — "every top-level class needs a JSDoc, every method
13
+ * needs one when `chromeScriptJsDoc` is at `warning`/`error`, every
14
+ * top-level function needs a matching @param/@returns block" — is
15
+ * identical to the `.sys.mjs` rule once you remove the `export` framing,
16
+ * so the per-declaration validators are reused verbatim from the export
17
+ * module.
18
+ */
19
+ import type { PatchLintIssue } from '../types/commands/index.js';
20
+ import type { PatchLintSeverityGate } from '../types/config.js';
21
+ import type { JsDocIssue, ValidateExportJsDocOptions } from './patch-lint-jsdoc.js';
22
+ /**
23
+ * Validates JSDoc on top-level declarations in a chrome-subscript `.js`
24
+ * source file. A parse failure returns an empty issue list — chrome
25
+ * subscripts that use module-only syntax (rare) silently disable the
26
+ * rule rather than emitting confusing parse-error issues.
27
+ *
28
+ * @param source - File content
29
+ * @param options - Optional gates (e.g. class-method JSDoc severity).
30
+ * `classMethodMode` defaults to `'off'` — the orchestrator passes the
31
+ * `chromeScriptJsDoc` severity here so a single knob controls both
32
+ * class-level and method-level enforcement.
33
+ * @returns Array of JSDoc issues found
34
+ */
35
+ export declare function validateChromeScriptJsDoc(source: string, options?: ValidateExportJsDocOptions): JsDocIssue[];
36
+ /**
37
+ * Per-file dispatch: applies the chrome-subscript JSDoc rule to one file
38
+ * if it qualifies, mapping {@link JsDocIssue} into {@link PatchLintIssue}.
39
+ * Extracted so the orchestrator in `patch-lint.ts` stays under the
40
+ * project's per-file line budget — `patch-lint.ts` invokes this helper
41
+ * inline once per affected JS/MJS file. Returns an empty array when the
42
+ * file does not qualify (not a chrome subscript, not patch-owned, or the
43
+ * mode is `'off'` / unset). The orchestrator pre-computes `isChromeOwned`
44
+ * (true iff the file is a patch-owned `.js` non-`.sys.mjs`) so the call
45
+ * site fits on a single line.
46
+ */
47
+ export declare function lintChromeScriptJsDocForFile(file: string, content: string, isChromeOwned: boolean, mode: PatchLintSeverityGate | undefined): PatchLintIssue[];
@@ -0,0 +1,87 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * AST-based JSDoc validation for top-level declarations in patch-owned
4
+ * chrome subscripts (`browser/base/content/<binaryName>*.js` and similar
5
+ * `.js` files loaded via `Services.scriptloader.loadSubScript`).
6
+ *
7
+ * Why a separate module from {@link ./patch-lint-jsdoc.ts}: chrome
8
+ * subscripts are NOT ES modules. They are parsed as scripts (no static
9
+ * `export`) and their top-level `class`/`function` declarations are
10
+ * exposed to the loading window as globals rather than declared exports.
11
+ * The export-walker in `patch-lint-jsdoc.ts` would never visit them.
12
+ *
13
+ * The rule shape — "every top-level class needs a JSDoc, every method
14
+ * needs one when `chromeScriptJsDoc` is at `warning`/`error`, every
15
+ * top-level function needs a matching @param/@returns block" — is
16
+ * identical to the `.sys.mjs` rule once you remove the `export` framing,
17
+ * so the per-declaration validators are reused verbatim from the export
18
+ * module.
19
+ */
20
+ import { parseScript } from './ast-utils.js';
21
+ import { validateClassDecl, validateClassMethods, validateFunctionDecl, } from './patch-lint-jsdoc.js';
22
+ /**
23
+ * Validates JSDoc on top-level declarations in a chrome-subscript `.js`
24
+ * source file. A parse failure returns an empty issue list — chrome
25
+ * subscripts that use module-only syntax (rare) silently disable the
26
+ * rule rather than emitting confusing parse-error issues.
27
+ *
28
+ * @param source - File content
29
+ * @param options - Optional gates (e.g. class-method JSDoc severity).
30
+ * `classMethodMode` defaults to `'off'` — the orchestrator passes the
31
+ * `chromeScriptJsDoc` severity here so a single knob controls both
32
+ * class-level and method-level enforcement.
33
+ * @returns Array of JSDoc issues found
34
+ */
35
+ export function validateChromeScriptJsDoc(source, options) {
36
+ const classMethodMode = options?.classMethodMode ?? 'off';
37
+ const comments = [];
38
+ let ast;
39
+ try {
40
+ ast = parseScript(source, comments);
41
+ }
42
+ catch {
43
+ return [];
44
+ }
45
+ const issues = [];
46
+ const body = ast.body;
47
+ for (const node of body) {
48
+ if (node.type === 'FunctionDeclaration') {
49
+ validateFunctionDecl(node, comments, source, issues);
50
+ }
51
+ else if (node.type === 'ClassDeclaration') {
52
+ validateClassDecl(node, comments, source, issues);
53
+ if (classMethodMode !== 'off') {
54
+ validateClassMethods(node, comments, source, issues, classMethodMode);
55
+ }
56
+ }
57
+ // Top-level `var Foo = class {...}` / `let Foo = function() {...}`
58
+ // patterns are intentionally out of scope for V1 — chrome subscripts
59
+ // overwhelmingly use bare `class`/`function` declarations and the
60
+ // variable-init form would require unwrapping the initializer. Add
61
+ // it later if a real chrome subscript uses that shape.
62
+ }
63
+ return issues;
64
+ }
65
+ /**
66
+ * Per-file dispatch: applies the chrome-subscript JSDoc rule to one file
67
+ * if it qualifies, mapping {@link JsDocIssue} into {@link PatchLintIssue}.
68
+ * Extracted so the orchestrator in `patch-lint.ts` stays under the
69
+ * project's per-file line budget — `patch-lint.ts` invokes this helper
70
+ * inline once per affected JS/MJS file. Returns an empty array when the
71
+ * file does not qualify (not a chrome subscript, not patch-owned, or the
72
+ * mode is `'off'` / unset). The orchestrator pre-computes `isChromeOwned`
73
+ * (true iff the file is a patch-owned `.js` non-`.sys.mjs`) so the call
74
+ * site fits on a single line.
75
+ */
76
+ export function lintChromeScriptJsDocForFile(file, content, isChromeOwned, mode) {
77
+ if (!isChromeOwned || !mode || mode === 'off')
78
+ return [];
79
+ const jsdocIssues = validateChromeScriptJsDoc(content, { classMethodMode: mode });
80
+ return jsdocIssues.map((issue) => ({
81
+ file,
82
+ check: issue.check,
83
+ message: issue.message,
84
+ severity: issue.severity ?? mode,
85
+ }));
86
+ }
87
+ //# sourceMappingURL=patch-lint-chrome-jsdoc.js.map
@@ -6,6 +6,9 @@
6
6
  * Separated from `patch-lint.ts` to keep both files within the
7
7
  * project's per-file line budget.
8
8
  */
9
+ import type * as acorn from 'acorn';
10
+ import type { ClassDeclaration, FunctionDeclaration } from 'estree';
11
+ import type { AcornESTreeNode } from './ast-utils.js';
9
12
  export type JsDocCheck = 'missing-jsdoc' | 'jsdoc-param-mismatch' | 'jsdoc-missing-returns' | 'missing-jsdoc-class-method' | 'jsdoc-class-method-param-mismatch' | 'jsdoc-class-method-missing-returns';
10
13
  export interface JsDocIssue {
11
14
  line: number;
@@ -19,6 +22,40 @@ export interface ValidateExportJsDocOptions {
19
22
  /** Gate for class-method JSDoc enforcement. Default 'off' (no walking). */
20
23
  classMethodMode?: ClassMethodMode;
21
24
  }
25
+ /**
26
+ * Validates a top-level function declaration: requires an attached JSDoc
27
+ * comment, then checks @param name matching and @returns presence. Exported
28
+ * so the chrome-subscript validator (`patch-lint-chrome-jsdoc.ts`) can reuse
29
+ * it on `parseScript`-produced declarations — the rule shape is identical
30
+ * between ES-module exports and chrome-subscript top-level declarations.
31
+ *
32
+ * @param fn - FunctionDeclaration AST node
33
+ * @param comments - All Acorn comments collected from the source
34
+ * @param source - Original source text (for line-number resolution)
35
+ * @param issues - Output sink for JSDoc issues
36
+ * @param lookupStart - Optional offset to use when locating the attached
37
+ * JSDoc (defaults to `fn.start`). Used by the export-walker so the JSDoc
38
+ * is found relative to the `export` keyword rather than the inner decl.
39
+ */
40
+ export declare function validateFunctionDecl(fn: AcornESTreeNode<FunctionDeclaration>, comments: acorn.Comment[], source: string, issues: JsDocIssue[], lookupStart?: number): void;
41
+ /**
42
+ * Validates a top-level class declaration: requires an attached JSDoc
43
+ * comment on the class itself. Method-level checks live in
44
+ * {@link validateClassMethods}. Exported for reuse in the chrome-subscript
45
+ * validator.
46
+ */
47
+ export declare function validateClassDecl(cls: AcornESTreeNode<ClassDeclaration>, comments: acorn.Comment[], source: string, issues: JsDocIssue[], lookupStart?: number): void;
48
+ /**
49
+ * Walks an exported class body and emits class-method JSDoc issues per
50
+ * the configured severity. Skip rules (in evaluation order):
51
+ * 1. private syntax (`#foo`) and underscore-prefixed names
52
+ * 2. zero-parameter constructors
53
+ * 3. methods whose JSDoc carries `@private` or `@internal`
54
+ *
55
+ * Pure-override skip (`super.method(...args)`-only bodies bypassing the
56
+ * @returns check) is deferred — V1 keeps the rule simple.
57
+ */
58
+ export declare function validateClassMethods(cls: AcornESTreeNode<ClassDeclaration>, comments: acorn.Comment[], source: string, issues: JsDocIssue[], severity: 'warning' | 'error'): void;
22
59
  /**
23
60
  * Validates JSDoc on exported declarations in a `.sys.mjs` source file.
24
61
  *
@@ -166,7 +166,22 @@ function validateParamsAndReturns(fnNode, jsDoc, issues, ctx) {
166
166
  // ---------------------------------------------------------------------------
167
167
  // Top-level export validation
168
168
  // ---------------------------------------------------------------------------
169
- function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
169
+ /**
170
+ * Validates a top-level function declaration: requires an attached JSDoc
171
+ * comment, then checks @param name matching and @returns presence. Exported
172
+ * so the chrome-subscript validator (`patch-lint-chrome-jsdoc.ts`) can reuse
173
+ * it on `parseScript`-produced declarations — the rule shape is identical
174
+ * between ES-module exports and chrome-subscript top-level declarations.
175
+ *
176
+ * @param fn - FunctionDeclaration AST node
177
+ * @param comments - All Acorn comments collected from the source
178
+ * @param source - Original source text (for line-number resolution)
179
+ * @param issues - Output sink for JSDoc issues
180
+ * @param lookupStart - Optional offset to use when locating the attached
181
+ * JSDoc (defaults to `fn.start`). Used by the export-walker so the JSDoc
182
+ * is found relative to the `export` keyword rather than the inner decl.
183
+ */
184
+ export function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
170
185
  const name = fn.id.name;
171
186
  const start = lookupStart !== undefined ? lookupStart : fn.start;
172
187
  const line = lineAt(source, start);
@@ -186,7 +201,13 @@ function validateFunctionDecl(fn, comments, source, issues, lookupStart) {
186
201
  returnsCheck: 'jsdoc-missing-returns',
187
202
  });
188
203
  }
189
- function validateClassDecl(cls, comments, source, issues, lookupStart) {
204
+ /**
205
+ * Validates a top-level class declaration: requires an attached JSDoc
206
+ * comment on the class itself. Method-level checks live in
207
+ * {@link validateClassMethods}. Exported for reuse in the chrome-subscript
208
+ * validator.
209
+ */
210
+ export function validateClassDecl(cls, comments, source, issues, lookupStart) {
190
211
  const name = cls.id.name;
191
212
  const start = lookupStart !== undefined ? lookupStart : cls.start;
192
213
  const line = lineAt(source, start);
@@ -254,7 +275,7 @@ function classMethodLabel(className, method, name) {
254
275
  * Pure-override skip (`super.method(...args)`-only bodies bypassing the
255
276
  * @returns check) is deferred — V1 keeps the rule simple.
256
277
  */
257
- function validateClassMethods(cls, comments, source, issues, severity) {
278
+ export function validateClassMethods(cls, comments, source, issues, severity) {
258
279
  const className = cls.id.name;
259
280
  for (const member of cls.body.body) {
260
281
  if (member.type !== 'MethodDefinition')
@@ -1,10 +1,17 @@
1
1
  /**
2
- * Patch ownership resolution for `.sys.mjs` files.
2
+ * Patch ownership resolution for JS-shaped files.
3
3
  *
4
4
  * A file is "patch-owned" when it was created by the project's patch
5
5
  * queue rather than being an upstream Firefox file that happens to be
6
- * modified. This module computes the set of patch-owned `.sys.mjs`
7
- * paths so lint rules can scope enforcement to project code only.
6
+ * modified. This module computes the set of patch-owned paths so lint
7
+ * rules can scope enforcement to project code only.
8
+ *
9
+ * The two resolvers are kept separate (one per extension predicate)
10
+ * because the downstream rules differ: `.sys.mjs` files go through
11
+ * `runCheckJs` (TypeScript checkJs) and the export-walker JSDoc rule;
12
+ * chrome subscripts (`.js` non-`.sys.mjs`) only get the script-walker
13
+ * JSDoc rule. Mixing them in a single set would silently broaden
14
+ * `runCheckJs` to chrome subscripts, which it is not designed for.
8
15
  */
9
16
  import type { PatchQueueContext } from './patch-lint-cross.js';
10
17
  /**
@@ -23,3 +30,14 @@ import type { PatchQueueContext } from './patch-lint-cross.js';
23
30
  * @returns Set of patch-owned `.sys.mjs` file paths
24
31
  */
25
32
  export declare function resolvePatchOwnedSysMjs(currentNewFiles: Set<string>, patchQueueCtx?: PatchQueueContext): Set<string>;
33
+ /**
34
+ * Returns the set of file paths that are patch-owned chrome subscripts
35
+ * (`.js` files that are not `.sys.mjs` modules — typically
36
+ * `browser/base/content/<binaryName>*.js` and similar). Same ownership
37
+ * semantics as {@link resolvePatchOwnedSysMjs}.
38
+ *
39
+ * @param currentNewFiles - Files newly created in the current diff
40
+ * @param patchQueueCtx - Optional cross-patch context for queue-wide ownership
41
+ * @returns Set of patch-owned chrome-subscript file paths
42
+ */
43
+ export declare function resolvePatchOwnedChromeScripts(currentNewFiles: Set<string>, patchQueueCtx?: PatchQueueContext): Set<string>;
@@ -1,13 +1,41 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
- * Patch ownership resolution for `.sys.mjs` files.
3
+ * Patch ownership resolution for JS-shaped files.
4
4
  *
5
5
  * A file is "patch-owned" when it was created by the project's patch
6
6
  * queue rather than being an upstream Firefox file that happens to be
7
- * modified. This module computes the set of patch-owned `.sys.mjs`
8
- * paths so lint rules can scope enforcement to project code only.
7
+ * modified. This module computes the set of patch-owned paths so lint
8
+ * rules can scope enforcement to project code only.
9
+ *
10
+ * The two resolvers are kept separate (one per extension predicate)
11
+ * because the downstream rules differ: `.sys.mjs` files go through
12
+ * `runCheckJs` (TypeScript checkJs) and the export-walker JSDoc rule;
13
+ * chrome subscripts (`.js` non-`.sys.mjs`) only get the script-walker
14
+ * JSDoc rule. Mixing them in a single set would silently broaden
15
+ * `runCheckJs` to chrome subscripts, which it is not designed for.
9
16
  */
10
17
  import { collectNewFileCreatorsByPath } from './patch-lint-cross.js';
18
+ /**
19
+ * Returns the set of patch-owned files matching `predicate`. Internal
20
+ * helper shared by the per-extension resolvers below.
21
+ */
22
+ function resolveOwned(currentNewFiles, patchQueueCtx, predicate) {
23
+ const owned = new Set();
24
+ for (const file of currentNewFiles) {
25
+ if (predicate(file)) {
26
+ owned.add(file);
27
+ }
28
+ }
29
+ if (patchQueueCtx) {
30
+ const creators = collectNewFileCreatorsByPath(patchQueueCtx);
31
+ for (const [file, owners] of creators) {
32
+ if (predicate(file) && owners.length > 0) {
33
+ owned.add(file);
34
+ }
35
+ }
36
+ }
37
+ return owned;
38
+ }
11
39
  /**
12
40
  * Returns the set of file paths that are patch-owned `.sys.mjs` files.
13
41
  *
@@ -24,20 +52,19 @@ import { collectNewFileCreatorsByPath } from './patch-lint-cross.js';
24
52
  * @returns Set of patch-owned `.sys.mjs` file paths
25
53
  */
26
54
  export function resolvePatchOwnedSysMjs(currentNewFiles, patchQueueCtx) {
27
- const owned = new Set();
28
- for (const file of currentNewFiles) {
29
- if (file.endsWith('.sys.mjs')) {
30
- owned.add(file);
31
- }
32
- }
33
- if (patchQueueCtx) {
34
- const creators = collectNewFileCreatorsByPath(patchQueueCtx);
35
- for (const [file, owners] of creators) {
36
- if (file.endsWith('.sys.mjs') && owners.length > 0) {
37
- owned.add(file);
38
- }
39
- }
40
- }
41
- return owned;
55
+ return resolveOwned(currentNewFiles, patchQueueCtx, (file) => file.endsWith('.sys.mjs'));
56
+ }
57
+ /**
58
+ * Returns the set of file paths that are patch-owned chrome subscripts
59
+ * (`.js` files that are not `.sys.mjs` modules — typically
60
+ * `browser/base/content/<binaryName>*.js` and similar). Same ownership
61
+ * semantics as {@link resolvePatchOwnedSysMjs}.
62
+ *
63
+ * @param currentNewFiles - Files newly created in the current diff
64
+ * @param patchQueueCtx - Optional cross-patch context for queue-wide ownership
65
+ * @returns Set of patch-owned chrome-subscript file paths
66
+ */
67
+ export function resolvePatchOwnedChromeScripts(currentNewFiles, patchQueueCtx) {
68
+ return resolveOwned(currentNewFiles, patchQueueCtx, (file) => file.endsWith('.js') && !file.endsWith('.sys.mjs'));
42
69
  }
43
70
  //# sourceMappingURL=patch-lint-ownership.js.map
@@ -5,7 +5,7 @@ export { runCheckJs } from './patch-lint-checkjs.js';
5
5
  export { buildPatchQueueContext, collectNewFileCreatorsByPath, type ExtractedSpecifier, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, type PatchQueueContext, type PatchQueueEntry, } from './patch-lint-cross.js';
6
6
  export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
7
7
  export { type JsDocCheck, type JsDocIssue, validateExportJsDoc } from './patch-lint-jsdoc.js';
8
- export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
8
+ export { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
9
9
  /**
10
10
  * Counts the total lines in a unified diff and the number of non-binary
11
11
  * text lines, so binary hunks do not inflate patch size checks.
@@ -55,9 +55,14 @@ export declare function lintNewFileHeaders(repoDir: string, newFiles: string[],
55
55
  * @param newFiles - Set of files that are newly created in this patch
56
56
  * @param config - Project configuration
57
57
  * @param patchOwnedFiles - Optional set of patch-owned `.sys.mjs` paths for scoped JSDoc enforcement
58
+ * @param patchOwnedChromeScripts - Optional set of patch-owned chrome
59
+ * subscripts (`.js` non-`.sys.mjs`) for scoped chrome-script JSDoc
60
+ * enforcement. Built by {@link resolvePatchOwnedChromeScripts}; passed
61
+ * separately from `patchOwnedFiles` so the `runCheckJs` consumer (which
62
+ * only accepts `.sys.mjs`) is not silently broadened.
58
63
  * @returns Array of lint issues
59
64
  */
60
- export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig, patchOwnedFiles?: Set<string>): Promise<PatchLintIssue[]>;
65
+ export declare function lintPatchedJs(repoDir: string, affectedFiles: string[], newFiles: Set<string>, config: FireForgeConfig, patchOwnedFiles?: Set<string>, patchOwnedChromeScripts?: Set<string>): Promise<PatchLintIssue[]>;
61
66
  /**
62
67
  * Checks that modifications to existing (non-new) JS/MJS files include at
63
68
  * least one `// BINARYNAME:` comment in the added lines.
@@ -7,10 +7,11 @@ import { hasRawCssColors, stripJsComments } from '../utils/regex.js';
7
7
  import { loadFurnaceConfig } from './furnace-config.js';
8
8
  import { containsUpstreamLicenseText, getLicenseHeader, hasAnyLicenseHeader, hasAnyLicenseHeaderAnyStyle, } from './license-headers.js';
9
9
  import { runCheckJs } from './patch-lint-checkjs.js';
10
+ import { lintChromeScriptJsDocForFile } from './patch-lint-chrome-jsdoc.js';
10
11
  import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
11
12
  import { AGGREGATE_PATCH_FILE } from './patch-lint-diff-tag.js';
12
13
  import { validateExportJsDoc } from './patch-lint-jsdoc.js';
13
- import { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
14
+ import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
14
15
  // ---------------------------------------------------------------------------
15
16
  // Cross-patch lint re-exports
16
17
  // ---------------------------------------------------------------------------
@@ -24,7 +25,7 @@ export { runCheckJs } from './patch-lint-checkjs.js';
24
25
  export { buildPatchQueueContext, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, } from './patch-lint-cross.js';
25
26
  export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
26
27
  export { validateExportJsDoc } from './patch-lint-jsdoc.js';
27
- export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
28
+ export { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
28
29
  // ---------------------------------------------------------------------------
29
30
  // Helpers
30
31
  // ---------------------------------------------------------------------------
@@ -403,9 +404,14 @@ export async function lintNewFileHeaders(repoDir, newFiles, config) {
403
404
  * @param newFiles - Set of files that are newly created in this patch
404
405
  * @param config - Project configuration
405
406
  * @param patchOwnedFiles - Optional set of patch-owned `.sys.mjs` paths for scoped JSDoc enforcement
407
+ * @param patchOwnedChromeScripts - Optional set of patch-owned chrome
408
+ * subscripts (`.js` non-`.sys.mjs`) for scoped chrome-script JSDoc
409
+ * enforcement. Built by {@link resolvePatchOwnedChromeScripts}; passed
410
+ * separately from `patchOwnedFiles` so the `runCheckJs` consumer (which
411
+ * only accepts `.sys.mjs`) is not silently broadened.
406
412
  * @returns Array of lint issues
407
413
  */
408
- export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles) {
414
+ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles, patchOwnedChromeScripts) {
409
415
  const jsFiles = affectedFiles.filter(isJsFile);
410
416
  if (jsFiles.length === 0)
411
417
  return [];
@@ -476,9 +482,20 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
476
482
  });
477
483
  }
478
484
  }
479
- // 3b. Assertion floor for patch-introduced browser-chrome tests
485
+ // 3a. JSDoc on top-level declarations in patch-owned chrome subscripts.
486
+ // Dispatched out-of-line to keep this file under the per-file line
487
+ // budget. See `patch-lint-chrome-jsdoc.ts` for the rule body.
488
+ const isChromeOwned = file.endsWith('.js') &&
489
+ !isSysMjs &&
490
+ (patchOwnedChromeScripts ? patchOwnedChromeScripts.has(file) : isNew);
491
+ const chromeMode = config.patchLint?.chromeScriptJsDoc;
492
+ issues.push(...lintChromeScriptJsDocForFile(file, content, isChromeOwned, chromeMode));
493
+ // 3b. Assertion floor for browser-chrome tests touched by this patch
494
+ // (covers both newly introduced files and modified upstream tests —
495
+ // a patch that strips the last `Assert.equal` from an existing
496
+ // browser_*.js silently passed under the old `isNew` gate).
480
497
  const assertionFloor = config.patchLint?.testAssertionFloor;
481
- if (assertionFloor && assertionFloor !== 'off' && isNew && isBrowserChromeTestFile(file)) {
498
+ if (assertionFloor && assertionFloor !== 'off' && isBrowserChromeTestFile(file)) {
482
499
  const ASSERTION_TOKENS = ['Assert.', 'ok(', 'is(', 'isnot(', 'isDeeply('];
483
500
  const hasAssertion = ASSERTION_TOKENS.some((tok) => strippedContent.includes(tok));
484
501
  if (!hasAssertion) {
@@ -703,10 +720,11 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
703
720
  const newFiles = detectNewFilesInDiff(diffContent);
704
721
  const { textLines: lineCount } = countNonBinaryDiffLines(diffContent);
705
722
  const patchOwnedFiles = resolvePatchOwnedSysMjs(newFiles, patchQueueCtx);
723
+ const patchOwnedChromeScripts = resolvePatchOwnedChromeScripts(newFiles, patchQueueCtx);
706
724
  const [cssIssues, headerIssues, jsIssues, modifiedHeaderIssues] = await Promise.all([
707
725
  lintPatchedCss(repoDir, affectedFiles, diffContent, config),
708
726
  lintNewFileHeaders(repoDir, [...newFiles], config),
709
- lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles),
727
+ lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles, patchOwnedChromeScripts),
710
728
  lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
711
729
  ]);
712
730
  const modCommentIssues = lintModificationComments(diffContent, config);
@@ -316,6 +316,14 @@ export interface TestOptions {
316
316
  * values appear after `--headless` if both are set.
317
317
  */
318
318
  machArg?: string[];
319
+ /**
320
+ * Override the Marionette control port (default 2828) used by the
321
+ * stale-browser probe, the `--doctor` preflight, and the auto-forwarded
322
+ * `--setpref=marionette.port=<n>` arg passed to mach. Set this when a
323
+ * stale process holds the default port and `kill` is not an option, or
324
+ * when a CI runner reserves a different port for parallel test runs.
325
+ */
326
+ marionettePort?: number;
319
327
  }
320
328
  /**
321
329
  * Options for the furnace apply command.
@@ -74,8 +74,19 @@ export interface PatchLintConfig {
74
74
  rawColorAllowlist?: string[];
75
75
  /** Enforce JSDoc on class-method exports in patch-owned .sys.mjs files. Default: 'off'. */
76
76
  jsdocClassMethods?: PatchLintSeverityGate;
77
- /** Require ≥1 assertion in patch-introduced browser_*.js test files. Default: 'off'. */
77
+ /** Require ≥1 assertion in any patch-touched browser_*.js test file (new or modified). Default: 'off'. */
78
78
  testAssertionFloor?: PatchLintSeverityGate;
79
+ /**
80
+ * Enforce JSDoc on top-level classes (and their methods) and functions
81
+ * in patch-owned chrome subscripts (`.js` files loaded via
82
+ * `Services.scriptloader.loadSubScript`, e.g.
83
+ * `browser/base/content/<binaryName>*.js`). Distinct from
84
+ * `jsdocClassMethods` because chrome subscripts are parsed as scripts,
85
+ * not ES modules — using one flag for both would silently disable the
86
+ * rule when a chrome subscript was fed to the module parser. Default:
87
+ * 'off'.
88
+ */
89
+ chromeScriptJsDoc?: PatchLintSeverityGate;
79
90
  }
80
91
  /**
81
92
  * Build mode for mach.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.18.8",
3
+ "version": "0.18.9",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",