@hominis/fireforge 0.49.2 → 0.51.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 (400) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +1 -1
  3. package/dist/bin/fireforge.js +6 -0
  4. package/dist/build-info.json +4 -4
  5. package/dist/src/commands/build-prelock.d.ts +20 -0
  6. package/dist/src/commands/build-prelock.js +50 -0
  7. package/dist/src/commands/build.d.ts +3 -1
  8. package/dist/src/commands/build.js +13 -6
  9. package/dist/src/commands/check-report.d.ts +41 -0
  10. package/dist/src/commands/check-report.js +91 -0
  11. package/dist/src/commands/check-run.d.ts +33 -0
  12. package/dist/src/commands/check-run.js +112 -0
  13. package/dist/src/commands/check-steps.d.ts +75 -0
  14. package/dist/src/commands/check-steps.js +200 -0
  15. package/dist/src/commands/check.d.ts +32 -0
  16. package/dist/src/commands/check.js +160 -0
  17. package/dist/src/commands/config.js +5 -6
  18. package/dist/src/commands/declarations-report.d.ts +43 -0
  19. package/dist/src/commands/declarations-report.js +76 -0
  20. package/dist/src/commands/doctor-engine-eslint.js +6 -1
  21. package/dist/src/commands/doctor-source-pin.js +4 -1
  22. package/dist/src/commands/export-adopt-check.d.ts +38 -0
  23. package/dist/src/commands/export-adopt-check.js +98 -0
  24. package/dist/src/commands/export-flow.js +29 -14
  25. package/dist/src/commands/export-placement-gate.d.ts +6 -2
  26. package/dist/src/commands/export-placement-gate.js +8 -0
  27. package/dist/src/commands/export-shared.d.ts +2 -1
  28. package/dist/src/commands/export-staged-registration.d.ts +46 -0
  29. package/dist/src/commands/export-staged-registration.js +91 -0
  30. package/dist/src/commands/forward-import-remedy.d.ts +32 -0
  31. package/dist/src/commands/forward-import-remedy.js +63 -0
  32. package/dist/src/commands/furnace/apply.js +3 -3
  33. package/dist/src/commands/furnace/chrome-doc-remove.d.ts +2 -1
  34. package/dist/src/commands/furnace/chrome-doc-remove.js +2 -2
  35. package/dist/src/commands/furnace/chrome-doc.d.ts +2 -1
  36. package/dist/src/commands/furnace/chrome-doc.js +2 -2
  37. package/dist/src/commands/furnace/create.js +2 -2
  38. package/dist/src/commands/furnace/deploy-named.d.ts +48 -0
  39. package/dist/src/commands/furnace/deploy-named.js +107 -0
  40. package/dist/src/commands/furnace/deploy.d.ts +3 -2
  41. package/dist/src/commands/furnace/deploy.js +50 -86
  42. package/dist/src/commands/furnace/diff.js +26 -3
  43. package/dist/src/commands/furnace/index.js +66 -41
  44. package/dist/src/commands/furnace/init.js +13 -6
  45. package/dist/src/commands/furnace/override.js +4 -2
  46. package/dist/src/commands/furnace/preview.js +4 -3
  47. package/dist/src/commands/furnace/refresh.js +2 -2
  48. package/dist/src/commands/furnace/remove.js +4 -3
  49. package/dist/src/commands/furnace/rename-dry-run.d.ts +29 -0
  50. package/dist/src/commands/furnace/rename-dry-run.js +127 -0
  51. package/dist/src/commands/furnace/rename-helpers.d.ts +7 -0
  52. package/dist/src/commands/furnace/rename-helpers.js +15 -1
  53. package/dist/src/commands/furnace/rename-registrations.d.ts +41 -0
  54. package/dist/src/commands/furnace/rename-registrations.js +98 -0
  55. package/dist/src/commands/furnace/rename.d.ts +2 -1
  56. package/dist/src/commands/furnace/rename.js +28 -66
  57. package/dist/src/commands/furnace/scan.d.ts +9 -4
  58. package/dist/src/commands/furnace/scan.js +11 -9
  59. package/dist/src/commands/furnace/sync.js +3 -2
  60. package/dist/src/commands/furnace/validate-report.d.ts +45 -0
  61. package/dist/src/commands/furnace/validate-report.js +91 -0
  62. package/dist/src/commands/furnace/validate.d.ts +1 -1
  63. package/dist/src/commands/furnace/validate.js +151 -113
  64. package/dist/src/commands/furnace/validation-output.js +14 -3
  65. package/dist/src/commands/lint-per-patch.js +36 -61
  66. package/dist/src/commands/lint-per-run-checkjs.js +2 -2
  67. package/dist/src/commands/lint-report.d.ts +8 -4
  68. package/dist/src/commands/lint-report.js +20 -5
  69. package/dist/src/commands/lint.js +15 -34
  70. package/dist/src/commands/manifest.js +4 -0
  71. package/dist/src/commands/park.d.ts +22 -0
  72. package/dist/src/commands/park.js +232 -0
  73. package/dist/src/commands/patch/delete-gap-notice.js +3 -1
  74. package/dist/src/commands/patch/delete.d.ts +1 -1
  75. package/dist/src/commands/patch/delete.js +24 -104
  76. package/dist/src/commands/patch/index.d.ts +3 -3
  77. package/dist/src/commands/patch/index.js +10 -4
  78. package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
  79. package/dist/src/commands/patch/lint-ignore-from.js +142 -0
  80. package/dist/src/commands/patch/lint-ignore.d.ts +5 -1
  81. package/dist/src/commands/patch/lint-ignore.js +78 -31
  82. package/dist/src/commands/patch/move-files-create.js +34 -18
  83. package/dist/src/commands/patch/move-files-into.js +15 -7
  84. package/dist/src/commands/patch/move-files.js +6 -2
  85. package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
  86. package/dist/src/commands/patch/removal-created-files.js +83 -0
  87. package/dist/src/commands/patch/removal-dependents.d.ts +63 -0
  88. package/dist/src/commands/patch/removal-dependents.js +133 -0
  89. package/dist/src/commands/patch/rename.js +20 -1
  90. package/dist/src/commands/patch/reserve.d.ts +33 -0
  91. package/dist/src/commands/patch/reserve.js +152 -0
  92. package/dist/src/commands/patch/retire.d.ts +34 -0
  93. package/dist/src/commands/patch/retire.js +133 -0
  94. package/dist/src/commands/patch/split-plan.d.ts +17 -8
  95. package/dist/src/commands/patch/split-plan.js +60 -28
  96. package/dist/src/commands/patch/split-staged.d.ts +108 -0
  97. package/dist/src/commands/patch/split-staged.js +202 -0
  98. package/dist/src/commands/patch/split.d.ts +5 -0
  99. package/dist/src/commands/patch/split.js +62 -17
  100. package/dist/src/commands/patch/stage.d.ts +34 -0
  101. package/dist/src/commands/patch/stage.js +153 -0
  102. package/dist/src/commands/patch/staged-dependency-batch.d.ts +48 -0
  103. package/dist/src/commands/patch/staged-dependency-batch.js +176 -0
  104. package/dist/src/commands/patch/staged-dependency-entries.d.ts +36 -0
  105. package/dist/src/commands/patch/staged-dependency-entries.js +52 -0
  106. package/dist/src/commands/patch/staged-dependency-from.d.ts +14 -0
  107. package/dist/src/commands/patch/staged-dependency-from.js +108 -0
  108. package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
  109. package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
  110. package/dist/src/commands/patch/staged-dependency.js +54 -139
  111. package/dist/src/commands/re-export-adjacent-registrations.d.ts +20 -0
  112. package/dist/src/commands/re-export-adjacent-registrations.js +75 -0
  113. package/dist/src/commands/re-export-adjacent.d.ts +50 -2
  114. package/dist/src/commands/re-export-adjacent.js +160 -40
  115. package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
  116. package/dist/src/commands/re-export-bulk-scan.js +22 -7
  117. package/dist/src/commands/re-export-drift.d.ts +36 -8
  118. package/dist/src/commands/re-export-drift.js +73 -49
  119. package/dist/src/commands/re-export-files.js +11 -8
  120. package/dist/src/commands/re-export-hunks.d.ts +35 -0
  121. package/dist/src/commands/re-export-hunks.js +220 -0
  122. package/dist/src/commands/re-export-lint.d.ts +2 -1
  123. package/dist/src/commands/re-export-lint.js +23 -10
  124. package/dist/src/commands/re-export-options.js +74 -6
  125. package/dist/src/commands/re-export-preflight.d.ts +23 -0
  126. package/dist/src/commands/re-export-preflight.js +160 -0
  127. package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
  128. package/dist/src/commands/re-export-purity-scope.js +131 -0
  129. package/dist/src/commands/re-export-register.js +56 -17
  130. package/dist/src/commands/re-export-removed.d.ts +13 -2
  131. package/dist/src/commands/re-export-removed.js +57 -12
  132. package/dist/src/commands/re-export-scan-owner.d.ts +44 -0
  133. package/dist/src/commands/re-export-scan-owner.js +116 -0
  134. package/dist/src/commands/re-export-scan.d.ts +19 -9
  135. package/dist/src/commands/re-export-scan.js +60 -24
  136. package/dist/src/commands/re-export-single.js +122 -46
  137. package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
  138. package/dist/src/commands/re-export-stale-registrations.js +62 -0
  139. package/dist/src/commands/re-export-summary.d.ts +28 -0
  140. package/dist/src/commands/re-export-summary.js +36 -0
  141. package/dist/src/commands/re-export.js +71 -57
  142. package/dist/src/commands/rebase/index.js +17 -17
  143. package/dist/src/commands/run.js +16 -2
  144. package/dist/src/commands/status-check.d.ts +55 -16
  145. package/dist/src/commands/status-check.js +92 -37
  146. package/dist/src/commands/status-json.d.ts +22 -9
  147. package/dist/src/commands/status-json.js +47 -32
  148. package/dist/src/commands/status-output.js +3 -1
  149. package/dist/src/commands/status-scope.d.ts +37 -0
  150. package/dist/src/commands/status-scope.js +54 -0
  151. package/dist/src/commands/status.js +66 -23
  152. package/dist/src/commands/test-browser-preflight.js +19 -3
  153. package/dist/src/commands/test-build-furnace.d.ts +22 -0
  154. package/dist/src/commands/test-build-furnace.js +54 -0
  155. package/dist/src/commands/test-build-phase.js +64 -41
  156. package/dist/src/commands/test-checkjs.js +10 -2
  157. package/dist/src/commands/test-diagnose.js +7 -0
  158. package/dist/src/commands/test-dispatch.d.ts +39 -0
  159. package/dist/src/commands/test-dispatch.js +211 -0
  160. package/dist/src/commands/test-furnace-reach.d.ts +37 -0
  161. package/dist/src/commands/test-furnace-reach.js +132 -0
  162. package/dist/src/commands/test-modes.d.ts +16 -1
  163. package/dist/src/commands/test-modes.js +24 -1
  164. package/dist/src/commands/test-multi-verdict.d.ts +61 -0
  165. package/dist/src/commands/test-multi-verdict.js +111 -0
  166. package/dist/src/commands/test-orchestrate.d.ts +52 -0
  167. package/dist/src/commands/test-orchestrate.js +240 -0
  168. package/dist/src/commands/test-perf-scope.d.ts +50 -0
  169. package/dist/src/commands/test-perf-scope.js +138 -0
  170. package/dist/src/commands/test-prelock.d.ts +51 -0
  171. package/dist/src/commands/test-prelock.js +273 -0
  172. package/dist/src/commands/test-register.js +69 -10
  173. package/dist/src/commands/test-results.d.ts +62 -0
  174. package/dist/src/commands/test-results.js +62 -0
  175. package/dist/src/commands/test-run.d.ts +13 -55
  176. package/dist/src/commands/test-run.js +74 -99
  177. package/dist/src/commands/test-shards.d.ts +110 -0
  178. package/dist/src/commands/test-shards.js +255 -0
  179. package/dist/src/commands/test-stale-gate.d.ts +5 -1
  180. package/dist/src/commands/test-stale-gate.js +60 -28
  181. package/dist/src/commands/test-verdict.d.ts +11 -1
  182. package/dist/src/commands/test-verdict.js +28 -1
  183. package/dist/src/commands/test.d.ts +6 -1
  184. package/dist/src/commands/test.js +120 -124
  185. package/dist/src/commands/token-lint.d.ts +14 -0
  186. package/dist/src/commands/token-lint.js +151 -0
  187. package/dist/src/commands/token.js +10 -0
  188. package/dist/src/commands/verify-scope.d.ts +28 -0
  189. package/dist/src/commands/verify-scope.js +67 -0
  190. package/dist/src/commands/verify.d.ts +10 -2
  191. package/dist/src/commands/verify.js +67 -36
  192. package/dist/src/core/build-baseline-types.d.ts +46 -0
  193. package/dist/src/core/build-baseline-validate.js +19 -0
  194. package/dist/src/core/build-baseline.d.ts +5 -1
  195. package/dist/src/core/build-baseline.js +3 -1
  196. package/dist/src/core/build-overwrite-guard.d.ts +50 -0
  197. package/dist/src/core/build-overwrite-guard.js +84 -4
  198. package/dist/src/core/build-prepare.d.ts +13 -0
  199. package/dist/src/core/build-prepare.js +42 -10
  200. package/dist/src/core/build-run-record.d.ts +74 -0
  201. package/dist/src/core/build-run-record.js +136 -0
  202. package/dist/src/core/checkjs-support-heads.d.ts +41 -0
  203. package/dist/src/core/checkjs-support-heads.js +104 -0
  204. package/dist/src/core/config-paths.d.ts +2 -2
  205. package/dist/src/core/config-paths.js +10 -0
  206. package/dist/src/core/config-validate-check.d.ts +8 -0
  207. package/dist/src/core/config-validate-check.js +219 -0
  208. package/dist/src/core/config-validate-file-size.d.ts +9 -6
  209. package/dist/src/core/config-validate-file-size.js +91 -2
  210. package/dist/src/core/config-validate-history.d.ts +11 -0
  211. package/dist/src/core/config-validate-history.js +38 -0
  212. package/dist/src/core/config-validate-patch-policy.js +100 -0
  213. package/dist/src/core/config-validate-test-toolchains.js +21 -0
  214. package/dist/src/core/config-validate-token-scales.d.ts +9 -0
  215. package/dist/src/core/config-validate-token-scales.js +92 -0
  216. package/dist/src/core/config-validate.js +20 -5
  217. package/dist/src/core/config.d.ts +5 -0
  218. package/dist/src/core/config.js +16 -0
  219. package/dist/src/core/coverage-dispatch.d.ts +24 -0
  220. package/dist/src/core/coverage-dispatch.js +44 -0
  221. package/dist/src/core/coverage-extend.d.ts +70 -45
  222. package/dist/src/core/coverage-extend.js +199 -144
  223. package/dist/src/core/coverage-records.d.ts +108 -0
  224. package/dist/src/core/coverage-records.js +384 -0
  225. package/dist/src/core/destructive.d.ts +7 -4
  226. package/dist/src/core/destructive.js +10 -10
  227. package/dist/src/core/engine-session-lock.d.ts +16 -1
  228. package/dist/src/core/engine-session-lock.js +38 -10
  229. package/dist/src/core/export-moved-code-shape.d.ts +49 -11
  230. package/dist/src/core/export-moved-code-shape.js +135 -44
  231. package/dist/src/core/file-lock-queue.d.ts +67 -0
  232. package/dist/src/core/file-lock-queue.js +221 -0
  233. package/dist/src/core/file-lock-shared.d.ts +57 -0
  234. package/dist/src/core/file-lock-shared.js +117 -0
  235. package/dist/src/core/file-lock.d.ts +13 -25
  236. package/dist/src/core/file-lock.js +163 -140
  237. package/dist/src/core/furnace-apply-output.d.ts +10 -0
  238. package/dist/src/core/furnace-apply-output.js +31 -1
  239. package/dist/src/core/furnace-apply.js +22 -12
  240. package/dist/src/core/furnace-composed-source.d.ts +76 -0
  241. package/dist/src/core/furnace-composed-source.js +167 -0
  242. package/dist/src/core/furnace-config-validate-exceptions.d.ts +16 -0
  243. package/dist/src/core/furnace-config-validate-exceptions.js +102 -0
  244. package/dist/src/core/furnace-config-validate.js +14 -0
  245. package/dist/src/core/furnace-css-fragments.d.ts +10 -0
  246. package/dist/src/core/furnace-css-fragments.js +26 -0
  247. package/dist/src/core/furnace-operation.d.ts +3 -3
  248. package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
  249. package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
  250. package/dist/src/core/furnace-registration-remove.d.ts +22 -1
  251. package/dist/src/core/furnace-registration-remove.js +53 -17
  252. package/dist/src/core/furnace-registration-rename.d.ts +26 -0
  253. package/dist/src/core/furnace-registration-rename.js +77 -0
  254. package/dist/src/core/furnace-registration.d.ts +14 -4
  255. package/dist/src/core/furnace-registration.js +44 -10
  256. package/dist/src/core/furnace-staleness.d.ts +7 -0
  257. package/dist/src/core/furnace-staleness.js +34 -24
  258. package/dist/src/core/furnace-validate-accessibility.d.ts +8 -4
  259. package/dist/src/core/furnace-validate-accessibility.js +71 -28
  260. package/dist/src/core/furnace-validate-exceptions.d.ts +48 -0
  261. package/dist/src/core/furnace-validate-exceptions.js +80 -0
  262. package/dist/src/core/furnace-validate-icon-button.d.ts +7 -0
  263. package/dist/src/core/furnace-validate-icon-button.js +78 -0
  264. package/dist/src/core/furnace-validate.js +1 -1
  265. package/dist/src/core/harness-port-fallback.d.ts +60 -0
  266. package/dist/src/core/harness-port-fallback.js +96 -0
  267. package/dist/src/core/history-log-file.d.ts +61 -0
  268. package/dist/src/core/history-log-file.js +164 -0
  269. package/dist/src/core/history-log.d.ts +2 -1
  270. package/dist/src/core/history-log.js +2 -1
  271. package/dist/src/core/json-preserving-edit.d.ts +22 -0
  272. package/dist/src/core/json-preserving-edit.js +199 -0
  273. package/dist/src/core/line-prefixer.d.ts +18 -0
  274. package/dist/src/core/line-prefixer.js +34 -0
  275. package/dist/src/core/mach.d.ts +7 -0
  276. package/dist/src/core/mach.js +32 -10
  277. package/dist/src/core/module-resolution-preflight.d.ts +19 -3
  278. package/dist/src/core/module-resolution-preflight.js +24 -6
  279. package/dist/src/core/negative-control-compiled.d.ts +27 -0
  280. package/dist/src/core/negative-control-compiled.js +73 -0
  281. package/dist/src/core/negative-control-diff.d.ts +51 -0
  282. package/dist/src/core/negative-control-diff.js +179 -0
  283. package/dist/src/core/negative-control.d.ts +150 -0
  284. package/dist/src/core/negative-control.js +399 -0
  285. package/dist/src/core/objdir-install-manifests.d.ts +73 -0
  286. package/dist/src/core/objdir-install-manifests.js +226 -0
  287. package/dist/src/core/park-store.d.ts +63 -0
  288. package/dist/src/core/park-store.js +216 -0
  289. package/dist/src/core/patch-export-update.d.ts +6 -0
  290. package/dist/src/core/patch-export-update.js +13 -3
  291. package/dist/src/core/patch-export.d.ts +3 -2
  292. package/dist/src/core/patch-export.js +22 -16
  293. package/dist/src/core/patch-files.js +26 -8
  294. package/dist/src/core/patch-lint-checkjs-any-import.d.ts +39 -0
  295. package/dist/src/core/patch-lint-checkjs-any-import.js +63 -0
  296. package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
  297. package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
  298. package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
  299. package/dist/src/core/patch-lint-checkjs.js +20 -13
  300. package/dist/src/core/patch-lint-code-lines.d.ts +59 -0
  301. package/dist/src/core/patch-lint-code-lines.js +105 -0
  302. package/dist/src/core/patch-lint-cross.d.ts +3 -1
  303. package/dist/src/core/patch-lint-cross.js +17 -4
  304. package/dist/src/core/patch-lint-css.d.ts +8 -2
  305. package/dist/src/core/patch-lint-css.js +24 -14
  306. package/dist/src/core/patch-lint-file-size.d.ts +12 -5
  307. package/dist/src/core/patch-lint-file-size.js +22 -11
  308. package/dist/src/core/patch-lint-forward-registration-extended.d.ts +23 -0
  309. package/dist/src/core/patch-lint-forward-registration-extended.js +36 -11
  310. package/dist/src/core/patch-lint-forward-registration.js +10 -0
  311. package/dist/src/core/patch-lint-ignore.d.ts +196 -0
  312. package/dist/src/core/patch-lint-ignore.js +429 -0
  313. package/dist/src/core/patch-lint-ownership.d.ts +10 -0
  314. package/dist/src/core/patch-lint-ownership.js +26 -0
  315. package/dist/src/core/patch-lint-size-thresholds.d.ts +52 -0
  316. package/dist/src/core/patch-lint-size-thresholds.js +110 -0
  317. package/dist/src/core/patch-lint-size.d.ts +117 -0
  318. package/dist/src/core/patch-lint-size.js +331 -0
  319. package/dist/src/core/patch-lint-staged-registration.d.ts +18 -1
  320. package/dist/src/core/patch-lint-staged-registration.js +20 -0
  321. package/dist/src/core/patch-lint-token-scale.d.ts +57 -0
  322. package/dist/src/core/patch-lint-token-scale.js +286 -0
  323. package/dist/src/core/patch-lint.d.ts +11 -83
  324. package/dist/src/core/patch-lint.js +66 -288
  325. package/dist/src/core/patch-lock.js +7 -6
  326. package/dist/src/core/patch-manifest-consistency.js +7 -6
  327. package/dist/src/core/patch-manifest-io.d.ts +6 -3
  328. package/dist/src/core/patch-manifest-io.js +27 -9
  329. package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
  330. package/dist/src/core/patch-manifest-key-order.js +61 -0
  331. package/dist/src/core/patch-manifest-stage.d.ts +46 -0
  332. package/dist/src/core/patch-manifest-stage.js +96 -0
  333. package/dist/src/core/patch-manifest-validate.js +61 -3
  334. package/dist/src/core/patch-order-legal.d.ts +26 -0
  335. package/dist/src/core/patch-order-legal.js +88 -0
  336. package/dist/src/core/patch-policy-content.d.ts +40 -0
  337. package/dist/src/core/patch-policy-content.js +216 -0
  338. package/dist/src/core/patch-policy.d.ts +22 -5
  339. package/dist/src/core/patch-policy.js +26 -12
  340. package/dist/src/core/patch-retired.d.ts +57 -0
  341. package/dist/src/core/patch-retired.js +92 -0
  342. package/dist/src/core/patch-subset.d.ts +16 -0
  343. package/dist/src/core/patch-subset.js +66 -0
  344. package/dist/src/core/preflight-wait.d.ts +20 -0
  345. package/dist/src/core/preflight-wait.js +38 -0
  346. package/dist/src/core/rebase-from-version.d.ts +46 -0
  347. package/dist/src/core/rebase-from-version.js +71 -0
  348. package/dist/src/core/run-log.d.ts +26 -0
  349. package/dist/src/core/run-log.js +106 -4
  350. package/dist/src/core/run-profile.d.ts +7 -0
  351. package/dist/src/core/run-profile.js +7 -1
  352. package/dist/src/core/smoke-patterns.d.ts +5 -0
  353. package/dist/src/core/smoke-patterns.js +13 -0
  354. package/dist/src/core/test-harness-crash.d.ts +19 -1
  355. package/dist/src/core/test-harness-crash.js +8 -9
  356. package/dist/src/core/test-harness-signature.d.ts +33 -0
  357. package/dist/src/core/test-harness-signature.js +23 -1
  358. package/dist/src/core/test-raw-log.d.ts +66 -0
  359. package/dist/src/core/test-raw-log.js +122 -0
  360. package/dist/src/core/test-run-completeness.d.ts +90 -0
  361. package/dist/src/core/test-run-completeness.js +184 -0
  362. package/dist/src/core/test-stale-check.d.ts +27 -1
  363. package/dist/src/core/test-stale-check.js +39 -49
  364. package/dist/src/core/test-stale-exempt.d.ts +40 -0
  365. package/dist/src/core/test-stale-exempt.js +70 -0
  366. package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
  367. package/dist/src/core/test-xpcshell-retry.js +2 -1
  368. package/dist/src/core/token-lint.d.ts +53 -0
  369. package/dist/src/core/token-lint.js +131 -0
  370. package/dist/src/core/token-sibling-sheets.d.ts +46 -0
  371. package/dist/src/core/token-sibling-sheets.js +119 -0
  372. package/dist/src/core/tree-guard.js +5 -0
  373. package/dist/src/core/typecheck-shim.d.ts +6 -2
  374. package/dist/src/core/typecheck-shim.js +205 -4
  375. package/dist/src/core/typecheck.js +3 -1
  376. package/dist/src/index.d.ts +14 -3
  377. package/dist/src/index.js +6 -1
  378. package/dist/src/types/commands/index.d.ts +2 -2
  379. package/dist/src/types/commands/options.d.ts +212 -24
  380. package/dist/src/types/commands/patches.d.ts +118 -1
  381. package/dist/src/types/config.d.ts +264 -0
  382. package/dist/src/types/config.js +9 -0
  383. package/dist/src/types/furnace.d.ts +63 -1
  384. package/dist/src/types/furnace.js +7 -0
  385. package/dist/src/types/index.d.ts +1 -1
  386. package/dist/src/utils/options.d.ts +36 -7
  387. package/dist/src/utils/options.js +94 -8
  388. package/dist/src/utils/path-glob.d.ts +15 -0
  389. package/dist/src/utils/path-glob.js +47 -0
  390. package/dist/src/utils/regex.d.ts +15 -3
  391. package/dist/src/utils/regex.js +37 -16
  392. package/docs/configuration.md +369 -11
  393. package/docs/exit-codes.md +18 -1
  394. package/docs/furnace.md +139 -3
  395. package/docs/lifecycle-invariants.md +65 -30
  396. package/docs/machine-output.md +120 -12
  397. package/docs/patch-workflow.md +458 -22
  398. package/docs/run-logs.md +20 -1
  399. package/docs/testing.md +346 -21
  400. package/package.json +3 -3
