@hominis/fireforge 0.45.0 → 0.46.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 (359) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor.d.ts +2 -0
  18. package/dist/src/commands/doctor.js +7 -8
  19. package/dist/src/commands/download.js +35 -24
  20. package/dist/src/commands/export-all.js +21 -4
  21. package/dist/src/commands/export-flow.js +15 -45
  22. package/dist/src/commands/export-placement-conflicts.js +3 -3
  23. package/dist/src/commands/export-placement-gate.js +2 -8
  24. package/dist/src/commands/export-placement-policy.js +3 -5
  25. package/dist/src/commands/export-shared.d.ts +42 -23
  26. package/dist/src/commands/export-shared.js +4 -19
  27. package/dist/src/commands/export.js +10 -1
  28. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  29. package/dist/src/commands/furnace/authoring-config.js +19 -0
  30. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  31. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  32. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  33. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  34. package/dist/src/commands/furnace/create-templates.js +2 -1
  35. package/dist/src/commands/furnace/create.js +25 -19
  36. package/dist/src/commands/furnace/deploy.js +11 -2
  37. package/dist/src/commands/furnace/init.js +2 -1
  38. package/dist/src/commands/furnace/override.js +28 -19
  39. package/dist/src/commands/furnace/refresh.js +9 -1
  40. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  41. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  42. package/dist/src/commands/furnace/remove.js +2 -6
  43. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  44. package/dist/src/commands/furnace/rename.js +29 -29
  45. package/dist/src/commands/furnace/status.js +12 -9
  46. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  47. package/dist/src/commands/furnace/test-file-name.js +39 -0
  48. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  49. package/dist/src/commands/furnace/validation-output.js +3 -8
  50. package/dist/src/commands/import.js +2 -9
  51. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  52. package/dist/src/commands/lint-per-patch.js +19 -4
  53. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  54. package/dist/src/commands/lint.d.ts +2 -1
  55. package/dist/src/commands/lint.js +1 -1
  56. package/dist/src/commands/manifest.js +1 -1
  57. package/dist/src/commands/package.js +2 -4
  58. package/dist/src/commands/patch/compact.js +14 -15
  59. package/dist/src/commands/patch/delete.js +15 -25
  60. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  61. package/dist/src/commands/patch/entry-projection.js +24 -0
  62. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  63. package/dist/src/commands/patch/lint-ignore.js +19 -29
  64. package/dist/src/commands/patch/move-files-create.js +24 -10
  65. package/dist/src/commands/patch/move-files-into.js +38 -34
  66. package/dist/src/commands/patch/move-files.js +4 -13
  67. package/dist/src/commands/patch/rename.js +27 -41
  68. package/dist/src/commands/patch/reorder.js +25 -38
  69. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  70. package/dist/src/commands/patch/split-plan.js +4 -8
  71. package/dist/src/commands/patch/split.js +45 -43
  72. package/dist/src/commands/patch/staged-dependency.js +16 -22
  73. package/dist/src/commands/patch/tier.js +15 -25
  74. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  75. package/dist/src/commands/patch-rename-map.js +36 -0
  76. package/dist/src/commands/pending-resolution.d.ts +15 -0
  77. package/dist/src/commands/pending-resolution.js +25 -0
  78. package/dist/src/commands/re-export-adjacent.d.ts +24 -2
  79. package/dist/src/commands/re-export-adjacent.js +32 -16
  80. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  81. package/dist/src/commands/re-export-drift.js +4 -4
  82. package/dist/src/commands/re-export-files.d.ts +2 -3
  83. package/dist/src/commands/re-export-files.js +38 -29
  84. package/dist/src/commands/re-export-lint.d.ts +3 -4
  85. package/dist/src/commands/re-export-lint.js +9 -2
  86. package/dist/src/commands/re-export-scan.js +5 -2
  87. package/dist/src/commands/re-export-single.d.ts +22 -3
  88. package/dist/src/commands/re-export-single.js +19 -11
  89. package/dist/src/commands/re-export.js +28 -11
  90. package/dist/src/commands/rebase/abort.js +3 -8
  91. package/dist/src/commands/rebase/continue.js +12 -12
  92. package/dist/src/commands/rebase/index.js +2 -2
  93. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  94. package/dist/src/commands/rebase/patch-loop.js +2 -7
  95. package/dist/src/commands/resolve.js +18 -10
  96. package/dist/src/commands/run.d.ts +0 -13
  97. package/dist/src/commands/run.js +12 -25
  98. package/dist/src/commands/setup-support.d.ts +16 -4
  99. package/dist/src/commands/setup-support.js +16 -16
  100. package/dist/src/commands/setup.js +3 -9
  101. package/dist/src/commands/source.js +8 -1
  102. package/dist/src/commands/status-check.js +2 -2
  103. package/dist/src/commands/status-output.js +2 -2
  104. package/dist/src/commands/status.js +3 -4
  105. package/dist/src/commands/test-build-phase.d.ts +2 -2
  106. package/dist/src/commands/test-build-phase.js +31 -7
  107. package/dist/src/commands/test-modes.d.ts +2 -2
  108. package/dist/src/commands/test-modes.js +0 -1
  109. package/dist/src/commands/test-register.js +5 -3
  110. package/dist/src/commands/test-run.js +25 -18
  111. package/dist/src/commands/test-stale-gate.js +4 -4
  112. package/dist/src/commands/test-verdict.d.ts +16 -2
  113. package/dist/src/commands/test-verdict.js +18 -3
  114. package/dist/src/commands/test.js +42 -5
  115. package/dist/src/commands/token-list.d.ts +26 -0
  116. package/dist/src/commands/token-list.js +153 -0
  117. package/dist/src/commands/token.js +17 -1
  118. package/dist/src/commands/tree.js +7 -2
  119. package/dist/src/commands/typecheck.js +9 -9
  120. package/dist/src/commands/wire.js +4 -3
  121. package/dist/src/core/branding.d.ts +0 -7
  122. package/dist/src/core/branding.js +3 -2
  123. package/dist/src/core/browser-wire.js +6 -3
  124. package/dist/src/core/build-audit-platform.d.ts +29 -0
  125. package/dist/src/core/build-audit-platform.js +140 -0
  126. package/dist/src/core/build-audit-registration.js +3 -2
  127. package/dist/src/core/build-audit-skip.d.ts +57 -0
  128. package/dist/src/core/build-audit-skip.js +112 -0
  129. package/dist/src/core/build-audit.d.ts +15 -2
  130. package/dist/src/core/build-audit.js +110 -10
  131. package/dist/src/core/build-baseline.d.ts +45 -28
  132. package/dist/src/core/build-baseline.js +6 -29
  133. package/dist/src/core/build-jar-escalation.js +3 -2
  134. package/dist/src/core/build-overwrite-guard.js +1 -1
  135. package/dist/src/core/build-prepare.d.ts +0 -8
  136. package/dist/src/core/build-prepare.js +7 -12
  137. package/dist/src/core/config-paths.d.ts +2 -6
  138. package/dist/src/core/config-paths.js +4 -2
  139. package/dist/src/core/config-state.d.ts +0 -6
  140. package/dist/src/core/config-state.js +1 -19
  141. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  142. package/dist/src/core/config-validate-build-audit.js +80 -0
  143. package/dist/src/core/config-validate-file-size.js +1 -1
  144. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  145. package/dist/src/core/config-validate-patch-policy.js +3 -3
  146. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  147. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  148. package/dist/src/core/config-validate.js +14 -3
  149. package/dist/src/core/config.d.ts +2 -2
  150. package/dist/src/core/config.js +11 -15
  151. package/dist/src/core/coverage-extend.js +4 -4
  152. package/dist/src/core/discard-baseline.js +7 -7
  153. package/dist/src/core/engine-precondition.d.ts +1 -9
  154. package/dist/src/core/engine-precondition.js +3 -8
  155. package/dist/src/core/engine-session-lock.js +13 -17
  156. package/dist/src/core/export-moved-code-shape.js +2 -2
  157. package/dist/src/core/file-lock-owner.d.ts +103 -0
  158. package/dist/src/core/file-lock-owner.js +259 -0
  159. package/dist/src/core/file-lock.d.ts +2 -7
  160. package/dist/src/core/file-lock.js +40 -91
  161. package/dist/src/core/firefox-cache.d.ts +13 -1
  162. package/dist/src/core/firefox-cache.js +59 -34
  163. package/dist/src/core/firefox-download.d.ts +15 -0
  164. package/dist/src/core/firefox-download.js +37 -21
  165. package/dist/src/core/firefox-extract.d.ts +6 -5
  166. package/dist/src/core/firefox-extract.js +36 -18
  167. package/dist/src/core/firefox.d.ts +30 -32
  168. package/dist/src/core/firefox.js +5 -36
  169. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  170. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  171. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  172. package/dist/src/core/furnace-apply-helpers.js +8 -19
  173. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  174. package/dist/src/core/furnace-apply.d.ts +1 -1
  175. package/dist/src/core/furnace-apply.js +30 -10
  176. package/dist/src/core/furnace-config.d.ts +1 -1
  177. package/dist/src/core/furnace-config.js +23 -36
  178. package/dist/src/core/furnace-constants.js +3 -2
  179. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  180. package/dist/src/core/furnace-dir-entry.js +21 -0
  181. package/dist/src/core/furnace-operation.d.ts +2 -0
  182. package/dist/src/core/furnace-operation.js +3 -1
  183. package/dist/src/core/furnace-refresh.d.ts +18 -6
  184. package/dist/src/core/furnace-refresh.js +3 -6
  185. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  186. package/dist/src/core/furnace-registration-ast.js +0 -3
  187. package/dist/src/core/furnace-registration.d.ts +2 -1
  188. package/dist/src/core/furnace-registration.js +22 -29
  189. package/dist/src/core/furnace-scanner.js +3 -6
  190. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  191. package/dist/src/core/furnace-validate-registration.js +5 -7
  192. package/dist/src/core/git-base.d.ts +3 -1
  193. package/dist/src/core/git-base.js +25 -3
  194. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  195. package/dist/src/core/git-diff-new-file.js +119 -0
  196. package/dist/src/core/git-diff.d.ts +9 -9
  197. package/dist/src/core/git-diff.js +88 -157
  198. package/dist/src/core/git-file-ops.d.ts +0 -7
  199. package/dist/src/core/git-file-ops.js +4 -28
  200. package/dist/src/core/git-readonly-index.d.ts +0 -2
  201. package/dist/src/core/git-readonly-index.js +0 -4
  202. package/dist/src/core/git-status.d.ts +7 -0
  203. package/dist/src/core/git-status.js +21 -9
  204. package/dist/src/core/git.d.ts +0 -6
  205. package/dist/src/core/git.js +14 -43
  206. package/dist/src/core/harness-orphans.js +8 -7
  207. package/dist/src/core/history-log.d.ts +19 -0
  208. package/dist/src/core/history-log.js +29 -0
  209. package/dist/src/core/mach-error-hints.d.ts +0 -5
  210. package/dist/src/core/mach-error-hints.js +1 -1
  211. package/dist/src/core/mach-known-noise-filter.d.ts +4 -3
  212. package/dist/src/core/mach-known-noise-filter.js +7 -9
  213. package/dist/src/core/mach-mozconfig.js +2 -1
  214. package/dist/src/core/mach-python.d.ts +2 -0
  215. package/dist/src/core/mach-python.js +12 -7
  216. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  217. package/dist/src/core/mach-resource-shim.js +112 -9
  218. package/dist/src/core/mach.d.ts +39 -53
  219. package/dist/src/core/mach.js +13 -81
  220. package/dist/src/core/marionette-port.d.ts +0 -17
  221. package/dist/src/core/marionette-port.js +12 -58
  222. package/dist/src/core/marionette-preflight.js +9 -27
  223. package/dist/src/core/mochitest-server-port.js +3 -3
  224. package/dist/src/core/moz-manifest-rules.js +37 -33
  225. package/dist/src/core/patch-apply-fuzz.js +4 -4
  226. package/dist/src/core/patch-apply.d.ts +4 -5
  227. package/dist/src/core/patch-apply.js +6 -12
  228. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  229. package/dist/src/core/patch-export-coverage.js +1 -30
  230. package/dist/src/core/patch-export-update.d.ts +18 -1
  231. package/dist/src/core/patch-export-update.js +4 -3
  232. package/dist/src/core/patch-export.d.ts +11 -21
  233. package/dist/src/core/patch-export.js +44 -66
  234. package/dist/src/core/patch-files.d.ts +0 -4
  235. package/dist/src/core/patch-files.js +0 -11
  236. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  237. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  238. package/dist/src/core/patch-lint-cache.js +3 -6
  239. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  240. package/dist/src/core/patch-lint-checkjs.js +37 -24
  241. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  242. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  243. package/dist/src/core/patch-lint-cross.js +49 -92
  244. package/dist/src/core/patch-lint-css.js +10 -6
  245. package/dist/src/core/patch-lint-file-size.js +3 -3
  246. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  247. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  248. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  249. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  250. package/dist/src/core/patch-lint-prettier.js +1 -1
  251. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  252. package/dist/src/core/patch-lint-queue-types.js +12 -0
  253. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  254. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  255. package/dist/src/core/patch-lint.d.ts +14 -9
  256. package/dist/src/core/patch-lint.js +4 -3
  257. package/dist/src/core/patch-lock.js +3 -3
  258. package/dist/src/core/patch-manifest-io.js +8 -7
  259. package/dist/src/core/patch-parse.d.ts +19 -8
  260. package/dist/src/core/patch-parse.js +25 -0
  261. package/dist/src/core/patch-policy.js +15 -27
  262. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  263. package/dist/src/core/patch-source-metadata.js +0 -4
  264. package/dist/src/core/patch-transform.d.ts +0 -8
  265. package/dist/src/core/patch-transform.js +0 -10
  266. package/dist/src/core/process-cpu-time.js +1 -1
  267. package/dist/src/core/rebase-session.d.ts +2 -8
  268. package/dist/src/core/rebase-session.js +5 -17
  269. package/dist/src/core/register-browser-content.js +3 -2
  270. package/dist/src/core/register-browser-test.js +2 -1
  271. package/dist/src/core/register-module.js +2 -1
  272. package/dist/src/core/register-scaffold.js +5 -4
  273. package/dist/src/core/register-shared-css.js +2 -1
  274. package/dist/src/core/run-log-redact.d.ts +38 -0
  275. package/dist/src/core/run-log-redact.js +69 -0
  276. package/dist/src/core/run-log.d.ts +13 -2
  277. package/dist/src/core/run-log.js +67 -18
  278. package/dist/src/core/signal-critical.js +2 -4
  279. package/dist/src/core/smoke-patterns.d.ts +0 -1
  280. package/dist/src/core/smoke-patterns.js +6 -11
  281. package/dist/src/core/state-file.d.ts +12 -1
  282. package/dist/src/core/state-file.js +14 -1
  283. package/dist/src/core/status-classify.js +2 -1
  284. package/dist/src/core/test-changed-prefs.js +26 -11
  285. package/dist/src/core/test-harness-crash.js +2 -2
  286. package/dist/src/core/test-stale-check.d.ts +0 -2
  287. package/dist/src/core/test-stale-check.js +2 -2
  288. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  289. package/dist/src/core/test-xpcshell-retry.js +14 -11
  290. package/dist/src/core/token-category.d.ts +33 -0
  291. package/dist/src/core/token-category.js +56 -48
  292. package/dist/src/core/token-dark-mode.js +2 -14
  293. package/dist/src/core/token-inventory.d.ts +56 -0
  294. package/dist/src/core/token-inventory.js +109 -0
  295. package/dist/src/core/token-manager.d.ts +1 -1
  296. package/dist/src/core/token-manager.js +1 -1
  297. package/dist/src/core/token-variant.js +21 -22
  298. package/dist/src/core/toolchain-preflight.js +1 -1
  299. package/dist/src/core/tree-cow.d.ts +0 -2
  300. package/dist/src/core/tree-cow.js +2 -2
  301. package/dist/src/core/tree-store.js +4 -5
  302. package/dist/src/core/typecheck.js +2 -2
  303. package/dist/src/core/wire-dom-fragment.js +6 -17
  304. package/dist/src/core/wire-utils.js +5 -17
  305. package/dist/src/core/xpcshell-appdir.js +2 -4
  306. package/dist/src/errors/base.d.ts +53 -1
  307. package/dist/src/errors/base.js +79 -11
  308. package/dist/src/errors/build.js +20 -28
  309. package/dist/src/errors/config.d.ts +0 -14
  310. package/dist/src/errors/config.js +13 -34
  311. package/dist/src/errors/download.d.ts +13 -0
  312. package/dist/src/errors/download.js +53 -24
  313. package/dist/src/errors/furnace.js +8 -9
  314. package/dist/src/errors/git.d.ts +8 -8
  315. package/dist/src/errors/git.js +20 -30
  316. package/dist/src/errors/patch.js +8 -10
  317. package/dist/src/errors/rebase.js +7 -7
  318. package/dist/src/types/commands/options.d.ts +77 -123
  319. package/dist/src/types/config.d.ts +34 -2
  320. package/dist/src/types/index.d.ts +0 -1
  321. package/dist/src/utils/assert.d.ts +3 -15
  322. package/dist/src/utils/assert.js +1 -18
  323. package/dist/src/utils/build-info.d.ts +1 -1
  324. package/dist/src/utils/child-env.d.ts +2 -1
  325. package/dist/src/utils/elapsed.js +10 -1
  326. package/dist/src/utils/fs.js +8 -13
  327. package/dist/src/utils/hash.d.ts +12 -0
  328. package/dist/src/utils/hash.js +16 -0
  329. package/dist/src/utils/line-dispatch.d.ts +15 -0
  330. package/dist/src/utils/line-dispatch.js +48 -0
  331. package/dist/src/utils/options.d.ts +1 -7
  332. package/dist/src/utils/options.js +6 -7
  333. package/dist/src/utils/package-root.js +3 -12
  334. package/dist/src/utils/platform.d.ts +0 -4
  335. package/dist/src/utils/process-group.d.ts +2 -1
  336. package/dist/src/utils/process-group.js +20 -19
  337. package/dist/src/utils/process.d.ts +40 -26
  338. package/dist/src/utils/process.js +115 -166
  339. package/dist/src/utils/ps-duration.js +3 -3
  340. package/dist/src/utils/sleep.d.ts +26 -0
  341. package/dist/src/utils/sleep.js +31 -0
  342. package/dist/src/utils/validation.d.ts +2 -38
  343. package/dist/src/utils/validation.js +7 -16
  344. package/docs/configuration.md +89 -1
  345. package/docs/exit-codes.md +31 -19
  346. package/docs/lifecycle-invariants.md +18 -12
  347. package/docs/machine-output.md +31 -8
  348. package/docs/patch-workflow.md +2 -1
  349. package/docs/run-logs.md +44 -4
  350. package/docs/testing.md +8 -1
  351. package/package.json +8 -7
  352. package/dist/src/commands/rebase.d.ts +0 -4
  353. package/dist/src/commands/rebase.js +0 -5
  354. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  355. package/dist/src/core/patch-artifact-normalize.js +0 -24
  356. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  357. package/dist/src/core/patch-lint-reexports.js +0 -8
  358. package/dist/src/core/wire-targets.d.ts +0 -7
  359. package/dist/src/core/wire-targets.js +0 -8
