@hominis/fireforge 0.30.1 → 0.32.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 (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +22 -0
  3. package/dist/src/commands/export-all.js +9 -16
  4. package/dist/src/commands/export-flow.d.ts +6 -0
  5. package/dist/src/commands/export-flow.js +6 -1
  6. package/dist/src/commands/export-placement-gate.d.ts +38 -0
  7. package/dist/src/commands/export-placement-gate.js +105 -0
  8. package/dist/src/commands/export-shared.d.ts +28 -0
  9. package/dist/src/commands/export-shared.js +46 -1
  10. package/dist/src/commands/export.js +52 -113
  11. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
  12. package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
  13. package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
  14. package/dist/src/commands/furnace/create.d.ts +1 -2
  15. package/dist/src/commands/furnace/deploy.js +36 -114
  16. package/dist/src/commands/furnace/refresh.js +52 -32
  17. package/dist/src/commands/furnace/sync.js +2 -0
  18. package/dist/src/commands/import.js +108 -73
  19. package/dist/src/commands/lint-per-patch.d.ts +3 -1
  20. package/dist/src/commands/lint-per-patch.js +265 -74
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +193 -88
  23. package/dist/src/commands/patch/compact.d.ts +5 -2
  24. package/dist/src/commands/patch/compact.js +85 -25
  25. package/dist/src/commands/patch/delete.js +17 -17
  26. package/dist/src/commands/patch/index.js +2 -0
  27. package/dist/src/commands/patch/lint-ignore.js +3 -16
  28. package/dist/src/commands/patch/move-files.js +2 -0
  29. package/dist/src/commands/patch/patch-context.d.ts +41 -0
  30. package/dist/src/commands/patch/patch-context.js +53 -0
  31. package/dist/src/commands/patch/rename.js +10 -15
  32. package/dist/src/commands/patch/reorder.d.ts +0 -2
  33. package/dist/src/commands/patch/reorder.js +18 -19
  34. package/dist/src/commands/patch/split-plan.d.ts +66 -0
  35. package/dist/src/commands/patch/split-plan.js +178 -0
  36. package/dist/src/commands/patch/split.d.ts +30 -0
  37. package/dist/src/commands/patch/split.js +283 -0
  38. package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
  39. package/dist/src/commands/patch/staged-dependency.js +4 -17
  40. package/dist/src/commands/patch/tier.js +4 -17
  41. package/dist/src/commands/re-export-files.js +4 -1
  42. package/dist/src/commands/re-export-scan.js +8 -1
  43. package/dist/src/commands/re-export.js +8 -1
  44. package/dist/src/commands/rebase/summary.d.ts +1 -5
  45. package/dist/src/commands/rebase/summary.js +1 -1
  46. package/dist/src/commands/status-output.js +77 -68
  47. package/dist/src/commands/test-diagnose.d.ts +23 -0
  48. package/dist/src/commands/test-diagnose.js +210 -0
  49. package/dist/src/commands/test-run.d.ts +68 -0
  50. package/dist/src/commands/test-run.js +97 -0
  51. package/dist/src/commands/test.js +214 -263
  52. package/dist/src/commands/token.js +15 -1
  53. package/dist/src/commands/wire.js +109 -78
  54. package/dist/src/core/build-audit.d.ts +1 -1
  55. package/dist/src/core/build-audit.js +2 -46
  56. package/dist/src/core/build-baseline-types.d.ts +38 -0
  57. package/dist/src/core/build-baseline-types.js +10 -0
  58. package/dist/src/core/build-baseline.d.ts +1 -31
  59. package/dist/src/core/build-prepare.d.ts +1 -1
  60. package/dist/src/core/build-prepare.js +2 -45
  61. package/dist/src/core/config-paths.d.ts +0 -8
  62. package/dist/src/core/config-paths.js +4 -4
  63. package/dist/src/core/config-state.d.ts +0 -6
  64. package/dist/src/core/config-state.js +1 -1
  65. package/dist/src/core/config-validate-patch-policy.js +12 -13
  66. package/dist/src/core/config-validate.js +74 -28
  67. package/dist/src/core/engine-changes.d.ts +24 -0
  68. package/dist/src/core/engine-changes.js +64 -0
  69. package/dist/src/core/firefox-cache.d.ts +0 -5
  70. package/dist/src/core/firefox-cache.js +1 -1
  71. package/dist/src/core/firefox-download.d.ts +0 -6
  72. package/dist/src/core/firefox-download.js +1 -1
  73. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  74. package/dist/src/core/furnace-apply-helpers.js +11 -20
  75. package/dist/src/core/furnace-apply.d.ts +1 -1
  76. package/dist/src/core/furnace-apply.js +1 -1
  77. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  78. package/dist/src/core/furnace-checksum-utils.js +15 -0
  79. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  80. package/dist/src/core/furnace-config-validate.js +133 -0
  81. package/dist/src/core/furnace-config.d.ts +4 -32
  82. package/dist/src/core/furnace-config.js +15 -111
  83. package/dist/src/core/furnace-constants.d.ts +0 -10
  84. package/dist/src/core/furnace-constants.js +2 -2
  85. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  86. package/dist/src/core/furnace-css-fragments.js +243 -0
  87. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  88. package/dist/src/core/furnace-jsconfig.js +191 -0
  89. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  90. package/dist/src/core/furnace-validate-helpers.js +40 -1
  91. package/dist/src/core/furnace-validate-registration.js +16 -1
  92. package/dist/src/core/furnace-validate.js +54 -2
  93. package/dist/src/core/git-base.d.ts +15 -0
  94. package/dist/src/core/git-base.js +32 -0
  95. package/dist/src/core/git-diff.d.ts +8 -0
  96. package/dist/src/core/git-diff.js +224 -59
  97. package/dist/src/core/git-file-ops.d.ts +39 -12
  98. package/dist/src/core/git-file-ops.js +84 -3
  99. package/dist/src/core/lint-cache.d.ts +0 -13
  100. package/dist/src/core/lint-cache.js +5 -5
  101. package/dist/src/core/mach.d.ts +22 -1
  102. package/dist/src/core/mach.js +27 -2
  103. package/dist/src/core/manifest-register.d.ts +5 -16
  104. package/dist/src/core/manifest-register.js +3 -1
  105. package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
  106. package/dist/src/core/patch-lint-checkjs.js +263 -71
  107. package/dist/src/core/patch-lint-css.d.ts +23 -0
  108. package/dist/src/core/patch-lint-css.js +172 -0
  109. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  110. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  111. package/dist/src/core/patch-lint-observer.js +168 -0
  112. package/dist/src/core/patch-lint.d.ts +34 -11
  113. package/dist/src/core/patch-lint.js +24 -161
  114. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  115. package/dist/src/core/patch-manifest-io.js +44 -2
  116. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  117. package/dist/src/core/patch-manifest-validate.js +1 -1
  118. package/dist/src/core/patch-manifest.d.ts +1 -1
  119. package/dist/src/core/patch-manifest.js +1 -1
  120. package/dist/src/core/patch-policy.d.ts +0 -4
  121. package/dist/src/core/patch-policy.js +10 -4
  122. package/dist/src/core/register-browser-content.d.ts +1 -1
  123. package/dist/src/core/register-module.d.ts +1 -1
  124. package/dist/src/core/register-result.d.ts +21 -0
  125. package/dist/src/core/register-result.js +9 -0
  126. package/dist/src/core/register-shared-css.d.ts +1 -1
  127. package/dist/src/core/register-test-manifest.d.ts +1 -1
  128. package/dist/src/core/test-harness-crash.d.ts +61 -0
  129. package/dist/src/core/test-harness-crash.js +140 -0
  130. package/dist/src/core/test-stale-check.d.ts +1 -1
  131. package/dist/src/core/test-stale-check.js +2 -46
  132. package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
  133. package/dist/src/core/test-xpcshell-retry.js +10 -3
  134. package/dist/src/core/token-dark-mode.js +14 -26
  135. package/dist/src/core/token-manager.d.ts +4 -0
  136. package/dist/src/core/token-manager.js +70 -16
  137. package/dist/src/core/typecheck-shim.d.ts +3 -22
  138. package/dist/src/core/typecheck-shim.js +69 -7
  139. package/dist/src/core/wire-utils.js +37 -44
  140. package/dist/src/types/commands/index.d.ts +1 -1
  141. package/dist/src/types/commands/options.d.ts +122 -0
  142. package/dist/src/types/config.d.ts +11 -2
  143. package/dist/src/types/furnace.d.ts +12 -1
  144. package/dist/src/utils/elapsed.d.ts +0 -2
  145. package/dist/src/utils/elapsed.js +1 -1
  146. package/dist/src/utils/fs.d.ts +0 -5
  147. package/dist/src/utils/fs.js +1 -1
  148. package/dist/src/utils/regex.d.ts +0 -6
  149. package/dist/src/utils/regex.js +3 -3
  150. package/dist/src/utils/validation.d.ts +0 -8
  151. package/dist/src/utils/validation.js +2 -2
  152. package/package.json +6 -4
@@ -312,8 +312,33 @@ export async function test(engineDir, testPaths = [], args = []) {
312
312
  }
313
313
  /**
314
314
  * Runs mach test while capturing streamed output for better diagnostics.
315
+ *
316
+ * @param env - Optional extra environment variables for the mach process
317
+ * (merged over `process.env` by the exec layer). Used by
318
+ * `fireforge test --perf-samples` to publish the artifact-path contract.
319
+ */
320
+ export async function testWithOutput(engineDir, testPaths = [], args = [], env) {
321
+ return runMachCapture(['test', ...testPaths, ...args], engineDir, env ? { env } : {});
322
+ }
323
+ /**
324
+ * Runs `mach xpcshell-test` (the suite-specific xpcshell command) while
325
+ * capturing output. Unlike the generic `mach test`, the suite-specific
326
+ * commands degrade a broken mozlog resource monitor to a warning instead of
327
+ * crashing at startup, so `fireforge test` dispatches single-suite runs here
328
+ * to stay resilient to the host psutil failure (field report E1).
329
+ *
330
+ * Signature mirrors {@link testWithOutput} so the two are interchangeable in
331
+ * the dispatch path.
332
+ */
333
+ export async function xpcshellTestWithOutput(engineDir, testPaths = [], args = [], env) {
334
+ return runMachCapture(['xpcshell-test', ...testPaths, ...args], engineDir, env ? { env } : {});
335
+ }
336
+ /**
337
+ * Runs `mach mochitest` (covers browser-chrome / mochitest flavors) while
338
+ * capturing output. The suite-specific counterpart to {@link testWithOutput}
339
+ * for non-xpcshell single-suite runs — see {@link xpcshellTestWithOutput}.
315
340
  */
316
- export async function testWithOutput(engineDir, testPaths = [], args = []) {
317
- return runMachCapture(['test', ...testPaths, ...args], engineDir);
341
+ export async function mochitestWithOutput(engineDir, testPaths = [], args = [], env) {
342
+ return runMachCapture(['mochitest', ...testPaths, ...args], engineDir, env ? { env } : {});
318
343
  }
319
344
  //# sourceMappingURL=mach.js.map
@@ -1,22 +1,11 @@
1
1
  /**
2
2
  * Manifest registration barrel — re-exports all registration targets
3
- * and provides the shared RegisterResult interface.
3
+ * and the shared RegisterResult interface (which lives in
4
+ * `register-result.ts` so the leaf modules can import it without
5
+ * creating a cycle through this barrel).
4
6
  */
5
- /**
6
- * Result of a manifest registration operation.
7
- */
8
- export interface RegisterResult {
9
- /** The manifest file that was modified */
10
- manifest: string;
11
- /** The entry that was inserted */
12
- entry: string;
13
- /** The entry after which the new entry was inserted (for user display) */
14
- previousEntry?: string | undefined;
15
- /** Whether the entry already existed (skipped) */
16
- skipped: boolean;
17
- /** Whether --after target was not found and fell back to alphabetical */
18
- afterFallback?: boolean | undefined;
19
- }
7
+ import type { RegisterResult } from './register-result.js';
8
+ export type { RegisterResult } from './register-result.js';
20
9
  export { registerBrowserContent } from './register-browser-content.js';
21
10
  export { registerFireForgeModule } from './register-module.js';
22
11
  export { registerSharedCSS } from './register-shared-css.js';
@@ -1,7 +1,9 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * Manifest registration barrel — re-exports all registration targets
4
- * and provides the shared RegisterResult interface.
4
+ * and the shared RegisterResult interface (which lives in
5
+ * `register-result.ts` so the leaf modules can import it without
6
+ * creating a cycle through this barrel).
5
7
  */
6
8
  import { join } from 'node:path';
7
9
  import { GeneralError } from '../errors/base.js';
@@ -15,35 +15,89 @@
15
15
  * `patchLint.checkJsStrict` only tightens `strict` / `noImplicitAny`
16
16
  * and optional allowlisted `checkJsCompilerOptions`; it does not change
17
17
  * shim composition or suppressed diagnostic codes.
18
+ *
19
+ * Resolution scope vs reporting scope: the TS program is built over a
20
+ * *resolution* set (every patch-owned `.sys.mjs` the run cares about, so
21
+ * cross-patch `resource:///` imports resolve to their real sources), while
22
+ * diagnostics are emitted only for files in the *report* scope. Splitting
23
+ * the two lets per-patch lint build one queue-wide program and attribute
24
+ * findings per patch, and lets export/re-export resolve cross-patch imports
25
+ * while reporting only the patch under export.
18
26
  */
19
27
  import type { PatchLintIssue } from '../types/commands/index.js';
20
28
  import type { PatchLintCheckJsCompilerOptions, PatchLintConfig } from '../types/config.js';
29
+ /** How a checkJs run controls reporting and resolution; see module docs. */
30
+ export interface CheckJsMode {
31
+ strict: boolean;
32
+ compilerOptions?: PatchLintCheckJsCompilerOptions;
33
+ }
34
+ /**
35
+ * Result of {@link runCheckJsGrouped}: diagnostics attributed to the
36
+ * patch-owned file they originate in (`byFile`, keyed by repo-relative
37
+ * path), plus run-level errors that have no owning file (`global` — e.g.
38
+ * TypeScript missing or an unreadable extra shim).
39
+ */
40
+ export interface GroupedCheckJsResult {
41
+ byFile: Map<string, PatchLintIssue[]>;
42
+ global: PatchLintIssue[];
43
+ }
44
+ /**
45
+ * Builds the checkJs program **once** over `resolutionOwned` and returns its
46
+ * diagnostics grouped by originating file. Callers slice the result by their
47
+ * own report scope — per-patch lint attributes each file to its owning
48
+ * patch, export/re-export keeps only the patch under export. Resolution
49
+ * always spans every file in `resolutionOwned`, so cross-patch
50
+ * `resource:///`/`chrome://` imports resolve to real sources.
51
+ *
52
+ * @param repoDir - Absolute engine (repository) directory
53
+ * @param resolutionOwned - Patch-owned `.sys.mjs` paths (relative to repoDir)
54
+ * the program should see and resolve against
55
+ * @param extraShimPath - Optional project-relative extra `.d.ts` appended to
56
+ * the built-in Firefox-globals shim (from `patchLint.checkJsExtraShim`)
57
+ * @param projectRoot - Absolute project root for resolving `extraShimPath`
58
+ * @param mode - Strictness preset plus allowlisted compiler-option overrides
59
+ * @returns Diagnostics grouped per owning file plus run-level errors
60
+ */
61
+ export declare function runCheckJsGrouped(repoDir: string, resolutionOwned: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode): Promise<GroupedCheckJsResult>;
21
62
  /**
22
- * Runs TypeScript's checkJs pass on patch-owned `.sys.mjs` files.
63
+ * Flattens a {@link runCheckJsGrouped} run into a single issue list. When
64
+ * `reportScope` is supplied, only diagnostics from files in that set are
65
+ * returned (resolution still spans every owned file); omitting it reports
66
+ * every owned file's diagnostics — the historical whole-set behaviour.
23
67
  *
24
- * @param repoDir - Absolute path to the engine (repository) directory
25
- * @param patchOwnedFiles - Set of patch-owned `.sys.mjs` file paths (relative to repoDir)
26
- * @param extraShimPath - Optional project-relative path to an additional
27
- * `.d.ts` file whose contents are concatenated to the built-in
28
- * Firefox-globals shim. Sourced from `patchLint.checkJsExtraShim`.
29
- * Resolved against `projectRoot` (one level up from `repoDir` is the
30
- * wrong root — patches sit inside `engine/` while the shim lives at
31
- * the project root, so the caller passes both).
32
- * @param projectRoot - Absolute project root for resolving `extraShimPath`.
33
- * Defaults to `repoDir` for back-compat with callers that don't
34
- * pass an extra shim (no resolution actually happens in that case).
35
- * @param mode - When `strict` is true, enables `strict` and `noImplicitAny`
36
- * (CI-style). Optional `compilerOptions` merges allowlisted boolean
37
- * overrides after that preset (from `patchLint.checkJsCompilerOptions`).
38
- * Omitted or `{ strict: false }` preserves the historical loose preset.
68
+ * @param repoDir - Absolute engine (repository) directory
69
+ * @param patchOwnedFiles - Patch-owned `.sys.mjs` paths to resolve against
70
+ * @param extraShimPath - Optional project-relative extra `.d.ts`
71
+ * @param projectRoot - Absolute project root for resolving `extraShimPath`
72
+ * @param mode - Strictness preset plus allowlisted compiler-option overrides
73
+ * @param reportScope - When set, restrict reported diagnostics to these
74
+ * repo-relative files
39
75
  * @returns Array of lint issues from TS diagnostics
40
76
  */
41
- export declare function runCheckJs(repoDir: string, patchOwnedFiles: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: {
42
- strict: boolean;
43
- compilerOptions?: PatchLintCheckJsCompilerOptions;
44
- }): Promise<PatchLintIssue[]>;
77
+ export declare function runCheckJs(repoDir: string, patchOwnedFiles: Set<string>, extraShimPath?: string, projectRoot?: string, mode?: CheckJsMode, reportScope?: ReadonlySet<string>): Promise<PatchLintIssue[]>;
45
78
  /**
46
79
  * Invokes {@link runCheckJs} for a `patchLint` block with `checkJs: true`.
47
80
  * `projectRoot` is the FireForge project root (`dirname(engine)`).
81
+ *
82
+ * @param repoDir - Absolute engine (repository) directory
83
+ * @param patchOwnedFiles - Patch-owned `.sys.mjs` paths to resolve against
84
+ * @param patchLint - The resolved `patchLint` config block
85
+ * @param projectRoot - FireForge project root for shim resolution
86
+ * @param reportScope - Optional repo-relative files to report on (export /
87
+ * re-export passes the patch under export so cross-patch resolution does
88
+ * not surface other patches' diagnostics)
89
+ */
90
+ export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string, reportScope?: ReadonlySet<string>): Promise<PatchLintIssue[]>;
91
+ /**
92
+ * Grouped variant of {@link invokePatchLintCheckJs}: builds one queue-wide
93
+ * checkJs program over `patchOwnedFiles` and returns its findings grouped by
94
+ * owning file. The per-patch lint orchestrator calls this **once per run**
95
+ * and attributes each file's findings to its owning patch, instead of
96
+ * rebuilding the same program for every patch in the queue.
97
+ *
98
+ * @param repoDir - Absolute engine (repository) directory
99
+ * @param patchOwnedFiles - Every patch-owned `.sys.mjs` in the queue
100
+ * @param patchLint - The resolved `patchLint` config block
101
+ * @param projectRoot - FireForge project root for shim resolution
48
102
  */
49
- export declare function invokePatchLintCheckJs(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string): Promise<PatchLintIssue[]>;
103
+ export declare function invokePatchLintCheckJsGrouped(repoDir: string, patchOwnedFiles: Set<string>, patchLint: PatchLintConfig, projectRoot: string): Promise<GroupedCheckJsResult>;
@@ -16,8 +16,16 @@
16
16
  * `patchLint.checkJsStrict` only tightens `strict` / `noImplicitAny`
17
17
  * and optional allowlisted `checkJsCompilerOptions`; it does not change
18
18
  * shim composition or suppressed diagnostic codes.
19
+ *
20
+ * Resolution scope vs reporting scope: the TS program is built over a
21
+ * *resolution* set (every patch-owned `.sys.mjs` the run cares about, so
22
+ * cross-patch `resource:///` imports resolve to their real sources), while
23
+ * diagnostics are emitted only for files in the *report* scope. Splitting
24
+ * the two lets per-patch lint build one queue-wide program and attribute
25
+ * findings per patch, and lets export/re-export resolve cross-patch imports
26
+ * while reporting only the patch under export.
19
27
  */
20
- import { resolve } from 'node:path';
28
+ import { basename, resolve } from 'node:path';
21
29
  import { pathExists } from '../utils/fs.js';
22
30
  import { verbose } from '../utils/logger.js';
23
31
  import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES } from './typecheck-shim.js';