@@ -1,11 +1,36 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
+ import { readdir } from 'node:fs/promises';
2
3
  import { join } from 'node:path';
3
4
  import { pathExists, readText } from '../utils/fs.js';
4
5
  import { createIssue } from './furnace-validate-helpers.js';
6
+ import { countUnlabelledIconButtons } from './furnace-validate-icon-button.js';
5
7
  import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-template-regions.js';
6
8
  /**
7
- * Validates accessibility patterns in a component's .mjs file.
8
- * Checks for ARIA roles, keyboard handlers, l10n, and focus delegation.
9
+ * Reads the composed component: `<tagName>.mjs` followed by its companion
10
+ * modules (`<tagName>-*.mjs`, tests and stories left out), so a template's
11
+ * findings do not depend on which of the component's modules it lives in.
12
+ * Returns undefined when the host module is missing.
13
+ */
14
+ async function readComposedComponentSource(componentDir, tagName) {
15
+ const hostPath = join(componentDir, `${tagName}.mjs`);
16
+ if (!(await pathExists(hostPath)))
17
+ return undefined;
18
+ const entries = await readdir(componentDir).catch(() => []);
19
+ const companions = entries
20
+ .filter((name) => name.startsWith(`${tagName}-`) &&
21
+ name.endsWith('.mjs') &&
22
+ !/\.(test|spec|stories)\./.test(name))
23
+ .sort();
24
+ const sources = [await readText(hostPath)];
25
+ for (const name of companions)
26
+ sources.push(await readText(join(componentDir, name)));
27
+ return sources.join('\n');
28
+ }
29
+ /**
30
+ * Validates accessibility patterns in a component's composed source: its
31
+ * host module and every companion module (see
32
+ * {@link readComposedComponentSource}). Checks for ARIA roles, keyboard
33
+ * handlers, l10n, and focus delegation.
9
34
  *
10
35
  * @param componentDir - Directory holding the component's sources
11
36
  * @param tagName - Custom element tag name, used to locate `<tagName>.mjs`
@@ -15,12 +40,13 @@ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-templa
15
40
  * (`keyboardCovered: true`) or via `composes` naming a native-interactive
16
41
  * inner element. Optional so stock/override callers and test fixtures
17
42
  * without config in scope can continue to call without changes.
43
+ * @param optInChecks - `furnace.json#optInChecks`. `icon-button-label`
44
+ * adds the icon-only `<moz-button>` naming check.
18
45
  */
