@hominis/fireforge 0.47.0 → 0.48.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 (105) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/dist/bin/fireforge.js +11 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
  5. package/dist/src/commands/doctor-macos-sdk.js +156 -0
  6. package/dist/src/commands/doctor.js +2 -0
  7. package/dist/src/commands/download.js +64 -3
  8. package/dist/src/commands/import.js +5 -2
  9. package/dist/src/commands/lint-per-patch.js +44 -6
  10. package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
  11. package/dist/src/commands/lint-per-run-checkjs.js +1 -0
  12. package/dist/src/commands/lint.js +10 -0
  13. package/dist/src/commands/re-export-lint.d.ts +17 -2
  14. package/dist/src/commands/re-export-lint.js +33 -4
  15. package/dist/src/commands/re-export.js +4 -2
  16. package/dist/src/commands/rebase/confirm.d.ts +12 -1
  17. package/dist/src/commands/rebase/confirm.js +6 -3
  18. package/dist/src/commands/rebase/index.js +77 -13
  19. package/dist/src/commands/run.js +43 -7
  20. package/dist/src/commands/status.js +5 -3
  21. package/dist/src/commands/test-harness-teardown.d.ts +62 -0
  22. package/dist/src/commands/test-harness-teardown.js +128 -0
  23. package/dist/src/commands/test-register.js +2 -1
  24. package/dist/src/commands/test-run.d.ts +2 -0
  25. package/dist/src/commands/test-run.js +36 -7
  26. package/dist/src/commands/test-verdict.d.ts +19 -4
  27. package/dist/src/commands/test-verdict.js +22 -4
  28. package/dist/src/commands/test.js +18 -10
  29. package/dist/src/commands/typecheck.d.ts +5 -0
  30. package/dist/src/commands/typecheck.js +7 -1
  31. package/dist/src/core/build-prepare.d.ts +2 -0
  32. package/dist/src/core/build-prepare.js +67 -35
  33. package/dist/src/core/config-paths.d.ts +1 -1
  34. package/dist/src/core/config-paths.js +2 -0
  35. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  36. package/dist/src/core/config-validate.js +7 -0
  37. package/dist/src/core/discard-baseline.js +2 -1
  38. package/dist/src/core/file-lock.js +16 -9
  39. package/dist/src/core/forward-registrations.d.ts +33 -0
  40. package/dist/src/core/forward-registrations.js +16 -0
  41. package/dist/src/core/git-file-ops.d.ts +21 -0
  42. package/dist/src/core/git-file-ops.js +75 -0
  43. package/dist/src/core/git-status.d.ts +14 -0
  44. package/dist/src/core/git-status.js +41 -2
  45. package/dist/src/core/harness-helper-pids.d.ts +98 -0
  46. package/dist/src/core/harness-helper-pids.js +240 -0
  47. package/dist/src/core/harness-orphans.d.ts +53 -8
  48. package/dist/src/core/harness-orphans.js +125 -44
  49. package/dist/src/core/mach-mozconfig.d.ts +2 -1
  50. package/dist/src/core/mach-mozconfig.js +2 -0
  51. package/dist/src/core/mach.d.ts +16 -0
  52. package/dist/src/core/mach.js +6 -1
  53. package/dist/src/core/marionette-preflight.js +9 -3
  54. package/dist/src/core/parent-exit-watchdog.d.ts +62 -0
  55. package/dist/src/core/parent-exit-watchdog.js +69 -0
  56. package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
  57. package/dist/src/core/patch-apply-fuzz.js +27 -6
  58. package/dist/src/core/patch-apply.d.ts +13 -0
  59. package/dist/src/core/patch-apply.js +27 -2
  60. package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
  61. package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
  62. package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
  63. package/dist/src/core/patch-lint-checkjs.js +8 -1
  64. package/dist/src/core/patch-lint-cross.d.ts +7 -1
  65. package/dist/src/core/patch-lint-cross.js +13 -12
  66. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
  67. package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
  68. package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
  69. package/dist/src/core/patch-lint-forward-registration.js +83 -37
  70. package/dist/src/core/patch-lint.d.ts +11 -0
  71. package/dist/src/core/patch-lint.js +20 -2
  72. package/dist/src/core/patch-transform.d.ts +14 -13
  73. package/dist/src/core/patch-transform.js +29 -17
  74. package/dist/src/core/rebase-dry-run.d.ts +48 -0
  75. package/dist/src/core/rebase-dry-run.js +105 -0
  76. package/dist/src/core/run-profile.d.ts +50 -0
  77. package/dist/src/core/run-profile.js +131 -0
  78. package/dist/src/core/status-binary.d.ts +1 -1
  79. package/dist/src/core/status-binary.js +3 -1
  80. package/dist/src/core/status-classify.js +29 -2
  81. package/dist/src/core/toolchain-preflight.d.ts +6 -0
  82. package/dist/src/core/toolchain-preflight.js +1 -1
  83. package/dist/src/core/typecheck.d.ts +11 -1
  84. package/dist/src/core/typecheck.js +85 -11
  85. package/dist/src/errors/rebase.d.ts +9 -0
  86. package/dist/src/errors/rebase.js +20 -0
  87. package/dist/src/index.d.ts +3 -1
  88. package/dist/src/index.js +2 -1
  89. package/dist/src/types/commands/options.d.ts +26 -1
  90. package/dist/src/types/config.d.ts +17 -0
  91. package/dist/src/utils/process-group.d.ts +5 -1
  92. package/dist/src/utils/process-group.js +7 -3
  93. package/dist/src/utils/process-input.d.ts +27 -0
  94. package/dist/src/utils/process-input.js +49 -0
  95. package/dist/src/utils/process.d.ts +20 -0
  96. package/dist/src/utils/process.js +16 -3
  97. package/dist/src/utils/validation.d.ts +11 -0
  98. package/dist/src/utils/validation.js +43 -0
  99. package/docs/configuration.md +88 -1
  100. package/docs/exit-codes.md +1 -1
  101. package/docs/lifecycle-invariants.md +21 -4
  102. package/docs/machine-output.md +22 -9
  103. package/docs/patch-workflow.md +35 -1
  104. package/docs/testing.md +68 -4
  105. package/package.json +7 -3
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Parsed `SourceFile`s reused across programs, keyed by file name and the
3
+ * parse options TypeScript asked for. The language service shares files
4
+ * across programs the same way (its document registry): a `SourceFile` is
5
+ * parsed and bound once, and every program that includes it reuses both.
6
+ * Only valid while the files cannot change and every program uses the same
7
+ * compiler options, which holds within one lint run.
8
+ */
9
+ export type CheckJsSourceFileCache = Map<string, import('typescript').SourceFile>;
10
+ /**
11
+ * Returns `host` with a shared-parse `getSourceFile` when a cache is given,
12
+ * or `host` itself when not.
13
+ * @param host - The program's compiler host
14
+ * @param cache - Shared parse cache, or undefined for an unshared program
15
+ * @param shimPath - Path the host serves the composed shim at
16
+ * @param shimSource - The composed shim text
17
+ */
18
+ export declare function withSharedSourceFiles(host: import('typescript').CompilerHost, cache: CheckJsSourceFileCache | undefined, shimPath: string, shimSource: string): import('typescript').CompilerHost;
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Parsed-source sharing across checkJs programs.
4
+ *
5
+ * Its own module rather than part of `patch-lint-checkjs.ts` only because
6
+ * that file sits on a 500-line budget.
7
+ */
8
+ import { sha256Hex } from '../utils/hash.js';
9
+ /**
10
+ * Wraps a host's `getSourceFile` so each file is parsed at most once per
11
+ * cache. The shim is keyed by its content as well, since the shim text is
12
+ * composed per call rather than read from disk.
13
+ */
14
+ function cachingGetSourceFile(cache, shimPath, shimSource, getSourceFile) {
15
+ const shimDigest = sha256Hex(shimSource);
16
+ return (fileName, languageVersionOrOptions, onError, shouldCreate) => {
17
+ const parse = typeof languageVersionOrOptions === 'object'
18
+ ? `${languageVersionOrOptions.languageVersion}|${String(languageVersionOrOptions.impliedNodeFormat)}`
19
+ : String(languageVersionOrOptions);
20
+ const key = fileName === shimPath ? `${fileName}\0${parse}\0${shimDigest}` : `${fileName}\0${parse}`;
21
+ const cached = cache.get(key);
22
+ if (cached !== undefined)
23
+ return cached;
24
+ const created = getSourceFile(fileName, languageVersionOrOptions, onError, shouldCreate);
25
+ if (created !== undefined)
26
+ cache.set(key, created);
27
+ return created;
28
+ };
29
+ }
30
+ /**
31
+ * Returns `host` with a shared-parse `getSourceFile` when a cache is given,
32
+ * or `host` itself when not.
33
+ * @param host - The program's compiler host
34
+ * @param cache - Shared parse cache, or undefined for an unshared program
35
+ * @param shimPath - Path the host serves the composed shim at
36
+ * @param shimSource - The composed shim text
37
+ */
38
+ export function withSharedSourceFiles(host, cache, shimPath, shimSource) {
39
+ if (cache === undefined)
40
+ return host;
41
+ return {
42
+ ...host,
43
+ getSourceFile: cachingGetSourceFile(cache, shimPath, shimSource, host.getSourceFile.bind(host)),
44
+ };
45
+ }
@@ -26,6 +26,7 @@
26
26
  */
