@hominis/fireforge 0.35.0 → 0.36.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 (355) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +34 -9
  3. package/dist/bin/fireforge.js +21 -6
  4. package/dist/src/cli.js +22 -5
  5. package/dist/src/commands/bootstrap-checks.js +0 -1
  6. package/dist/src/commands/bootstrap.d.ts +2 -1
  7. package/dist/src/commands/bootstrap.js +16 -7
  8. package/dist/src/commands/build.js +4 -3
  9. package/dist/src/commands/config.js +30 -10
  10. package/dist/src/commands/discard.js +18 -3
  11. package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
  12. package/dist/src/commands/doctor-check-core.js +0 -1
  13. package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
  14. package/dist/src/commands/doctor-external-toolchains.js +54 -0
  15. package/dist/src/commands/doctor-furnace-jar.js +0 -1
  16. package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
  17. package/dist/src/commands/doctor-furnace.js +2 -2
  18. package/dist/src/commands/doctor-working-tree.js +0 -1
  19. package/dist/src/commands/doctor.js +4 -1
  20. package/dist/src/commands/download.js +28 -13
  21. package/dist/src/commands/export-all.js +5 -3
  22. package/dist/src/commands/export-flow.js +12 -7
  23. package/dist/src/commands/export-placement-gate.js +0 -1
  24. package/dist/src/commands/export-placement-policy.js +0 -1
  25. package/dist/src/commands/export-shared.d.ts +5 -1
  26. package/dist/src/commands/export-shared.js +18 -6
  27. package/dist/src/commands/export.js +15 -10
  28. package/dist/src/commands/furnace/apply.js +22 -6
  29. package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
  30. package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
  31. package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
  32. package/dist/src/commands/furnace/chrome-doc.js +0 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +0 -1
  34. package/dist/src/commands/furnace/create-dry-run.js +0 -1
  35. package/dist/src/commands/furnace/create-features.js +0 -1
  36. package/dist/src/commands/furnace/create-mochikit.js +0 -1
  37. package/dist/src/commands/furnace/create-readback.js +0 -1
  38. package/dist/src/commands/furnace/create-templates.js +0 -1
  39. package/dist/src/commands/furnace/create-validation.js +0 -1
  40. package/dist/src/commands/furnace/create-xpcshell.js +0 -1
  41. package/dist/src/commands/furnace/create.js +0 -1
  42. package/dist/src/commands/furnace/deploy.js +7 -83
  43. package/dist/src/commands/furnace/diff.js +0 -1
  44. package/dist/src/commands/furnace/index.js +59 -25
  45. package/dist/src/commands/furnace/init.js +0 -1
  46. package/dist/src/commands/furnace/list.js +0 -1
  47. package/dist/src/commands/furnace/override.js +0 -1
  48. package/dist/src/commands/furnace/preview.js +3 -3
  49. package/dist/src/commands/furnace/refresh.js +0 -1
  50. package/dist/src/commands/furnace/remove-state.d.ts +15 -0
  51. package/dist/src/commands/furnace/remove-state.js +47 -1
  52. package/dist/src/commands/furnace/remove.js +3 -9
  53. package/dist/src/commands/furnace/rename-browser-test.js +1 -5
  54. package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
  55. package/dist/src/commands/furnace/rename-helpers.js +30 -1
  56. package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
  57. package/dist/src/commands/furnace/rename.js +15 -36
  58. package/dist/src/commands/furnace/scan.js +0 -1
  59. package/dist/src/commands/furnace/status.js +0 -1
  60. package/dist/src/commands/furnace/sync.js +76 -4
  61. package/dist/src/commands/furnace/validate.js +0 -1
  62. package/dist/src/commands/furnace/validation-output.js +0 -1
  63. package/dist/src/commands/import.js +35 -9
  64. package/dist/src/commands/lint-per-patch.js +19 -7
  65. package/dist/src/commands/lint.js +13 -3
  66. package/dist/src/commands/manifest.js +1 -2
  67. package/dist/src/commands/package.js +2 -1
  68. package/dist/src/commands/patch/compact.js +1 -1
  69. package/dist/src/commands/patch/delete.js +1 -1
  70. package/dist/src/commands/patch/index.js +1 -1
  71. package/dist/src/commands/patch/lint-ignore.js +21 -6
  72. package/dist/src/commands/patch/move-files.js +1 -1
  73. package/dist/src/commands/patch/patch-context.js +0 -1
  74. package/dist/src/commands/patch/rename.js +4 -4
  75. package/dist/src/commands/patch/reorder.js +3 -4
  76. package/dist/src/commands/patch/split-plan.js +2 -1
  77. package/dist/src/commands/patch/split.js +4 -4
  78. package/dist/src/commands/patch/staged-dependency.js +1 -1
  79. package/dist/src/commands/patch/tier.js +18 -5
  80. package/dist/src/commands/re-export-bulk-scan.js +0 -1
  81. package/dist/src/commands/re-export-files.js +1 -1
  82. package/dist/src/commands/re-export-options.js +0 -1
  83. package/dist/src/commands/re-export-scan.js +0 -1
  84. package/dist/src/commands/re-export.js +6 -4
  85. package/dist/src/commands/rebase/abort.js +2 -9
  86. package/dist/src/commands/rebase/confirm.js +0 -1
  87. package/dist/src/commands/rebase/conflict-summary.js +0 -1
  88. package/dist/src/commands/rebase/continue.js +0 -1
  89. package/dist/src/commands/rebase/index.d.ts +1 -1
  90. package/dist/src/commands/rebase/index.js +10 -15
  91. package/dist/src/commands/rebase/patch-loop.js +38 -7
  92. package/dist/src/commands/rebase/summary.js +2 -3
  93. package/dist/src/commands/rebase.js +0 -1
  94. package/dist/src/commands/register.js +1 -1
  95. package/dist/src/commands/reset.js +16 -9
  96. package/dist/src/commands/resolve.js +2 -2
  97. package/dist/src/commands/run.js +4 -4
  98. package/dist/src/commands/setup-support.js +0 -1
  99. package/dist/src/commands/setup.js +1 -2
  100. package/dist/src/commands/source.js +1 -2
  101. package/dist/src/commands/status-output.js +1 -12
  102. package/dist/src/commands/status.js +147 -133
  103. package/dist/src/commands/test-appdir.js +0 -1
  104. package/dist/src/commands/test-diagnose.js +18 -2
  105. package/dist/src/commands/test-modes.d.ts +15 -0
  106. package/dist/src/commands/test-modes.js +61 -0
  107. package/dist/src/commands/test-register.d.ts +4 -0
  108. package/dist/src/commands/test-register.js +64 -0
  109. package/dist/src/commands/test-run.d.ts +23 -7
  110. package/dist/src/commands/test-run.js +15 -14
  111. package/dist/src/commands/test.d.ts +0 -4
  112. package/dist/src/commands/test.js +85 -86
  113. package/dist/src/commands/token-coverage.js +0 -1
  114. package/dist/src/commands/token.js +1 -2
  115. package/dist/src/commands/typecheck.js +1 -1
  116. package/dist/src/commands/verify.js +3 -9
  117. package/dist/src/commands/watch.js +5 -1
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.js +0 -1
  120. package/dist/src/core/brand-validation.js +0 -1
  121. package/dist/src/core/branding.js +0 -1
  122. package/dist/src/core/browser-wire.js +0 -1
  123. package/dist/src/core/build-audit-platform.js +0 -1
  124. package/dist/src/core/build-audit-registration.js +0 -1
  125. package/dist/src/core/build-audit-resolve.js +0 -1
  126. package/dist/src/core/build-audit-transforms.js +0 -1
  127. package/dist/src/core/build-audit.js +0 -1
  128. package/dist/src/core/build-baseline-types.js +0 -1
  129. package/dist/src/core/build-baseline.js +0 -1
  130. package/dist/src/core/build-prepare.js +2 -2
  131. package/dist/src/core/config-mutate.js +0 -1
  132. package/dist/src/core/config-paths.d.ts +2 -2
  133. package/dist/src/core/config-paths.js +6 -1
  134. package/dist/src/core/config-state.js +0 -1
  135. package/dist/src/core/config-validate-patch-policy.js +0 -1
  136. package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
  137. package/dist/src/core/config-validate-test-toolchains.js +105 -0
  138. package/dist/src/core/config-validate.js +3 -1
  139. package/dist/src/core/config.js +0 -1
  140. package/dist/src/core/destructive.js +0 -1
  141. package/dist/src/core/diff-hunks.js +13 -7
  142. package/dist/src/core/engine-changes.js +0 -1
  143. package/dist/src/core/engine-session-lock.d.ts +12 -0
  144. package/dist/src/core/engine-session-lock.js +45 -0
  145. package/dist/src/core/file-lock.d.ts +8 -0
  146. package/dist/src/core/file-lock.js +157 -48
  147. package/dist/src/core/firefox-archive.d.ts +4 -0
  148. package/dist/src/core/firefox-archive.js +6 -1
  149. package/dist/src/core/firefox-cache.js +60 -4
  150. package/dist/src/core/firefox-download.js +0 -1
  151. package/dist/src/core/firefox-extract.js +75 -16
  152. package/dist/src/core/firefox-ignorefile.js +0 -1
  153. package/dist/src/core/firefox.d.ts +12 -0
  154. package/dist/src/core/firefox.js +64 -2
  155. package/dist/src/core/furnace-apply-dry-run.js +0 -1
  156. package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
  157. package/dist/src/core/furnace-apply-ftl.js +10 -5
  158. package/dist/src/core/furnace-apply-helpers.js +0 -1
  159. package/dist/src/core/furnace-apply-output.js +2 -2
  160. package/dist/src/core/furnace-apply.d.ts +7 -1
  161. package/dist/src/core/furnace-apply.js +47 -13
  162. package/dist/src/core/furnace-checksum-utils.js +0 -1
  163. package/dist/src/core/furnace-config-array-utils.js +0 -1
  164. package/dist/src/core/furnace-config-custom.js +0 -1
  165. package/dist/src/core/furnace-config-order.js +0 -1
  166. package/dist/src/core/furnace-config-tokens.js +0 -1
  167. package/dist/src/core/furnace-config-validate.js +0 -1
  168. package/dist/src/core/furnace-config.d.ts +11 -0
  169. package/dist/src/core/furnace-config.js +37 -1
  170. package/dist/src/core/furnace-constants.js +0 -1
  171. package/dist/src/core/furnace-css-fragments.js +0 -1
  172. package/dist/src/core/furnace-graph-utils.js +0 -1
  173. package/dist/src/core/furnace-jsconfig.d.ts +2 -3
  174. package/dist/src/core/furnace-jsconfig.js +25 -18
  175. package/dist/src/core/furnace-marker.js +0 -1
  176. package/dist/src/core/furnace-operation.js +0 -1
  177. package/dist/src/core/furnace-refresh.js +0 -1
  178. package/dist/src/core/furnace-registration-ast.d.ts +14 -0
  179. package/dist/src/core/furnace-registration-ast.js +1 -2
  180. package/dist/src/core/furnace-registration-remove.js +0 -1
  181. package/dist/src/core/furnace-registration-validate.js +1 -5
  182. package/dist/src/core/furnace-registration.js +0 -1
  183. package/dist/src/core/furnace-rollback.js +0 -1
  184. package/dist/src/core/furnace-scanner.js +0 -1
  185. package/dist/src/core/furnace-staleness.js +0 -1
  186. package/dist/src/core/furnace-state-persist.d.ts +72 -0
  187. package/dist/src/core/furnace-state-persist.js +118 -0
  188. package/dist/src/core/furnace-step-errors.d.ts +23 -0
  189. package/dist/src/core/furnace-step-errors.js +25 -0
  190. package/dist/src/core/furnace-stories.js +0 -1
  191. package/dist/src/core/furnace-validate-accessibility.js +0 -1
  192. package/dist/src/core/furnace-validate-checks.js +0 -1
  193. package/dist/src/core/furnace-validate-compatibility.js +0 -1
  194. package/dist/src/core/furnace-validate-helpers.js +0 -1
  195. package/dist/src/core/furnace-validate-registration.js +7 -7
  196. package/dist/src/core/furnace-validate-structure.d.ts +8 -0
  197. package/dist/src/core/furnace-validate-structure.js +11 -2
  198. package/dist/src/core/furnace-validate.js +0 -1
  199. package/dist/src/core/furnace-version-drift.d.ts +6 -3
  200. package/dist/src/core/furnace-version-drift.js +6 -4
  201. package/dist/src/core/git-base.js +0 -1
  202. package/dist/src/core/git-diff.js +0 -1
  203. package/dist/src/core/git-file-ops.js +0 -1
  204. package/dist/src/core/git-status.d.ts +13 -0
  205. package/dist/src/core/git-status.js +32 -2
  206. package/dist/src/core/git.d.ts +10 -1
  207. package/dist/src/core/git.js +63 -6
  208. package/dist/src/core/license-headers.js +0 -1
  209. package/dist/src/core/lint-cache.js +0 -1
  210. package/dist/src/core/lint-projection.js +0 -1
  211. package/dist/src/core/mach-build-artifacts.js +0 -1
  212. package/dist/src/core/mach-error-hints.js +0 -1
  213. package/dist/src/core/mach-mozconfig.js +0 -1
  214. package/dist/src/core/mach-python.js +0 -1
  215. package/dist/src/core/mach-resource-shim.js +0 -1
  216. package/dist/src/core/mach.js +0 -1
  217. package/dist/src/core/manifest-helpers.js +0 -1
  218. package/dist/src/core/manifest-register.js +0 -1
  219. package/dist/src/core/manifest-rules.js +0 -1
  220. package/dist/src/core/manifest-tokenizers.js +0 -1
  221. package/dist/src/core/marionette-port.d.ts +8 -0
  222. package/dist/src/core/marionette-port.js +34 -3
  223. package/dist/src/core/marionette-preflight.js +0 -1
  224. package/dist/src/core/markdown-table.js +0 -1
  225. package/dist/src/core/ownership-table.js +2 -9
  226. package/dist/src/core/parser-fallback.js +0 -1
  227. package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
  228. package/dist/src/core/patch-apply-fuzz.js +71 -34
  229. package/dist/src/core/patch-apply.d.ts +30 -0
  230. package/dist/src/core/patch-apply.js +101 -18
  231. package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
  232. package/dist/src/core/patch-artifact-normalize.js +19 -15
  233. package/dist/src/core/patch-export-coverage.js +0 -1
  234. package/dist/src/core/patch-export-metadata.js +0 -1
  235. package/dist/src/core/patch-export-update.js +2 -3
  236. package/dist/src/core/patch-export.d.ts +2 -6
  237. package/dist/src/core/patch-export.js +24 -48
  238. package/dist/src/core/patch-files.js +0 -1
  239. package/dist/src/core/patch-identifier-suggest.js +0 -1
  240. package/dist/src/core/patch-lint-checkjs.js +0 -1
  241. package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
  242. package/dist/src/core/patch-lint-cross.js +0 -1
  243. package/dist/src/core/patch-lint-css.js +0 -1
  244. package/dist/src/core/patch-lint-diff-tag.js +0 -1
  245. package/dist/src/core/patch-lint-diff.d.ts +2 -1
  246. package/dist/src/core/patch-lint-diff.js +17 -32
  247. package/dist/src/core/patch-lint-imports.js +0 -1
  248. package/dist/src/core/patch-lint-jsdoc.js +0 -1
  249. package/dist/src/core/patch-lint-observer.js +0 -1
  250. package/dist/src/core/patch-lint-ownership.js +0 -1
  251. package/dist/src/core/patch-lint-reexports.js +0 -1
  252. package/dist/src/core/patch-lint.d.ts +37 -2
  253. package/dist/src/core/patch-lint.js +47 -34
  254. package/dist/src/core/patch-lock.js +0 -1
  255. package/dist/src/core/patch-manifest-consistency.js +9 -1
  256. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  257. package/dist/src/core/patch-manifest-io.js +30 -5
  258. package/dist/src/core/patch-manifest-query.js +18 -10
  259. package/dist/src/core/patch-manifest-resolve.js +0 -1
  260. package/dist/src/core/patch-manifest-validate.js +0 -1
  261. package/dist/src/core/patch-manifest.d.ts +1 -1
  262. package/dist/src/core/patch-manifest.js +1 -2
  263. package/dist/src/core/patch-parse.d.ts +59 -0
  264. package/dist/src/core/patch-parse.js +209 -92
  265. package/dist/src/core/patch-policy.js +0 -1
  266. package/dist/src/core/patch-registration-refs.js +16 -26
  267. package/dist/src/core/patch-source-metadata.js +0 -1
  268. package/dist/src/core/patch-transform.d.ts +4 -0
  269. package/dist/src/core/patch-transform.js +30 -38
  270. package/dist/src/core/rebase-session.js +0 -1
  271. package/dist/src/core/register-browser-content.js +0 -1
  272. package/dist/src/core/register-module.js +0 -1
  273. package/dist/src/core/register-result.js +0 -1
  274. package/dist/src/core/register-scaffold.js +0 -1
  275. package/dist/src/core/register-shared-css.js +0 -1
  276. package/dist/src/core/register-test-manifest.js +0 -1
  277. package/dist/src/core/register-xpcshell-test.js +0 -1
  278. package/dist/src/core/shared-ftl.js +0 -1
  279. package/dist/src/core/signal-critical.js +0 -1
  280. package/dist/src/core/smoke-patterns.js +0 -1
  281. package/dist/src/core/state-file.js +0 -1
  282. package/dist/src/core/status-classify.d.ts +16 -0
  283. package/dist/src/core/status-classify.js +24 -16
  284. package/dist/src/core/test-harness-crash.d.ts +64 -7
  285. package/dist/src/core/test-harness-crash.js +196 -15
  286. package/dist/src/core/test-harness-output.js +0 -1
  287. package/dist/src/core/test-path-scope.d.ts +21 -13
  288. package/dist/src/core/test-path-scope.js +43 -28
  289. package/dist/src/core/test-stale-check.js +0 -1
  290. package/dist/src/core/test-stale-symlink.js +0 -1
  291. package/dist/src/core/test-xpcshell-retry.js +0 -1
  292. package/dist/src/core/token-coverage.js +0 -1
  293. package/dist/src/core/token-dark-mode.js +0 -1
  294. package/dist/src/core/token-docs.js +0 -1
  295. package/dist/src/core/token-manager.js +0 -1
  296. package/dist/src/core/token-scaffold.js +0 -1
  297. package/dist/src/core/token-variant.js +0 -1
  298. package/dist/src/core/toolchain-preflight.d.ts +31 -10
  299. package/dist/src/core/toolchain-preflight.js +97 -36
  300. package/dist/src/core/typecheck-shim.js +13 -1
  301. package/dist/src/core/typecheck.js +0 -1
  302. package/dist/src/core/wire-destroy.js +0 -1
  303. package/dist/src/core/wire-dom-fragment.js +0 -1
  304. package/dist/src/core/wire-init.js +0 -1
  305. package/dist/src/core/wire-subscript.js +0 -1
  306. package/dist/src/core/wire-targets.js +0 -1
  307. package/dist/src/core/wire-utils.js +0 -1
  308. package/dist/src/core/xpcshell-appdir.js +1 -4
  309. package/dist/src/errors/base.d.ts +29 -0
  310. package/dist/src/errors/base.js +35 -1
  311. package/dist/src/errors/build.js +0 -1
  312. package/dist/src/errors/codes.d.ts +6 -0
  313. package/dist/src/errors/codes.js +6 -1
  314. package/dist/src/errors/config.js +0 -1
  315. package/dist/src/errors/download.js +0 -1
  316. package/dist/src/errors/furnace.d.ts +11 -1
  317. package/dist/src/errors/furnace.js +6 -5
  318. package/dist/src/errors/git.js +0 -1
  319. package/dist/src/errors/patch.d.ts +17 -0
  320. package/dist/src/errors/patch.js +29 -1
  321. package/dist/src/errors/rebase.js +0 -1
  322. package/dist/src/errors/run.js +1 -1
  323. package/dist/src/index.js +0 -1
  324. package/dist/src/types/cli.js +0 -1
  325. package/dist/src/types/commands/index.js +0 -1
  326. package/dist/src/types/commands/options.d.ts +13 -5
  327. package/dist/src/types/commands/options.js +0 -1
  328. package/dist/src/types/commands/patches.d.ts +8 -0
  329. package/dist/src/types/commands/patches.js +0 -1
  330. package/dist/src/types/commands/project.d.ts +11 -2
  331. package/dist/src/types/commands/project.js +0 -1
  332. package/dist/src/types/config.d.ts +32 -0
  333. package/dist/src/types/config.js +0 -1
  334. package/dist/src/types/furnace.d.ts +10 -0
  335. package/dist/src/types/furnace.js +0 -1
  336. package/dist/src/types/index.js +0 -1
  337. package/dist/src/types/typecheck.js +0 -1
  338. package/dist/src/utils/elapsed.js +0 -1
  339. package/dist/src/utils/errors.js +0 -1
  340. package/dist/src/utils/fs.js +37 -5
  341. package/dist/src/utils/logger.d.ts +7 -0
  342. package/dist/src/utils/logger.js +85 -3
  343. package/dist/src/utils/options.d.ts +15 -0
  344. package/dist/src/utils/options.js +25 -1
  345. package/dist/src/utils/package-root.js +11 -4
  346. package/dist/src/utils/parse.js +0 -1
  347. package/dist/src/utils/paths.js +0 -1
  348. package/dist/src/utils/platform.js +0 -1
  349. package/dist/src/utils/process.d.ts +33 -4
  350. package/dist/src/utils/process.js +183 -28
  351. package/dist/src/utils/regex.d.ts +13 -3
  352. package/dist/src/utils/regex.js +53 -4
  353. package/dist/src/utils/validation.d.ts +0 -12
  354. package/dist/src/utils/validation.js +13 -26
  355. package/package.json +8 -7