@@ -25,56 +33,158 @@ import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES } from '.
25
33
  // Public API
26
34
  // ---------------------------------------------------------------------------
27
35
  /**
28
- * Runs TypeScript's checkJs pass on patch-owned `.sys.mjs` files.
36
+ * Builds the host-side module resolver for the checkJs pass: maps an import
37
+ * specifier to a patch-owned absolute path when the specifier's final
38
+ * segment uniquely matches an owned file. URL specifiers
39
+ * (chrome://browser/content/Foo.sys.mjs, resource:///modules/Foo.sys.mjs)
40
+ * are matched by basename, with a `.mjs` → `.sys.mjs` fallback for deployed
41
+ * widget URLs. Ambiguous or unknown basenames stay unresolved — loose
42
+ * wildcard typing beats guessing the wrong module — and relative specifiers
43
+ * are left to fail resolution (the relative-import lint rule bans them).
44
+ */
45
+ function createOwnedSpecifierResolver(ts, ownedAbsolute) {
46
+ const ownedByBasename = new Map();
47
+ for (const abs of ownedAbsolute) {
48
+ const base = basename(abs);
49
+ const list = ownedByBasename.get(base) ?? [];
50
+ list.push(abs);
51
+ ownedByBasename.set(base, list);
52
+ }
53
+ return (specifier) => {
54
+ if (specifier.startsWith('.'))
55
+ return undefined;
56
+ const cleaned = specifier.split(/[?#]/)[0] ?? specifier;
57
+ const segment = cleaned.slice(cleaned.lastIndexOf('/') + 1);
58
+ if (!segment)
59
+ return undefined;
60
+ const candidates = [...(ownedByBasename.get(segment) ?? [])];
61
+ if (segment.endsWith('.mjs') && !segment.endsWith('.sys.mjs')) {
62
+ candidates.push(...(ownedByBasename.get(segment.replace(/\.mjs$/, '.sys.mjs')) ?? []));
63
+ }
64
+ if (candidates.length !== 1)
65
+ return undefined;
66
+ return {
67
+ resolvedFileName: candidates[0],
68
+ extension: ts.Extension.Mjs,
69
+ isExternalLibraryImport: false,
70
+ };
71
+ };
72
+ }
73
+ /** Maps a resolved file path to the TS extension enum the host must report. */
74
+ function extensionForFile(ts, file) {
75
+ if (file.endsWith('.d.ts'))
76
+ return ts.Extension.Dts;
77
+ if (file.endsWith('.ts'))
78
+ return ts.Extension.Ts;
79
+ if (file.endsWith('.tsx'))
80
+ return ts.Extension.Tsx;
81
+ if (file.endsWith('.cjs'))
82
+ return ts.Extension.Cjs;
83
+ if (file.endsWith('.jsx'))
84
+ return ts.Extension.Jsx;
85
+ if (file.endsWith('.json'))
86
+ return ts.Extension.Json;
87
+ return ts.Extension.Mjs;
88
+ }
89
+ /**
90
+ * Builds a resolver for a reviewed `paths` mapping (route 2 of the
91
+ * cross-patch resolution work). Each pattern may contain a single `*`;
92
+ * matching targets are resolved relative to `baseDir` (the engine dir, like
93
+ * the rest of `patchLint` which is engine-relative). Resolved files are
94
+ * recorded via `onResolved` so the compiler host knows to read them from
95
+ * disk rather than returning empty content. No `baseUrl` is set, so this is
96
+ * TS5090-safe: `paths` resolution is host-driven here, not config-driven.
97
+ */
98
+ function createPathsResolver(ts, paths, baseDir, fileExists, onResolved) {
99
+ const entries = Object.entries(paths);
100
+ return (specifier) => {
101
+ for (const [pattern, targets] of entries) {
102
+ const star = pattern.indexOf('*');
103
+ let captured;
104
+ if (star === -1) {
105
+ if (specifier !== pattern)
106
+ continue;
107
+ captured = '';
108
+ }
109
+ else {
110
+ const prefix = pattern.slice(0, star);
111
+ const suffix = pattern.slice(star + 1);
112
+ if (specifier.length < prefix.length + suffix.length)
113
+ continue;
114
+ if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix))
115
+ continue;
116
+ captured = specifier.slice(prefix.length, specifier.length - suffix.length);
117
+ }
118
+ for (const target of targets) {
119
+ const rel = target.includes('*') ? target.replace('*', captured) : target;
120
+ const abs = resolve(baseDir, rel);
121
+ if (!fileExists(abs))
122
+ continue;
123
+ onResolved(abs);
124
+ return {
125
+ resolvedFileName: abs,
126
+ extension: extensionForFile(ts, abs),
127
+ isExternalLibraryImport: false,
128
+ };
129
+ }
130
+ }
131
+ return undefined;
132
+ };
133
+ }
134
+ /**
135
+ * Builds the checkJs program **once** over `resolutionOwned` and returns its
136
+ * diagnostics grouped by originating file. Callers slice the result by their
137
+ * own report scope — per-patch lint attributes each file to its owning
138
+ * patch, export/re-export keeps only the patch under export. Resolution
139
+ * always spans every file in `resolutionOwned`, so cross-patch
140
+ * `resource:///`/`chrome://` imports resolve to real sources.
29
141
  *
30
- * @param repoDir - Absolute path to the engine (repository) directory
31
- * @param patchOwnedFiles - Set of patch-owned `.sys.mjs` file paths (relative to repoDir)
32
- * @param extraShimPath - Optional project-relative path to an additional
33
- * `.d.ts` file whose contents are concatenated to the built-in
34
- * Firefox-globals shim. Sourced from `patchLint.checkJsExtraShim`.
35
- * Resolved against `projectRoot` (one level up from `repoDir` is the
36
- * wrong root patches sit inside `engine/` while the shim lives at
37
- * the project root, so the caller passes both).
38
- * @param projectRoot - Absolute project root for resolving `extraShimPath`.
39
- * Defaults to `repoDir` for back-compat with callers that don't
40
- * pass an extra shim (no resolution actually happens in that case).
41
- * @param mode - When `strict` is true, enables `strict` and `noImplicitAny`
42
- * (CI-style). Optional `compilerOptions` merges allowlisted boolean
43
- * overrides after that preset (from `patchLint.checkJsCompilerOptions`).
44
- * Omitted or `{ strict: false }` preserves the historical loose preset.
45
- * @returns Array of lint issues from TS diagnostics
142
+ * @param repoDir - Absolute engine (repository) directory
143
+ * @param resolutionOwned - Patch-owned `.sys.mjs` paths (relative to repoDir)
144
+ * the program should see and resolve against
145
+ * @param extraShimPath - Optional project-relative extra `.d.ts` appended to
146
+ * the built-in Firefox-globals shim (from `patchLint.checkJsExtraShim`)
147
+ * @param projectRoot - Absolute project root for resolving `extraShimPath`
148
+ * @param mode - Strictness preset plus allowlisted compiler-option overrides
149
+ * @returns Diagnostics grouped per owning file plus run-level errors
46
150
  */