@@ -20,8 +20,8 @@ function multiLineBlockNameMatches(blockLine, category) {
20
20
  return blockLine.replace(/^\s*\*\s*/, '').trim() === category;
21
21
  }
22
22
  /**
23
- * Body of the first `/* … *\/` comment on `line`, or `null` when the line
24
- * carries no closed block comment.
23
+ * Body of the first `/* … *\/` comment on `line`, or `undefined` when the
24
+ * line carries no closed block comment.
25
25
  *
26
26
  * Deliberately index arithmetic rather than a regex: every regex spelling of
27
27
  * "banner comment" this module used to carry was super-linear on a line that
@@ -32,10 +32,10 @@ function multiLineBlockNameMatches(blockLine, category) {
32
32
  function blockCommentBody(line) {
33
33
  const open = line.indexOf('/*');
34
34
  if (open === -1)
35
- return null;
35
+ return undefined;
36
36
  const close = line.indexOf('*/', open + 2);
37
37
  if (close === -1)
38
- return null;
38
+ return undefined;
39
39
  return line.slice(open + 2, close);
40
40
  }
41
41
  /**
@@ -49,14 +49,15 @@ function isSingleLineBannerLine(line) {
49
49
  return body !== undefined && body.length >= 2 && body.startsWith('=') && body.endsWith('=');
50
50
  }
51
51
  /**
52
- * Name declared by a single-line banner (`/* = Foo = *\/` → `Foo`), or `null`
53
- * when the line is not a named banner. The `=` runs on both sides are
54
- * required; a decorative all-`=` rule carries no name and yields `null`.
52
+ * Name declared by a single-line banner (`/* = Foo = *\/` → `Foo`), or
53
+ * `undefined` when the line is not a named banner. The `=` runs on both sides
54
+ * are required; a decorative all-`=` rule carries no name and yields
55
+ * `undefined`.
55
56
  */