@@ -8,6 +8,16 @@ export interface ExecResult {
8
8
  stderr: string;
9
9
  /** Process exit code */
10
10
  exitCode: number;
11
+ /**
12
+ * True when `stdout` was cut off at the 50 MB collector cap. Optional so
13
+ * existing mocks stay valid; absent means "not truncated". Callers that
14
+ * feed the output into safety decisions (e.g. archive-listing scans) must
15
+ * treat `true` as a hard failure — a truncated listing looks exactly like
16
+ * a complete one otherwise.
17
+ */
18
+ stdoutTruncated?: boolean;
19
+ /** True when `stderr` was cut off at the 50 MB collector cap. See {@link ExecResult.stdoutTruncated}. */
20
+ stderrTruncated?: boolean;
11
21
  }
12
22
  /**
13
23
  * Options for command execution.
@@ -49,6 +59,13 @@ export interface StreamOptions extends ExecOptions {
49
59
  * @returns Exit code of the process
50
60
  */
51
61
  export declare function execStream(command: string, args: string[], options?: StreamOptions): Promise<number>;
62
+ /**
63
+ * Waits (bounded) for every tracked child process to close. Called by the
64
+ * bin signal handler after forwarding SIGINT/SIGTERM, so the parent stays
65
+ * alive long enough for the grace-then-SIGKILL escalation to actually run.
66
+ * Resolves immediately when no children are active.
67
+ */
68
+ export declare function waitForActiveChildShutdown(timeoutMs: number): Promise<void>;
52
69
  /**
53
70
  * Executes a command and inherits stdio (shows output directly).
54
71
  *
@@ -81,8 +98,16 @@ export declare function execInheritCapture(command: string, args: string[], opti
81
98
  }): Promise<ExecResult>;
82
99
  /** Per-line callback for smoke-run stream dispatch. */