27
27
  import type { PatchLintIssue } from '../types/commands/index.js';
28
28
  import type { PatchLintCheckJsCompilerOptions, PatchLintConfig, PatchLintSeverityGate } from '../types/config.js';
29
+ import { type CheckJsSourceFileCache } from './patch-lint-checkjs-cache.js';
29
30
  /** How a checkJs run controls reporting and resolution. See module docs. */
30
31
  export interface CheckJsMode {
31
32
  strict: boolean;
@@ -76,6 +77,11 @@ export interface RunCheckJsGroupedInput {
76
77
  * targets.
77
78
  */
78
79
  rootScope?: ReadonlySet<string>;
80
+ /**
81
+ * Parsed source files shared across several programs built with the same
82
+ * compiler options and shim in one run. See {@link runCheckJsTestFilesGrouped}.
83
+ */
84
+ sourceFileCache?: CheckJsSourceFileCache;
79
85
  }
80
86
  /**
81
87
  * Builds the checkJs program once over `resolutionOwned` and returns its
@@ -30,6 +30,7 @@ import { toError } from '../utils/errors.js';
30
30
  import { pathExists } from '../utils/fs.js';
31
31
  import { verbose } from '../utils/logger.js';
32
32
  import { normalizePathSlashes } from '../utils/paths.js';
33
+ import { withSharedSourceFiles } from './patch-lint-checkjs-cache.js';
33
34
  import { collectUnmanagedCompanions, retargetUnmanagedCompanionHints, } from './patch-lint-unmanaged-companion.js';
34
35
  import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, TEST_HARNESS_SHIM, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
35
36
  // ---------------------------------------------------------------------------
@@ -340,7 +341,7 @@ export async function runCheckJsGrouped(input) {
340
341
  });
341
342
  },
342
343
  };
343
- const program = ts.createProgram(rootFiles, options, host);
344
+ const program = ts.createProgram(rootFiles, options, withSharedSourceFiles(host, input.sourceFileCache, shimPath, shimSource));
344
345
  const byFile = groupOwnedDiagnostics(ts, [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()], relByAbsolute, input.mode?.undefinedIdentifiers ?? 'warning');
345
346
  verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s) across ${byFile.size} owning file(s)`);
346
347
  return { byFile, global: [] };
@@ -461,6 +462,11 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
461
462
  const mode = modeFromPatchLintConfig(patchLint);
462
463
  const seenGlobal = new Set();
463
464
  const files = [...testFiles].sort((a, b) => a.localeCompare(b));
465
+ // Every test program below shares options and shim, and mostly the same
466
+ // files: TypeScript's lib declarations, the shim, and the directory's
467
+ // head.js helpers. Parsing them once instead of once per test file took
468
+ // a 467-test queue's pass from 66 s to a fraction of that.
469
+ const sourceFileCache = new Map();
464
470
  const companions = await collectUnmanagedCompanions(repoDir, files, testFiles);
465
471
  for (const file of files) {
466
472
  // Under a rootScope only the scoped files get their own program, but
@@ -487,6 +493,7 @@ export async function runCheckJsTestFilesGrouped(repoDir, testFiles, patchLint,
487
493
  projectRoot,
488
494
  mode,
489
495
  builtinShimSuffix: TEST_HARNESS_SHIM,
496
+ sourceFileCache,
490
497
  });
491
498
  const own = result.byFile.get(file);
492
499
  if (own && own.length > 0) {
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import type { PatchLintIssue, PatchMetadata } from '../types/commands/index.js';
13
13
  import type { FireForgeConfig, PatchPolicyConfig } from '../types/config.js';
14
+ import type { ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
14
15
  /**
15
16
  * One patch's contribution to {@link PatchQueueContext}.
16
17
  *
@@ -39,7 +40,7 @@ export interface PatchQueueEntry {
39
40
  * reconstructable `GIT binary patch`, or a payload-less
40
41
  * `Binary files … differ` stub. `newFiles` is text-only by design (a
41
42
  * binary authors no imports, so the import rules gain nothing from it and
42
- * `extractNewFileContent` correctly refuses to decode one), but "does a
43
+ * `extractNewFileContentFromDiff` correctly refuses to decode one), but "does a
43
44
  * later patch create this path" is a question about existence after apply,
44
45
  * not about content, and answering it from `newFiles` made every binary
45
46
  * creation invisible to the staged-registration rules.
@@ -76,6 +77,11 @@ export interface PatchQueueContext {
76
77
  * Absent = current behavior (hint always printed).
77
78
  */
78
79
  patchPolicy?: PatchPolicyConfig;
80
+ /**
81
+ * Which carriers the forward-registration rule checks, from
82
+ * `patchLint.forwardRegistration`. Absent = `support-files` only.
83
+ */
84
+ forwardRegistration?: ForwardRegistrationScope;
79
85
  }