47
- export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projectRoot, mode) {
48
- if (patchOwnedFiles.size === 0)
49
- return [];
151
+ export async function runCheckJsGrouped(repoDir, resolutionOwned, extraShimPath, projectRoot, mode) {
152
+ const empty = { byFile: new Map(), global: [] };
153
+ if (resolutionOwned.size === 0)
154
+ return empty;
50
155
  // Dynamic import — typescript stays as a dev dependency
51
156
  let ts;
52
157
  try {
53
158
  ts = await import('typescript');
54
159
  }
55
160
  catch {
56
- return [
57
- {
58
- file: '(checkJs)',
59
- check: 'checkjs-type-error',
60
- message: 'patchLint.checkJs is enabled but the "typescript" package is not installed. ' +
61
- 'Run "npm install typescript" to enable type checking.',
62
- severity: 'error',
63
- },
64
- ];
161
+ return {
162
+ byFile: new Map(),
163
+ global: [
164
+ {
165
+ file: '(checkJs)',
166
+ check: 'checkjs-type-error',
167
+ message: 'patchLint.checkJs is enabled but the "typescript" package is not installed. ' +
168
+ 'Run "npm install typescript" to enable type checking.',
169
+ severity: 'error',
170
+ },
171
+ ],
172
+ };
65
173
  }
66
174
  // Resolve absolute paths for root files, filtering to files that exist
67
175
  const rootFiles = [];
68
176
  const ownedAbsolute = new Set();
69
- for (const rel of patchOwnedFiles) {
177
+ const relByAbsolute = new Map();
178
+ for (const rel of resolutionOwned) {
70
179
  const abs = resolve(repoDir, rel);
71
180
  if (await pathExists(abs)) {
72
181
  rootFiles.push(abs);
73
182
  ownedAbsolute.add(abs);
183
+ relByAbsolute.set(abs, rel);
74
184
  }
75
185
  }
76
186
  if (rootFiles.length === 0)
77
- return [];
187
+ return empty;
78
188
  // Compose the shim. `extraShimPath` is project-relative (validated
79
189
  // by config-validate); resolve it against `projectRoot`. When the
80
190
  // caller passes neither, fall back to `repoDir` — the only way the
@@ -89,14 +199,17 @@ export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projec
89
199
  }
90
200
  }
91
201
  catch (err) {
92
- return [
93
- {
94
- file: extraShimPath ?? '(checkJs)',
95
- check: 'checkjs-type-error',
96
- message: err instanceof Error ? err.message : String(err),
97
- severity: 'error',
98
- },
99
- ];
202
+ return {
203
+ byFile: new Map(),
204
+ global: [
205
+ {
206
+ file: extraShimPath ?? '(checkJs)',
207
+ check: 'checkjs-type-error',
208
+ message: err instanceof Error ? err.message : String(err),
209
+ severity: 'error',
210
+ },
211
+ ],
212
+ };
100
213
  }
101
214
  const shimPath = resolve(repoDir, SHIM_FILENAME);
102
215
  rootFiles.push(shimPath);
@@ -108,12 +221,23 @@ export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projec
108
221
  strict: false,
109
222
  noImplicitAny: false,
110
223
  };