19
- export async function validateAccessibility(componentDir, tagName, customConfig) {
20
- const mjsPath = join(componentDir, `${tagName}.mjs`);
21
- if (!(await pathExists(mjsPath)))
46
+ export async function validateAccessibility(componentDir, tagName, customConfig, optInChecks = []) {
47
+ const content = await readComposedComponentSource(componentDir, tagName);
48
+ if (content === undefined)
22
49
  return [];
23
- const content = await readText(mjsPath);
24
50
  const issues = [];
25
51
  if (!hasAriaRole(content) && hasGenericInteractiveElement(content)) {
26
52
  issues.push(createIssue(tagName, 'warning', 'no-aria-role', 'Generic interactive markup has no native semantics. Prefer native elements, or add role= when native markup cannot provide the semantics.'));
@@ -38,10 +64,13 @@ export async function validateAccessibility(componentDir, tagName, customConfig)
38
64
  // activation flows through the inner element and a duplicate handler on
39
65
  // the wrapper would either no-op or fire twice alongside the child's
40
66
  // built-in keyboard path.
41
- const hasClickOnSynthetic = hasTemplateClickOnSyntheticInteractive(content);
67
+ const syntheticTags = syntheticClickTags(content);
42
68
  const keyboardCovered = isKeyboardCoveredByComposition(customConfig);
43
- if (hasClickOnSynthetic && !hasKeyboardHandler && !keyboardCovered) {
44
- issues.push(createIssue(tagName, 'warning', 'no-keyboard-handler', 'Interactive element has @click but no keyboard event handler (@keydown/@keypress/@keyup).'));
69
+ if (syntheticTags.length > 0 && !hasKeyboardHandler && !keyboardCovered) {
70
+ issues.push({
71
+ ...createIssue(tagName, 'warning', 'no-keyboard-handler', `Interactive element has @click but no keyboard event handler (@keydown/@keypress/@keyup), on ${syntheticTags.map((tag) => `<${tag}>`).join(', ')}.`),
72
+ tags: syntheticTags,
73
+ });
45
74
  }
46
75
  if (containsHardcodedTemplateText(content)) {
47
76
  issues.push(createIssue(tagName, 'warning', 'hardcoded-text', 'Possible hardcoded string found. Use data-l10n-id for localization.'));
@@ -56,6 +85,12 @@ export async function validateAccessibility(componentDir, tagName, customConfig)
56
85
  if (hasUnlabelledFormInput(content)) {
57
86
  issues.push(createIssue(tagName, 'warning', 'unlabelled-form-input', 'Form input without an accessible label. Add aria-label, aria-labelledby, or an associated <label> element.'));
58
87
  }
88
+ if (optInChecks.includes('icon-button-label')) {
89
+ const unlabelled = countUnlabelledIconButtons(content);
90
+ if (unlabelled > 0) {
91
+ issues.push(createIssue(tagName, 'warning', 'icon-button-label', `${unlabelled} icon-only <moz-button> without an accessible name. Add data-l10n-id (with the label in its Fluent message) or aria-label.`));
92
+ }
93
+ }
59
94
  return issues;
60
95
  }
61
96
  /** Detects whether template or script content assigns an ARIA role. */
@@ -155,6 +190,9 @@ const NATIVE_CLICK_INTERACTIVE_TAGS = new Set([
155
190
  'moz-radio',
156
191
  'moz-radio-group',
157
192
  'moz-menulist',
193
+ // Toolkit's panel-list row renders a button that takes Enter and Space
194
+ // and dispatches click.
195
+ 'panel-item',
158
196
  ]);
159
197
  /**
160
198
  * Returns true when `customConfig` declares that the component's keyboard
@@ -176,28 +214,33 @@ function isKeyboardCoveredByComposition(customConfig) {
176
214
  return composes.some((tag) => NATIVE_CLICK_INTERACTIVE_TAGS.has(tag));
177
215
  }
178
216
  /**
179
- * Returns true when `content` has at least one `@click=${...}` handler on a
180
- * *synthetic* interactive element (e.g. `<div @click>`), which lacks native
181
- * keyboard activation and therefore needs an explicit key handler for
182
- * Enter/Space. Returns false when every `@click` handler sits on a native
183
- * interactive element. Those already fire `click` on keyboard activation.
217
+ * Returns the tags (in first-seen order) of every *synthetic* interactive
218
+ * element carrying an `@click=${...}` handler (e.g. `<div @click>`), which
219
+ * lacks native keyboard activation and therefore needs an explicit key
220
+ * handler for Enter/Space. Empty when every `@click` handler sits on a
221
+ * native interactive element. Those already fire `click` on keyboard
222
+ * activation.
184
223
  */
185
- function hasTemplateClickOnSyntheticInteractive(content) {
224
+ function syntheticClickTags(content) {
186
225
  const pattern = /@click\s*=\s*\$\{/g;
226
+ const tags = [];
187
227
  let match;
188
228
  while ((match = pattern.exec(content)) !== null) {
189
- if (isClickOnSyntheticInteractive(content, match.index)) {
190
- return true;
191
- }
229
+ const tag = syntheticClickTag(content, match.index);
230
+ if (tag !== undefined && !tags.includes(tag))
231
+ tags.push(tag);
192
232
  }
193
- return false;
233
+ return tags;
194
234
  }
235
+ /** The tag a synthetic-element `@click` sits on, `?` when it cannot be told. */
236
+ const UNKNOWN_TAG = '?';
195
237
  /**
196
238
  * Given the offset of an `@click=${...}` occurrence, walks backwards to find
197
- * the opening `<tag` that owns it and decides whether that tag is a native
198
- * interactive element (no warning needed) or a synthetic one (warning needed).
239
+ * the opening `<tag` that owns it and returns that tag when it is a
240
+ * synthetic interactive element (warning needed), or undefined when it is a
241
+ * native one (no warning needed).
199
242
  */
200
- function isClickOnSyntheticInteractive(content, clickIndex) {
243
+ function syntheticClickTag(content, clickIndex) {
201
244
  // Find the nearest preceding `<` that starts a tag (skip `</` closers).
202
245
  let i = clickIndex - 1;
203
246
  let tagOpenIndex = -1;
@@ -210,15 +253,15 @@ function isClickOnSyntheticInteractive(content, clickIndex) {
210
253
  // which shouldn't happen for a well-formed template but we defensively
211
254
  // treat it as synthetic to preserve the prior-behaviour warning.
212
255
  if (content[i] === '>') {
213
- return true;
256
+ return UNKNOWN_TAG;
214
257
  }
215
258
  i--;
216
259
  }
217
260
  if (tagOpenIndex < 0)
218
- return true;
261
+ return UNKNOWN_TAG;
219
262
  const tagMatch = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(content.slice(tagOpenIndex));
220
263
  if (!tagMatch?.[1])
221
- return true;
264
+ return UNKNOWN_TAG;
222
265
  const tagName = tagMatch[1].toLowerCase();
223
266
  if (tagName === 'a') {
224
267
  // Bare <a> (no href) is non-interactive, so require a keyboard handler.
@@ -226,11 +269,11 @@ function isClickOnSyntheticInteractive(content, clickIndex) {
226
269
  // attribute text in between for an href attribute.
227
270
  const tagEnd = content.indexOf('>', tagOpenIndex);
228
271
  if (tagEnd < 0)
229
- return true;
272
+ return 'a';
230
273
  const attrs = content.slice(tagOpenIndex, tagEnd);
231
- return !/\shref\s*=/.test(attrs);
274
+ return /\shref\s*=/.test(attrs) ? undefined : 'a';
232
275
  }
233
- return !NATIVE_CLICK_INTERACTIVE_TAGS.has(tagName);
276
+ return NATIVE_CLICK_INTERACTIVE_TAGS.has(tagName) ? undefined : tagName;
234
277
  }
235
278
  /** ASCII punctuation that reads as decoration rather than localizable text. */
236
279
  const SYMBOL_ONLY_ASCII = '+-*=<>|/\\^~@#&!?%';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Applies `furnace.json#validationExceptions[]` to a `furnace validate`
3
+ * result. An exception suppresses every finding of its check on its
4
+ * component, or with `tag`, a finding whose element tags it (and its
5
+ * sibling tag entries) cover. One that suppresses nothing is stale: the
6
+ * finding was fixed (or renamed) and the review record outlived it, so it
7
+ * is reported as a warning naming the entry to delete, which keeps the
8
+ * list honest under `--max-warnings 0`.
9
+ *
10
+ * An exception never suppresses an error: an error blocks apply, and a
11
+ * review record cannot make a broken component deployable. The error stays
12
+ * counted and the matching entry is reported as
13
+ * `validation-exception-on-error`.
14
+ */
15
+ import type { FurnaceValidationException, ValidationIssue } from '../types/furnace.js';
16
+ /** The check name of the warning a stale exception produces. */
17
+ export declare const STALE_VALIDATION_EXCEPTION_CHECK = "stale-validation-exception";
18
+ /** The check name of the error an exception matching an error produces. */
19
+ export declare const EXCEPTION_ON_ERROR_CHECK = "validation-exception-on-error";
20
+ /** A finding an exception suppressed, with the exception's reason. */
21
+ export interface ExceptedValidationIssue extends ValidationIssue {
22
+ reason: string;
23
+ }
24
+ /** {@link applyValidationExceptions}'s result. */
25
+ export interface ValidationExceptionOutcome {
26
+ /** Findings that still count, stale-exception warnings included. */
27
+ issues: ValidationIssue[];
28
+ /** Findings an exception suppressed. */
29
+ excepted: ExceptedValidationIssue[];
30
+ /** Exceptions that matched no finding of a component this run judged. */
31
+ stale: FurnaceValidationException[];
32
+ }
33
+ /**
34
+ * Splits findings into counted and excepted, and reports stale exceptions.
35
+ *
36
+ * @param issues - Every finding of the run
37
+ * @param exceptions - `furnace.json#validationExceptions`
38
+ * @param judged - Whether the run judged an exception's component. A run
39
+ * scoped to one component cannot call another component's exception
40
+ * stale, while the full run judges them all (including components
41
+ * furnace.json no longer declares)
42
+ */
43
+ export declare function applyValidationExceptions(issues: readonly ValidationIssue[], exceptions: readonly FurnaceValidationException[] | undefined, judged: (component: string) => boolean): ValidationExceptionOutcome;
44
+ /** Counts error- and warning-severity findings. */
45
+ export declare function countValidationSeverities(issues: readonly ValidationIssue[]): {
46
+ errors: number;
47
+ warnings: number;
48
+ };
@@ -0,0 +1,80 @@
1
+ import { createIssue } from './furnace-validate-helpers.js';
2
+ /** The check name of the warning a stale exception produces. */
3
+ export const STALE_VALIDATION_EXCEPTION_CHECK = 'stale-validation-exception';
4
+ /** The check name of the error an exception matching an error produces. */
5
+ export const EXCEPTION_ON_ERROR_CHECK = 'validation-exception-on-error';
6
+ function matches(exception, issue) {
7
+ return exception.component === issue.component && exception.check === issue.check;
8
+ }
9
+ /**
10
+ * The exceptions that suppress a warning: an untagged one alone, or tagged
11
+ * ones that together cover every tag the finding names.
12
+ */
13
+ function coveringExceptions(issue, candidates) {
14
+ const untagged = candidates.find((entry) => entry.tag === undefined);
15
+ if (untagged !== undefined)
16
+ return [untagged];
17
+ const tags = issue.tags ?? [];
18
+ if (tags.length === 0)
19
+ return [];
20
+ const covering = tags.map((tag) => candidates.find((entry) => entry.tag === tag));
21
+ return covering.every((entry) => entry !== undefined) ? [...new Set(covering)] : [];
22
+ }
23
+ /**
24
+ * Splits findings into counted and excepted, and reports stale exceptions.
25
+ *
26
+ * @param issues - Every finding of the run
27
+ * @param exceptions - `furnace.json#validationExceptions`
28
+ * @param judged - Whether the run judged an exception's component. A run
29
+ * scoped to one component cannot call another component's exception
30
+ * stale, while the full run judges them all (including components
31
+ * furnace.json no longer declares)
32
+ */
33
+ export function applyValidationExceptions(issues, exceptions, judged) {
34
+ const outcome = { issues: [], excepted: [], stale: [] };
35
+ const list = exceptions ?? [];
36
+ const used = new Set();
37
+ const onError = new Set();
38
+ for (const issue of issues) {
39
+ const candidates = list.filter((entry) => matches(entry, issue));
40
+ if (issue.severity === 'error') {
41
+ for (const entry of candidates) {
42
+ used.add(entry);
43
+ onError.add(entry);
44
+ }
45
+ outcome.issues.push(issue);
46
+ continue;
47
+ }
48
+ const covering = coveringExceptions(issue, candidates);
49
+ if (covering.length === 0) {
50
+ // A tagged exception that covers some of the tags is not stale: its
51
+ // element still carries the finding, beside one it does not cover.
52
+ for (const entry of candidates) {
53
+ if (entry.tag !== undefined && issue.tags?.includes(entry.tag) === true)
54
+ used.add(entry);
55
+ }
56
+ outcome.issues.push(issue);
57
+ continue;
58
+ }
59
+ for (const entry of covering)
60
+ used.add(entry);
61
+ outcome.excepted.push({ ...issue, reason: covering.map((entry) => entry.reason).join('; ') });
62
+ }
63
+ for (const exception of onError) {
64
+ outcome.issues.push(createIssue(exception.component, 'error', EXCEPTION_ON_ERROR_CHECK, `furnace.json validationExceptions entry for [${exception.check}] matches an error, ` +
65
+ 'which an exception cannot suppress. Fix the error, then delete the entry.'));
66
+ }
67
+ for (const exception of list) {
68
+ if (used.has(exception) || !judged(exception.component))
69
+ continue;
70
+ outcome.stale.push(exception);
71
+ outcome.issues.push(createIssue(exception.component, 'warning', STALE_VALIDATION_EXCEPTION_CHECK, `furnace.json validationExceptions entry for [${exception.check}] matches no finding. ` +
72
+ 'Delete the entry: the finding it records is gone.'));
73
+ }
74
+ return outcome;
75
+ }
76
+ /** Counts error- and warning-severity findings. */
77
+ export function countValidationSeverities(issues) {
78
+ const errors = issues.filter((issue) => issue.severity === 'error').length;
79
+ return { errors, warnings: issues.length - errors };
80
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Counts icon-only `<moz-button>` elements in the module's Lit templates
3
+ * that carry neither `data-l10n-id` nor `aria-label`.
4
+ *
5
+ * @param content - The component module's source
6
+ */
7
+ export declare function countUnlabelledIconButtons(content: string): number;
@@ -0,0 +1,78 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * The opt-in `icon-button-label` check (`furnace.json#optInChecks`): an
4
+ * icon-only `<moz-button>` has no visible text, so its accessible name has
5
+ * to come from `data-l10n-id` (whose FTL message carries the label or
6
+ * `.aria-label`) or from `aria-label`. Without one, screen readers announce
7
+ * just "button".
8
+ *
9
+ * Scans the Lit `html` templates of the component module only. A button
10
+ * built with `document.createElement` is not seen.
11
+ */
12
+ import { collectLitTemplateRegions, parseTemplateAttributes } from './lit-template-regions.js';
13
+ /**
14
+ * Attributes (and their Lit property-binding spellings) that give the
15
+ * button an accessible name. `parseTemplateAttributes` lowercases names,
16
+ * so `.ariaLabel=${…}` arrives as `.arialabel`.
17
+ */
18
+ const NAMING_ATTRIBUTES = ['data-l10n-id', 'aria-label', '.arialabel'];
19
+ /** Attributes that give the button a visible label, so it is not icon-only. */
20
+ const LABEL_ATTRIBUTES = ['label', '.label'];
21
+ /**
22
+ * Whether the button's content has a non-blank character outside tags.
23
+ * Scans instead of stripping tags, so nested or unbalanced markup cannot
24
+ * leave a fragment behind that reads as text, and a `>` inside a quoted
25
+ * attribute value does not end its tag.
26
+ */
27
+ function hasVisibleText(markup) {
28
+ let inTag = false;
29
+ let quote;
30
+ for (const char of markup) {
31
+ if (quote !== undefined) {
32
+ if (char === quote)
33
+ quote = undefined;
34
+ }
35
+ else if (inTag) {
36
+ if (char === '"' || char === "'")
37
+ quote = char;
38
+ else if (char === '>')
39
+ inTag = false;
40
+ }
41
+ else if (char === '<') {
42
+ inTag = true;
43
+ }
44
+ else if (char.trim() !== '') {
45
+ return true;
46
+ }
47
+ }
48
+ return false;
49
+ }
50
+ function isIconOnly(attrs, innerText) {
51
+ if (LABEL_ATTRIBUTES.some((name) => attrs.has(name)))
52
+ return false;
53
+ if (hasVisibleText(innerText))
54
+ return false;
55
+ const type = attrs.get('type') ?? '';
56
+ return attrs.has('iconsrc') || attrs.has('.iconsrc') || /(^|\s)icon(\s|$)/.test(type);
57
+ }
58
+ /**
59
+ * Counts icon-only `<moz-button>` elements in the module's Lit templates
60
+ * that carry neither `data-l10n-id` nor `aria-label`.
61
+ *
62
+ * @param content - The component module's source
63
+ */
64
+ export function countUnlabelledIconButtons(content) {
65
+ let count = 0;
66
+ for (const region of collectLitTemplateRegions(content)) {
67
+ const pattern = /<moz-button\b((?:"[^"]*"|'[^']*'|[^'">])*)>([\s\S]*?)<\/moz-button>/gi;
68
+ for (const match of region.matchAll(pattern)) {
69
+ const attrs = parseTemplateAttributes(match[1] ?? '');
70
+ if (!isIconOnly(attrs, match[2] ?? ''))
71
+ continue;
72
+ if (NAMING_ATTRIBUTES.some((name) => attrs.has(name)))
73
+ continue;
74
+ count++;
75
+ }
76
+ }
77
+ return count;
78
+ }
@@ -49,7 +49,7 @@ export async function validateComponent(componentDir, tagName, type, config, roo
49
49
  // parameter, so this is a no-op for stock and override components.
50
50
  const customConfigForTag = type === 'custom' ? config?.custom[tagName] : undefined;
51
51
  issues.push(...(await validateStructure(componentDir, tagName, type, customConfigForTag)));
52
- issues.push(...(await validateAccessibility(componentDir, tagName, customConfigForTag)));
52
+ issues.push(...(await validateAccessibility(componentDir, tagName, customConfigForTag, config?.optInChecks)));
53
53
  issues.push(...(await validateCompatibility(componentDir, tagName, type, config, root)));
54
54
  if (root && config && type === 'override') {
55
55
  const forgeConfig = await loadConfig(root);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * `fireforge test --auto-ports` (or `test.mochitestPorts: "auto"`): let two
3
+ * checkouts run browser suites at the same time.
4
+ *
5
+ * Desktop `mach mochitest` has no `--http-port`/`--ssl-port` options; those
6
+ * exist only in the Android and iOS argument sets. What recent engines do
7
+ * have is a fallback inside `testing/mochitest/runtests.py`: before starting
8
+ * its servers the harness bind-probes the HTTP server, ssltunnel and
9
+ * WebSocket ports, and before launching the browser the Marionette port,
10
+ * and moves any that is busy to a free port, routing `mochi.test:8888`
11
+ * through its proxy to wherever the server actually landed. FireForge's
12
+ * own preflight refused before that fallback could run. In auto mode it
13
+ * steps aside for a holder that is not this checkout's own (a sibling
14
+ * checkout's live suite, or an unrelated listener), and keeps refusing for
15
+ * this checkout's own leftovers, exactly as in the default mode.
16
+ *
17
+ * Auto mode is opt-in, and it only takes effect when the engine's harness
18
+ * is seen to carry the fallback: on an engine without it, a busy port would
19
+ * still collide, so the default refusal stays in force there.
20
+ */
21
+ import type { TestOptions } from '../types/commands/index.js';
22
+ import type { FireForgeConfig } from '../types/config.js';
23
+ import { type MarionettePortHolder } from './marionette-port.js';
24
+ /** Which busy-port fallbacks the engine's mochitest harness carries. */
25
+ export interface HarnessPortFallback {
26
+ /** HTTP server, ssltunnel and WebSocket ports (`startServers`). */
27
+ serverPorts: boolean;
28
+ /** The Marionette port (before the browser launch). */
29
+ marionettePort: boolean;
30
+ }
31
+ /**
32
+ * Reads the engine's `runtests.py` for the busy-port fallbacks. An
33
+ * unreadable file reports neither, which keeps the default refusal.
34
+ */
35
+ export declare function detectHarnessPortFallback(engineDir: string): Promise<HarnessPortFallback>;
36
+ /** True when the operator asked for auto ports, by flag or by config. */
37
+ export declare function autoPortsRequested(options: TestOptions, config: FireForgeConfig): boolean;
38
+ /**
39
+ * True when the holder's command line points into THIS checkout's engine
40
+ * directory: our own leftover, which auto mode never steps aside for.
41
+ */
42
+ export declare function isThisCheckoutsHolder(holder: MarionettePortHolder, engineDir: string): boolean;
43
+ /**
44
+ * Resolves the effective auto-ports posture for a browser run: what the
45
+ * engine's harness can fall back from, or `undefined` when auto mode is
46
+ * off or the engine cannot honour it (in which case the default refusal
47
+ * stays, and an explicit request says why).
48
+ */
49
+ export declare function resolveAutoPorts(engineDir: string, options: TestOptions, config: FireForgeConfig): Promise<HarnessPortFallback | undefined>;
50
+ /**
51
+ * Steps aside for a busy port the harness will fall back from. Returns true
52
+ * (after a notice naming the holder) when the port is held by anything that
53
+ * is not this checkout's own process, false when the port is free or the
54
+ * holder is ours, so the caller's normal preflight runs.
55
+ *
56
+ * @param port - The default port the harness would use
57
+ * @param label - Human name of the port for the notice
58
+ * @param engineDir - This checkout's engine directory
59
+ */
60
+ export declare function deferBusyPortToHarness(port: number, label: string, engineDir: string): Promise<boolean>;
@@ -0,0 +1,96 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * `fireforge test --auto-ports` (or `test.mochitestPorts: "auto"`): let two
4
+ * checkouts run browser suites at the same time.
5
+ *
6
+ * Desktop `mach mochitest` has no `--http-port`/`--ssl-port` options; those
7
+ * exist only in the Android and iOS argument sets. What recent engines do
8
+ * have is a fallback inside `testing/mochitest/runtests.py`: before starting
9
+ * its servers the harness bind-probes the HTTP server, ssltunnel and
10
+ * WebSocket ports, and before launching the browser the Marionette port,
11
+ * and moves any that is busy to a free port, routing `mochi.test:8888`
12
+ * through its proxy to wherever the server actually landed. FireForge's
13
+ * own preflight refused before that fallback could run. In auto mode it
14
+ * steps aside for a holder that is not this checkout's own (a sibling
15
+ * checkout's live suite, or an unrelated listener), and keeps refusing for
16
+ * this checkout's own leftovers, exactly as in the default mode.
17
+ *
18
+ * Auto mode is opt-in, and it only takes effect when the engine's harness
19
+ * is seen to carry the fallback: on an engine without it, a busy port would
20
+ * still collide, so the default refusal stays in force there.
21
+ */
22
+ import { readFile } from 'node:fs/promises';
23
+ import { join } from 'node:path';
24
+ import { notice } from '../utils/logger.js';
25
+ import { isExplicitAbsolutePath, isPathInsideRoot } from '../utils/paths.js';
26
+ import { probeMarionettePort } from './marionette-port.js';
27
+ const RUNTESTS_PATH = 'testing/mochitest/runtests.py';
28
+ /**
29
+ * Reads the engine's `runtests.py` for the busy-port fallbacks. An
30
+ * unreadable file reports neither, which keeps the default refusal.
31
+ */
32
+ export async function detectHarnessPortFallback(engineDir) {
33
+ let source;
34
+ try {
35
+ source = await readFile(join(engineDir, RUNTESTS_PATH), 'utf8');
36
+ }
37
+ catch {
38
+ return { serverPorts: false, marionettePort: false };
39
+ }
40
+ const fallsBack = /falling back to port/.test(source);
41
+ return {
42
+ serverPorts: fallsBack && /\("httpPort",/.test(source) && /_port_in_use\(port\)/.test(source),
43
+ marionettePort: fallsBack && /_port_in_use\(marionette_port\)/.test(source),
44
+ };
45
+ }
46
+ /** True when the operator asked for auto ports, by flag or by config. */
47
+ export function autoPortsRequested(options, config) {
48
+ return options.autoPorts === true || config.test?.mochitestPorts === 'auto';
49
+ }
50
+ /**
51
+ * True when the holder's command line points into THIS checkout's engine
52
+ * directory: our own leftover, which auto mode never steps aside for.
53
+ */
54
+ export function isThisCheckoutsHolder(holder, engineDir) {
55
+ return holder.commandLine
56
+ .split(/\s+/)
57
+ .filter(isExplicitAbsolutePath)
58
+ .some((token) => isPathInsideRoot(engineDir, token));
59
+ }
60
+ /**
61
+ * Resolves the effective auto-ports posture for a browser run: what the
62
+ * engine's harness can fall back from, or `undefined` when auto mode is
63
+ * off or the engine cannot honour it (in which case the default refusal
64
+ * stays, and an explicit request says why).
65
+ */
66
+ export async function resolveAutoPorts(engineDir, options, config) {
67
+ if (!autoPortsRequested(options, config))
68
+ return undefined;
69
+ const fallback = await detectHarnessPortFallback(engineDir);
70
+ if (!fallback.serverPorts && !fallback.marionettePort) {
71
+ notice(`Auto ports requested, but engine/${RUNTESTS_PATH} has no busy-port fallback, so a busy ` +
72
+ 'mochitest or Marionette port is still refused as in the default mode.');
73
+ return undefined;
74
+ }
75
+ return fallback;
76
+ }
77
+ /**
78
+ * Steps aside for a busy port the harness will fall back from. Returns true
79
+ * (after a notice naming the holder) when the port is held by anything that
80
+ * is not this checkout's own process, false when the port is free or the
81
+ * holder is ours, so the caller's normal preflight runs.
82
+ *
83
+ * @param port - The default port the harness would use
84
+ * @param label - Human name of the port for the notice
85
+ * @param engineDir - This checkout's engine directory
86
+ */
87
+ export async function deferBusyPortToHarness(port, label, engineDir) {
88
+ const probe = await probeMarionettePort(port);
89
+ if (!probe.inUse || !probe.holder)
90
+ return false;
91
+ if (isThisCheckoutsHolder(probe.holder, engineDir))
92
+ return false;
93
+ notice(`${label} port ${port} is held by ${probe.holder.command} (PID ${probe.holder.pid}), which is ` +
94
+ "not this checkout's. Auto ports: the harness moves this run to a free port instead.");
95
+ return true;
96
+ }
@@ -0,0 +1,61 @@
1
+ /** Filename of the history log at its pre-0.50 location, inside `patches/`. */
2
+ export declare const LEGACY_HISTORY_LOG_FILENAME = ".fireforge-history.jsonl";
3
+ /** Default history log location, relative to the project root. */
4
+ export declare const DEFAULT_HISTORY_LOG_PATH: string;
5
+ /** Environment variable a caller sets to tag every record of its session. */
6
+ export declare const HISTORY_SESSION_ENV = "FIREFORGE_SESSION_ID";
7
+ /** Who wrote a history record. */
8
+ export interface HistoryRecordIdentity {
9
+ /** One id per FireForge process: every record one command writes shares it. */
10
+ invocationId: string;
11
+ /** The caller's `FIREFORGE_SESSION_ID`, when set. */
12
+ sessionId?: string;
13
+ pid: number;
14
+ ppid: number;
15
+ }
16
+ /**
17
+ * Identity fields stamped on every history record: one id per FireForge
18
+ * process (every record one command writes shares it), the process and its
19
+ * parent, and the caller's `FIREFORGE_SESSION_ID` when set. The parent PID
20
+ * is a weak hint (usually the shell); the environment variable is the
21
+ * reliable way for a session to tag its own lines.
22
+ */
23
+ export declare function historyRecordIdentity(): HistoryRecordIdentity;
24
+ /**
25
+ * Resolves the history log path for a project.
26
+ *
27
+ * @param projectRoot - Project root containing `fireforge.json`
28
+ * @returns Absolute path of the history log
29
+ */
30
+ export declare function resolveHistoryLogPath(projectRoot: string): Promise<string>;
31
+ /**
32
+ * Moves a history log left at the legacy `patches/` path to `target`.
33
+ *
34
+ * Records are never dropped, whatever else is happening:
35
+ * - The legacy file is first claimed by renaming it aside, so of two
36
+ * commands migrating at once exactly one does the work and neither
37
+ * duplicates it.
38
+ * - When `target` does not exist yet (the first run), the claimed file is
39
+ * hard-linked into place, which cannot overwrite a log another command
40
+ * created in the meantime, and keeps a large log a constant-time move.
41
+ * - When both exist (an older FireForge appended to the legacy path after
42
+ * a migration, or `history.path` changed), the legacy records are
43
+ * appended to the target in one write. They then follow newer records;
44
+ * every record carries its own `ts`.
45
+ *
46
+ * A crash between the claim and the move leaves the records in a
47
+ * `.migrating-*` file next to the legacy path, never deleted.
48
+ *
49
+ * @param patchesDir - The patches directory that may hold a legacy log
50
+ * @param target - Absolute path of the history log
51
+ */
52
+ export declare function migrateLegacyHistoryLog(patchesDir: string, target: string): Promise<void>;
53
+ /**
54
+ * Appends one serialized record to the project's history log, migrating a
55
+ * legacy log first.
56
+ *
57
+ * @param patchesDir - The project's `patches/` directory; the project root is
58
+ * its parent, as every caller derives it from `getProjectPaths`
59
+ * @param line - One JSON record, without the trailing newline
60
+ */
61
+ export declare function appendHistoryLine(patchesDir: string, line: string): Promise<void>;