83
100
  export type SmokeLineCallback = (line: string) => void;
84
- /** Options for {@link execSmokeRun}. */
85
- export interface SmokeRunOptions extends ExecOptions {
101
+ /**
102
+ * Options for {@link execSmokeRun}.
103
+ *
104
+ * Deliberately omits `ExecOptions.timeout`: the smoke run's only deadline is
105
+ * {@link SmokeRunOptions.smokeTimeoutMs}, which signals the whole process
106
+ * group. Inheriting `timeout` used to be a leaky trap — it was accepted by
107
+ * the type but silently ignored, so a caller setting it got no deadline at
108
+ * all.
109
+ */
110
+ export interface SmokeRunOptions extends Omit<ExecOptions, 'timeout'> {
86
111
  /**
87
112
  * Hard deadline in milliseconds. When it elapses the child process
88
113
  * group is sent SIGTERM and, after `killGraceMs`, SIGKILL. The returned
@@ -138,8 +163,12 @@ export interface SmokeRunResult extends ExecResult {
138
163
  * that inherited the group.
139
164
  *
140
165
  * Windows fallback: `detached: true` does not create an equivalent group
141
- * there, so we degrade to `child.kill()` and log a best-effort warning
142
- * via the `onStderrLine` callback if the caller wired one.
166
+ * there, so we kill the descendant tree with `taskkill /pid <pid> /T /F`
167
+ * instead. taskkill has no graceful-signal equivalent both the deadline
168
+ * and the grace re-invocation are forced kills, so Windows children get
169
+ * no shutdown window (best-effort; Windows is untested, see README).
170
+ * `child.kill()` is still sent as a direct-child fallback in case
171
+ * taskkill itself is unavailable.
143
172
  */
144
173
  export declare function execSmokeRun(command: string, args: string[], options: SmokeRunOptions): Promise<SmokeRunResult>;
145
174
  /**
@@ -1,16 +1,29 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { spawn } from 'node:child_process';
3
3
  import { constants as osConstants } from 'node:os';
4
+ import { StringDecoder } from 'node:string_decoder';
5
+ import { ExecTimeoutError } from '../errors/base.js';
4
6
  // 50 MB cap per stream to prevent OOM — large toolchain builds (e.g. Firefox, Chromium)
5
7
  // can easily blow past this, so we truncate rather than let the buffer grow unbounded.
8
+ // Callers for whom truncation is unacceptable (e.g. the archive-safety preflight
9
+ // scanning a full `tar -tvf` listing) must check `wasTruncated` and fail hard.
6
10
  const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
7
11
  function createStreamCollector(mirror) {
8
12
  const chunks = [];
9
13
  let totalLength = 0;
10
14
  let truncated = false;
15
+ // A per-stream StringDecoder keeps multibyte UTF-8 characters split across
16
+ // pipe chunk boundaries intact. Per-chunk `Buffer.toString()` turned a
17
+ // straddling character into two U+FFFD replacement chars, which silently
18
+ // broke regex matching over captured output (smoke-run allowlists, mach
19
+ // error hints) whenever a non-ASCII path or localized message landed on a
20
+ // 64 KB chunk boundary.
21
+ const decoder = new StringDecoder('utf8');
11
22
  return {
12
23
  onData: (data) => {
13
- const chunk = data.toString();
24
+ const chunk = decoder.write(data);
25
+ if (chunk.length === 0)
26
+ return;
14
27
  mirror?.write(chunk);
15
28
  if (truncated)
16
29
  return;
@@ -27,6 +40,7 @@ function createStreamCollector(mirror) {
27
40
  }
28
41
  },
29
42
  getText: () => chunks.join(''),
43
+ wasTruncated: () => truncated,
30
44
  };
31
45
  }
32
46
  function buildSignalFromTimeout(timeout) {
@@ -34,6 +48,19 @@ function buildSignalFromTimeout(timeout) {
34
48
  return undefined;
35
49
  return AbortSignal.timeout(timeout);
36
50
  }
51
+ /**
52
+ * Maps a child-process `error` event to the rejection the caller sees.
53
+ * When the caller set a `timeout` and the error is the AbortError produced
54
+ * by `AbortSignal.timeout` firing, rejects with a typed
55
+ * {@link ExecTimeoutError} naming the command and budget instead of Node's
56
+ * opaque `AbortError: The operation was aborted`.
57
+ */
58
+ function toExecRejection(error, command, args, timeout) {
59
+ if (timeout !== undefined && error.name === 'AbortError') {
60
+ return new ExecTimeoutError(command, args, timeout, error);
61
+ }
62
+ return error;
63
+ }
37
64
  function exitCodeFromClose(code, signal) {
38
65
  if (code !== null) {
39
66
  return code;
@@ -66,13 +93,15 @@ export async function exec(command, args, options = {}) {
66
93
  child.stdout.on('data', out.onData);
67
94
  child.stderr.on('data', err.onData);
68
95
  child.on('error', (error) => {
69
- reject(error);
96
+ reject(toExecRejection(error, command, args, options.timeout));
70
97
  });
71
98
  child.on('close', (code, signal) => {
72
99
  resolve({
73
100
  stdout: out.getText(),
74
101
  stderr: err.getText(),
75
102
  exitCode: exitCodeFromClose(code, signal),
103
+ stdoutTruncated: out.wasTruncated(),
104
+ stderrTruncated: err.wasTruncated(),
76
105
  });
77
106
  });
78
107
  });
@@ -92,20 +121,66 @@ export async function execStream(command, args, options = {}) {
92
121
  stdio: ['ignore', 'pipe', 'pipe'],
93
122
  signal: buildSignalFromTimeout(options.timeout),
94
123
  });
124
+ const stdoutDecoder = new StringDecoder('utf8');
125
+ const stderrDecoder = new StringDecoder('utf8');
95
126
  child.stdout.on('data', (data) => {
96
- options.onStdout?.(data.toString());
127
+ const chunk = stdoutDecoder.write(data);
128
+ if (chunk.length > 0)
129
+ options.onStdout?.(chunk);
97
130
  });
98
131
  child.stderr.on('data', (data) => {
99
- options.onStderr?.(data.toString());
132
+ const chunk = stderrDecoder.write(data);
133
+ if (chunk.length > 0)
134
+ options.onStderr?.(chunk);
100
135
  });
101
136
  child.on('error', (error) => {
102
- reject(error);
137
+ reject(toExecRejection(error, command, args, options.timeout));
103
138
  });
104
139
  child.on('close', (code, signal) => {
105
140
  resolve(exitCodeFromClose(code, signal));
106
141
  });
107
142
  });