224
+ // Allowlisted overrides. Booleans merge directly; a reviewed `paths`
225
+ // mapping is applied to the compiler options AND wired into the host
226
+ // resolver below so patch-owned modules can be typed from their real
227
+ // sources without a hand-generated ambient stub shim.
111
228
  const overrides = {};
229
+ let pathsMapping;
112
230
  const co = mode?.compilerOptions;
113
231
  if (co) {
114
232
  for (const key of Object.keys(co)) {
115
233
  const v = co[key];
116
- if (v !== undefined) {
234
+ if (v === undefined)
235
+ continue;
236
+ if (key === 'paths') {
237
+ pathsMapping = v;
238
+ overrides.paths = pathsMapping;
239
+ }
240
+ else {
117
241
  overrides[key] = v;
118
242
  }
119
243
  }
@@ -126,26 +250,35 @@ export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projec
126
250
  module: ts.ModuleKind.ESNext,
127
251
  moduleResolution: ts.ModuleResolutionKind.Bundler,
128
252
  skipLibCheck: true,
129
- // Do not follow import/reference directives into the Firefox tree.
130
- // We only want to check the patch-owned files themselves.
131
- // Without this, TS would try (and fail) to resolve every
132
- // resource:// and chrome:// import, flooding the output with
133
- // "Cannot find module" errors for upstream Firefox modules.
134
- noResolve: true,
253
+ // Module resolution is host-controlled (see resolveOwnedSpecifier
254
+ // below): imports that match a patch-owned file resolve to the real
255
+ // source so JSDoc type-guard predicates and @template generics
256
+ // survive the module boundary; everything else deliberately fails
257
+ // resolution, falling back to the chrome:*/resource:* ambient
258
+ // wildcards plus the suppressed "cannot find module" codes. The
259
+ // host resolver is authoritative — TS never crawls the Firefox
260
+ // tree looking for upstream modules.
135
261
  ...strictness,
136
262
  ...overrides,
137
263
  };
264
+ const resolveOwnedSpecifier = createOwnedSpecifierResolver(ts, ownedAbsolute);
138
265
  // Custom compiler host: reads patch-owned files from disk, returns
139
266
  // the shim for the shim path, and returns empty content for
140
267
  // anything else to avoid reading the full Firefox tree.
141
268
  const defaultHost = ts.createCompilerHost(options);
269
+ // Files pulled in via a reviewed `paths` mapping — outside the owned set
270
+ // but read from disk so the resolver's targets actually type-check.
271
+ const pathsResolved = new Set();
272
+ const resolveViaPaths = pathsMapping
273
+ ? createPathsResolver(ts, pathsMapping, repoDir, (f) => defaultHost.fileExists(f), (abs) => pathsResolved.add(abs))
274
+ : undefined;
142
275
  const host = {
143
276
  ...defaultHost,
144
277
  getSourceFile(fileName, languageVersion, onError) {
145
278
  if (fileName === shimPath) {
146
279
  return ts.createSourceFile(fileName, shimSource, languageVersion, true);
147
280
  }
148
- if (ownedAbsolute.has(fileName)) {
281
+ if (ownedAbsolute.has(fileName) || pathsResolved.has(fileName)) {
149
282
  return defaultHost.getSourceFile(fileName, languageVersion, onError);
150
283
  }
151
284
  // For lib files (lib.es*.d.ts) delegate to the default host
@@ -160,7 +293,7 @@ export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projec
160
293
  fileExists(fileName) {
161
294
  if (fileName === shimPath)
162
295
  return true;
163
- if (ownedAbsolute.has(fileName))
296
+ if (ownedAbsolute.has(fileName) || pathsResolved.has(fileName))
164
297
  return true;
165
298
  return defaultHost.fileExists(fileName);
166
299
  },
@@ -169,57 +302,116 @@ export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projec
169
302
  return shimSource;
170
303
  return defaultHost.readFile(fileName);
171
304
  },
305
+ resolveModuleNameLiterals(moduleLiterals) {
306
+ return moduleLiterals.map((literal) => {
307
+ const owned = resolveOwnedSpecifier(literal.text);
308
+ if (owned)
309
+ return { resolvedModule: owned };
310
+ return { resolvedModule: resolveViaPaths?.(literal.text) };
311
+ });
312
+ },
172
313
  };
173
314
  const program = ts.createProgram(rootFiles, options, host);
174
- const allDiagnostics = [
175
- ...program.getSemanticDiagnostics(),
176
- ...program.getSyntacticDiagnostics(),
177
- ];
178
- // Filter to diagnostics originating in patch-owned files only,
179
- // and suppress module-resolution / unknown-name noise that is
180
- // inherent to checking Firefox JS outside Mozilla's build system.
181
- const issues = [];
182
- for (const diag of allDiagnostics) {
315
+ const byFile = groupOwnedDiagnostics(ts, [...program.getSemanticDiagnostics(), ...program.getSyntacticDiagnostics()], relByAbsolute);
316
+ verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s) across ${byFile.size} owning file(s)`);
317
+ return { byFile, global: [] };
318
+ }
319
+ /**
320
+ * Groups TS diagnostics by the patch-owned file they originate in,
321
+ * suppressing module-resolution / unknown-name noise inherent to checking
322
+ * Firefox JS outside Mozilla's build system. Diagnostics from `paths`-resolved
323
+ * or shim files are dropped — only owned files (in `relByAbsolute`) carry
324
+ * findings.
325
+ */
326
+ function groupOwnedDiagnostics(ts, diagnostics, relByAbsolute) {
327
+ const byFile = new Map();
328
+ for (const diag of diagnostics) {
183
329
  if (SUPPRESSED_DIAGNOSTIC_CODES.has(diag.code))
184
330
  continue;
185
331
  const sourceFile = diag.file;
186
332
  if (!sourceFile)
187
333
  continue;
188
- if (!ownedAbsolute.has(sourceFile.fileName))
334
+ const relPath = relByAbsolute.get(sourceFile.fileName);
335
+ if (relPath === undefined)
189
336
  continue;
190
337
  const lineInfo = sourceFile.getLineAndCharacterOfPosition(diag.start ?? 0);
191
338
  const line = lineInfo.line + 1;
192
339
  const messageText = typeof diag.messageText === 'string'
193
340
  ? diag.messageText
194
341
  : ts.flattenDiagnosticMessageText(diag.messageText, '\n');
195
- // Find the relative path for the issue
196
- let relPath = sourceFile.fileName;
197
- for (const [rel, abs] of [...patchOwnedFiles].map((r) => [r, resolve(repoDir, r)])) {
198
- if (abs === sourceFile.fileName) {
199
- relPath = rel;
200
- break;
201
- }
202
- }
203
342
  const severity = diag.category === ts.DiagnosticCategory.Error ? 'error' : 'warning';
204
- issues.push({
343
+ const bucket = byFile.get(relPath) ?? [];
344
+ bucket.push({
205
345
  file: relPath,
206
346
  check: 'checkjs-type-error',
207
347
  message: `Line ${line}: ${messageText}`,
208
348
  severity,
209
349
  });
350
+ byFile.set(relPath, bucket);
351
+ }
352
+ return byFile;
353
+ }
354
+ /**
355
+ * Flattens a {@link runCheckJsGrouped} run into a single issue list. When
356
+ * `reportScope` is supplied, only diagnostics from files in that set are
357
+ * returned (resolution still spans every owned file); omitting it reports
358
+ * every owned file's diagnostics — the historical whole-set behaviour.
359
+ *
360
+ * @param repoDir - Absolute engine (repository) directory
361
+ * @param patchOwnedFiles - Patch-owned `.sys.mjs` paths to resolve against
362
+ * @param extraShimPath - Optional project-relative extra `.d.ts`
363
+ * @param projectRoot - Absolute project root for resolving `extraShimPath`
364
+ * @param mode - Strictness preset plus allowlisted compiler-option overrides
365
+ * @param reportScope - When set, restrict reported diagnostics to these
366
+ * repo-relative files
367
+ * @returns Array of lint issues from TS diagnostics
368
+ */
369
+ export async function runCheckJs(repoDir, patchOwnedFiles, extraShimPath, projectRoot, mode, reportScope) {
370
+ const { byFile, global } = await runCheckJsGrouped(repoDir, patchOwnedFiles, extraShimPath, projectRoot, mode);
371
+ const issues = [...global];
372
+ for (const [rel, list] of byFile) {
373
+ if (reportScope && !reportScope.has(rel))
374
+ continue;
375
+ issues.push(...list);
210
376
  }
211
- verbose(`checkJs: analyzed ${rootFiles.length - 1} file(s), found ${issues.length} issue(s)`);
212
377
  return issues;
213
378
  }
214
379
  /**
215
380
  * Invokes {@link runCheckJs} for a `patchLint` block with `checkJs: true`.
216
381
  * `projectRoot` is the FireForge project root (`dirname(engine)`).
382
+ *
383
+ * @param repoDir - Absolute engine (repository) directory
384
+ * @param patchOwnedFiles - Patch-owned `.sys.mjs` paths to resolve against
385
+ * @param patchLint - The resolved `patchLint` config block
386
+ * @param projectRoot - FireForge project root for shim resolution
387
+ * @param reportScope - Optional repo-relative files to report on (export /
388
+ * re-export passes the patch under export so cross-patch resolution does
389
+ * not surface other patches' diagnostics)
390
+ */
391
+ export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint, projectRoot, reportScope) {
392
+ const strict = patchLint.checkJsStrict === true;
393
+ const mode = strict && patchLint.checkJsCompilerOptions
394
+ ? { strict, compilerOptions: patchLint.checkJsCompilerOptions }
395
+ : { strict };
396
+ return runCheckJs(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, mode, reportScope);
397
+ }
398
+ /**
399
+ * Grouped variant of {@link invokePatchLintCheckJs}: builds one queue-wide
400
+ * checkJs program over `patchOwnedFiles` and returns its findings grouped by
401
+ * owning file. The per-patch lint orchestrator calls this **once per run**
402
+ * and attributes each file's findings to its owning patch, instead of
403
+ * rebuilding the same program for every patch in the queue.
404
+ *
405
+ * @param repoDir - Absolute engine (repository) directory
406
+ * @param patchOwnedFiles - Every patch-owned `.sys.mjs` in the queue
407
+ * @param patchLint - The resolved `patchLint` config block
408
+ * @param projectRoot - FireForge project root for shim resolution
217
409
  */
218
- export async function invokePatchLintCheckJs(repoDir, patchOwnedFiles, patchLint, projectRoot) {
410
+ export async function invokePatchLintCheckJsGrouped(repoDir, patchOwnedFiles, patchLint, projectRoot) {
219
411
  const strict = patchLint.checkJsStrict === true;
220
412
  const mode = strict && patchLint.checkJsCompilerOptions
221
413
  ? { strict, compilerOptions: patchLint.checkJsCompilerOptions }
222
414
  : { strict };
223
- return runCheckJs(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, mode);
415
+ return runCheckJsGrouped(repoDir, patchOwnedFiles, patchLint.checkJsExtraShim, projectRoot, mode);
224
416
  }
225
417
  //# sourceMappingURL=patch-lint-checkjs.js.map