56
57
  function singleLineBannerName(line) {
57
58
  const body = blockCommentBody(line);
58
- if (body === null)
59
- return null;
59
+ if (body === undefined)
60
+ return undefined;
60
61
  let start = 0;
61
62
  let end = body.length;
62
63
  while (start < end && /\s/.test(body[start] ?? ''))
@@ -65,16 +66,16 @@ function singleLineBannerName(line) {
65
66
  while (start < end && body[start] === '=')
66
67
  start++;
67
68
  if (start === leadingEqStart)
68
- return null;
69
+ return undefined;
69
70
  while (end > start && /\s/.test(body[end - 1] ?? ''))
70
71
  end--;
71
72
  const trailingEqEnd = end;
72
73
  while (end > start && body[end - 1] === '=')
73
74
  end--;
74
75
  if (end === trailingEqEnd)
75
- return null;
76
+ return undefined;
76
77
  const name = body.slice(start, end).trim();
77
- return name.length > 0 ? name : null;
78
+ return name.length > 0 ? name : undefined;
78
79
  }
79
80
  /**
80
81
  * True when `lines` contain a category header (single-line or multi-line
@@ -84,8 +85,7 @@ function singleLineBannerName(line) {
84
85
  */
85
86
  export function categoryHeaderExists(lines, category) {
86
87
  const singleLinePattern = singleLineBannerPattern(category);
87
- for (let i = 0; i < lines.length; i++) {
88
- const line = lines[i] ?? '';
88
+ for (const [i, line] of lines.entries()) {
89
89
  if (singleLinePattern.test(line)) {
90
90
  return true;
91
91
  }
@@ -115,29 +115,44 @@ export function categoryHeaderExists(lines, category) {
115
115
  * This helper exists as a pure inspector; it never throws on malformed
116
116
  * headers and silently skips shapes it cannot parse.
117
117
  */
118
+ /**
119
+ * Category name declared by the banner STARTING at `index`, or `undefined`
120
+ * when that line opens no banner.
121
+ *
122
+ * The single reader for both banner shapes, so the inventory walk, the
123
+ * "available categories" error body and the section finder cannot drift on
124
+ * what counts as a header.
125
+ *
126
+ * @param lines - Tokens CSS split into lines
127
+ * @param index - 0-based line index to test
128
+ * @returns The declared category name, or undefined
129
+ */
130
+ export function categoryBannerNameAt(lines, index) {
131
+ const line = lines[index] ?? '';
132
+ const extracted = singleLineBannerName(line);
133
+ if (extracted !== undefined)
134
+ return extracted;
135
+ if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
136
+ for (let j = index + 1; j < Math.min(index + 6, lines.length); j++) {
137
+ const blockLine = lines[j] ?? '';
138
+ if (/\*\//.test(blockLine))
139
+ break;
140
+ const trimmed = blockLine.replace(/^\s*\*\s*/, '').trim();
141
+ if (trimmed.length === 0)
142
+ continue;
143
+ if (/^=+$/.test(trimmed))
144
+ continue;
145
+ return trimmed;
146
+ }
147
+ }
148
+ return undefined;
149
+ }
118
150
  function discoverCategoryHeaders(lines) {
119
151
  const categories = new Set();
120
152
  for (let i = 0; i < lines.length; i++) {
121
- const line = lines[i] ?? '';
122
- const extracted = singleLineBannerName(line);
123
- if (extracted !== null) {
124
- categories.add(extracted);
125
- continue;
126
- }
127
- if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
128
- for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
129
- const blockLine = lines[j] ?? '';
130
- if (/\*\//.test(blockLine))
131
- break;
132
- const trimmed = blockLine.replace(/^\s*\*\s*/, '').trim();
133
- if (trimmed.length === 0)
134
- continue;
135
- if (/^=+$/.test(trimmed))
136
- continue;
137
- categories.add(trimmed);
138
- break;
139
- }
140
- }
153
+ const name = categoryBannerNameAt(lines, i);
154
+ if (name !== undefined)
155
+ categories.add(name);
141
156
  }
142
157
  return [...categories];
143
158
  }
@@ -177,7 +192,7 @@ export async function assertTokenCategoryExists(engineDir, tokensCssPath, catego
177
192
  */
178
193
  export function declareCategoryBanner(lines, category, tokensCssPath) {
179
194
  const bounds = findBaseRootBounds(lines);
180
- if (bounds === null) {
195
+ if (bounds === undefined) {
181
196
  throw new GeneralError(`Cannot create category "${category}": no :root block found in ${tokensCssPath}. ` +
182
197
  'Run "fireforge furnace init --force" to re-scaffold the tokens CSS file.');
183
198
  }
@@ -190,8 +205,7 @@ export function declareCategoryBanner(lines, category, tokensCssPath) {
190
205
  export function findCategorySection(lines, category, tokensCssPath) {
191
206
  const singleLinePattern = singleLineBannerPattern(category);
192
207
  let categoryLine = -1;
193
- for (let i = 0; i < lines.length; i++) {
194
- const line = lines[i] ?? '';
208
+ for (const [i, line] of lines.entries()) {
195
209
  // Check single-line format: /* = Category = */
196
210
  if (singleLinePattern.test(line)) {
197
211
  categoryLine = i;
@@ -245,16 +259,10 @@ export function findCategorySection(lines, category, tokensCssPath) {
245
259
  return { categoryLine, sectionEnd };
246
260
  }
247
261
  /** 0-based open/close line indices of the base `:root {` block. */
248
- function findBaseRootBounds(lines) {
249
- let open = -1;
250
- for (let i = 0; i < lines.length; i++) {
251
- if (/:root\s*\{/.test(lines[i] ?? '')) {
252
- open = i;
253
- break;
254
- }
255
- }
262
+ export function findBaseRootBounds(lines) {
263
+ const open = lines.findIndex((line) => /:root\s*\{/.test(line));
256
264
  if (open === -1)
257
- return null;
265
+ return undefined;
258
266
  for (let i = open + 1; i < lines.length; i++) {
259
267
  if (/^\s*\}/.test(lines[i] ?? '')) {
260
268
  return { open, close: i };
@@ -263,7 +271,7 @@ function findBaseRootBounds(lines) {
263
271
  return { open, close: -1 };
264
272
  }
265
273
  /** Masks block-comment content per line so declarations inside comments never match. */
266
- function maskCommentLines(lines) {
274
+ export function maskCommentLines(lines) {
267
275
  const masked = lines.join('\n').replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, ' '));
268
276
  return masked.split('\n');
269
277
  }
@@ -276,7 +284,7 @@ function maskCommentLines(lines) {
276
284
  */
277
285
  export function findTokenDeclarationInRoot(lines, tokenName) {
278
286
  const bounds = findBaseRootBounds(lines);
279
- if (bounds === null || bounds.close === -1)
287
+ if (bounds === undefined || bounds.close === -1)
280
288
  return undefined;
281
289
  const masked = maskCommentLines(lines);
282
290
  const declPattern = new RegExp(`^\\s*${escapeRegex(tokenName)}\\s*:`);
@@ -87,13 +87,7 @@ export function stripBlockCommentsInLines(lines) {
87
87
  */
88
88
  export function findDarkRootInsertionIndex(lines) {
89
89
  const stripped = stripBlockCommentsInLines(lines);
90
- let darkMediaLine = -1;
91
- for (let i = 0; i < stripped.length; i++) {
92
- if (/prefers-color-scheme:\s*dark/.test(stripped[i] ?? '')) {
93
- darkMediaLine = i;
94
- break;
95
- }
96
- }
90
+ const darkMediaLine = stripped.findIndex((line) => /prefers-color-scheme:\s*dark/.test(line));
97
91
  if (darkMediaLine === -1)
98
92
  return null;
99
93
  // Walk the comment-stripped lines after the @media header and find
@@ -172,13 +166,7 @@ export function findBlockCloseIndex(stripped, startLine) {
172
166
  */
173
167
  export function findDarkMediaCloseIndex(lines) {
174
168
  const stripped = stripBlockCommentsInLines(lines);
175
- let darkMediaLine = -1;
176
- for (let i = 0; i < stripped.length; i++) {
177
- if (/prefers-color-scheme:\s*dark/.test(stripped[i] ?? '')) {
178
- darkMediaLine = i;
179
- break;
180
- }
181
- }
169
+ const darkMediaLine = stripped.findIndex((line) => /prefers-color-scheme:\s*dark/.test(line));
182
170
  if (darkMediaLine === -1)
183
171
  return -1;
184
172
  return findBlockCloseIndex(stripped, darkMediaLine);
@@ -0,0 +1,56 @@
1
+ /** One token declaration inside the base `:root` block. */
2
+ export interface TokenInventoryEntry {
3
+ /** Custom-property name, including the leading `--`. */
4
+ name: string;
5
+ /** 1-based line number in the tokens CSS file. */
6
+ line: number;
7
+ /** Declared value, trimmed, without the trailing `;`. */
8
+ value: string;
9
+ }
10
+ /**
11
+ * Tokens grouped under the banner that precedes them, in file order.
12
+ * `category` is null for declarations that sit above the first banner —
13
+ * a real state in a hand-edited file, and one the report must show rather
14
+ * than silently drop.
15
+ */
16
+ export interface TokenCategoryInventory {
17
+ category: string | null;
18
+ tokens: TokenInventoryEntry[];
19
+ }
20
+ /** One block that declares a given token, with the value it declares. */
21
+ export interface TokenBlockValue {
22
+ /**
23
+ * Selector trail of the declaring block, outermost first, joined with
24
+ * ` > ` — for example `:root` or
25
+ * `@media (prefers-color-scheme: dark) > :root`.
26
+ */
27
+ block: string;
28
+ value: string;
29
+ /** 1-based line number of the declaration. */
30
+ line: number;
31
+ }
32
+ /**
33
+ * Collects every base-`:root` token declaration grouped by the category
34
+ * banner above it, in document order.
35
+ *
36
+ * Only the base block is walked: the dark `@media` and `:root[variant]`
37
+ * companions mirror declarations rather than owning them, so listing them
38
+ * would report the same token two or three times under no category at all.
39
+ *
40
+ * @param lines - Tokens CSS split into lines
41
+ * @returns Categories in file order, each with its tokens in file order
42
+ */
43
+ export declare function collectTokenInventory(lines: string[]): TokenCategoryInventory[];
44
+ /**
45
+ * Every declaration of `tokenName`, in every block of the file, labelled by
46
+ * the selector trail of the block that declares it.
47
+ *
48
+ * Deliberately not restricted to the base `:root`: the question `token show`
49
+ * answers is "what does this token resolve to", and a token whose dark
50
+ * override differs from its base value is precisely the case worth seeing.
51
+ *
52
+ * @param lines - Tokens CSS split into lines
53
+ * @param tokenName - Custom-property name including the leading `--`
54
+ * @returns Declarations in file order
55
+ */
56
+ export declare function collectTokenBlockValues(lines: string[], tokenName: string): TokenBlockValue[];
@@ -0,0 +1,109 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Read-only inventory of the tokens CSS file: which categories exist, which
4
+ * tokens each declares, and what a single token resolves to in every theme
5
+ * block that mentions it.
6
+ *
7
+ * `token add` requires `--category` and, until now, nothing reported the
8
+ * categories a project actually has — so the only way to name one correctly
9
+ * was to hand-parse a neighbouring `= Category =` banner out of the file.
10
+ * This module is that report. It deliberately reuses the banner and
11
+ * `:root`-bounds primitives from `token-category.ts` rather than parsing
12
+ * tokens CSS a second way: two readers that disagree on what a banner is
13
+ * would put `token list` and `token add` in different sections of the same
14
+ * file.
15
+ */
16
+ import { categoryBannerNameAt, findBaseRootBounds, maskCommentLines } from './token-category.js';
17
+ /** Matches a custom-property declaration and captures its name and value. */
18
+ const DECLARATION_PATTERN = /^\s*(--[A-Za-z0-9_-]+)\s*:\s*([^;]*);?/;
19
+ /**
20
+ * Collects every base-`:root` token declaration grouped by the category
21
+ * banner above it, in document order.
22
+ *
23
+ * Only the base block is walked: the dark `@media` and `:root[variant]`
24
+ * companions mirror declarations rather than owning them, so listing them
25
+ * would report the same token two or three times under no category at all.
26
+ *
27
+ * @param lines - Tokens CSS split into lines
28
+ * @returns Categories in file order, each with its tokens in file order
29
+ */
30
+ export function collectTokenInventory(lines) {
31
+ const bounds = findBaseRootBounds(lines);
32
+ if (bounds === undefined || bounds.close === -1)
33
+ return [];
34
+ const masked = maskCommentLines(lines);
35
+ const groups = [];
36
+ let current;
37
+ for (let i = bounds.open + 1; i < bounds.close; i++) {
38
+ // Banners are read from the RAW lines: `maskCommentLines` blanks comment
39
+ // bodies, which is exactly what a banner is.
40
+ const banner = categoryBannerNameAt(lines, i);
41
+ if (banner !== undefined) {
42
+ current = { category: banner, tokens: [] };
43
+ groups.push(current);
44
+ continue;
45
+ }
46
+ const match = DECLARATION_PATTERN.exec(masked[i] ?? '');
47
+ if (!match?.[1])
48
+ continue;
49
+ if (current === undefined) {
50
+ current = { category: null, tokens: [] };
51
+ groups.push(current);
52
+ }
53
+ current.tokens.push({ name: match[1], line: i + 1, value: (match[2] ?? '').trim() });
54
+ }
55
+ // A banner with no declarations under it is still a category `token add
56
+ // --category` accepts, so empty groups are kept.
57
+ return groups;
58
+ }
59
+ /**
60
+ * Every declaration of `tokenName`, in every block of the file, labelled by
61
+ * the selector trail of the block that declares it.
62
+ *
63
+ * Deliberately not restricted to the base `:root`: the question `token show`
64
+ * answers is "what does this token resolve to", and a token whose dark
65
+ * override differs from its base value is precisely the case worth seeing.
66
+ *
67
+ * @param lines - Tokens CSS split into lines
68
+ * @param tokenName - Custom-property name including the leading `--`
69
+ * @returns Declarations in file order
70
+ */
71
+ export function collectTokenBlockValues(lines, tokenName) {
72
+ const masked = maskCommentLines(lines);
73
+ const trail = [];
74
+ const found = [];
75
+ for (let i = 0; i < masked.length; i++) {
76
+ const line = masked[i] ?? '';
77
+ const match = DECLARATION_PATTERN.exec(line);
78
+ if (match?.[1] === tokenName) {
79
+ found.push({
80
+ block: trail.length > 0 ? trail.join(' > ') : '(file scope)',
81
+ value: (match[2] ?? '').trim(),
82
+ line: i + 1,
83
+ });
84
+ }
85
+ // Brace tracking after the declaration test, so a one-line block cannot
86
+ // swallow the declaration it opens on. Selector text is whatever
87
+ // precedes the `{`; an empty selector (a continuation line) still needs
88
+ // a stack entry or the closing `}` pops the wrong one.
89
+ for (const char of line) {
90
+ if (char === '{') {
91
+ trail.push(selectorBefore(line, trail.length));
92
+ }
93
+ else if (char === '}') {
94
+ trail.pop();
95
+ }
96
+ }
97
+ }
98
+ return found;
99
+ }
100
+ /**
101
+ * Selector text introducing the `depth`-th open brace on `line`. Falls back
102
+ * to a positional label so the trail stays well-formed on a minified or
103
+ * continuation line that opens a block with no visible selector.
104
+ */
105
+ function selectorBefore(line, depth) {
106
+ const open = line.indexOf('{');
107
+ const text = open === -1 ? '' : line.slice(0, open).trim();
108
+ return text.length > 0 ? text : `(block ${String(depth + 1)})`;
109
+ }
@@ -10,7 +10,7 @@ export declare const TOKEN_MODES: readonly ["auto", "static", "override"];
10
10
  /** Dark mode behaviour for a token. */
11
11
  export type TokenMode = (typeof TOKEN_MODES)[number];
12
12
  /** Narrows an arbitrary string to a {@link TokenMode}. */
13
- export declare const isTokenMode: (value: string) => value is "override" | "auto" | "static";
13
+ export declare const isTokenMode: (value: string) => value is "auto" | "override" | "static";
14
14
  /**
15
15
  * Options for adding a token.
16
16
  */
@@ -177,7 +177,7 @@ function evaluateBaseTokenIdempotency(lines, options, tokensCssPath) {
177
177
  ? 'remove the stray declaration first'
178
178
  : `pass --category "${existing.category}" to target the existing declaration, or remove it first`;
179
179
  throw new GeneralError(`Token "${options.tokenName}" is already declared outside category "${options.category}" ` +
180
- `(${tokensCssPath}:${String(existing.line)}${sectionSuffix}). ` +
180
+ `(${tokensCssPath}:${existing.line}${sectionSuffix}). ` +
181
181
  `A token belongs to exactly one category — ${remedy}.`);
182
182
  }
183
183
  /**
@@ -32,29 +32,29 @@ const ATTRIBUTE_FRAGMENT_PATTERN = /^\[[a-zA-Z][a-zA-Z0-9_-]*(?:=(?:"[a-zA-Z0-9_
32
32
  const QUALIFIER_PATTERN = /^:[a-zA-Z][a-zA-Z0-9-]*(?:\(([^()]*)\))?$/;
33
33
  /**
34
34
  * Splits a run of consecutive attribute fragments into its groups, or
35
- * returns `null` when the run is not exactly that — a group that does not
35
+ * returns `undefined` when the run is not exactly that — a group that does not
36
36
  * open with `[`, an unclosed group, an empty run, or a group whose name or
37
37
  * value is not identifier-safe.
38
38
  *
39
39
  * @param run - The `[…][…]` text with no `:root` prefix
40
- * @returns The individual fragments, or null when the run is not valid
40
+ * @returns The individual fragments, or undefined when the run is not valid
41
41
  */
42
42
  function splitAttributeGroups(run) {
43
43
  const groups = [];
44
44
  let cursor = 0;
45
45
  while (cursor < run.length) {
46
46
  if (run[cursor] !== '[')
47
- return null;
47
+ return undefined;
48
48
  const close = run.indexOf(']', cursor);
49
49
  if (close === -1)
50
- return null;
50
+ return undefined;
51
51
  const group = run.slice(cursor, close + 1);
52
52
  if (!ATTRIBUTE_FRAGMENT_PATTERN.test(group))
53
- return null;
53
+ return undefined;
54
54
  groups.push(group);
55
55
  cursor = close + 1;
56
56
  }
57
- return groups.length > 0 ? groups : null;
57
+ return groups.length > 0 ? groups : undefined;
58
58
  }
59
59
  /** Rewrites `=value` / `='value'` to the double-quoted Mozilla convention. */
60
60
  function normalizeAttributeFragment(group) {
@@ -96,10 +96,10 @@ export function validateVariantSelector(raw) {
96
96
  // The parse must have consumed the WHOLE fragment: anything it declined to
97
97
  // read (a stray space, a trailing combinator) is text the matcher would
98
98
  // not see either, so accepting it would author a block nothing can find.
99
- if (parts === null || `${parts.attributes}${parts.qualifier}` !== `:root${value}`)
99
+ if (parts === undefined || `${parts.attributes}${parts.qualifier}` !== `:root${value}`)
100
100
  return reject;
101
101
  const groups = splitAttributeGroups(parts.attributes.slice(':root'.length));
102
- if (groups === null)
102
+ if (groups === undefined)
103
103
  return reject;
104
104
  let qualifier = '';
105
105
  if (parts.qualifier !== '') {
@@ -112,7 +112,7 @@ export function validateVariantSelector(raw) {
112
112
  }
113
113
  else {
114
114
  const innerGroups = splitAttributeGroups(inner);
115
- if (innerGroups === null)
115
+ if (innerGroups === undefined)
116
116
  return reject;
117
117
  const name = parts.qualifier.slice(0, parts.qualifier.indexOf('('));
118
118
  qualifier = `${name}(${innerGroups.map(normalizeAttributeFragment).join('')})`;
@@ -131,7 +131,7 @@ function canonicalSelector(selector) {
131
131
  }
132
132
  /**
133
133
  * Splits the `:root[…]` selector on `line` into its attribute run and any
134
- * trailing qualifier, or `null` when the line carries none.
134
+ * trailing qualifier, or `undefined` when the line carries none.
135
135
  *
136
136
  * Index arithmetic rather than `/:root\[[^{]*\]/`: `[^{]` also matches `]`,
137
137
  * so that pattern backtracks quadratically on a line repeating `:root[`
@@ -155,7 +155,7 @@ function canonicalSelector(selector) {
155
155
  function rootAttributeSelector(line) {
156
156
  const start = line.indexOf(':root[');
157
157
  if (start === -1)
158
- return null;
158
+ return undefined;
159
159
  const brace = line.indexOf('{', start);
160
160
  const limit = brace === -1 ? line.length : brace;
161
161
  let cursor = start + ':root'.length;
@@ -168,13 +168,13 @@ function rootAttributeSelector(line) {
168
168
  end = cursor;
169
169
  }
170
170
  if (end === start + ':root'.length)
171
- return null;
171
+ return undefined;
172
172
  return { attributes: line.slice(start, end), qualifier: line.slice(end, limit).trim() };
173
173
  }
174
174
  /**
175
175
  * Finds the top-level `:root<variant>` block whose attribute selector
176
176
  * matches `variant` (compared canonically). Returns the opening-brace and
177
- * closing-brace line indices, or `null` when no such block exists.
177
+ * closing-brace line indices, or `undefined` when no such block exists.
178
178
  *
179
179
  * Scans a comment-stripped mirror of `lines` so braces inside comments do
180
180
  * not offset the depth counter; the returned indices line up with the
@@ -189,10 +189,9 @@ function findVariantBlock(lines, variant) {
189
189
  const requested = rootAttributeSelector(`:root${variant} {`);
190
190
  const want = canonicalSelector(requested?.attributes ?? `:root${variant}`);
191
191
  const wantQualifier = canonicalSelector(requested?.qualifier ?? '');
192
- for (let i = 0; i < stripped.length; i++) {
193
- const line = stripped[i] ?? '';
192
+ for (const [i, line] of stripped.entries()) {
194
193
  const selector = rootAttributeSelector(line);
195
- if (selector === null || canonicalSelector(selector.attributes) !== want)
194
+ if (selector === undefined || canonicalSelector(selector.attributes) !== want)
196
195
  continue;
197
196
  // An UNqualified request still matches a qualified block: that is the
198
197
  // 0.44.2 behaviour `variantBlockQualifier` exists to report, since
@@ -219,7 +218,7 @@ function findVariantBlock(lines, variant) {
219
218
  continue;
220
219
  return { open: openLine, close, qualifier: selector.qualifier };
221
220
  }
222
- return null;
221
+ return undefined;
223
222
  }
224
223
  /**
225
224
  * Returns the line index at which a brand-new `:root<variant>` block should
@@ -229,8 +228,8 @@ function findVariantBlock(lines, variant) {
229
228
  */
230
229
  function findVariantBlockInsertionPoint(lines) {
231
230
  const stripped = stripBlockCommentsInLines(lines);
232
- for (let i = 0; i < stripped.length; i++) {
233
- if (/^\s*:root\s*\{/.test(stripped[i] ?? '')) {
231
+ for (const [i, line] of stripped.entries()) {
232
+ if (/^\s*:root\s*\{/.test(line)) {
234
233
  const close = findBlockCloseIndex(stripped, i);
235
234
  if (close !== -1)
236
235
  return close + 1;
@@ -245,7 +244,7 @@ function findVariantBlockInsertionPoint(lines) {
245
244
  * the block, which is only wanted for the explicit `--variant` flow.
246
245
  */
247
246
  export function variantBlockExists(lines, variant) {
248
- return findVariantBlock(lines, variant) !== null;
247
+ return findVariantBlock(lines, variant) !== undefined;
249
248
  }
250
249
  /**
251
250
  * The pseudo-class qualifier on the matched `:root<variant>` block, or
@@ -300,8 +299,8 @@ export function findVariantBlockDeclaration(lines, variant, tokenName) {
300
299
  .join('\n')
301
300
  .replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, ' '))
302
301
  .split('\n');
303
- for (let i = 0; i < stripped.length; i++) {
304
- if ((stripped[i] ?? '').includes(`${tokenName}:`))
302
+ for (const [i, line] of stripped.entries()) {
303
+ if (line.includes(`${tokenName}:`))
305
304
  return { line: block.open + i + 1 };
306
305
  }
307
306
  return undefined;
@@ -121,7 +121,7 @@ export function formatMajorVersionHopNotice(previousVersion, newVersion) {
121
121
  return undefined;
122
122
  if (previousMajor === newMajor)
123
123
  return undefined;
124
- return (`Firefox major version changed (${String(previousMajor)} → ${String(newMajor)}): ` +
124
+ return (`Firefox major version changed (${previousMajor} → ${newMajor}): ` +
125
125
  'upstream toolchain minimums (cbindgen, Rust, …) may have moved with it. ' +
126
126
  'Consider running "fireforge bootstrap" before the next build.');
127
127
  }
@@ -2,8 +2,6 @@
2
2
  export type CloneExecutor = (command: string, args: string[]) => Promise<void>;
3
3
  /** How this host can materialise a tree. */
4
4
  export type CowCapability = 'clonefile' | 'reflink' | 'none';
5
- /** Per-platform `cp` argv prefix for a CoW copy of one entry. */
6
- export declare function cowCopyArgs(capability: Exclude<CowCapability, 'none'>): string[];
7
5
  /**
8
6
  * Probes CoW support in `dir` by cloning a tiny probe file there.
9
7
  * Non-darwin/linux platforms report 'none' without probing.
@@ -20,11 +20,11 @@ import { exec } from '../utils/process.js';
20
20
  const defaultExecutor = async (command, args) => {
21
21
  const result = await exec(command, args);
22
22
  if (result.exitCode !== 0) {
23
- throw new Error(`${command} ${args.join(' ')} failed (exit ${String(result.exitCode)}): ${result.stderr}`);
23
+ throw new Error(`${command} ${args.join(' ')} failed (exit ${result.exitCode}): ${result.stderr}`);
24
24
  }
25
25
  };
26
26
  /** Per-platform `cp` argv prefix for a CoW copy of one entry. */
27
- export function cowCopyArgs(capability) {
27
+ function cowCopyArgs(capability) {
28
28
  return capability === 'clonefile' ? ['-c', '-R', '-p'] : ['--reflink=always', '-a'];
29
29
  }
30
30
  /**
@@ -19,6 +19,7 @@ import { GeneralError } from '../errors/base.js';
19
19
  import { mapWithConcurrency } from '../utils/concurrency.js';
20
20
  import { getNodeErrorCode, isProcessAlive, toError } from '../utils/errors.js';
21
21
  import { pathExists, readJson, writeJson } from '../utils/fs.js';
22
+ import { sha256Hex } from '../utils/hash.js';
22
23
  import { verbose, warn } from '../utils/logger.js';
23
24
  import { isObject } from '../utils/validation.js';
24
25
  import { getBuildBaselinePath } from './build-baseline.js';
@@ -128,7 +129,7 @@ export async function readTreeMarker(root) {
128
129
  const code = getNodeErrorCode(error);
129
130
  if (code === 'ENOENT' || code === 'ENOTDIR')
130
131
  return { kind: 'absent' };
131
- verbose(`Unreadable tree marker at ${markerPath}: ${String(error)}`);
132
+ verbose(`Unreadable tree marker at ${markerPath}: ${toError(error).message}`);
132
133
  return { kind: 'corrupt', reason: `the marker could not be read (${toError(error).message})` };
133
134
  }
134
135
  }
@@ -178,9 +179,7 @@ export async function computePrimaryFingerprint(primaryRoot) {
178
179
  }
179
180
  let engineFingerprint;
180
181
  try {
181
- engineFingerprint = createHash('sha256')
182
- .update(await getAllDiff(engineDir))
183
- .digest('hex');
182
+ engineFingerprint = sha256Hex(await getAllDiff(engineDir));
184
183
  }
185
184
  catch {
186
185
  engineFingerprint = null;
@@ -461,7 +460,7 @@ export async function removeTree(primaryRoot, name, options = {}) {
461
460
  ]) {
462
461
  const state = await inspectTreeLock(lockPath);
463
462
  if (state.kind === 'held') {
464
- throw new GeneralError(`Refusing to remove tree "${name}": a live process (pid ${String(state.pid)}) holds ${lockPath}. ` +
463
+ throw new GeneralError(`Refusing to remove tree "${name}": a live process (pid ${state.pid}) holds ${lockPath}. ` +
465
464
  'Wait for it to finish, then retry.');
466
465
  }
467
466
  if (state.kind === 'unknown' && options.force !== true) {
@@ -144,7 +144,7 @@ export async function runTypecheck(projectRoot, cfg) {
144
144
  */
145
145
  function resolveProjectExtraShim(cfg, projectPath) {
146
146
  const overrides = cfg.projectOverrides;
147
- if (overrides && Object.prototype.hasOwnProperty.call(overrides, projectPath)) {
147
+ if (overrides && Object.hasOwn(overrides, projectPath)) {
148
148
  const value = overrides[projectPath];
149
149
  return value === null ? undefined : value;
150
150
  }
@@ -298,7 +298,7 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
298
298
  }
299
299
  issues.push(diagnosticToIssue(ts, diag, absConfig, projectPath));
300
300
  }
301
- verbose(`typecheck: ${projectPath} — analyzed ${String(parsed.fileNames.length)} file(s), found ${String(issues.length)} issue(s)`);
301
+ verbose(`typecheck: ${projectPath} — analyzed ${parsed.fileNames.length} file(s), found ${issues.length} issue(s)`);
302
302
  return {
303
303
  project: projectPath,
304
304
  issues,