108
143
  }
144
+ /**
145
+ * Close-promises of children whose shutdown the CLI must wait for when a
146
+ * termination signal arrives. The bin signal handler used to `process.exit`
147
+ * within a microtask of the first Ctrl+C, which made `execInherit`'s
148
+ * documented SIGTERM → grace → SIGKILL escalation unreachable: the parent
149
+ * was gone before the 1500 ms grace timer could fire, so a hung Firefox
150
+ * was orphaned forever instead of being SIGKILLed and a healthy one lost
151
+ * its AsyncShutdown flush window.
152
+ */
153
+ const activeChildClosures = new Set();
154
+ function trackChildClosure() {
155
+ let resolveClosed;
156
+ const closed = new Promise((resolve) => {
157
+ resolveClosed = resolve;
158
+ });
159
+ activeChildClosures.add(closed);
160
+ return {
161
+ settle: () => {
162
+ resolveClosed?.();
163
+ activeChildClosures.delete(closed);
164
+ },
165
+ };
166
+ }
167
+ /**
168
+ * Waits (bounded) for every tracked child process to close. Called by the
169
+ * bin signal handler after forwarding SIGINT/SIGTERM, so the parent stays
170
+ * alive long enough for the grace-then-SIGKILL escalation to actually run.
171
+ * Resolves immediately when no children are active.
172
+ */
173
+ export async function waitForActiveChildShutdown(timeoutMs) {
174
+ if (activeChildClosures.size === 0)
175
+ return;
176
+ await Promise.race([
177
+ Promise.allSettled([...activeChildClosures]),
178
+ new Promise((resolve) => {
179
+ const timer = setTimeout(resolve, timeoutMs);
180
+ timer.unref();
181
+ }),
182
+ ]);
183
+ }
109
184
  /**
110
185
  * Executes a command and inherits stdio (shows output directly).
111
186
  *
@@ -132,12 +207,15 @@ export async function execInherit(command, args, options = {}) {
132
207
  });
133
208
  const graceMs = options.shutdownGraceMs ?? 1500;
134
209
  const { dispose } = installGracefulShutdownForwarder(child, graceMs);
210
+ const closure = trackChildClosure();
135
211
  child.on('error', (error) => {
136
212
  dispose();
137
- reject(error);
213
+ closure.settle();
214
+ reject(toExecRejection(error, command, args, options.timeout));
138
215
  });
139
216
  child.on('close', (code, signal) => {
140
217
  dispose();
218
+ closure.settle();
141
219
  resolve(exitCodeFromClose(code, signal));
142
220
  });
143
221
  });
@@ -220,16 +298,21 @@ export async function execInheritCapture(command, args, options = {}) {
220
298
  child.stderr.on('data', err.onData);
221
299
  const graceMs = options.shutdownGraceMs ?? 1500;
222
300
  const { dispose } = installGracefulShutdownForwarder(child, graceMs);
301
+ const closure = trackChildClosure();
223
302
  child.on('error', (error) => {
224
303
  dispose();
225
- reject(error);
304
+ closure.settle();
305
+ reject(toExecRejection(error, command, args, options.timeout));
226
306
  });
227
307
  child.on('close', (code, signal) => {
228
308
  dispose();
309
+ closure.settle();
229
310
  resolve({
230
311
  stdout: out.getText(),
231
312
  stderr: err.getText(),
232
313
  exitCode: exitCodeFromClose(code, signal),
314
+ stdoutTruncated: out.wasTruncated(),
315
+ stderrTruncated: err.wasTruncated(),
233
316
  });
234
317
  });
235
318
  });
@@ -248,8 +331,12 @@ export async function execInheritCapture(command, args, options = {}) {
248
331
  * that inherited the group.
249
332
  *
250
333
  * Windows fallback: `detached: true` does not create an equivalent group
251
- * there, so we degrade to `child.kill()` and log a best-effort warning
252
- * via the `onStderrLine` callback if the caller wired one.
334
+ * there, so we kill the descendant tree with `taskkill /pid <pid> /T /F`
335
+ * instead. taskkill has no graceful-signal equivalent both the deadline
336
+ * and the grace re-invocation are forced kills, so Windows children get
337
+ * no shutdown window (best-effort; Windows is untested, see README).
338
+ * `child.kill()` is still sent as a direct-child fallback in case
339
+ * taskkill itself is unavailable.
253
340
  */