80
86
  /**
81
87
  * Builds a {@link PatchQueueContext} by reading every .patch file in the
@@ -11,7 +11,6 @@
11
11
  * module.
12
12
  */
13
13
  import { basename } from 'node:path';
14
- import { toError } from '../utils/errors.js';
15
14
  import { readText } from '../utils/fs.js';
16
15
  import { verbose } from '../utils/logger.js';
17
16
  import { stripJsComments } from '../utils/regex.js';
@@ -24,7 +23,7 @@ import { lintPatchQueueModuleRegistrations } from './patch-lint-module-registrat
24
23
  import { describeRegistrationFailure, findStagedRegistrationFailure, isLaterOwner, quoteRegistrationLine, } from './patch-lint-staged-registration.js';
25
24
  import { loadPatchesManifest } from './patch-manifest-io.js';
26
25
  import { categoryRangeForOrder, categoryRangeLabel } from './patch-policy.js';
27
- import { extractNewFileContent } from './patch-transform.js';
26
+ import { extractNewFileContentsFromDiff } from './patch-transform.js';
28
27
  /**
29
28
  * Builds a {@link PatchQueueContext} by reading every .patch file in the
30
29
  * directory and extracting new-file content for each creation.
@@ -50,15 +49,11 @@ export async function buildPatchQueueContext(patchesDir, config) {
50
49
  for (const patch of patches) {
51
50
  const diff = await readText(patch.path);
52
51
  const newFilePaths = detectNewFilesInDiff(diff);
53
- const newFiles = new Map();
54
- for (const newFile of newFilePaths) {
55
- try {
56
- const content = await extractNewFileContent(patch.path, newFile);
57
- newFiles.set(newFile, content);
58
- }
59
- catch (error) {
60
- verbose(`Skipping forward-import scan for ${newFile} in ${patch.filename}: ${toError(error).message}`);
61
- }
52
+ // One parse of the body already in hand. Extracting per file re-read
53
+ // the whole patch from disk for every file it creates.
54
+ const { contents: newFiles, refused } = extractNewFileContentsFromDiff(diff, newFilePaths);
55
+ for (const newFile of refused) {
56
+ verbose(`Skipping forward-import scan for ${newFile} in ${patch.filename}: binary section cannot be extracted as text`);
62
57
  }
63
58
  // Added-line content for every file the patch modifies but does not
64
59
  // create. Fed to the forward-import rule so imports introduced into
@@ -87,7 +82,13 @@ export async function buildPatchQueueContext(patchesDir, config) {
87
82
  }
88
83
  // Sort by order so rules can rely on entries being in apply order.
89
84
  entries.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
90
- return { entries, ...(config?.patchPolicy ? { patchPolicy: config.patchPolicy } : {}) };
85
+ return {
86
+ entries,
87
+ ...(config?.patchPolicy ? { patchPolicy: config.patchPolicy } : {}),
88
+ ...(config?.patchLint?.forwardRegistration !== undefined
89
+ ? { forwardRegistration: config.patchLint.forwardRegistration }
90
+ : {}),
91
+ };
91
92
  }
92
93
  // The creators map (split out of `lintPatchQueueDuplicateCreations` so
93
94
  // `status --ownership` can consume the structured data) lives in
@@ -0,0 +1,38 @@
1
+ import type { PatchQueueForwardRegistrationEntry, PatchQueueView } from './patch-lint-queue-types.js';
2
+ /** Which carriers `forward-registration` checks. */
3
+ export type ForwardRegistrationScope = 'support-files' | 'extended';
4
+ /** The carrier kind a forward registration was found in. */
5
+ export type ForwardRegistrationKind = 'support-files' | 'jar-mn' | 'moz-build' | 'test-manifest' | 'custom-elements';
6
+ /** One undeclared forward registration: a line naming a file a later patch creates. */
7
+ export interface ForwardRegistration {
8
+ /** The patch that adds the registration line. */
9
+ patch: string;
10
+ /** Carrier kind. */
11
+ kind: ForwardRegistrationKind;
12
+ /** Engine-relative path of the carrier file (the manifest, jar.mn, …). */
13
+ file: string;
14
+ /** The added line, whitespace-trimmed, that names the target. */
15
+ line: string;
16
+ /** The token on that line that names the target, as written. */
17
+ token: string;
18
+ /** Engine-relative path of the file the later patch creates. */
19
+ creates: string;
20
+ /** The later patch that creates it. */
21
+ owner: string;
22
+ /** How many distinct added lines in the carrier name the same target. */
23
+ occurrences: number;
24
+ /** Paste-and-run declaration that records the stage as intentional. */
25
+ command: string;
26
+ }
27
+ /** The discharge command, in the exact shape the support-files arm prints. */
28
+ export declare function formatRegistrationDeclaration(patch: string, file: string, line: string, creates: string, owner: string): string;
29
+ /**
30
+ * Finds every undeclared forward registration in the extended carriers:
31
+ * a registration line a patch adds that names a file only a later-ordered
32
+ * patch creates. Creators come from `createdFiles` (the `new file mode`
33
+ * sections), never `filesAffected`.
34
+ *
35
+ * @param ctx - Queue in application order
36
+ * @returns One record per (patch, carrier, created file), first line quoted
37
+ */
38
+ export declare function findExtendedForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>): ForwardRegistration[];
@@ -0,0 +1,240 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The opt-in arms of the `forward-registration` rule
4
+ * (`patchLint.forwardRegistration: "extended"`).
5
+ *
6
+ * The default rule covers test-manifest `support-files` only. These arms
7
+ * cover the carriers whose target resolves WITHOUT the build system:
8
+ *
9
+ * - a jar.mn pair line: the packaged source is on the line, in parentheses,
10
+ * relative to the jar.mn directory (or `/`-rooted at the source root);
11
+ * - a moz.build path token (a quoted string with a `.` or `/`), relative to
12
+ * the moz.build directory (or `/`-rooted);
13
+ * - a test manifest's section header (`["browser_foo.js"]`) and `head`,
14
+ * relative to the manifest directory;
15
+ * - a `customElements.js` `chrome://<pkg>/content/<path>` URL, resolved
16
+ * through the jar.mn lines the same queue adds (`content/<pkg>/<path>`
17
+ * is the jar target that URL serves).
18
+ *
19
+ * Anything else stays silent: a preprocessor or locale substitution, a
20
+ * `**` or `!` pattern, a path that climbs out of the tree, a chrome URL no
21
+ * queue jar.mn line packages. A rule that guessed there would refuse
22
+ * correct queues on a Firefox-sized tree.
23
+ *
24
+ * Opt-in because a queue that predates it can carry many such edges on a
25
+ * ratchet, and a new error on install would red a `--max-warnings 0` gate.
26
+ */
27
+ import { basename, dirname, posix } from 'node:path';
28
+ import { normalizePathSlashes } from '../utils/paths.js';
29
+ import { escapeRegex } from '../utils/regex.js';
30
+ import { parseJarMnEntry } from './build-audit-registration.js';
31
+ import { isLaterOwner, quoteRegistrationLine } from './patch-lint-staged-registration.js';
32
+ const TEST_MANIFEST_BASENAMES = new Set([
33
+ 'xpcshell.toml',
34
+ 'xpcshell.ini',
35
+ 'mochitest.toml',
36
+ 'mochitest.ini',
37
+ 'browser.toml',
38
+ 'browser.ini',
39
+ 'chrome.toml',
40
+ 'chrome.ini',
41
+ 'a11y.toml',
42
+ 'a11y.ini',
43
+ ]);
44
+ /**
45
+ * Resolves a path token against a base directory into a matcher. A leading
46
+ * `/` is rooted at the source tree. Only a `*` inside one segment is
47
+ * expanded. Substitutions (`%`, `@`), `**`, `!` and paths that climb out of
48
+ * the tree resolve to nothing.
49
+ */
50
+ function resolvePathToken(baseDir, token) {
51
+ if (/[%@!\s]/.test(token) || token.includes('**'))
52
+ return undefined;
53
+ const joined = token.startsWith('/')
54
+ ? posix.normalize(token.slice(1))
55
+ : posix.normalize(posix.join(baseDir, normalizePathSlashes(token)));
56
+ if (joined.startsWith('..') || joined === '.' || joined.length === 0)
57
+ return undefined;
58
+ if (!joined.includes('*')) {
59
+ return { exact: joined, matches: (candidate) => candidate === joined };
60
+ }
61
+ const pattern = new RegExp(`^${joined
62
+ .split('*')
63
+ .map((part) => escapeRegex(part))
64
+ .join('[^/]*')}$`);
65
+ return { matches: (candidate) => pattern.test(candidate) };
66
+ }
67
+ function carrierDir(file) {
68
+ const dir = normalizePathSlashes(dirname(file));
69
+ return dir === '.' ? '' : dir;
70
+ }
71
+ function jarMnReferences(file, content) {
72
+ const refs = [];
73
+ for (const raw of content.split('\n')) {
74
+ const parsed = parseJarMnEntry(raw);
75
+ if (!parsed)
76
+ continue;
77
+ const resolved = resolvePathToken(carrierDir(file), parsed.source);
78
+ if (resolved)
79
+ refs.push({ kind: 'jar-mn', file, line: raw.trim(), token: parsed.source, ...resolved });
80
+ }
81
+ return refs;
82
+ }
83
+ function mozBuildReferences(file, content) {
84
+ const refs = [];
85
+ for (const raw of content.split('\n')) {
86
+ if (raw.trimStart().startsWith('#'))
87
+ continue;
88
+ for (const quoted of raw.matchAll(/["']([^"']+)["']/g)) {
89
+ const token = quoted[1] ?? '';
90
+ // A path names a file: it has an extension or a directory. Bare words
91
+ // ("Firefox", a define's value) are not paths.
92
+ if (!token.includes('.') && !token.includes('/'))
93
+ continue;
94
+ const resolved = resolvePathToken(carrierDir(file), token);
95
+ if (resolved)
96
+ refs.push({ kind: 'moz-build', file, line: raw.trim(), token, ...resolved });
97
+ }
98
+ }
99
+ return refs;
100
+ }
101
+ function testManifestReferences(file, content) {
102
+ const refs = [];
103
+ for (const raw of content.split('\n')) {
104
+ const section = /^\s*\[\s*["']?([^"'\]]+?)["']?\s*\]\s*$/.exec(raw);
105
+ const head = /^\s*head\s*=\s*["']?([^"'\s]+)["']?\s*$/.exec(raw);
106
+ const token = section?.[1] ?? head?.[1];
107
+ if (token === undefined || token === 'DEFAULT')
108
+ continue;
109
+ const resolved = resolvePathToken(carrierDir(file), token);
110
+ if (resolved)
111
+ refs.push({ kind: 'test-manifest', file, line: raw.trim(), token, ...resolved });
112
+ }
113
+ return refs;
114
+ }
115
+ /**
116
+ * jar target → packaged source, over every jar.mn line the queue adds. Only
117
+ * exact (wildcard-free) sources can back a chrome URL lookup.
118
+ */
119
+ function buildJarTargetIndex(entries) {
120
+ const index = new Map();
121
+ for (const entry of entries) {
122
+ for (const [file, content] of [...entry.newFiles, ...entry.modifiedFileAdditions]) {
123
+ if (basename(normalizePathSlashes(file)) !== 'jar.mn')
124
+ continue;
125
+ for (const raw of content.split('\n')) {
126
+ const parsed = parseJarMnEntry(raw);
127
+ if (!parsed)
128
+ continue;
129
+ const resolved = resolvePathToken(carrierDir(file), parsed.source);
130
+ if (resolved?.exact !== undefined)
131
+ index.set(parsed.target, resolved.exact);
132
+ }
133
+ }
134
+ }
135
+ return index;
136
+ }
137
+ function customElementsReferences(file, content, jarTargets) {
138
+ const refs = [];
139
+ for (const raw of content.split('\n')) {
140
+ for (const url of raw.matchAll(/chrome:\/\/([\w.-]+)\/content\/([^"'\s)]+)/g)) {
141
+ const source = jarTargets.get(`content/${url[1] ?? ''}/${url[2] ?? ''}`);
142
+ if (source === undefined)
143
+ continue;
144
+ refs.push({
145
+ kind: 'custom-elements',
146
+ file,
147
+ line: raw.trim(),
148
+ token: url[0],
149
+ exact: source,
150
+ matches: (candidate) => candidate === source,
151
+ });
152
+ }
153
+ }
154
+ return refs;
155
+ }
156
+ function referencesFor(entry, jarTargets) {
157
+ const refs = [];
158
+ for (const [rawPath, content] of [...entry.newFiles, ...entry.modifiedFileAdditions]) {
159
+ const file = normalizePathSlashes(rawPath);
160
+ const name = basename(file);
161
+ if (name === 'jar.mn')
162
+ refs.push(...jarMnReferences(file, content));
163
+ else if (name === 'moz.build')
164
+ refs.push(...mozBuildReferences(file, content));
165
+ else if (TEST_MANIFEST_BASENAMES.has(name))
166
+ refs.push(...testManifestReferences(file, content));
167
+ else if (name === 'customElements.js') {
168
+ refs.push(...customElementsReferences(file, content, jarTargets));
169
+ }
170
+ }
171
+ return refs;
172
+ }
173
+ function isDeclared(entry, file, created) {
174
+ return (entry.metadata?.stagedDependencies?.registrations ?? []).some((registration) => normalizePathSlashes(registration.file) === file &&
175
+ normalizePathSlashes(registration.creates) === created);
176
+ }
177
+ /** The discharge command, in the exact shape the support-files arm prints. */
178
+ export function formatRegistrationDeclaration(patch, file, line, creates, owner) {
179
+ return (`fireforge patch staged-dependency ${patch} --add --kind registration ` +
180
+ `--file ${file} --line "${quoteRegistrationLine(line)}" ` +
181
+ `--creates ${creates} --owner ${owner}`);
182
+ }
183
+ /**
184
+ * Finds every undeclared forward registration in the extended carriers:
185
+ * a registration line a patch adds that names a file only a later-ordered
186
+ * patch creates. Creators come from `createdFiles` (the `new file mode`
187
+ * sections), never `filesAffected`.
188
+ *
189
+ * @param ctx - Queue in application order
190
+ * @returns One record per (patch, carrier, created file), first line quoted
191
+ */
192
+ export function findExtendedForwardRegistrations(ctx) {
193
+ const jarTargets = buildJarTargetIndex(ctx.entries);
194
+ const creatorsByPath = new Map();
195
+ for (const entry of ctx.entries) {
196
+ for (const created of entry.createdFiles) {
197
+ const path = normalizePathSlashes(created);
198
+ const list = creatorsByPath.get(path) ?? [];
199
+ list.push(entry);
200
+ creatorsByPath.set(path, list);
201
+ }
202
+ }
203
+ const found = new Map();
204
+ const linesByKey = new Map();
205
+ for (const entry of ctx.entries) {
206
+ for (const ref of referencesFor(entry, jarTargets)) {
207
+ const candidates = ref.exact !== undefined
208
+ ? (creatorsByPath.get(ref.exact) ?? []).map((owner) => [ref.exact ?? '', owner])
209
+ : [...creatorsByPath].flatMap(([path, owners]) => ref.matches(path) ? owners.map((owner) => [path, owner]) : []);
210
+ for (const [created, later] of candidates) {
211
+ if (!isLaterOwner(later, entry))
212
+ continue;
213
+ if (isDeclared(entry, ref.file, created))
214
+ continue;
215
+ const key = `${entry.filename}|${ref.file}|${created}|${later.filename}`;
216
+ const existing = found.get(key);
217
+ if (existing !== undefined) {
218
+ const lines = linesByKey.get(key) ?? new Set();
219
+ lines.add(ref.line);
220
+ linesByKey.set(key, lines);
221
+ existing.occurrences = lines.size;
222
+ continue;
223
+ }
224
+ linesByKey.set(key, new Set([ref.line]));
225
+ found.set(key, {
226
+ patch: entry.filename,
227
+ kind: ref.kind,
228
+ file: ref.file,
229
+ line: ref.line,
230
+ token: ref.token,
231
+ creates: created,
232
+ owner: later.filename,
233
+ occurrences: 1,
234
+ command: formatRegistrationDeclaration(entry.filename, ref.file, ref.line, created, later.filename),
235
+ });
236
+ }
237
+ }
238
+ }
239
+ return [...found.values()];
240
+ }
@@ -1,4 +1,5 @@
1
1
  import type { PatchLintIssue } from '../types/commands/index.js';
2
+ import { type ForwardRegistration, type ForwardRegistrationScope } from './patch-lint-forward-registration-extended.js';
2
3
  import type { PatchQueueForwardRegistrationEntry, PatchQueueView } from './patch-lint-queue-types.js';
3
4
  /**
4
5
  * One `support-files` entry, with the line the patch actually adds it on.
@@ -48,11 +49,23 @@ export declare function extractSupportFileEntries(content: string): SupportFileE
48
49
  */
49
50
  export declare function buildSupportFileMatcher(manifestPath: string, entry: string): ((candidate: string) => boolean) | undefined;
50
51
  /**
51
- * Flags every `support-files` registration a patch introduces that names a
52
- * file only a later-ordered patch creates, unless the staged dependency is
53
- * declared.
52
+ * Every undeclared forward registration in the queue, over the carriers
53
+ * `scope` selects: `support-files` (the default) or `extended` (adds
54
+ * jar.mn, moz.build, test-manifest sections and customElements.js; see
55
+ * `patch-lint-forward-registration-extended.ts`).
54
56
  *
55
- * @param ctx - Projected queue in application order
57
+ * @param ctx - Queue in application order
58
+ * @param scope - Which carriers to check
59
+ */
60
+ export declare function collectForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>, scope?: ForwardRegistrationScope): ForwardRegistration[];
61
+ /**
62
+ * Flags every forward registration the queue's scope selects, unless the
63
+ * staged dependency is declared.
64
+ *
65
+ * @param ctx - Projected queue in application order, with its
66
+ * `forwardRegistration` scope (default `support-files`)
56
67
  * @returns One issue per undeclared forward registration
57
68
  */
58
- export declare function lintPatchQueueForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry>): PatchLintIssue[];
69
+ export declare function lintPatchQueueForwardRegistrations(ctx: PatchQueueView<PatchQueueForwardRegistrationEntry> & {
70
+ forwardRegistration?: ForwardRegistrationScope;
71
+ }): PatchLintIssue[];