254
341
  export async function execSmokeRun(command, args, options) {
255
342
  return new Promise((resolve, reject) => {
@@ -261,25 +348,28 @@ export async function execSmokeRun(command, args, options) {
261
348
  // A new process group on POSIX lets us signal the whole descendant
262
349
  // tree at once, which is essential for mach → python → firefox →
263
350
  // content-process chains. On Windows spawn ignores this for our
264
- // purposes and we fall back to child.kill below.
351
+ // purposes and we fall back to a taskkill /T tree kill below.
265
352
  detached: usesProcessGroup,
266
353
  });
267
354
  const out = createStreamCollector(options.mirror?.stdout);
268
355
  const err = createStreamCollector(options.mirror?.stderr);
356
+ const stdoutLineDecoder = new StringDecoder('utf8');
357
+ const stderrLineDecoder = new StringDecoder('utf8');
269
358
  let stdoutBuffer = '';
270
359
  let stderrBuffer = '';
271
360
  child.stdout.on('data', (data) => {
272
361
  out.onData(data);
273
- stdoutBuffer += data.toString();
362
+ stdoutBuffer += stdoutLineDecoder.write(data);
274
363
  stdoutBuffer = dispatchCompleteLines(stdoutBuffer, options.onStdoutLine);
275
364
  });
276
365
  child.stderr.on('data', (data) => {
277
366
  err.onData(data);
278
- stderrBuffer += data.toString();
367
+ stderrBuffer += stderrLineDecoder.write(data);
279
368
  stderrBuffer = dispatchCompleteLines(stderrBuffer, options.onStderrLine);
280
369
  });
281
370
  let timedOut = false;
282
371
  let graceTimer;
372
+ let signalGraceTimer;
283
373
  const signalChildGroup = (signal) => {
284
374
  if (child.exitCode !== null || child.signalCode !== null)
285
375
  return;
@@ -294,6 +384,15 @@ export async function execSmokeRun(command, args, options) {
294
384
  process.kill(-targetPid, signal);
295
385
  }
296
386
  else {
387
+ // No process group on Windows — taskkill /T walks the descendant
388
+ // tree instead. Always forced (/F): there is no SIGTERM analogue,
389
+ // so the grace window only exists on POSIX.
390
+ spawn('taskkill', ['/pid', String(targetPid), '/T', '/F'], {
391
+ stdio: 'ignore',
392
+ }).on('error', () => {
393
+ // taskkill unavailable — nothing more we can do beyond the
394
+ // direct-child kill below.
395
+ });
297
396
  child.kill(signal);
298
397
  }
299
398
  }
@@ -310,16 +409,52 @@ export async function execSmokeRun(command, args, options) {
310
409
  graceTimer.unref();
311
410
  }, options.smokeTimeoutMs);
312
411
  deadlineTimer.unref();
412
+ // Forward parent SIGINT/SIGTERM to the whole child process group. The
413
+ // smoke child is a group leader (detached), so it does NOT receive the
414
+ // terminal's Ctrl+C SIGINT — without this forwarder, the parent exits
415
+ // and the entire mach → firefox tree is orphaned. Second signal (or
416
+ // grace expiry) escalates to a group SIGKILL.
417
+ const forwardedSignals = new Set();
418
+ const onParentSignal = (signal) => {
419
+ if (forwardedSignals.has(signal)) {
420
+ signalChildGroup('SIGKILL');
421
+ return;
422
+ }
423
+ forwardedSignals.add(signal);
424
+ signalChildGroup('SIGTERM');
425
+ signalGraceTimer = setTimeout(() => {
426
+ signalChildGroup('SIGKILL');
427
+ }, options.killGraceMs ?? 10000);
428
+ signalGraceTimer.unref();
429
+ };
430
+ const onSigint = () => {
431
+ onParentSignal('SIGINT');
432
+ };
433
+ const onSigterm = () => {
434
+ onParentSignal('SIGTERM');
435
+ };
436
+ process.on('SIGINT', onSigint);
437
+ process.on('SIGTERM', onSigterm);
438
+ const closure = trackChildClosure();
439
+ const cleanupSignalForwarding = () => {
440
+ process.off('SIGINT', onSigint);
441
+ process.off('SIGTERM', onSigterm);
442
+ if (signalGraceTimer)
443
+ clearTimeout(signalGraceTimer);
444
+ closure.settle();
445
+ };
313
446
  child.on('error', (error) => {
314
447
  clearTimeout(deadlineTimer);
315
448
  if (graceTimer)
316
449
  clearTimeout(graceTimer);
450
+ cleanupSignalForwarding();
317
451
  reject(error);
318
452
  });
319
453
  child.on('close', (code, signal) => {
320
454
  clearTimeout(deadlineTimer);
321
455
  if (graceTimer)
322
456
  clearTimeout(graceTimer);
457
+ cleanupSignalForwarding();
323
458
  // Flush any remaining partial line (child ended without a trailing newline).
324
459
  if (stdoutBuffer.length > 0)
325
460
  options.onStdoutLine?.(stdoutBuffer);
@@ -335,24 +470,45 @@ export async function execSmokeRun(command, args, options) {
335
470
  });
336
471
  }
337
472
  /**
338
- * Drains complete newline-terminated lines from `buffer`, dispatching each
339
- * trimmed-of-newline line to `cb`. Returns the remaining partial line that
340
- * has not yet been terminated callers should keep accumulating into it.
473
+ * Cap on the partial-line tail kept between chunks. A child emitting one
474
+ * enormous line with no terminator (minified JS dumped to stderr, a raw
475
+ * binary blob) previously grew the buffer without bound the 50 MB cap
476
+ * only guarded the collector, not these line buffers. When the tail
477
+ * exceeds the cap it is dispatched as a synthetic (oversized) line so the
478
+ * matchers still get to see its content, then the buffer resets.
479
+ */
480
+ const MAX_PARTIAL_LINE_SIZE = 1024 * 1024;
481
+ /**
482
+ * Drains complete lines from `buffer`, dispatching each to `cb`. Treats
483
+ * `\n`, `\r\n`, and lone `\r` as line terminators — the lone-`\r` case
484
+ * matters for progress-bar style output (mach, cargo) that repaints a line
485
+ * with carriage returns and never sends a newline; before `\r` counted as a
486
+ * terminator those updates accumulated indefinitely. A single trailing `\r`
487
+ * is held back since it may be the first half of a `\r\n` pair split across
488
+ * chunks. Returns the remaining partial line — callers keep accumulating
489
+ * into it.
341
490
  */
342
491
  function dispatchCompleteLines(buffer, cb) {
343
- if (!cb) {
344
- // No listener — still need to collapse accumulated newlines so the
345
- // buffer does not grow forever. Keep only the partial-line tail.
346
- const lastNewline = buffer.lastIndexOf('\n');
347
- if (lastNewline === -1)
348
- return buffer;
349
- return buffer.slice(lastNewline + 1);
492
+ let searchFrom = 0;
493
+ for (;;) {
494
+ const nl = buffer.indexOf('\n', searchFrom);
495
+ const cr = buffer.indexOf('\r', searchFrom);
496
+ const idx = nl === -1 ? cr : cr === -1 ? nl : Math.min(nl, cr);
497
+ if (idx === -1)
498
+ break;
499
+ if (buffer[idx] === '\r' && idx === buffer.length - 1) {
500
+ // Possible first half of a chunk-split \r\n — wait for the next chunk.
501
+ break;
502
+ }
503
+ const line = buffer.slice(0, idx);
504
+ const terminatorLength = buffer[idx] === '\r' && buffer[idx + 1] === '\n' ? 2 : 1;
505
+ buffer = buffer.slice(idx + terminatorLength);
506
+ searchFrom = 0;
507
+ cb?.(line);
350
508
  }
351
- let idx;
352
- while ((idx = buffer.indexOf('\n')) !== -1) {
353
- const line = buffer.slice(0, idx).replace(/\r$/, '');
354
- buffer = buffer.slice(idx + 1);
355
- cb(line);
509
+ if (buffer.length > MAX_PARTIAL_LINE_SIZE) {
510
+ cb?.(buffer);
511
+ return '';
356
512
  }
357
513
  return buffer;
358
514
  }
@@ -388,4 +544,3 @@ export async function executableExists(name) {
388
544
  const path = await findExecutable(name);
389
545
  return path !== undefined;
390
546
  }
391
- //# sourceMappingURL=process.js.map
@@ -7,9 +7,19 @@ export declare function escapeRegex(str: string): string;
7
7
  */
8
8
  export declare function hasRawCssColors(content: string): boolean;
9
9
  /**
10
- * Strips JS single-line and multi-line comments from source code, replacing them
11
- * with spaces of equal length to preserve character offsets. String literals
12
- * (single-quoted, double-quoted, and template) are preserved intact.
10
+ * Strips JS single-line and multi-line comments from source code, replacing
11
+ * comment bytes with spaces (newlines inside block comments are kept) so
12
+ * both character offsets AND line numbers are preserved. String literals
13
+ * are preserved intact.
14
+ *
15
+ * Uses acorn's tokenizer, which understands regex literals — the previous
16
+ * pure-regex implementation did not model them, so two adjacent slashes
17
+ * inside a regex (e.g. the tail of `/https?:\/\//`) were treated as a `//`
18
+ * line comment and the rest of the line was blanked, corrupting whatever
19
+ * lint pass consumed the stripped text. Firefox chrome sources contain
20
+ * preprocessor directives (`#ifdef`) and other constructs acorn cannot
21
+ * tokenize; those fall back to the legacy regex strip, which handles
22
+ * strings but NOT regex literals (documented limitation of the fallback).
13
23
  */
14
24
  export declare function stripJsComments(source: string): string;
15
25
  /**
@@ -1,4 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
+ import { tokenizer as acornTokenizer, tokTypes as acornTokTypes } from 'acorn';
2
3
  /**
3
4
  * Escapes special regex characters in a string.
4
5
  */
@@ -22,11 +23,60 @@ export function hasRawCssColors(content) {
22
23
  return CSS_HEX_COLOR.test(content) || CSS_RGB_COLOR.test(content) || CSS_HSL_COLOR.test(content);
23
24
  }
24
25
  /**
25
- * Strips JS single-line and multi-line comments from source code, replacing them
26
- * with spaces of equal length to preserve character offsets. String literals
27
- * (single-quoted, double-quoted, and template) are preserved intact.
26
+ * Strips JS single-line and multi-line comments from source code, replacing
27
+ * comment bytes with spaces (newlines inside block comments are kept) so
28
+ * both character offsets AND line numbers are preserved. String literals
29
+ * are preserved intact.
30
+ *
31
+ * Uses acorn's tokenizer, which understands regex literals — the previous
32
+ * pure-regex implementation did not model them, so two adjacent slashes
33
+ * inside a regex (e.g. the tail of `/https?:\/\//`) were treated as a `//`
34
+ * line comment and the rest of the line was blanked, corrupting whatever
35
+ * lint pass consumed the stripped text. Firefox chrome sources contain
36
+ * preprocessor directives (`#ifdef`) and other constructs acorn cannot
37
+ * tokenize; those fall back to the legacy regex strip, which handles
38
+ * strings but NOT regex literals (documented limitation of the fallback).
28
39
  */
29
40
  export function stripJsComments(source) {
41
+ const comments = [];
42
+ try {
43
+ const t = acornTokenizer(source, {
44
+ ecmaVersion: 'latest',
45
+ sourceType: 'module',
46
+ allowHashBang: true,
47
+ onComment: (_isBlock, _text, start, end) => {
48
+ comments.push({ start, end });
49
+ },
50
+ });
51
+ // Drive the tokenizer to EOF; we only care about onComment callbacks.
52
+ for (;;) {
53
+ const token = t.getToken();
54
+ if (token.type === acornTokTypes.eof)
55
+ break;
56
+ }
57
+ }
58
+ catch {
59
+ return stripJsCommentsLegacy(source);
60
+ }
61
+ if (comments.length === 0)
62
+ return source;
63
+ const chars = source.split('');
64
+ for (const { start, end } of comments) {
65
+ for (let i = start; i < end; i++) {
66
+ if (chars[i] !== '\n' && chars[i] !== '\r') {
67
+ chars[i] = ' ';
68
+ }
69
+ }
70
+ }
71
+ return chars.join('');
72
+ }
73
+ /**
74
+ * Legacy pure-regex comment strip, used only when acorn cannot tokenize
75
+ * the source (preprocessor directives, syntax errors). Strings survive;
76
+ * regex literals are NOT modeled, so `//` inside a regex blanks the rest
77
+ * of the line.
78
+ */
79
+ function stripJsCommentsLegacy(source) {
30
80
  return source.replace(/\/\/.*$|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`/gm, (match) => (match.startsWith('/') ? ' '.repeat(match.length) : match));
31
81
  }
32
82
  /**
@@ -37,4 +87,3 @@ export function countRawCssColors(content) {
37
87
  (content.match(CSS_RGB_COLOR_G)?.length ?? 0) +
38
88
  (content.match(CSS_HSL_COLOR_G)?.length ?? 0));
39
89
  }
40
- //# sourceMappingURL=regex.js.map
@@ -52,18 +52,6 @@ export declare function isObject(value: unknown): value is Record<string, unknow
52
52
  * @returns True if value is an array
53
53
  */
54
54
  export declare function isArray(value: unknown): value is unknown[];
55
- /**
56
- * Asserts that a value is a string or throws.
57
- * @param value - Value to check
58
- * @param name - Name of the field for error messages
59
- */
60
- export declare function assertString(value: unknown, name: string): asserts value is string;
61
- /**
62
- * Asserts that a value is a non-null object or throws.
63
- * @param value - Value to check
64
- * @param name - Name of the field for error messages
65
- */
66
- export declare function assertObject(value: unknown, name: string): asserts value is Record<string, unknown>;
67
55
  /**
68
56
  * Validates a Firefox version string.
69
57
  * Accepts formats like "140.9.0", "140.9.1", "140.9.0esr", "147.0b1"
@@ -18,7 +18,10 @@ export function isString(value) {
18
18
  * @returns True if value is a finite number
19
19
  */
20
20
  export function isNumber(value) {
21
- return typeof value === 'number' && !Number.isNaN(value);
21
+ // Number.isFinite matches the documented contract: NaN AND ±Infinity are
22
+ // excluded (a JSON config field of 1e999 parses to Infinity and used to
23
+ // validate as a legal number).
24
+ return typeof value === 'number' && Number.isFinite(value);
22
25
  }
23
26
  /**
24
27
  * Checks whether a value is a positive integer (greater than zero).
@@ -75,26 +78,6 @@ export function isObject(value) {
75
78
  export function isArray(value) {
76
79
  return Array.isArray(value);
77
80
  }
78
- /**
79
- * Asserts that a value is a string or throws.
80
- * @param value - Value to check
81
- * @param name - Name of the field for error messages
82
- */
83
- export function assertString(value, name) {
84
- if (!isString(value)) {
85
- throw new InvalidArgumentError(`Expected ${name} to be a string, got ${typeof value}`, name);
86
- }
87
- }
88
- /**
89
- * Asserts that a value is a non-null object or throws.
90
- * @param value - Value to check
91
- * @param name - Name of the field for error messages
92
- */
93
- export function assertObject(value, name) {
94
- if (!isObject(value)) {
95
- throw new InvalidArgumentError(`Expected ${name} to be an object, got ${typeof value}`, name);
96
- }
97
- }
98
81
  /**
99
82
  * Validates a Firefox version string.
100
83
  * Accepts formats like "140.9.0", "140.9.1", "140.9.0esr", "147.0b1"
@@ -232,11 +215,16 @@ export function validateTokenName(name) {
232
215
  if (/[\x00-\x1f\x7f]/.test(ident)) {
233
216
  return `Token name must not contain control characters: "${name}"`;
234
217
  }
235
- if (ident.includes('*/')) {
236
- return `Token name must not contain "*/" (would break CSS comments): "${name}"`;
218
+ if (ident.includes('*/') || ident.includes('/*')) {
219
+ // `/*` is the worse of the two by this function's own standard: an
220
+ // ident containing it opens an UNTERMINATED comment in tokens.css
221
+ // that swallows every following declaration.
222
+ return `Token name must not contain "/*" or "*/" (would break CSS comments): "${name}"`;
237
223
  }
238
- // Reject characters that would break CSS declaration syntax
239
- if (/[{}();!]/.test(ident)) {
224
+ // Reject characters that would break CSS declaration syntax. Quotes are
225
+ // included: a quote character in an ident opens a CSS string that eats
226
+ // the rest of the declaration block.
227
+ if (/[{}();!'"]/.test(ident)) {
240
228
  return `Token name contains characters that would corrupt CSS syntax: "${name}"`;
241
229
  }
242
230
  return undefined;
@@ -270,4 +258,3 @@ export function validatePatchName(name) {
270
258
  return 'Name can only contain letters, numbers, hyphens, underscores, and spaces';
271
259
  return undefined;
272
260
  }
273
- //# sourceMappingURL=validation.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",
@@ -20,9 +20,9 @@
20
20
  "fireforge": "tsx bin/fireforge.ts",
21
21
  "build": "npm run clean && tsc -p tsconfig.build.json",
22
22
  "clean": "node ./scripts/clean.mjs",
23
- "lint": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts",
24
- "lint:ci": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts --max-warnings 0",
25
- "lint:fix": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts --fix",
23
+ "lint": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts vitest.pack.config.ts",
24
+ "lint:ci": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts vitest.pack.config.ts --max-warnings 0",
25
+ "lint:fix": "eslint src/ bin/ scripts/ eslint.config.js vitest.config.ts vitest.pack.config.ts --fix",
26
26
  "typecheck": "tsc --noEmit -p tsconfig.json",
27
27
  "deadcode:check": "knip",
28
28
  "cycles:check": "dpdm --no-output --no-tree --no-warning --exit-code circular:1 src/index.ts bin/fireforge.ts",
@@ -32,10 +32,10 @@
32
32
  "test:coverage": "vitest run --coverage && node ./scripts/check-coverage-thresholds.mjs",
33
33
  "test:firefox-full": "node ./scripts/run-full-firefox-integration.mjs",
34
34
  "whitespace:check": "node ./scripts/check-worktree-whitespace.mjs",
35
- "pack:verify": "vitest run src/__tests__/wrapper-smoke.test.ts",
35
+ "pack:verify": "vitest run --config vitest.pack.config.ts",
36
36
  "pack:dry-run": "npm pack --dry-run --json --silent",
37
- "format": "prettier --write --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\"",
38
- "format:check": "prettier --check --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\"",
37
+ "format": "prettier --write --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"vitest.pack.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\" \"docs/**/*.md\" \"knip.json\" \"docs/**/*.md\" \"knip.json\"",
38
+ "format:check": "prettier --check --ignore-unknown \"src/**/*.ts\" \"bin/**/*.ts\" \"scripts/**/*.mjs\" \"package.json\" \"eslint.config.js\" \"vitest.config.ts\" \"vitest.pack.config.ts\" \"tsconfig.json\" \"tsconfig.build.json\" \".lintstagedrc.json\" \".prettierignore\" \".prettierrc\" \".gitignore\" \"README.md\" \"CHANGELOG.md\" \"docs/**/*.md\" \"knip.json\" \"docs/**/*.md\" \"knip.json\"",
39
39
  "prepare": "node -e \"import('./scripts/prepare.mjs').catch((error) => { if (error && typeof error === 'object' && 'code' in error && error.code === 'ERR_MODULE_NOT_FOUND') process.exit(0); throw error; })\"",
40
40
  "prepack": "npm run build",
41
41
  "release:check": "npm run format:check && npm run whitespace:check && npm run lint:ci && npm run typecheck && npm run deadcode:check && npm run cycles:check && npm run test:coverage && npm run pack:verify && npm run pack:dry-run",
@@ -57,6 +57,7 @@
57
57
  "acorn": "^8.14.0",
58
58
  "commander": "^15.0.0",
59
59
  "estree-walker": "^3.0.3",
60
+ "jsonc-parser": "^3.3.1",
60
61
  "magic-string": "^0.30.17",
61
62
  "picocolors": "^1.1.0"
62
63
  },