@hyperfrontend/versioning 0.1.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 (353) hide show
  1. package/ARCHITECTURE.md +593 -0
  2. package/CHANGELOG.md +35 -0
  3. package/FUNDING.md +141 -0
  4. package/LICENSE.md +21 -0
  5. package/README.md +195 -0
  6. package/SECURITY.md +82 -0
  7. package/changelog/compare/diff.d.ts +128 -0
  8. package/changelog/compare/diff.d.ts.map +1 -0
  9. package/changelog/compare/index.cjs.js +628 -0
  10. package/changelog/compare/index.cjs.js.map +1 -0
  11. package/changelog/compare/index.d.ts +4 -0
  12. package/changelog/compare/index.d.ts.map +1 -0
  13. package/changelog/compare/index.esm.js +612 -0
  14. package/changelog/compare/index.esm.js.map +1 -0
  15. package/changelog/compare/is-equal.d.ts +114 -0
  16. package/changelog/compare/is-equal.d.ts.map +1 -0
  17. package/changelog/index.cjs.js +6448 -0
  18. package/changelog/index.cjs.js.map +1 -0
  19. package/changelog/index.d.ts +6 -0
  20. package/changelog/index.d.ts.map +1 -0
  21. package/changelog/index.esm.js +6358 -0
  22. package/changelog/index.esm.js.map +1 -0
  23. package/changelog/models/changelog.d.ts +86 -0
  24. package/changelog/models/changelog.d.ts.map +1 -0
  25. package/changelog/models/commit-ref.d.ts +51 -0
  26. package/changelog/models/commit-ref.d.ts.map +1 -0
  27. package/changelog/models/entry.d.ts +84 -0
  28. package/changelog/models/entry.d.ts.map +1 -0
  29. package/changelog/models/index.cjs.js +2043 -0
  30. package/changelog/models/index.cjs.js.map +1 -0
  31. package/changelog/models/index.d.ts +11 -0
  32. package/changelog/models/index.d.ts.map +1 -0
  33. package/changelog/models/index.esm.js +2026 -0
  34. package/changelog/models/index.esm.js.map +1 -0
  35. package/changelog/models/schema.d.ts +68 -0
  36. package/changelog/models/schema.d.ts.map +1 -0
  37. package/changelog/models/section.d.ts +25 -0
  38. package/changelog/models/section.d.ts.map +1 -0
  39. package/changelog/operations/add-entry.d.ts +56 -0
  40. package/changelog/operations/add-entry.d.ts.map +1 -0
  41. package/changelog/operations/add-item.d.ts +18 -0
  42. package/changelog/operations/add-item.d.ts.map +1 -0
  43. package/changelog/operations/filter-by-predicate.d.ts +81 -0
  44. package/changelog/operations/filter-by-predicate.d.ts.map +1 -0
  45. package/changelog/operations/filter-by-range.d.ts +63 -0
  46. package/changelog/operations/filter-by-range.d.ts.map +1 -0
  47. package/changelog/operations/filter-entries.d.ts +9 -0
  48. package/changelog/operations/filter-entries.d.ts.map +1 -0
  49. package/changelog/operations/index.cjs.js +2455 -0
  50. package/changelog/operations/index.cjs.js.map +1 -0
  51. package/changelog/operations/index.d.ts +15 -0
  52. package/changelog/operations/index.d.ts.map +1 -0
  53. package/changelog/operations/index.esm.js +2411 -0
  54. package/changelog/operations/index.esm.js.map +1 -0
  55. package/changelog/operations/merge.d.ts +88 -0
  56. package/changelog/operations/merge.d.ts.map +1 -0
  57. package/changelog/operations/remove-entry.d.ts +45 -0
  58. package/changelog/operations/remove-entry.d.ts.map +1 -0
  59. package/changelog/operations/remove-section.d.ts +50 -0
  60. package/changelog/operations/remove-section.d.ts.map +1 -0
  61. package/changelog/operations/transform.d.ts +143 -0
  62. package/changelog/operations/transform.d.ts.map +1 -0
  63. package/changelog/parse/index.cjs.js +1282 -0
  64. package/changelog/parse/index.cjs.js.map +1 -0
  65. package/changelog/parse/index.d.ts +5 -0
  66. package/changelog/parse/index.d.ts.map +1 -0
  67. package/changelog/parse/index.esm.js +1275 -0
  68. package/changelog/parse/index.esm.js.map +1 -0
  69. package/changelog/parse/line.d.ts +48 -0
  70. package/changelog/parse/line.d.ts.map +1 -0
  71. package/changelog/parse/parser.d.ts +16 -0
  72. package/changelog/parse/parser.d.ts.map +1 -0
  73. package/changelog/parse/tokenizer.d.ts +49 -0
  74. package/changelog/parse/tokenizer.d.ts.map +1 -0
  75. package/changelog/serialize/index.cjs.js +574 -0
  76. package/changelog/serialize/index.cjs.js.map +1 -0
  77. package/changelog/serialize/index.d.ts +6 -0
  78. package/changelog/serialize/index.d.ts.map +1 -0
  79. package/changelog/serialize/index.esm.js +564 -0
  80. package/changelog/serialize/index.esm.js.map +1 -0
  81. package/changelog/serialize/templates.d.ts +81 -0
  82. package/changelog/serialize/templates.d.ts.map +1 -0
  83. package/changelog/serialize/to-json.d.ts +57 -0
  84. package/changelog/serialize/to-json.d.ts.map +1 -0
  85. package/changelog/serialize/to-string.d.ts +30 -0
  86. package/changelog/serialize/to-string.d.ts.map +1 -0
  87. package/commits/index.cjs.js +648 -0
  88. package/commits/index.cjs.js.map +1 -0
  89. package/commits/index.d.ts +3 -0
  90. package/commits/index.d.ts.map +1 -0
  91. package/commits/index.esm.js +629 -0
  92. package/commits/index.esm.js.map +1 -0
  93. package/commits/models/breaking.d.ts +39 -0
  94. package/commits/models/breaking.d.ts.map +1 -0
  95. package/commits/models/commit-type.d.ts +32 -0
  96. package/commits/models/commit-type.d.ts.map +1 -0
  97. package/commits/models/conventional.d.ts +49 -0
  98. package/commits/models/conventional.d.ts.map +1 -0
  99. package/commits/models/index.cjs.js +207 -0
  100. package/commits/models/index.cjs.js.map +1 -0
  101. package/commits/models/index.d.ts +7 -0
  102. package/commits/models/index.d.ts.map +1 -0
  103. package/commits/models/index.esm.js +193 -0
  104. package/commits/models/index.esm.js.map +1 -0
  105. package/commits/parse/body.d.ts +18 -0
  106. package/commits/parse/body.d.ts.map +1 -0
  107. package/commits/parse/footer.d.ts +16 -0
  108. package/commits/parse/footer.d.ts.map +1 -0
  109. package/commits/parse/header.d.ts +15 -0
  110. package/commits/parse/header.d.ts.map +1 -0
  111. package/commits/parse/index.cjs.js +505 -0
  112. package/commits/parse/index.cjs.js.map +1 -0
  113. package/commits/parse/index.d.ts +5 -0
  114. package/commits/parse/index.d.ts.map +1 -0
  115. package/commits/parse/index.esm.js +499 -0
  116. package/commits/parse/index.esm.js.map +1 -0
  117. package/commits/parse/message.d.ts +17 -0
  118. package/commits/parse/message.d.ts.map +1 -0
  119. package/commits/utils/replace-char.d.ts +19 -0
  120. package/commits/utils/replace-char.d.ts.map +1 -0
  121. package/flow/executor/execute.d.ts +72 -0
  122. package/flow/executor/execute.d.ts.map +1 -0
  123. package/flow/executor/index.cjs.js +4402 -0
  124. package/flow/executor/index.cjs.js.map +1 -0
  125. package/flow/executor/index.d.ts +3 -0
  126. package/flow/executor/index.d.ts.map +1 -0
  127. package/flow/executor/index.esm.js +4398 -0
  128. package/flow/executor/index.esm.js.map +1 -0
  129. package/flow/factory.d.ts +58 -0
  130. package/flow/factory.d.ts.map +1 -0
  131. package/flow/index.cjs.js +8506 -0
  132. package/flow/index.cjs.js.map +1 -0
  133. package/flow/index.d.ts +7 -0
  134. package/flow/index.d.ts.map +1 -0
  135. package/flow/index.esm.js +8451 -0
  136. package/flow/index.esm.js.map +1 -0
  137. package/flow/models/flow.d.ts +130 -0
  138. package/flow/models/flow.d.ts.map +1 -0
  139. package/flow/models/index.cjs.js +285 -0
  140. package/flow/models/index.cjs.js.map +1 -0
  141. package/flow/models/index.d.ts +7 -0
  142. package/flow/models/index.d.ts.map +1 -0
  143. package/flow/models/index.esm.js +268 -0
  144. package/flow/models/index.esm.js.map +1 -0
  145. package/flow/models/step.d.ts +108 -0
  146. package/flow/models/step.d.ts.map +1 -0
  147. package/flow/models/types.d.ts +150 -0
  148. package/flow/models/types.d.ts.map +1 -0
  149. package/flow/presets/conventional.d.ts +59 -0
  150. package/flow/presets/conventional.d.ts.map +1 -0
  151. package/flow/presets/independent.d.ts +61 -0
  152. package/flow/presets/independent.d.ts.map +1 -0
  153. package/flow/presets/index.cjs.js +3903 -0
  154. package/flow/presets/index.cjs.js.map +1 -0
  155. package/flow/presets/index.d.ts +4 -0
  156. package/flow/presets/index.d.ts.map +1 -0
  157. package/flow/presets/index.esm.js +3889 -0
  158. package/flow/presets/index.esm.js.map +1 -0
  159. package/flow/presets/synced.d.ts +65 -0
  160. package/flow/presets/synced.d.ts.map +1 -0
  161. package/flow/steps/analyze-commits.d.ts +19 -0
  162. package/flow/steps/analyze-commits.d.ts.map +1 -0
  163. package/flow/steps/calculate-bump.d.ts +27 -0
  164. package/flow/steps/calculate-bump.d.ts.map +1 -0
  165. package/flow/steps/create-commit.d.ts +16 -0
  166. package/flow/steps/create-commit.d.ts.map +1 -0
  167. package/flow/steps/create-tag.d.ts +22 -0
  168. package/flow/steps/create-tag.d.ts.map +1 -0
  169. package/flow/steps/fetch-registry.d.ts +19 -0
  170. package/flow/steps/fetch-registry.d.ts.map +1 -0
  171. package/flow/steps/generate-changelog.d.ts +25 -0
  172. package/flow/steps/generate-changelog.d.ts.map +1 -0
  173. package/flow/steps/index.cjs.js +3523 -0
  174. package/flow/steps/index.cjs.js.map +1 -0
  175. package/flow/steps/index.d.ts +8 -0
  176. package/flow/steps/index.d.ts.map +1 -0
  177. package/flow/steps/index.esm.js +3504 -0
  178. package/flow/steps/index.esm.js.map +1 -0
  179. package/flow/steps/update-packages.d.ts +25 -0
  180. package/flow/steps/update-packages.d.ts.map +1 -0
  181. package/flow/utils/interpolate.d.ts +11 -0
  182. package/flow/utils/interpolate.d.ts.map +1 -0
  183. package/git/factory.d.ts +233 -0
  184. package/git/factory.d.ts.map +1 -0
  185. package/git/index.cjs.js +2863 -0
  186. package/git/index.cjs.js.map +1 -0
  187. package/git/index.d.ts +5 -0
  188. package/git/index.d.ts.map +1 -0
  189. package/git/index.esm.js +2785 -0
  190. package/git/index.esm.js.map +1 -0
  191. package/git/models/commit.d.ts +129 -0
  192. package/git/models/commit.d.ts.map +1 -0
  193. package/git/models/index.cjs.js +755 -0
  194. package/git/models/index.cjs.js.map +1 -0
  195. package/git/models/index.d.ts +7 -0
  196. package/git/models/index.d.ts.map +1 -0
  197. package/git/models/index.esm.js +729 -0
  198. package/git/models/index.esm.js.map +1 -0
  199. package/git/models/ref.d.ts +120 -0
  200. package/git/models/ref.d.ts.map +1 -0
  201. package/git/models/tag.d.ts +141 -0
  202. package/git/models/tag.d.ts.map +1 -0
  203. package/git/operations/commit.d.ts +97 -0
  204. package/git/operations/commit.d.ts.map +1 -0
  205. package/git/operations/head-info.d.ts +29 -0
  206. package/git/operations/head-info.d.ts.map +1 -0
  207. package/git/operations/index.cjs.js +1954 -0
  208. package/git/operations/index.cjs.js.map +1 -0
  209. package/git/operations/index.d.ts +14 -0
  210. package/git/operations/index.d.ts.map +1 -0
  211. package/git/operations/index.esm.js +1903 -0
  212. package/git/operations/index.esm.js.map +1 -0
  213. package/git/operations/log.d.ts +104 -0
  214. package/git/operations/log.d.ts.map +1 -0
  215. package/git/operations/manage-tags.d.ts +60 -0
  216. package/git/operations/manage-tags.d.ts.map +1 -0
  217. package/git/operations/query-tags.d.ts +88 -0
  218. package/git/operations/query-tags.d.ts.map +1 -0
  219. package/git/operations/stage.d.ts +66 -0
  220. package/git/operations/stage.d.ts.map +1 -0
  221. package/git/operations/status.d.ts +173 -0
  222. package/git/operations/status.d.ts.map +1 -0
  223. package/index.cjs.js +16761 -0
  224. package/index.cjs.js.map +1 -0
  225. package/index.d.ts +102 -0
  226. package/index.d.ts.map +1 -0
  227. package/index.esm.js +16427 -0
  228. package/index.esm.js.map +1 -0
  229. package/package.json +200 -0
  230. package/registry/factory.d.ts +18 -0
  231. package/registry/factory.d.ts.map +1 -0
  232. package/registry/index.cjs.js +543 -0
  233. package/registry/index.cjs.js.map +1 -0
  234. package/registry/index.d.ts +5 -0
  235. package/registry/index.d.ts.map +1 -0
  236. package/registry/index.esm.js +535 -0
  237. package/registry/index.esm.js.map +1 -0
  238. package/registry/models/index.cjs.js +69 -0
  239. package/registry/models/index.cjs.js.map +1 -0
  240. package/registry/models/index.d.ts +6 -0
  241. package/registry/models/index.d.ts.map +1 -0
  242. package/registry/models/index.esm.js +66 -0
  243. package/registry/models/index.esm.js.map +1 -0
  244. package/registry/models/package-info.d.ts +55 -0
  245. package/registry/models/package-info.d.ts.map +1 -0
  246. package/registry/models/registry.d.ts +62 -0
  247. package/registry/models/registry.d.ts.map +1 -0
  248. package/registry/models/version-info.d.ts +67 -0
  249. package/registry/models/version-info.d.ts.map +1 -0
  250. package/registry/npm/cache.d.ts +50 -0
  251. package/registry/npm/cache.d.ts.map +1 -0
  252. package/registry/npm/client.d.ts +30 -0
  253. package/registry/npm/client.d.ts.map +1 -0
  254. package/registry/npm/index.cjs.js +456 -0
  255. package/registry/npm/index.cjs.js.map +1 -0
  256. package/registry/npm/index.d.ts +4 -0
  257. package/registry/npm/index.d.ts.map +1 -0
  258. package/registry/npm/index.esm.js +451 -0
  259. package/registry/npm/index.esm.js.map +1 -0
  260. package/semver/compare/compare.d.ts +100 -0
  261. package/semver/compare/compare.d.ts.map +1 -0
  262. package/semver/compare/index.cjs.js +386 -0
  263. package/semver/compare/index.cjs.js.map +1 -0
  264. package/semver/compare/index.d.ts +3 -0
  265. package/semver/compare/index.d.ts.map +1 -0
  266. package/semver/compare/index.esm.js +370 -0
  267. package/semver/compare/index.esm.js.map +1 -0
  268. package/semver/compare/sort.d.ts +36 -0
  269. package/semver/compare/sort.d.ts.map +1 -0
  270. package/semver/format/index.cjs.js +58 -0
  271. package/semver/format/index.cjs.js.map +1 -0
  272. package/semver/format/index.d.ts +2 -0
  273. package/semver/format/index.d.ts.map +1 -0
  274. package/semver/format/index.esm.js +53 -0
  275. package/semver/format/index.esm.js.map +1 -0
  276. package/semver/format/to-string.d.ts +31 -0
  277. package/semver/format/to-string.d.ts.map +1 -0
  278. package/semver/increment/bump.d.ts +37 -0
  279. package/semver/increment/bump.d.ts.map +1 -0
  280. package/semver/increment/index.cjs.js +223 -0
  281. package/semver/increment/index.cjs.js.map +1 -0
  282. package/semver/increment/index.d.ts +2 -0
  283. package/semver/increment/index.d.ts.map +1 -0
  284. package/semver/increment/index.esm.js +219 -0
  285. package/semver/increment/index.esm.js.map +1 -0
  286. package/semver/index.cjs.js +1499 -0
  287. package/semver/index.cjs.js.map +1 -0
  288. package/semver/index.d.ts +6 -0
  289. package/semver/index.d.ts.map +1 -0
  290. package/semver/index.esm.js +1458 -0
  291. package/semver/index.esm.js.map +1 -0
  292. package/semver/models/index.cjs.js +153 -0
  293. package/semver/models/index.cjs.js.map +1 -0
  294. package/semver/models/index.d.ts +5 -0
  295. package/semver/models/index.d.ts.map +1 -0
  296. package/semver/models/index.esm.js +139 -0
  297. package/semver/models/index.esm.js.map +1 -0
  298. package/semver/models/range.d.ts +83 -0
  299. package/semver/models/range.d.ts.map +1 -0
  300. package/semver/models/version.d.ts +78 -0
  301. package/semver/models/version.d.ts.map +1 -0
  302. package/semver/parse/index.cjs.js +799 -0
  303. package/semver/parse/index.cjs.js.map +1 -0
  304. package/semver/parse/index.d.ts +5 -0
  305. package/semver/parse/index.d.ts.map +1 -0
  306. package/semver/parse/index.esm.js +793 -0
  307. package/semver/parse/index.esm.js.map +1 -0
  308. package/semver/parse/range.d.ts +38 -0
  309. package/semver/parse/range.d.ts.map +1 -0
  310. package/semver/parse/version.d.ts +49 -0
  311. package/semver/parse/version.d.ts.map +1 -0
  312. package/workspace/discovery/changelog-path.d.ts +21 -0
  313. package/workspace/discovery/changelog-path.d.ts.map +1 -0
  314. package/workspace/discovery/dependencies.d.ts +145 -0
  315. package/workspace/discovery/dependencies.d.ts.map +1 -0
  316. package/workspace/discovery/discover-changelogs.d.ts +76 -0
  317. package/workspace/discovery/discover-changelogs.d.ts.map +1 -0
  318. package/workspace/discovery/index.cjs.js +2300 -0
  319. package/workspace/discovery/index.cjs.js.map +1 -0
  320. package/workspace/discovery/index.d.ts +13 -0
  321. package/workspace/discovery/index.d.ts.map +1 -0
  322. package/workspace/discovery/index.esm.js +2283 -0
  323. package/workspace/discovery/index.esm.js.map +1 -0
  324. package/workspace/discovery/packages.d.ts +83 -0
  325. package/workspace/discovery/packages.d.ts.map +1 -0
  326. package/workspace/index.cjs.js +4445 -0
  327. package/workspace/index.cjs.js.map +1 -0
  328. package/workspace/index.d.ts +52 -0
  329. package/workspace/index.d.ts.map +1 -0
  330. package/workspace/index.esm.js +4394 -0
  331. package/workspace/index.esm.js.map +1 -0
  332. package/workspace/models/index.cjs.js +284 -0
  333. package/workspace/models/index.cjs.js.map +1 -0
  334. package/workspace/models/index.d.ts +10 -0
  335. package/workspace/models/index.d.ts.map +1 -0
  336. package/workspace/models/index.esm.js +261 -0
  337. package/workspace/models/index.esm.js.map +1 -0
  338. package/workspace/models/project.d.ts +118 -0
  339. package/workspace/models/project.d.ts.map +1 -0
  340. package/workspace/models/workspace.d.ts +139 -0
  341. package/workspace/models/workspace.d.ts.map +1 -0
  342. package/workspace/operations/batch-update.d.ts +99 -0
  343. package/workspace/operations/batch-update.d.ts.map +1 -0
  344. package/workspace/operations/cascade-bump.d.ts +125 -0
  345. package/workspace/operations/cascade-bump.d.ts.map +1 -0
  346. package/workspace/operations/index.cjs.js +2675 -0
  347. package/workspace/operations/index.cjs.js.map +1 -0
  348. package/workspace/operations/index.d.ts +12 -0
  349. package/workspace/operations/index.d.ts.map +1 -0
  350. package/workspace/operations/index.esm.js +2663 -0
  351. package/workspace/operations/index.esm.js.map +1 -0
  352. package/workspace/operations/validate.d.ts +85 -0
  353. package/workspace/operations/validate.d.ts.map +1 -0
@@ -0,0 +1,3889 @@
1
+ /**
2
+ * Creates a version flow.
3
+ *
4
+ * @param id - Flow identifier
5
+ * @param name - Human-readable flow name
6
+ * @param steps - Ordered steps to execute
7
+ * @param options - Optional flow configuration
8
+ * @returns A VersionFlow object
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const myFlow = createFlow(
13
+ * 'custom',
14
+ * 'Custom Release Flow',
15
+ * [fetchStep, analyzeStep, bumpStep],
16
+ * { description: 'My custom versioning workflow' }
17
+ * )
18
+ * ```
19
+ */
20
+ function createFlow(id, name, steps, options = {}) {
21
+ return {
22
+ id,
23
+ name,
24
+ steps,
25
+ description: options.description,
26
+ config: options.config ?? {},
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Safe copies of JSON built-in methods.
32
+ *
33
+ * These references are captured at module initialization time to protect against
34
+ * prototype pollution attacks. Import only what you need for tree-shaking.
35
+ *
36
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/json
37
+ */
38
+ // Capture references at module initialization time
39
+ const _JSON = globalThis.JSON;
40
+ /**
41
+ * (Safe copy) Converts a JavaScript Object Notation (JSON) string into an object.
42
+ */
43
+ const parse = _JSON.parse;
44
+ /**
45
+ * (Safe copy) Converts a JavaScript value to a JavaScript Object Notation (JSON) string.
46
+ */
47
+ const stringify = _JSON.stringify;
48
+
49
+ /**
50
+ * Creates a flow step.
51
+ *
52
+ * @param id - Unique step identifier
53
+ * @param name - Human-readable step name
54
+ * @param execute - Step executor function
55
+ * @param options - Optional step configuration
56
+ * @returns A FlowStep object
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const fetchStep = createStep(
61
+ * 'fetch-registry',
62
+ * 'Fetch Registry Version',
63
+ * async (ctx) => {
64
+ * const version = await ctx.registry.getLatestVersion(ctx.packageName)
65
+ * return {
66
+ * status: 'success',
67
+ * stateUpdates: { publishedVersion: version },
68
+ * message: `Found published version: ${version}`
69
+ * }
70
+ * }
71
+ * )
72
+ * ```
73
+ */
74
+ function createStep(id, name, execute, options = {}) {
75
+ return {
76
+ id,
77
+ name,
78
+ execute,
79
+ description: options.description,
80
+ skipIf: options.skipIf,
81
+ continueOnError: options.continueOnError,
82
+ dependsOn: options.dependsOn,
83
+ };
84
+ }
85
+ /**
86
+ * Creates a skipped step result.
87
+ *
88
+ * @param message - Explanation for why the step was skipped
89
+ * @returns A FlowStepResult with 'skipped' status
90
+ */
91
+ function createSkippedResult(message) {
92
+ return {
93
+ status: 'skipped',
94
+ message,
95
+ };
96
+ }
97
+
98
+ const FETCH_REGISTRY_STEP_ID = 'fetch-registry';
99
+ /**
100
+ * Creates the fetch-registry step.
101
+ *
102
+ * This step:
103
+ * 1. Queries the registry for the latest published version
104
+ * 2. Reads the current version from package.json
105
+ * 3. Determines if this is a first release
106
+ *
107
+ * State updates:
108
+ * - publishedVersion: Latest version on registry (null if not published)
109
+ * - currentVersion: Version from local package.json
110
+ * - isFirstRelease: True if never published
111
+ *
112
+ * @returns A FlowStep that fetches registry information
113
+ */
114
+ function createFetchRegistryStep() {
115
+ return createStep(FETCH_REGISTRY_STEP_ID, 'Fetch Registry Version', async (ctx) => {
116
+ const { registry, tree, projectRoot, packageName, logger } = ctx;
117
+ // Read local package.json for current version
118
+ const packageJsonPath = `${projectRoot}/package.json`;
119
+ let currentVersion = '0.0.0';
120
+ try {
121
+ const content = tree.read(packageJsonPath, 'utf-8');
122
+ if (content) {
123
+ const pkg = parse(content);
124
+ currentVersion = pkg.version ?? '0.0.0';
125
+ }
126
+ }
127
+ catch (error) {
128
+ logger.warn(`Could not read package.json: ${error}`);
129
+ }
130
+ // Query registry for published version
131
+ let publishedVersion = null;
132
+ let isFirstRelease = true;
133
+ try {
134
+ publishedVersion = await registry.getLatestVersion(packageName);
135
+ isFirstRelease = publishedVersion === null;
136
+ }
137
+ catch (error) {
138
+ // Package might not exist yet, which is fine
139
+ logger.debug(`Registry query failed (package may not exist): ${error}`);
140
+ isFirstRelease = true;
141
+ }
142
+ const message = isFirstRelease ? `First release (local: ${currentVersion})` : `Published: ${publishedVersion}, Local: ${currentVersion}`;
143
+ return {
144
+ status: 'success',
145
+ stateUpdates: {
146
+ publishedVersion,
147
+ currentVersion,
148
+ isFirstRelease,
149
+ },
150
+ message,
151
+ };
152
+ });
153
+ }
154
+
155
+ /**
156
+ * Safe copies of Error built-ins via factory functions.
157
+ *
158
+ * Since constructors cannot be safely captured via Object.assign, this module
159
+ * provides factory functions that use Reflect.construct internally.
160
+ *
161
+ * These references are captured at module initialization time to protect against
162
+ * prototype pollution attacks. Import only what you need for tree-shaking.
163
+ *
164
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/error
165
+ */
166
+ // Capture references at module initialization time
167
+ const _Error = globalThis.Error;
168
+ const _Reflect$2 = globalThis.Reflect;
169
+ /**
170
+ * (Safe copy) Creates a new Error using the captured Error constructor.
171
+ * Use this instead of `new Error()`.
172
+ *
173
+ * @param message - Optional error message.
174
+ * @param options - Optional error options.
175
+ * @returns A new Error instance.
176
+ */
177
+ const createError = (message, options) => _Reflect$2.construct(_Error, [message, options]);
178
+
179
+ /**
180
+ * Replaces all occurrences of a character in a string.
181
+ * Uses character-by-character iteration to avoid regex (ReDoS-safe).
182
+ *
183
+ * @param input - The input string
184
+ * @param target - The character to replace
185
+ * @param replacement - The replacement character
186
+ * @returns String with all occurrences replaced
187
+ */
188
+ function replaceChar(input, target, replacement) {
189
+ const result = [];
190
+ for (let i = 0; i < input.length; i++) {
191
+ result.push(input[i] === target ? replacement : input[i]);
192
+ }
193
+ return result.join('');
194
+ }
195
+ /**
196
+ * Replaces all hyphens with spaces.
197
+ * Convenience wrapper for normalizing footer keys.
198
+ *
199
+ * @param input - The input string
200
+ * @returns String with hyphens replaced by spaces
201
+ */
202
+ function hyphenToSpace(input) {
203
+ return replaceChar(input, '-', ' ');
204
+ }
205
+
206
+ /**
207
+ * Parses the body section of a commit message.
208
+ *
209
+ * The body starts after the first blank line and continues until
210
+ * we encounter a footer (key: value or key #value pattern) or end of message.
211
+ *
212
+ * @param lines - All lines of the commit message
213
+ * @param startIndex - Index to start looking for body (after header)
214
+ * @returns Parsed body or undefined if no body
215
+ */
216
+ function parseBody(lines, startIndex) {
217
+ // Skip blank lines to find body start
218
+ let pos = startIndex;
219
+ while (pos < lines.length && lines[pos].trim() === '') {
220
+ pos++;
221
+ }
222
+ if (pos >= lines.length) {
223
+ return undefined;
224
+ }
225
+ // If the first non-blank line is a footer, there's no body
226
+ if (isFooterLine(lines[pos])) {
227
+ return undefined;
228
+ }
229
+ const bodyLines = [];
230
+ // Collect body lines until we hit a footer or end
231
+ while (pos < lines.length) {
232
+ const line = lines[pos];
233
+ // Check if this line looks like a footer
234
+ if (isFooterLine(line)) {
235
+ break;
236
+ }
237
+ // Check for blank line followed by a footer
238
+ if (line.trim() === '' && pos + 1 < lines.length && isFooterLine(lines[pos + 1])) {
239
+ break;
240
+ }
241
+ bodyLines.push(line);
242
+ pos++;
243
+ }
244
+ // Trim trailing blank lines from body
245
+ while (bodyLines.length > 0 && bodyLines[bodyLines.length - 1].trim() === '') {
246
+ bodyLines.pop();
247
+ }
248
+ if (bodyLines.length === 0) {
249
+ return undefined;
250
+ }
251
+ return {
252
+ body: bodyLines.join('\n'),
253
+ endIndex: pos,
254
+ };
255
+ }
256
+ /**
257
+ * Checks if a line looks like a footer.
258
+ *
259
+ * Footer format:
260
+ * - key: value
261
+ * - key #value (for issue references)
262
+ * - BREAKING CHANGE: description
263
+ * - BREAKING-CHANGE: description
264
+ *
265
+ * @param line - The line to check
266
+ * @returns True if the line looks like a footer
267
+ */
268
+ function isFooterLine(line) {
269
+ if (!line)
270
+ return false;
271
+ const trimmed = line.trim();
272
+ if (trimmed === '')
273
+ return false;
274
+ // Check for BREAKING CHANGE or BREAKING-CHANGE
275
+ if (trimmed.startsWith('BREAKING CHANGE:') || trimmed.startsWith('BREAKING-CHANGE:')) {
276
+ return true;
277
+ }
278
+ // Check for token: value or token #value pattern
279
+ let pos = 0;
280
+ // Skip leading whitespace
281
+ while (pos < trimmed.length && trimmed[pos] === ' ') {
282
+ pos++;
283
+ }
284
+ // Read token (alphanumeric and hyphens)
285
+ const tokenStart = pos;
286
+ while (pos < trimmed.length) {
287
+ const char = trimmed[pos];
288
+ const code = char.charCodeAt(0);
289
+ if ((code >= 97 && code <= 122) || // a-z
290
+ (code >= 65 && code <= 90) || // A-Z
291
+ (code >= 48 && code <= 57) || // 0-9
292
+ code === 45 // -
293
+ ) {
294
+ pos++;
295
+ }
296
+ else {
297
+ break;
298
+ }
299
+ }
300
+ // Must have at least one character in token
301
+ if (pos === tokenStart)
302
+ return false;
303
+ // Must be followed by : or space-#
304
+ if (trimmed[pos] === ':') {
305
+ return true;
306
+ }
307
+ if (trimmed[pos] === ' ' && trimmed[pos + 1] === '#') {
308
+ return true;
309
+ }
310
+ return false;
311
+ }
312
+
313
+ /**
314
+ * Checks if a footer key indicates a breaking change.
315
+ *
316
+ * @param key - The footer key to check
317
+ * @returns True if the key indicates a breaking change
318
+ */
319
+ function isBreakingFooterKey(key) {
320
+ const normalized = hyphenToSpace(key.toUpperCase());
321
+ return normalized === 'BREAKING CHANGE';
322
+ }
323
+
324
+ /**
325
+ * Parses the footer section of a commit message.
326
+ *
327
+ * @param lines - All lines of the commit message
328
+ * @param startIndex - Index where footers start
329
+ * @returns Parsed footers
330
+ */
331
+ function parseFooters(lines, startIndex) {
332
+ const footers = [];
333
+ let breakingDescription;
334
+ let pos = startIndex;
335
+ // Skip blank lines
336
+ while (pos < lines.length && lines[pos].trim() === '') {
337
+ pos++;
338
+ }
339
+ while (pos < lines.length) {
340
+ const line = lines[pos];
341
+ const footer = parseFooterLine(line);
342
+ if (footer) {
343
+ footers.push(footer);
344
+ // Check for breaking change
345
+ if (isBreakingFooterKey(footer.key)) {
346
+ breakingDescription = footer.value;
347
+ // Breaking description may span multiple lines
348
+ pos++;
349
+ while (pos < lines.length && !isNewFooter(lines[pos])) {
350
+ const nextLine = lines[pos];
351
+ if (nextLine.trim() !== '') {
352
+ breakingDescription += '\n' + nextLine;
353
+ }
354
+ pos++;
355
+ }
356
+ continue;
357
+ }
358
+ }
359
+ pos++;
360
+ }
361
+ return { footers, breakingDescription };
362
+ }
363
+ /**
364
+ * Parses a single footer line.
365
+ *
366
+ * @param line - The line to parse
367
+ * @returns The parsed CommitFooter or null if not a valid footer
368
+ */
369
+ function parseFooterLine(line) {
370
+ if (!line)
371
+ return null;
372
+ const trimmed = line.trim();
373
+ if (trimmed === '')
374
+ return null;
375
+ // Check for BREAKING CHANGE or BREAKING-CHANGE
376
+ if (trimmed.startsWith('BREAKING CHANGE:')) {
377
+ return {
378
+ key: 'BREAKING CHANGE',
379
+ value: trimmed.slice(16).trim(),
380
+ separator: ':',
381
+ };
382
+ }
383
+ if (trimmed.startsWith('BREAKING-CHANGE:')) {
384
+ return {
385
+ key: 'BREAKING-CHANGE',
386
+ value: trimmed.slice(16).trim(),
387
+ separator: ':',
388
+ };
389
+ }
390
+ // Parse token: value or token #value
391
+ let pos = 0;
392
+ // Read token
393
+ const tokenStart = pos;
394
+ while (pos < trimmed.length) {
395
+ const char = trimmed[pos];
396
+ const code = char.charCodeAt(0);
397
+ if ((code >= 97 && code <= 122) || // a-z
398
+ (code >= 65 && code <= 90) || // A-Z
399
+ (code >= 48 && code <= 57) || // 0-9
400
+ code === 45 // -
401
+ ) {
402
+ pos++;
403
+ }
404
+ else {
405
+ break;
406
+ }
407
+ }
408
+ if (pos === tokenStart)
409
+ return null;
410
+ const key = trimmed.slice(tokenStart, pos);
411
+ // Check separator
412
+ let separator;
413
+ let valueStart;
414
+ if (trimmed[pos] === ':') {
415
+ separator = ':';
416
+ valueStart = pos + 1;
417
+ }
418
+ else if (trimmed[pos] === ' ' && trimmed[pos + 1] === '#') {
419
+ separator = ' #';
420
+ valueStart = pos + 2;
421
+ }
422
+ else {
423
+ return null;
424
+ }
425
+ // Skip whitespace after separator (for : case)
426
+ if (separator === ':') {
427
+ while (valueStart < trimmed.length && trimmed[valueStart] === ' ') {
428
+ valueStart++;
429
+ }
430
+ }
431
+ const value = trimmed.slice(valueStart);
432
+ return { key, value, separator };
433
+ }
434
+ /**
435
+ * Checks if a line starts a new footer.
436
+ *
437
+ * @param line - The line to check
438
+ * @returns True if the line starts a new footer
439
+ */
440
+ function isNewFooter(line) {
441
+ if (!line)
442
+ return false;
443
+ return parseFooterLine(line) !== null;
444
+ }
445
+
446
+ /**
447
+ * Parses a conventional commit header line.
448
+ *
449
+ * @param line - The first line of the commit message
450
+ * @returns Parsed header with type, scope, subject, and breaking flag
451
+ */
452
+ function parseHeader$1(line) {
453
+ let pos = 0;
454
+ const len = line.length;
455
+ // Extract type (alphanumeric characters until ( or : or !)
456
+ const typeStart = pos;
457
+ while (pos < len) {
458
+ const char = line[pos];
459
+ const code = char.charCodeAt(0);
460
+ // a-z, A-Z, 0-9
461
+ if ((code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57)) {
462
+ pos++;
463
+ }
464
+ else {
465
+ break;
466
+ }
467
+ }
468
+ const type = line.slice(typeStart, pos).toLowerCase();
469
+ // Check for scope in parentheses
470
+ let scope;
471
+ if (line[pos] === '(') {
472
+ pos++; // skip (
473
+ const scopeStart = pos;
474
+ while (pos < len && line[pos] !== ')') {
475
+ pos++;
476
+ }
477
+ scope = line.slice(scopeStart, pos);
478
+ if (line[pos] === ')') {
479
+ pos++; // skip )
480
+ }
481
+ }
482
+ // Check for breaking change indicator (!)
483
+ const breaking = line[pos] === '!';
484
+ if (breaking) {
485
+ pos++;
486
+ }
487
+ // Expect colon
488
+ if (line[pos] === ':') {
489
+ pos++;
490
+ }
491
+ // Skip whitespace after colon
492
+ while (pos < len && line[pos] === ' ') {
493
+ pos++;
494
+ }
495
+ // Rest is subject
496
+ const subject = line.slice(pos).trim();
497
+ return {
498
+ type,
499
+ scope,
500
+ subject,
501
+ breaking,
502
+ };
503
+ }
504
+
505
+ /**
506
+ * Maximum commit message length (10KB)
507
+ */
508
+ const MAX_MESSAGE_LENGTH = 10 * 1024;
509
+ /**
510
+ * Parses a conventional commit message.
511
+ *
512
+ * @param message - The complete commit message
513
+ * @returns Parsed ConventionalCommit object
514
+ * @throws {Error} If message exceeds maximum length
515
+ */
516
+ function parseConventionalCommit(message) {
517
+ if (message.length > MAX_MESSAGE_LENGTH) {
518
+ throw createError(`Commit message exceeds maximum length of ${MAX_MESSAGE_LENGTH} characters`);
519
+ }
520
+ const lines = splitLines(message);
521
+ if (lines.length === 0) {
522
+ return {
523
+ type: '',
524
+ subject: '',
525
+ footers: [],
526
+ breaking: false,
527
+ raw: message,
528
+ };
529
+ }
530
+ // Parse header (first line)
531
+ const header = parseHeader$1(lines[0]);
532
+ // Parse body and footers
533
+ let body;
534
+ let footersStartIndex = 1;
535
+ if (lines.length > 1) {
536
+ const bodyResult = parseBody(lines, 1);
537
+ if (bodyResult) {
538
+ body = bodyResult.body;
539
+ footersStartIndex = bodyResult.endIndex;
540
+ }
541
+ }
542
+ // Parse footers
543
+ const footersResult = parseFooters(lines, footersStartIndex);
544
+ const breakingDescriptionFromFooter = footersResult.breakingDescription;
545
+ // Determine breaking status
546
+ const breaking = header.breaking || footersResult.footers.some((f) => hyphenToSpace(f.key.toUpperCase()) === 'BREAKING CHANGE');
547
+ // Determine breaking description
548
+ let breakingDescription;
549
+ if (header.breaking && header.subject) {
550
+ // If breaking via !, the subject may describe the breaking change
551
+ breakingDescription = header.subject;
552
+ }
553
+ if (breakingDescriptionFromFooter) {
554
+ breakingDescription = breakingDescriptionFromFooter;
555
+ }
556
+ return {
557
+ type: header.type,
558
+ scope: header.scope,
559
+ subject: header.subject,
560
+ body,
561
+ footers: footersResult.footers,
562
+ breaking,
563
+ breakingDescription,
564
+ raw: message,
565
+ };
566
+ }
567
+ /**
568
+ * Splits a message into lines, handling different line endings.
569
+ *
570
+ * @param message - The message to split into lines
571
+ * @returns An array of lines from the message
572
+ */
573
+ function splitLines(message) {
574
+ const lines = [];
575
+ let currentLine = '';
576
+ let i = 0;
577
+ while (i < message.length) {
578
+ const char = message[i];
579
+ if (char === '\r') {
580
+ lines.push(currentLine);
581
+ currentLine = '';
582
+ // Skip \n if this is \r\n
583
+ if (message[i + 1] === '\n') {
584
+ i++;
585
+ }
586
+ }
587
+ else if (char === '\n') {
588
+ lines.push(currentLine);
589
+ currentLine = '';
590
+ }
591
+ else {
592
+ currentLine += char;
593
+ }
594
+ i++;
595
+ }
596
+ // Add final line if not empty
597
+ if (currentLine || message.endsWith('\n') || message.endsWith('\r')) {
598
+ lines.push(currentLine);
599
+ }
600
+ return lines;
601
+ }
602
+
603
+ const ANALYZE_COMMITS_STEP_ID = 'analyze-commits';
604
+ /**
605
+ * Creates the analyze-commits step.
606
+ *
607
+ * This step:
608
+ * 1. Finds the last release tag for this package
609
+ * 2. Gets all commits since that tag (or all commits if first release)
610
+ * 3. Parses each commit using conventional commit format
611
+ * 4. Filters to only release-worthy commits
612
+ *
613
+ * State updates:
614
+ * - lastReleaseTag: Tag name of last release (null if first release)
615
+ * - commits: Array of parsed conventional commits
616
+ *
617
+ * @returns A FlowStep that analyzes commits
618
+ */
619
+ function createAnalyzeCommitsStep() {
620
+ return createStep(ANALYZE_COMMITS_STEP_ID, 'Analyze Commits', async (ctx) => {
621
+ const { git, projectName, packageName, config, logger, state } = ctx;
622
+ // Find the last release tag for this package
623
+ let lastReleaseTag = null;
624
+ if (!state.isFirstRelease) {
625
+ // Try to find a tag matching the package name pattern
626
+ const tags = git.getTagsForPackage(packageName);
627
+ if (tags.length > 0) {
628
+ // Tags are returned in reverse chronological order
629
+ lastReleaseTag = tags[0].name;
630
+ logger.debug(`Found last release tag: ${lastReleaseTag}`);
631
+ }
632
+ else {
633
+ // Try with project name format
634
+ const projectTags = git.getTagsForPackage(projectName);
635
+ if (projectTags.length > 0) {
636
+ lastReleaseTag = projectTags[0].name;
637
+ logger.debug(`Found last release tag (project format): ${lastReleaseTag}`);
638
+ }
639
+ }
640
+ }
641
+ // Get commits
642
+ let rawCommits;
643
+ if (lastReleaseTag) {
644
+ rawCommits = git.getCommitsSince(lastReleaseTag);
645
+ logger.debug(`Found ${rawCommits.length} commits since ${lastReleaseTag}`);
646
+ }
647
+ else {
648
+ // First release - get all commits (limit to recent for performance)
649
+ rawCommits = git.getCommitLog({ maxCount: 100 });
650
+ logger.debug(`First release - analyzing up to ${rawCommits.length} commits`);
651
+ }
652
+ // Parse commits using conventional commit format
653
+ const commits = [];
654
+ const releaseTypes = config.releaseTypes ?? ['feat', 'fix', 'perf', 'revert'];
655
+ for (const rawCommit of rawCommits) {
656
+ const parsed = parseConventionalCommit(rawCommit.message);
657
+ if (parsed.type && releaseTypes.includes(parsed.type)) {
658
+ commits.push(parsed);
659
+ }
660
+ }
661
+ const message = commits.length > 0
662
+ ? `Found ${commits.length} releasable commits (${rawCommits.length} total)`
663
+ : `No releasable commits found (${rawCommits.length} total)`;
664
+ return {
665
+ status: 'success',
666
+ stateUpdates: {
667
+ lastReleaseTag,
668
+ commits,
669
+ },
670
+ message,
671
+ };
672
+ }, {
673
+ dependsOn: ['fetch-registry'],
674
+ });
675
+ }
676
+
677
+ /**
678
+ * Converts a SemVer to its canonical string representation.
679
+ *
680
+ * @param version - The version to format
681
+ * @returns The version string (e.g., "1.2.3-alpha.1+build.123")
682
+ */
683
+ function format(version) {
684
+ let result = `${version.major}.${version.minor}.${version.patch}`;
685
+ if (version.prerelease.length > 0) {
686
+ result += '-' + version.prerelease.join('.');
687
+ }
688
+ if (version.build.length > 0) {
689
+ result += '+' + version.build.join('.');
690
+ }
691
+ return result;
692
+ }
693
+
694
+ /**
695
+ * Safe copies of Number built-in methods and constants.
696
+ *
697
+ * These references are captured at module initialization time to protect against
698
+ * prototype pollution attacks. Import only what you need for tree-shaking.
699
+ *
700
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/number
701
+ */
702
+ // Capture references at module initialization time
703
+ const _parseInt = globalThis.parseInt;
704
+ const _isNaN = globalThis.isNaN;
705
+ // ============================================================================
706
+ // Parsing
707
+ // ============================================================================
708
+ /**
709
+ * (Safe copy) Parses a string and returns an integer.
710
+ */
711
+ const parseInt = _parseInt;
712
+ // ============================================================================
713
+ // Global Type Checking (legacy, less strict)
714
+ // ============================================================================
715
+ /**
716
+ * (Safe copy) Global isNaN function (coerces to number first, less strict than Number.isNaN).
717
+ */
718
+ const globalIsNaN = _isNaN;
719
+
720
+ /**
721
+ * Creates a new SemVer object.
722
+ *
723
+ * @param options - Version components
724
+ * @returns A new SemVer object
725
+ */
726
+ function createSemVer(options) {
727
+ return {
728
+ major: options.major,
729
+ minor: options.minor,
730
+ patch: options.patch,
731
+ prerelease: options.prerelease ?? [],
732
+ build: options.build ?? [],
733
+ raw: options.raw,
734
+ };
735
+ }
736
+
737
+ /**
738
+ * Increments a version based on the bump type.
739
+ *
740
+ * @param version - The version to increment
741
+ * @param type - The type of bump (major, minor, patch, etc.)
742
+ * @param prereleaseId - Optional prerelease identifier for prerelease bumps
743
+ * @returns A new incremented SemVer
744
+ *
745
+ * @example
746
+ * increment(parseVersion('1.2.3'), 'minor') // 1.3.0
747
+ * increment(parseVersion('1.2.3'), 'major') // 2.0.0
748
+ * increment(parseVersion('1.2.3'), 'prerelease', 'alpha') // 1.2.4-alpha.0
749
+ */
750
+ function increment(version, type, prereleaseId) {
751
+ switch (type) {
752
+ case 'major':
753
+ return createSemVer({
754
+ major: version.major + 1,
755
+ minor: 0,
756
+ patch: 0,
757
+ prerelease: [],
758
+ build: [],
759
+ });
760
+ case 'minor':
761
+ return createSemVer({
762
+ major: version.major,
763
+ minor: version.minor + 1,
764
+ patch: 0,
765
+ prerelease: [],
766
+ build: [],
767
+ });
768
+ case 'patch':
769
+ // If version has prerelease, just remove it (1.2.3-alpha -> 1.2.3)
770
+ if (version.prerelease.length > 0) {
771
+ return createSemVer({
772
+ major: version.major,
773
+ minor: version.minor,
774
+ patch: version.patch,
775
+ prerelease: [],
776
+ build: [],
777
+ });
778
+ }
779
+ return createSemVer({
780
+ major: version.major,
781
+ minor: version.minor,
782
+ patch: version.patch + 1,
783
+ prerelease: [],
784
+ build: [],
785
+ });
786
+ case 'premajor':
787
+ return createSemVer({
788
+ major: version.major + 1,
789
+ minor: 0,
790
+ patch: 0,
791
+ prerelease: ['alpha', '0'],
792
+ build: [],
793
+ });
794
+ case 'preminor':
795
+ return createSemVer({
796
+ major: version.major,
797
+ minor: version.minor + 1,
798
+ patch: 0,
799
+ prerelease: ['alpha', '0'],
800
+ build: [],
801
+ });
802
+ case 'prepatch':
803
+ return createSemVer({
804
+ major: version.major,
805
+ minor: version.minor,
806
+ patch: version.patch + 1,
807
+ prerelease: ['alpha', '0'],
808
+ build: [],
809
+ });
810
+ case 'prerelease':
811
+ return incrementPrerelease(version);
812
+ case 'none':
813
+ default:
814
+ return version;
815
+ }
816
+ }
817
+ /**
818
+ * Increments the prerelease portion of a version.
819
+ *
820
+ * @param version - The version to increment
821
+ * @param id - Optional prerelease identifier
822
+ * @returns A new version with incremented prerelease
823
+ */
824
+ function incrementPrerelease(version, id) {
825
+ const prerelease = [...version.prerelease];
826
+ if (prerelease.length === 0) {
827
+ // No existing prerelease - start at patch+1 with id.0
828
+ return createSemVer({
829
+ major: version.major,
830
+ minor: version.minor,
831
+ patch: version.patch + 1,
832
+ prerelease: ['alpha', '0'],
833
+ build: [],
834
+ });
835
+ }
836
+ // Check if the last identifier is numeric
837
+ const lastIdx = prerelease.length - 1;
838
+ const last = prerelease[lastIdx];
839
+ const lastNum = parseInt(last, 10);
840
+ if (!globalIsNaN(lastNum)) {
841
+ // Increment the numeric part
842
+ prerelease[lastIdx] = String(lastNum + 1);
843
+ }
844
+ else {
845
+ // Append .0
846
+ prerelease.push('0');
847
+ }
848
+ return createSemVer({
849
+ major: version.major,
850
+ minor: version.minor,
851
+ patch: version.patch,
852
+ prerelease,
853
+ build: [],
854
+ });
855
+ }
856
+
857
+ /**
858
+ * Maximum version string length to prevent memory exhaustion.
859
+ */
860
+ const MAX_VERSION_LENGTH = 256;
861
+ /**
862
+ * Parses a semantic version string.
863
+ *
864
+ * Accepts versions in the format: MAJOR.MINOR.PATCH[-prerelease][+build]
865
+ * Optional leading 'v' or '=' prefixes are stripped.
866
+ *
867
+ * @param input - The version string to parse
868
+ * @returns A ParseVersionResult with the parsed version or error
869
+ *
870
+ * @example
871
+ * parseVersion('1.2.3') // { success: true, version: { major: 1, minor: 2, patch: 3, ... } }
872
+ * parseVersion('v1.0.0-alpha.1+build.123') // { success: true, ... }
873
+ * parseVersion('invalid') // { success: false, error: '...' }
874
+ */
875
+ function parseVersion(input) {
876
+ // Input validation
877
+ if (!input || typeof input !== 'string') {
878
+ return { success: false, error: 'Version string is required' };
879
+ }
880
+ if (input.length > MAX_VERSION_LENGTH) {
881
+ return { success: false, error: `Version string exceeds maximum length of ${MAX_VERSION_LENGTH}` };
882
+ }
883
+ // Strip leading whitespace
884
+ let pos = 0;
885
+ while (pos < input.length && isWhitespace$1(input.charCodeAt(pos))) {
886
+ pos++;
887
+ }
888
+ // Strip trailing whitespace
889
+ let end = input.length;
890
+ while (end > pos && isWhitespace$1(input.charCodeAt(end - 1))) {
891
+ end--;
892
+ }
893
+ // Strip optional leading 'v' or '='
894
+ if (pos < end) {
895
+ const code = input.charCodeAt(pos);
896
+ if (code === 118 || code === 86) {
897
+ // 'v' or 'V'
898
+ pos++;
899
+ }
900
+ else if (code === 61) {
901
+ // '='
902
+ pos++;
903
+ }
904
+ }
905
+ // Parse major version
906
+ const majorResult = parseNumericIdentifier(input, pos, end);
907
+ if (!majorResult.success) {
908
+ return { success: false, error: majorResult.error ?? 'Invalid major version' };
909
+ }
910
+ pos = majorResult.endPos;
911
+ // Expect dot
912
+ if (pos >= end || input.charCodeAt(pos) !== 46) {
913
+ // '.'
914
+ return { success: false, error: 'Expected "." after major version' };
915
+ }
916
+ pos++;
917
+ // Parse minor version
918
+ const minorResult = parseNumericIdentifier(input, pos, end);
919
+ if (!minorResult.success) {
920
+ return { success: false, error: minorResult.error ?? 'Invalid minor version' };
921
+ }
922
+ pos = minorResult.endPos;
923
+ // Expect dot
924
+ if (pos >= end || input.charCodeAt(pos) !== 46) {
925
+ // '.'
926
+ return { success: false, error: 'Expected "." after minor version' };
927
+ }
928
+ pos++;
929
+ // Parse patch version
930
+ const patchResult = parseNumericIdentifier(input, pos, end);
931
+ if (!patchResult.success) {
932
+ return { success: false, error: patchResult.error ?? 'Invalid patch version' };
933
+ }
934
+ pos = patchResult.endPos;
935
+ // Parse optional prerelease
936
+ const prerelease = [];
937
+ if (pos < end && input.charCodeAt(pos) === 45) {
938
+ // '-'
939
+ pos++;
940
+ const prereleaseResult = parseIdentifiers(input, pos, end, [43]); // Stop at '+'
941
+ if (!prereleaseResult.success) {
942
+ return { success: false, error: prereleaseResult.error ?? 'Invalid prerelease' };
943
+ }
944
+ prerelease.push(...prereleaseResult.identifiers);
945
+ pos = prereleaseResult.endPos;
946
+ }
947
+ // Parse optional build metadata
948
+ const build = [];
949
+ if (pos < end && input.charCodeAt(pos) === 43) {
950
+ // '+'
951
+ pos++;
952
+ const buildResult = parseIdentifiers(input, pos, end, []);
953
+ if (!buildResult.success) {
954
+ return { success: false, error: buildResult.error ?? 'Invalid build metadata' };
955
+ }
956
+ build.push(...buildResult.identifiers);
957
+ pos = buildResult.endPos;
958
+ }
959
+ // Check for trailing characters
960
+ if (pos < end) {
961
+ return { success: false, error: `Unexpected character at position ${pos}: "${input[pos]}"` };
962
+ }
963
+ return {
964
+ success: true,
965
+ version: createSemVer({
966
+ major: majorResult.value,
967
+ minor: minorResult.value,
968
+ patch: patchResult.value,
969
+ prerelease,
970
+ build,
971
+ raw: input,
972
+ }),
973
+ };
974
+ }
975
+ /**
976
+ * Parses a numeric identifier (non-negative integer, no leading zeros except for "0").
977
+ *
978
+ * @param input - Input string to parse
979
+ * @param start - Start position in the input
980
+ * @param end - End position in the input
981
+ * @returns Numeric parsing result
982
+ */
983
+ function parseNumericIdentifier(input, start, end) {
984
+ if (start >= end) {
985
+ return { success: false, value: 0, endPos: start, error: 'Expected numeric identifier' };
986
+ }
987
+ let pos = start;
988
+ const firstCode = input.charCodeAt(pos);
989
+ // Must start with a digit
990
+ if (!isDigit(firstCode)) {
991
+ return { success: false, value: 0, endPos: pos, error: 'Expected digit' };
992
+ }
993
+ // Check for leading zero (only "0" is valid, not "01", "007", etc.)
994
+ if (firstCode === 48 && pos + 1 < end && isDigit(input.charCodeAt(pos + 1))) {
995
+ return { success: false, value: 0, endPos: pos, error: 'Numeric identifier cannot have leading zeros' };
996
+ }
997
+ // Consume digits
998
+ let value = 0;
999
+ while (pos < end && isDigit(input.charCodeAt(pos))) {
1000
+ value = value * 10 + (input.charCodeAt(pos) - 48);
1001
+ pos++;
1002
+ // Prevent overflow
1003
+ if (value > Number.MAX_SAFE_INTEGER) {
1004
+ return { success: false, value: 0, endPos: pos, error: 'Numeric identifier is too large' };
1005
+ }
1006
+ }
1007
+ return { success: true, value, endPos: pos };
1008
+ }
1009
+ /**
1010
+ * Parses dot-separated identifiers (for prerelease/build).
1011
+ *
1012
+ * @param input - Input string to parse
1013
+ * @param start - Start position in the input
1014
+ * @param end - End position in the input
1015
+ * @param stopCodes - Character codes that signal end of identifiers
1016
+ * @returns Identifiers parsing result
1017
+ */
1018
+ function parseIdentifiers(input, start, end, stopCodes) {
1019
+ const identifiers = [];
1020
+ let pos = start;
1021
+ while (pos < end) {
1022
+ // Check for stop characters
1023
+ if (stopCodes.includes(input.charCodeAt(pos))) {
1024
+ break;
1025
+ }
1026
+ // Parse one identifier
1027
+ const identStart = pos;
1028
+ while (pos < end) {
1029
+ const code = input.charCodeAt(pos);
1030
+ // Stop at dot or stop characters
1031
+ if (code === 46 || stopCodes.includes(code)) {
1032
+ break;
1033
+ }
1034
+ // Must be alphanumeric or hyphen
1035
+ if (!isAlphanumeric(code) && code !== 45) {
1036
+ return { success: false, identifiers: [], endPos: pos, error: `Invalid character in identifier: "${input[pos]}"` };
1037
+ }
1038
+ pos++;
1039
+ }
1040
+ // Empty identifier is not allowed
1041
+ if (pos === identStart) {
1042
+ return { success: false, identifiers: [], endPos: pos, error: 'Empty identifier' };
1043
+ }
1044
+ identifiers.push(input.slice(identStart, pos));
1045
+ // Consume dot separator
1046
+ if (pos < end && input.charCodeAt(pos) === 46) {
1047
+ pos++;
1048
+ // Dot at end is invalid
1049
+ if (pos >= end || stopCodes.includes(input.charCodeAt(pos))) {
1050
+ return { success: false, identifiers: [], endPos: pos, error: 'Identifier expected after dot' };
1051
+ }
1052
+ }
1053
+ }
1054
+ return { success: true, identifiers, endPos: pos };
1055
+ }
1056
+ /**
1057
+ * Checks if a character code is a digit (0-9).
1058
+ *
1059
+ * @param code - Character code to check
1060
+ * @returns True if the code represents a digit
1061
+ */
1062
+ function isDigit(code) {
1063
+ return code >= 48 && code <= 57;
1064
+ }
1065
+ /**
1066
+ * Checks if a character code is alphanumeric or hyphen.
1067
+ *
1068
+ * @param code - Character code to check
1069
+ * @returns True if the code represents an alphanumeric character
1070
+ */
1071
+ function isAlphanumeric(code) {
1072
+ return ((code >= 48 && code <= 57) || // 0-9
1073
+ (code >= 65 && code <= 90) || // A-Z
1074
+ (code >= 97 && code <= 122) // a-z
1075
+ );
1076
+ }
1077
+ /**
1078
+ * Checks if a character code is whitespace.
1079
+ *
1080
+ * @param code - Character code to check
1081
+ * @returns True if the code represents whitespace
1082
+ */
1083
+ function isWhitespace$1(code) {
1084
+ return code === 32 || code === 9 || code === 10 || code === 13;
1085
+ }
1086
+
1087
+ const CALCULATE_BUMP_STEP_ID = 'calculate-bump';
1088
+ /**
1089
+ * Determines the highest bump type from analyzed commits.
1090
+ *
1091
+ * Priority: major > minor > patch > none
1092
+ * Breaking changes always result in major (post-1.0.0) or minor (pre-1.0.0).
1093
+ *
1094
+ * @param commits - Parsed conventional commits
1095
+ * @param minorTypes - Types that trigger minor bumps
1096
+ * @param patchTypes - Types that trigger patch bumps
1097
+ * @param currentVersion - Current version string (for pre-1.0.0 handling)
1098
+ * @returns The highest bump type needed
1099
+ */
1100
+ function calculateBumpFromCommits(commits, minorTypes, patchTypes, currentVersion) {
1101
+ if (commits.length === 0) {
1102
+ return 'none';
1103
+ }
1104
+ // Check for breaking changes
1105
+ const hasBreaking = commits.some((c) => c.breaking);
1106
+ if (hasBreaking) {
1107
+ // Pre-1.0.0: breaking changes are minor, not major
1108
+ const parsed = parseVersion(currentVersion);
1109
+ if (parsed.success && parsed.version && parsed.version.major === 0) {
1110
+ return 'minor';
1111
+ }
1112
+ return 'major';
1113
+ }
1114
+ // Check for minor-triggering types
1115
+ const hasMinor = commits.some((c) => c.type && minorTypes.includes(c.type));
1116
+ if (hasMinor) {
1117
+ return 'minor';
1118
+ }
1119
+ // Check for patch-triggering types
1120
+ const hasPatch = commits.some((c) => c.type && patchTypes.includes(c.type));
1121
+ if (hasPatch) {
1122
+ return 'patch';
1123
+ }
1124
+ return 'none';
1125
+ }
1126
+ /**
1127
+ * Creates the calculate-bump step.
1128
+ *
1129
+ * This step:
1130
+ * 1. Analyzes commit types and breaking changes
1131
+ * 2. Determines the appropriate bump level
1132
+ * 3. Calculates the next version
1133
+ *
1134
+ * State updates:
1135
+ * - bumpType: 'major' | 'minor' | 'patch' | 'none'
1136
+ * - nextVersion: Calculated next version string
1137
+ *
1138
+ * @returns A FlowStep that calculates version bump
1139
+ */
1140
+ function createCalculateBumpStep() {
1141
+ return createStep(CALCULATE_BUMP_STEP_ID, 'Calculate Version Bump', async (ctx) => {
1142
+ const { config, state, logger } = ctx;
1143
+ const { commits, currentVersion, isFirstRelease } = state;
1144
+ // Handle first release
1145
+ if (isFirstRelease) {
1146
+ const firstVersion = config.firstReleaseVersion ?? '0.1.0';
1147
+ logger.info(`First release: using version ${firstVersion}`);
1148
+ return {
1149
+ status: 'success',
1150
+ stateUpdates: {
1151
+ bumpType: 'minor',
1152
+ nextVersion: firstVersion,
1153
+ },
1154
+ message: `First release: ${firstVersion}`,
1155
+ };
1156
+ }
1157
+ // Check for forced bump type (releaseAs)
1158
+ if (config.releaseAs) {
1159
+ const forcedBumpType = config.releaseAs;
1160
+ const current = parseVersion(currentVersion ?? '0.0.0');
1161
+ if (!current.success || !current.version) {
1162
+ return {
1163
+ status: 'failed',
1164
+ error: createError(`Invalid current version: ${currentVersion}`),
1165
+ message: `Could not parse current version: ${currentVersion}`,
1166
+ };
1167
+ }
1168
+ const next = increment(current.version, forcedBumpType);
1169
+ const nextVersion = format(next);
1170
+ logger.info(`Forced ${forcedBumpType} bump via releaseAs: ${currentVersion} → ${nextVersion}`);
1171
+ return {
1172
+ status: 'success',
1173
+ stateUpdates: {
1174
+ bumpType: forcedBumpType,
1175
+ nextVersion,
1176
+ },
1177
+ message: `${forcedBumpType} bump (forced): ${currentVersion} → ${nextVersion}`,
1178
+ };
1179
+ }
1180
+ // No commits = no bump needed
1181
+ if (!commits || commits.length === 0) {
1182
+ return createSkippedResult('No releasable commits found');
1183
+ }
1184
+ const minorTypes = config.minorTypes ?? ['feat'];
1185
+ const patchTypes = config.patchTypes ?? ['fix', 'perf', 'revert'];
1186
+ const bumpType = calculateBumpFromCommits(commits, minorTypes, patchTypes, currentVersion ?? '0.0.0');
1187
+ if (bumpType === 'none') {
1188
+ return {
1189
+ status: 'success',
1190
+ stateUpdates: {
1191
+ bumpType: 'none',
1192
+ nextVersion: undefined,
1193
+ },
1194
+ message: 'No version bump needed',
1195
+ };
1196
+ }
1197
+ // Calculate next version
1198
+ const current = parseVersion(currentVersion ?? '0.0.0');
1199
+ if (!current.success || !current.version) {
1200
+ return {
1201
+ status: 'failed',
1202
+ error: createError(`Invalid current version: ${currentVersion}`),
1203
+ message: `Could not parse current version: ${currentVersion}`,
1204
+ };
1205
+ }
1206
+ const next = increment(current.version, bumpType);
1207
+ const nextVersion = format(next);
1208
+ return {
1209
+ status: 'success',
1210
+ stateUpdates: {
1211
+ bumpType,
1212
+ nextVersion,
1213
+ },
1214
+ message: `${bumpType} bump: ${currentVersion} → ${nextVersion}`,
1215
+ };
1216
+ }, {
1217
+ dependsOn: ['analyze-commits'],
1218
+ });
1219
+ }
1220
+ /**
1221
+ * Creates a step that checks for idempotency.
1222
+ *
1223
+ * This step prevents redundant releases by checking if the
1224
+ * calculated version is already published.
1225
+ *
1226
+ * @returns A FlowStep that checks idempotency
1227
+ */
1228
+ function createCheckIdempotencyStep() {
1229
+ return createStep('check-idempotency', 'Check Idempotency', async (ctx) => {
1230
+ const { registry, packageName, state } = ctx;
1231
+ const { nextVersion, bumpType } = state;
1232
+ // No bump = nothing to check
1233
+ if (!nextVersion || bumpType === 'none') {
1234
+ return {
1235
+ status: 'success',
1236
+ message: 'No version to check (no bump needed)',
1237
+ };
1238
+ }
1239
+ // Check if version is already published
1240
+ const isPublished = await registry.isVersionPublished(packageName, nextVersion);
1241
+ if (isPublished) {
1242
+ return {
1243
+ status: 'skipped',
1244
+ stateUpdates: {
1245
+ bumpType: 'none',
1246
+ nextVersion: undefined,
1247
+ },
1248
+ message: `Version ${nextVersion} is already published`,
1249
+ };
1250
+ }
1251
+ return {
1252
+ status: 'success',
1253
+ message: `Version ${nextVersion} is not yet published`,
1254
+ };
1255
+ }, {
1256
+ dependsOn: ['calculate-bump'],
1257
+ });
1258
+ }
1259
+
1260
+ /**
1261
+ * Safe copies of Date built-in via factory function and static methods.
1262
+ *
1263
+ * Since constructors cannot be safely captured via Object.assign, this module
1264
+ * provides a factory function that uses Reflect.construct internally.
1265
+ *
1266
+ * These references are captured at module initialization time to protect against
1267
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1268
+ *
1269
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/date
1270
+ */
1271
+ // Capture references at module initialization time
1272
+ const _Date = globalThis.Date;
1273
+ const _Reflect$1 = globalThis.Reflect;
1274
+ function createDate(...args) {
1275
+ return _Reflect$1.construct(_Date, args);
1276
+ }
1277
+
1278
+ /**
1279
+ * Creates a new changelog item.
1280
+ *
1281
+ * @param description - The description text of the change
1282
+ * @param options - Optional configuration for scope, commits, references, and breaking flag
1283
+ * @returns A new ChangelogItem object
1284
+ */
1285
+ function createChangelogItem(description, options) {
1286
+ return {
1287
+ description,
1288
+ scope: options?.scope,
1289
+ commits: options?.commits ?? [],
1290
+ references: options?.references ?? [],
1291
+ breaking: options?.breaking ?? false,
1292
+ };
1293
+ }
1294
+ /**
1295
+ * Creates a new changelog section.
1296
+ *
1297
+ * @param type - The type of section (features, fixes, breaking, etc.)
1298
+ * @param heading - The display heading for the section
1299
+ * @param items - Optional array of changelog items in this section
1300
+ * @returns A new ChangelogSection object
1301
+ */
1302
+ function createChangelogSection(type, heading, items = []) {
1303
+ return {
1304
+ type,
1305
+ heading,
1306
+ items,
1307
+ };
1308
+ }
1309
+ /**
1310
+ * Creates a new changelog entry.
1311
+ *
1312
+ * @param version - The version string (e.g., '1.0.0')
1313
+ * @param options - Optional configuration for date, sections, and other properties
1314
+ * @returns A new ChangelogEntry object
1315
+ */
1316
+ function createChangelogEntry(version, options) {
1317
+ return {
1318
+ version,
1319
+ date: options?.date ?? null,
1320
+ unreleased: options?.unreleased ?? false,
1321
+ compareUrl: options?.compareUrl,
1322
+ sections: options?.sections ?? [],
1323
+ rawContent: options?.rawContent,
1324
+ };
1325
+ }
1326
+
1327
+ /**
1328
+ * Maps section headings to their canonical types.
1329
+ * Used during parsing to normalize different heading styles.
1330
+ */
1331
+ const SECTION_TYPE_MAP = {
1332
+ // Breaking changes
1333
+ 'breaking changes': 'breaking',
1334
+ breaking: 'breaking',
1335
+ 'breaking change': 'breaking',
1336
+ // Features
1337
+ features: 'features',
1338
+ feature: 'features',
1339
+ added: 'features',
1340
+ new: 'features',
1341
+ // Fixes
1342
+ fixes: 'fixes',
1343
+ fix: 'fixes',
1344
+ 'bug fixes': 'fixes',
1345
+ bugfixes: 'fixes',
1346
+ fixed: 'fixes',
1347
+ // Performance
1348
+ performance: 'performance',
1349
+ 'performance improvements': 'performance',
1350
+ perf: 'performance',
1351
+ // Documentation
1352
+ documentation: 'documentation',
1353
+ docs: 'documentation',
1354
+ // Deprecations
1355
+ deprecations: 'deprecations',
1356
+ deprecated: 'deprecations',
1357
+ // Refactoring
1358
+ refactoring: 'refactoring',
1359
+ refactor: 'refactoring',
1360
+ 'code refactoring': 'refactoring',
1361
+ // Tests
1362
+ tests: 'tests',
1363
+ test: 'tests',
1364
+ testing: 'tests',
1365
+ // Build
1366
+ build: 'build',
1367
+ 'build system': 'build',
1368
+ dependencies: 'build',
1369
+ // CI
1370
+ ci: 'ci',
1371
+ 'continuous integration': 'ci',
1372
+ // Chores
1373
+ chores: 'chores',
1374
+ chore: 'chores',
1375
+ maintenance: 'chores',
1376
+ // Other
1377
+ other: 'other',
1378
+ miscellaneous: 'other',
1379
+ misc: 'other',
1380
+ changed: 'other',
1381
+ changes: 'other',
1382
+ removed: 'other',
1383
+ security: 'other',
1384
+ };
1385
+ /**
1386
+ * Standard section headings for serialization.
1387
+ * Maps section types to their preferred heading text.
1388
+ */
1389
+ const SECTION_HEADINGS = {
1390
+ breaking: 'Breaking Changes',
1391
+ features: 'Features',
1392
+ fixes: 'Bug Fixes',
1393
+ performance: 'Performance',
1394
+ documentation: 'Documentation',
1395
+ deprecations: 'Deprecations',
1396
+ refactoring: 'Refactoring',
1397
+ tests: 'Tests',
1398
+ build: 'Build',
1399
+ ci: 'CI',
1400
+ chores: 'Chores',
1401
+ other: 'Other',
1402
+ };
1403
+ /**
1404
+ * Determines the section type from a heading string.
1405
+ * Returns 'other' if the heading is not recognized.
1406
+ *
1407
+ * @param heading - The heading string to parse
1408
+ * @returns The corresponding ChangelogSectionType
1409
+ */
1410
+ function getSectionType(heading) {
1411
+ const normalized = heading.toLowerCase().trim();
1412
+ return SECTION_TYPE_MAP[normalized] ?? 'other';
1413
+ }
1414
+
1415
+ /**
1416
+ * Safe copies of Map built-in via factory function.
1417
+ *
1418
+ * Since constructors cannot be safely captured via Object.assign, this module
1419
+ * provides a factory function that uses Reflect.construct internally.
1420
+ *
1421
+ * These references are captured at module initialization time to protect against
1422
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1423
+ *
1424
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/map
1425
+ */
1426
+ // Capture references at module initialization time
1427
+ const _Map = globalThis.Map;
1428
+ const _Reflect = globalThis.Reflect;
1429
+ /**
1430
+ * (Safe copy) Creates a new Map using the captured Map constructor.
1431
+ * Use this instead of `new Map()`.
1432
+ *
1433
+ * @param iterable - Optional iterable of key-value pairs.
1434
+ * @returns A new Map instance.
1435
+ */
1436
+ const createMap = (iterable) => _Reflect.construct(_Map, iterable ? [iterable] : []);
1437
+
1438
+ /**
1439
+ * Safe copies of Object built-in methods.
1440
+ *
1441
+ * These references are captured at module initialization time to protect against
1442
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1443
+ *
1444
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/object
1445
+ */
1446
+ // Capture references at module initialization time
1447
+ const _Object = globalThis.Object;
1448
+ /**
1449
+ * (Safe copy) Returns an array of key/values of the enumerable own properties of an object.
1450
+ */
1451
+ const entries = _Object.entries;
1452
+
1453
+ /**
1454
+ * Safe copies of URL built-ins via factory functions.
1455
+ *
1456
+ * Provides safe references to URL and URLSearchParams.
1457
+ * These references are captured at module initialization time to protect against
1458
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1459
+ *
1460
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/url
1461
+ */
1462
+ // Capture references at module initialization time
1463
+ const _URL = globalThis.URL;
1464
+ /**
1465
+ * (Safe copy) Creates an object URL for the given object.
1466
+ * Use this instead of `URL.createObjectURL()`.
1467
+ *
1468
+ * Note: This is a browser-only API. In Node.js environments, this will throw.
1469
+ */
1470
+ typeof _URL.createObjectURL === 'function'
1471
+ ? _URL.createObjectURL.bind(_URL)
1472
+ : () => {
1473
+ throw new Error('URL.createObjectURL is not available in this environment');
1474
+ };
1475
+ /**
1476
+ * (Safe copy) Revokes an object URL previously created with createObjectURL.
1477
+ * Use this instead of `URL.revokeObjectURL()`.
1478
+ *
1479
+ * Note: This is a browser-only API. In Node.js environments, this will throw.
1480
+ */
1481
+ typeof _URL.revokeObjectURL === 'function'
1482
+ ? _URL.revokeObjectURL.bind(_URL)
1483
+ : () => {
1484
+ throw new Error('URL.revokeObjectURL is not available in this environment');
1485
+ };
1486
+
1487
+ /**
1488
+ * Safe copies of Math built-in methods.
1489
+ *
1490
+ * These references are captured at module initialization time to protect against
1491
+ * prototype pollution attacks. Import only what you need for tree-shaking.
1492
+ *
1493
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/math
1494
+ */
1495
+ // Capture references at module initialization time
1496
+ const _Math = globalThis.Math;
1497
+ // ============================================================================
1498
+ // Min/Max
1499
+ // ============================================================================
1500
+ /**
1501
+ * (Safe copy) Returns the larger of zero or more numbers.
1502
+ */
1503
+ const max = _Math.max;
1504
+
1505
+ /**
1506
+ * Line Parser
1507
+ *
1508
+ * Utilities for parsing individual changelog lines without regex.
1509
+ */
1510
+ /**
1511
+ * Parses a version string from a heading.
1512
+ * Examples: "1.2.3", "v1.2.3", "[1.2.3]", "1.2.3 - 2024-01-01"
1513
+ *
1514
+ * @param heading - The heading string to parse
1515
+ * @returns An object containing the parsed version, date, and optional compareUrl
1516
+ */
1517
+ function parseVersionFromHeading(heading) {
1518
+ const trimmed = heading.trim();
1519
+ // Check for unreleased
1520
+ const lowerHeading = trimmed.toLowerCase();
1521
+ if (lowerHeading === 'unreleased' || lowerHeading === '[unreleased]') {
1522
+ return { version: 'Unreleased', date: null };
1523
+ }
1524
+ let pos = 0;
1525
+ let version = '';
1526
+ let date = null;
1527
+ let compareUrl;
1528
+ // Skip leading [ if present
1529
+ if (trimmed[pos] === '[') {
1530
+ pos++;
1531
+ }
1532
+ // Skip leading 'v' if present
1533
+ if (trimmed[pos] === 'v' || trimmed[pos] === 'V') {
1534
+ pos++;
1535
+ }
1536
+ // Parse version number (digits and dots)
1537
+ const versionStart = pos;
1538
+ while (pos < trimmed.length) {
1539
+ const char = trimmed[pos];
1540
+ const code = char.charCodeAt(0);
1541
+ // Allow digits, dots, hyphens (for prerelease), plus signs
1542
+ if ((code >= 48 && code <= 57) || // 0-9
1543
+ char === '.' ||
1544
+ char === '-' ||
1545
+ char === '+' ||
1546
+ (code >= 97 && code <= 122) || // a-z (for prerelease tags like alpha, beta, rc)
1547
+ (code >= 65 && code <= 90) // A-Z
1548
+ ) {
1549
+ pos++;
1550
+ }
1551
+ else {
1552
+ break;
1553
+ }
1554
+ }
1555
+ version = trimmed.slice(versionStart, pos);
1556
+ // Skip trailing ] if present
1557
+ if (trimmed[pos] === ']') {
1558
+ pos++;
1559
+ }
1560
+ // Skip whitespace and separator
1561
+ while (pos < trimmed.length && (trimmed[pos] === ' ' || trimmed[pos] === '-' || trimmed[pos] === '–')) {
1562
+ pos++;
1563
+ }
1564
+ // Try to parse date (YYYY-MM-DD format)
1565
+ if (pos < trimmed.length) {
1566
+ const dateMatch = extractDate(trimmed.slice(pos));
1567
+ if (dateMatch) {
1568
+ date = dateMatch.date;
1569
+ pos += dateMatch.length;
1570
+ }
1571
+ }
1572
+ // Skip to check for compare URL (in parentheses or link)
1573
+ while (pos < trimmed.length && trimmed[pos] === ' ') {
1574
+ pos++;
1575
+ }
1576
+ // Check for link at end: [compare](url)
1577
+ if (pos < trimmed.length) {
1578
+ const linkMatch = extractLink(trimmed.slice(pos));
1579
+ if (linkMatch?.url) {
1580
+ compareUrl = linkMatch.url;
1581
+ }
1582
+ }
1583
+ return { version, date, compareUrl };
1584
+ }
1585
+ /**
1586
+ * Extracts a date in YYYY-MM-DD format from a string.
1587
+ *
1588
+ * @param str - The string to extract a date from
1589
+ * @returns The extracted date and its length, or null if no date found
1590
+ */
1591
+ function extractDate(str) {
1592
+ let pos = 0;
1593
+ // Skip optional parentheses
1594
+ if (str[pos] === '(')
1595
+ pos++;
1596
+ // Parse year (4 digits)
1597
+ const yearStart = pos;
1598
+ while (pos < str.length && pos - yearStart < 4) {
1599
+ const code = str.charCodeAt(pos);
1600
+ if (code >= 48 && code <= 57) {
1601
+ pos++;
1602
+ }
1603
+ else {
1604
+ break;
1605
+ }
1606
+ }
1607
+ if (pos - yearStart !== 4)
1608
+ return null;
1609
+ // Expect - or /
1610
+ if (str[pos] !== '-' && str[pos] !== '/')
1611
+ return null;
1612
+ const separator = str[pos];
1613
+ pos++;
1614
+ // Parse month (2 digits)
1615
+ const monthStart = pos;
1616
+ while (pos < str.length && pos - monthStart < 2) {
1617
+ const code = str.charCodeAt(pos);
1618
+ if (code >= 48 && code <= 57) {
1619
+ pos++;
1620
+ }
1621
+ else {
1622
+ break;
1623
+ }
1624
+ }
1625
+ if (pos - monthStart !== 2)
1626
+ return null;
1627
+ // Expect same separator
1628
+ if (str[pos] !== separator)
1629
+ return null;
1630
+ pos++;
1631
+ // Parse day (2 digits)
1632
+ const dayStart = pos;
1633
+ while (pos < str.length && pos - dayStart < 2) {
1634
+ const code = str.charCodeAt(pos);
1635
+ if (code >= 48 && code <= 57) {
1636
+ pos++;
1637
+ }
1638
+ else {
1639
+ break;
1640
+ }
1641
+ }
1642
+ if (pos - dayStart !== 2)
1643
+ return null;
1644
+ // Skip optional closing parenthesis
1645
+ if (str[pos] === ')')
1646
+ pos++;
1647
+ const dateStr = str.slice(yearStart, dayStart + 2);
1648
+ const date = slashToHyphen(dateStr);
1649
+ return { date, length: pos };
1650
+ }
1651
+ /**
1652
+ * Replaces forward slashes with hyphens (ReDoS-safe).
1653
+ *
1654
+ * @param input - The input string
1655
+ * @returns String with forward slashes replaced by hyphens
1656
+ */
1657
+ function slashToHyphen(input) {
1658
+ const result = [];
1659
+ for (let i = 0; i < input.length; i++) {
1660
+ result.push(input[i] === '/' ? '-' : input[i]);
1661
+ }
1662
+ return result.join('');
1663
+ }
1664
+ /**
1665
+ * Extracts a markdown link from a string.
1666
+ *
1667
+ * @param str - The string to extract a link from
1668
+ * @returns The extracted link text, url, and length, or null if no link found
1669
+ */
1670
+ function extractLink(str) {
1671
+ if (str[0] !== '[')
1672
+ return null;
1673
+ let pos = 1;
1674
+ let depth = 1;
1675
+ // Find closing ]
1676
+ while (pos < str.length && depth > 0) {
1677
+ if (str[pos] === '[')
1678
+ depth++;
1679
+ else if (str[pos] === ']')
1680
+ depth--;
1681
+ pos++;
1682
+ }
1683
+ if (depth !== 0)
1684
+ return null;
1685
+ const text = str.slice(1, pos - 1);
1686
+ // Expect (
1687
+ if (str[pos] !== '(')
1688
+ return null;
1689
+ pos++;
1690
+ const urlStart = pos;
1691
+ depth = 1;
1692
+ while (pos < str.length && depth > 0) {
1693
+ if (str[pos] === '(')
1694
+ depth++;
1695
+ else if (str[pos] === ')')
1696
+ depth--;
1697
+ pos++;
1698
+ }
1699
+ if (depth !== 0)
1700
+ return null;
1701
+ const url = str.slice(urlStart, pos - 1);
1702
+ return { text, url, length: pos };
1703
+ }
1704
+ /**
1705
+ * Parses commit references from a line.
1706
+ * Examples: (abc1234), [abc1234], commit abc1234
1707
+ *
1708
+ * @param text - The text to parse for commit references
1709
+ * @param baseUrl - Optional base URL for constructing commit links
1710
+ * @returns An array of parsed CommitRef objects
1711
+ */
1712
+ function parseCommitRefs(text, baseUrl) {
1713
+ const refs = [];
1714
+ let pos = 0;
1715
+ while (pos < text.length) {
1716
+ // Look for potential hash patterns
1717
+ // Common formats: (abc1234), [abc1234], abc1234fabcdef
1718
+ // Check for parenthetical hash
1719
+ if (text[pos] === '(' || text[pos] === '[') {
1720
+ const closeChar = text[pos] === '(' ? ')' : ']';
1721
+ const start = pos + 1;
1722
+ pos++;
1723
+ // Read potential hash
1724
+ while (pos < text.length && isHexDigit(text[pos])) {
1725
+ pos++;
1726
+ }
1727
+ // Check if valid hash (7-40 hex chars)
1728
+ const hash = text.slice(start, pos);
1729
+ if (hash.length >= 7 && hash.length <= 40 && text[pos] === closeChar) {
1730
+ refs.push({
1731
+ hash,
1732
+ shortHash: hash.slice(0, 7),
1733
+ url: baseUrl ? `${baseUrl}/commit/${hash}` : undefined,
1734
+ });
1735
+ pos++; // skip closing bracket
1736
+ continue;
1737
+ }
1738
+ }
1739
+ pos++;
1740
+ }
1741
+ return refs;
1742
+ }
1743
+ /**
1744
+ * Parses issue/PR references from a line.
1745
+ * Examples: #123, GH-123, closes #123
1746
+ *
1747
+ * @param text - The text to parse for issue references
1748
+ * @param baseUrl - Optional base URL for constructing issue links
1749
+ * @returns An array of parsed IssueRef objects
1750
+ */
1751
+ function parseIssueRefs(text, baseUrl) {
1752
+ const refs = [];
1753
+ let pos = 0;
1754
+ while (pos < text.length) {
1755
+ // Look for # followed by digits
1756
+ if (text[pos] === '#') {
1757
+ pos++;
1758
+ const numStart = pos;
1759
+ while (pos < text.length && isDigitChar(text[pos])) {
1760
+ pos++;
1761
+ }
1762
+ if (pos > numStart) {
1763
+ const number = parseInt(text.slice(numStart, pos), 10);
1764
+ // Check context for PR vs issue
1765
+ const beforeHash = text.slice(max(0, numStart - 10), numStart - 1).toLowerCase();
1766
+ const type = beforeHash.includes('pr') || beforeHash.includes('pull') ? 'pull-request' : 'issue';
1767
+ refs.push({
1768
+ number,
1769
+ type,
1770
+ url: baseUrl ? `${baseUrl}/issues/${number}` : undefined,
1771
+ });
1772
+ continue;
1773
+ }
1774
+ }
1775
+ pos++;
1776
+ }
1777
+ return refs;
1778
+ }
1779
+ /**
1780
+ * Parses the scope from a changelog item.
1781
+ * Example: "**scope:** description" -> { scope: "scope", description: "description" }
1782
+ *
1783
+ * @param text - The text to parse for scope
1784
+ * @returns An object with optional scope and the description
1785
+ */
1786
+ function parseScopeFromItem(text) {
1787
+ const trimmed = text.trim();
1788
+ // Check for **scope:** pattern (colon inside or outside bold)
1789
+ if (trimmed.startsWith('**')) {
1790
+ let pos = 2;
1791
+ const scopeStart = pos;
1792
+ // Read until ** or :
1793
+ while (pos < trimmed.length && trimmed[pos] !== '*' && trimmed[pos] !== ':') {
1794
+ pos++;
1795
+ }
1796
+ // Handle **scope:** pattern (colon before closing **)
1797
+ if (trimmed[pos] === ':' && trimmed[pos + 1] === '*' && trimmed[pos + 2] === '*') {
1798
+ const scope = trimmed.slice(scopeStart, pos);
1799
+ pos += 3; // skip :**
1800
+ // Skip whitespace
1801
+ while (trimmed[pos] === ' ')
1802
+ pos++;
1803
+ return { scope, description: trimmed.slice(pos) };
1804
+ }
1805
+ // Handle **scope**: pattern (colon after closing **)
1806
+ if (trimmed[pos] === '*' && trimmed[pos + 1] === '*') {
1807
+ const scope = trimmed.slice(scopeStart, pos);
1808
+ pos += 2; // skip **
1809
+ // Skip : if present
1810
+ if (trimmed[pos] === ':')
1811
+ pos++;
1812
+ // Skip whitespace
1813
+ while (trimmed[pos] === ' ')
1814
+ pos++;
1815
+ return { scope, description: trimmed.slice(pos) };
1816
+ }
1817
+ }
1818
+ // Check for scope: pattern (without bold)
1819
+ const colonPos = trimmed.indexOf(':');
1820
+ if (colonPos > 0 && colonPos < 30) {
1821
+ // scope shouldn't be too long
1822
+ const potentialScope = trimmed.slice(0, colonPos);
1823
+ // Scope should be a simple identifier (letters, numbers, hyphens)
1824
+ if (isValidScope(potentialScope)) {
1825
+ const description = trimmed.slice(colonPos + 1).trim();
1826
+ return { scope: potentialScope, description };
1827
+ }
1828
+ }
1829
+ return { description: trimmed };
1830
+ }
1831
+ /**
1832
+ * Checks if a string is a valid scope (alphanumeric with hyphens).
1833
+ *
1834
+ * @param str - The string to check
1835
+ * @returns True if the string is a valid scope identifier
1836
+ */
1837
+ function isValidScope(str) {
1838
+ if (!str || str.length === 0)
1839
+ return false;
1840
+ for (let i = 0; i < str.length; i++) {
1841
+ const code = str.charCodeAt(i);
1842
+ if (!(code >= 48 && code <= 57) && // 0-9
1843
+ !(code >= 65 && code <= 90) && // A-Z
1844
+ !(code >= 97 && code <= 122) && // a-z
1845
+ code !== 45 // -
1846
+ ) {
1847
+ return false;
1848
+ }
1849
+ }
1850
+ return true;
1851
+ }
1852
+ /**
1853
+ * Checks if a character is a hex digit.
1854
+ *
1855
+ * @param char - The character to check
1856
+ * @returns True if the character is a hex digit (0-9, A-F, a-f)
1857
+ */
1858
+ function isHexDigit(char) {
1859
+ const code = char.charCodeAt(0);
1860
+ return ((code >= 48 && code <= 57) || // 0-9
1861
+ (code >= 65 && code <= 70) || // A-F
1862
+ (code >= 97 && code <= 102) // a-f
1863
+ );
1864
+ }
1865
+ /**
1866
+ * Checks if a character is a digit.
1867
+ *
1868
+ * @param char - The character to check
1869
+ * @returns True if the character is a digit (0-9)
1870
+ */
1871
+ function isDigitChar(char) {
1872
+ const code = char.charCodeAt(0);
1873
+ return code >= 48 && code <= 57;
1874
+ }
1875
+
1876
+ /**
1877
+ * Changelog Tokenizer
1878
+ *
1879
+ * A state machine tokenizer that processes markdown character-by-character.
1880
+ * No regex is used to ensure ReDoS safety.
1881
+ */
1882
+ /**
1883
+ * Maximum input length to prevent memory exhaustion (1MB)
1884
+ */
1885
+ const MAX_INPUT_LENGTH = 1024 * 1024;
1886
+ /**
1887
+ * Tokenizes a changelog markdown string into tokens.
1888
+ *
1889
+ * @param input - The markdown content to tokenize
1890
+ * @returns Array of tokens
1891
+ * @throws {Error} If input exceeds maximum length
1892
+ */
1893
+ function tokenize(input) {
1894
+ if (input.length > MAX_INPUT_LENGTH) {
1895
+ throw createError(`Input exceeds maximum length of ${MAX_INPUT_LENGTH} characters`);
1896
+ }
1897
+ const state = {
1898
+ pos: 0,
1899
+ line: 1,
1900
+ column: 1,
1901
+ input,
1902
+ tokens: [],
1903
+ };
1904
+ while (state.pos < state.input.length) {
1905
+ const char = state.input[state.pos];
1906
+ // Check for newline
1907
+ if (char === '\n') {
1908
+ consumeNewline(state);
1909
+ continue;
1910
+ }
1911
+ // Check for carriage return (handle \r\n)
1912
+ if (char === '\r') {
1913
+ state.pos++;
1914
+ if (state.input[state.pos] === '\n') {
1915
+ consumeNewline(state);
1916
+ }
1917
+ continue;
1918
+ }
1919
+ // At start of line, check for special markers
1920
+ if (state.column === 1) {
1921
+ // Check for heading
1922
+ if (char === '#') {
1923
+ consumeHeading(state);
1924
+ continue;
1925
+ }
1926
+ // Check for list item
1927
+ if ((char === '-' || char === '*') && isWhitespace(state.input[state.pos + 1])) {
1928
+ consumeListItem(state);
1929
+ continue;
1930
+ }
1931
+ }
1932
+ // Check for inline markdown elements
1933
+ if (char === '[') {
1934
+ consumeLink(state);
1935
+ continue;
1936
+ }
1937
+ if (char === '`') {
1938
+ consumeCode(state);
1939
+ continue;
1940
+ }
1941
+ if (char === '*' && state.input[state.pos + 1] === '*') {
1942
+ consumeBold(state);
1943
+ continue;
1944
+ }
1945
+ // Default: consume as text
1946
+ consumeText(state);
1947
+ }
1948
+ // Add EOF token
1949
+ pushToken(state, 'eof', '');
1950
+ return state.tokens;
1951
+ }
1952
+ /**
1953
+ * Consumes a newline character.
1954
+ *
1955
+ * @param state - The tokenizer state to update
1956
+ */
1957
+ function consumeNewline(state) {
1958
+ // Check if this is a blank line (previous token was also newline or at start)
1959
+ const prevToken = state.tokens[state.tokens.length - 1];
1960
+ const isBlank = prevToken?.type === 'newline' || prevToken?.type === 'blank-line' || state.tokens.length === 0;
1961
+ pushToken(state, isBlank ? 'blank-line' : 'newline', '\n');
1962
+ state.pos++;
1963
+ state.line++;
1964
+ state.column = 1;
1965
+ }
1966
+ /**
1967
+ * Consumes a heading (# through ####).
1968
+ *
1969
+ * @param state - The tokenizer state to update
1970
+ */
1971
+ function consumeHeading(state) {
1972
+ const startColumn = state.column;
1973
+ let level = 0;
1974
+ // Count # characters
1975
+ while (state.input[state.pos] === '#' && level < 4) {
1976
+ state.pos++;
1977
+ state.column++;
1978
+ level++;
1979
+ }
1980
+ // Skip whitespace after #
1981
+ while (isWhitespace(state.input[state.pos]) && state.input[state.pos] !== '\n') {
1982
+ state.pos++;
1983
+ state.column++;
1984
+ }
1985
+ // Consume the rest of the line as heading content
1986
+ const contentStart = state.pos;
1987
+ while (state.pos < state.input.length && state.input[state.pos] !== '\n') {
1988
+ state.pos++;
1989
+ state.column++;
1990
+ }
1991
+ const content = state.input.slice(contentStart, state.pos);
1992
+ const type = `heading-${level}`;
1993
+ state.tokens.push({
1994
+ type,
1995
+ value: content.trim(),
1996
+ line: state.line,
1997
+ column: startColumn,
1998
+ });
1999
+ }
2000
+ /**
2001
+ * Consumes a list item (- or *).
2002
+ *
2003
+ * @param state - The tokenizer state to update
2004
+ */
2005
+ function consumeListItem(state) {
2006
+ const startColumn = state.column;
2007
+ // Skip the marker and whitespace
2008
+ state.pos++; // skip - or *
2009
+ state.column++;
2010
+ while (isWhitespace(state.input[state.pos]) && state.input[state.pos] !== '\n') {
2011
+ state.pos++;
2012
+ state.column++;
2013
+ }
2014
+ // Consume the rest of the line as list item content
2015
+ const contentStart = state.pos;
2016
+ while (state.pos < state.input.length && state.input[state.pos] !== '\n') {
2017
+ state.pos++;
2018
+ state.column++;
2019
+ }
2020
+ const content = state.input.slice(contentStart, state.pos);
2021
+ state.tokens.push({
2022
+ type: 'list-item',
2023
+ value: content,
2024
+ line: state.line,
2025
+ column: startColumn,
2026
+ });
2027
+ }
2028
+ /**
2029
+ * Consumes a markdown link [text](url).
2030
+ *
2031
+ * @param state - The tokenizer state to update
2032
+ */
2033
+ function consumeLink(state) {
2034
+ const startColumn = state.column;
2035
+ const startLine = state.line;
2036
+ // Skip opening [
2037
+ state.pos++;
2038
+ state.column++;
2039
+ // Find closing ]
2040
+ const textStart = state.pos;
2041
+ let depth = 1;
2042
+ while (state.pos < state.input.length && depth > 0) {
2043
+ const char = state.input[state.pos];
2044
+ if (char === '[')
2045
+ depth++;
2046
+ else if (char === ']')
2047
+ depth--;
2048
+ else if (char === '\n') {
2049
+ // Link text shouldn't span lines; emit '[' as text and reset
2050
+ state.tokens.push({
2051
+ type: 'text',
2052
+ value: '[',
2053
+ line: startLine,
2054
+ column: startColumn,
2055
+ });
2056
+ state.pos = textStart;
2057
+ state.column = startColumn + 1;
2058
+ return;
2059
+ }
2060
+ if (depth > 0) {
2061
+ state.pos++;
2062
+ state.column++;
2063
+ }
2064
+ }
2065
+ if (depth !== 0) {
2066
+ // No closing ], emit '[' as text and reset
2067
+ state.tokens.push({
2068
+ type: 'text',
2069
+ value: '[',
2070
+ line: startLine,
2071
+ column: startColumn,
2072
+ });
2073
+ state.pos = textStart;
2074
+ state.column = startColumn + 1;
2075
+ return;
2076
+ }
2077
+ const linkText = state.input.slice(textStart, state.pos);
2078
+ state.pos++; // skip ]
2079
+ state.column++;
2080
+ // Check for (url)
2081
+ if (state.input[state.pos] === '(') {
2082
+ state.pos++; // skip (
2083
+ state.column++;
2084
+ const urlStart = state.pos;
2085
+ depth = 1;
2086
+ while (state.pos < state.input.length && depth > 0) {
2087
+ const char = state.input[state.pos];
2088
+ if (char === '(')
2089
+ depth++;
2090
+ else if (char === ')')
2091
+ depth--;
2092
+ else if (char === '\n') {
2093
+ // URL shouldn't span lines
2094
+ break;
2095
+ }
2096
+ if (depth > 0) {
2097
+ state.pos++;
2098
+ state.column++;
2099
+ }
2100
+ }
2101
+ if (depth === 0) {
2102
+ const linkUrl = state.input.slice(urlStart, state.pos);
2103
+ state.pos++; // skip )
2104
+ state.column++;
2105
+ // Emit link-text token
2106
+ state.tokens.push({
2107
+ type: 'link-text',
2108
+ value: linkText,
2109
+ line: startLine,
2110
+ column: startColumn,
2111
+ });
2112
+ // Emit link-url token
2113
+ state.tokens.push({
2114
+ type: 'link-url',
2115
+ value: linkUrl,
2116
+ line: startLine,
2117
+ column: startColumn + linkText.length + 3,
2118
+ });
2119
+ return;
2120
+ }
2121
+ }
2122
+ // No URL part, emit as text
2123
+ state.tokens.push({
2124
+ type: 'text',
2125
+ value: `[${linkText}]`,
2126
+ line: startLine,
2127
+ column: startColumn,
2128
+ });
2129
+ }
2130
+ /**
2131
+ * Consumes a code span `code`.
2132
+ *
2133
+ * @param state - The tokenizer state to update
2134
+ */
2135
+ function consumeCode(state) {
2136
+ const startColumn = state.column;
2137
+ const startLine = state.line;
2138
+ state.pos++; // skip opening `
2139
+ state.column++;
2140
+ const contentStart = state.pos;
2141
+ while (state.pos < state.input.length && state.input[state.pos] !== '`' && state.input[state.pos] !== '\n') {
2142
+ state.pos++;
2143
+ state.column++;
2144
+ }
2145
+ if (state.input[state.pos] === '`') {
2146
+ const content = state.input.slice(contentStart, state.pos);
2147
+ state.pos++; // skip closing `
2148
+ state.column++;
2149
+ state.tokens.push({
2150
+ type: 'code',
2151
+ value: content,
2152
+ line: startLine,
2153
+ column: startColumn,
2154
+ });
2155
+ }
2156
+ else {
2157
+ // No closing `, emit as text
2158
+ state.tokens.push({
2159
+ type: 'text',
2160
+ value: '`' + state.input.slice(contentStart, state.pos),
2161
+ line: startLine,
2162
+ column: startColumn,
2163
+ });
2164
+ }
2165
+ }
2166
+ /**
2167
+ * Consumes bold text **text**.
2168
+ *
2169
+ * @param state - The tokenizer state to update
2170
+ */
2171
+ function consumeBold(state) {
2172
+ const startColumn = state.column;
2173
+ const startLine = state.line;
2174
+ state.pos += 2; // skip opening **
2175
+ state.column += 2;
2176
+ const contentStart = state.pos;
2177
+ while (state.pos < state.input.length) {
2178
+ // Check for closing **
2179
+ if (state.input[state.pos] === '*' && state.input[state.pos + 1] === '*') {
2180
+ break;
2181
+ }
2182
+ if (state.input[state.pos] === '\n') {
2183
+ state.line++;
2184
+ state.column = 0;
2185
+ }
2186
+ state.pos++;
2187
+ state.column++;
2188
+ }
2189
+ if (state.input[state.pos] === '*' && state.input[state.pos + 1] === '*') {
2190
+ const content = state.input.slice(contentStart, state.pos);
2191
+ state.pos += 2; // skip closing **
2192
+ state.column += 2;
2193
+ state.tokens.push({
2194
+ type: 'bold',
2195
+ value: content,
2196
+ line: startLine,
2197
+ column: startColumn,
2198
+ });
2199
+ }
2200
+ else {
2201
+ // No closing **, emit as text
2202
+ state.tokens.push({
2203
+ type: 'text',
2204
+ value: '**' + state.input.slice(contentStart, state.pos),
2205
+ line: startLine,
2206
+ column: startColumn,
2207
+ });
2208
+ }
2209
+ }
2210
+ /**
2211
+ * Consumes plain text until a special character or end of line.
2212
+ *
2213
+ * @param state - The tokenizer state to update
2214
+ */
2215
+ function consumeText(state) {
2216
+ const startColumn = state.column;
2217
+ const startLine = state.line;
2218
+ const startPos = state.pos;
2219
+ while (state.pos < state.input.length) {
2220
+ const char = state.input[state.pos];
2221
+ // Stop at newline
2222
+ if (char === '\n' || char === '\r') {
2223
+ break;
2224
+ }
2225
+ // Stop at special characters (but only if they start a pattern)
2226
+ if (char === '[')
2227
+ break;
2228
+ if (char === '`')
2229
+ break;
2230
+ if (char === '*' && state.input[state.pos + 1] === '*')
2231
+ break;
2232
+ state.pos++;
2233
+ state.column++;
2234
+ }
2235
+ const content = state.input.slice(startPos, state.pos);
2236
+ if (content.length > 0) {
2237
+ state.tokens.push({
2238
+ type: 'text',
2239
+ value: content,
2240
+ line: startLine,
2241
+ column: startColumn,
2242
+ });
2243
+ }
2244
+ }
2245
+ /**
2246
+ * Pushes a token to the state.
2247
+ *
2248
+ * @param state - The tokenizer state to update
2249
+ * @param type - Token classification (e.g., 'heading-1', 'text', 'link-url')
2250
+ * @param value - Text content associated with the token
2251
+ */
2252
+ function pushToken(state, type, value) {
2253
+ state.tokens.push({
2254
+ type,
2255
+ value,
2256
+ line: state.line,
2257
+ column: state.column,
2258
+ });
2259
+ }
2260
+ /**
2261
+ * Checks if a character is whitespace (but not newline).
2262
+ *
2263
+ * @param char - The character to check
2264
+ * @returns True if the character is whitespace (space or tab)
2265
+ */
2266
+ function isWhitespace(char) {
2267
+ return char === ' ' || char === '\t';
2268
+ }
2269
+
2270
+ /**
2271
+ * Changelog Parser
2272
+ *
2273
+ * Parses a changelog markdown string into a structured Changelog object.
2274
+ * Uses a state machine tokenizer for ReDoS-safe parsing.
2275
+ */
2276
+ /**
2277
+ * Parses a changelog markdown string into a Changelog object.
2278
+ *
2279
+ * @param content - The markdown content to parse
2280
+ * @param source - Optional source file path
2281
+ * @returns Parsed Changelog object
2282
+ */
2283
+ function parseChangelog(content, source) {
2284
+ const tokens = tokenize(content);
2285
+ const state = {
2286
+ tokens,
2287
+ pos: 0,
2288
+ warnings: [],
2289
+ };
2290
+ // Parse header
2291
+ const header = parseHeader(state);
2292
+ // Parse entries
2293
+ const entries = parseEntries(state);
2294
+ // Detect format
2295
+ const format = detectFormat(header, entries);
2296
+ // Build metadata
2297
+ const metadata = {
2298
+ format,
2299
+ isConventional: format === 'conventional',
2300
+ repositoryUrl: state.repositoryUrl,
2301
+ warnings: state.warnings,
2302
+ };
2303
+ return {
2304
+ source,
2305
+ header,
2306
+ entries,
2307
+ metadata,
2308
+ };
2309
+ }
2310
+ /**
2311
+ * Parses the changelog header section.
2312
+ *
2313
+ * @param state - The parser state containing tokens and position
2314
+ * @returns The parsed ChangelogHeader with title, description, and links
2315
+ */
2316
+ function parseHeader(state) {
2317
+ let title = '# Changelog';
2318
+ const description = [];
2319
+ const links = [];
2320
+ // Look for h1 title
2321
+ const headingToken = currentToken(state);
2322
+ if (headingToken?.type === 'heading-1') {
2323
+ title = `# ${headingToken.value}`;
2324
+ advance(state);
2325
+ }
2326
+ // Skip newlines
2327
+ skipNewlines(state);
2328
+ // Collect description lines until we hit h2 (version entry)
2329
+ while (!isEOF(state) && currentToken(state)?.type !== 'heading-2') {
2330
+ const token = currentToken(state);
2331
+ if (!token)
2332
+ break;
2333
+ if (token.type === 'text') {
2334
+ description.push(token.value);
2335
+ }
2336
+ else if (token.type === 'link-text') {
2337
+ // Check for link definition
2338
+ const nextToken = peek(state, 1);
2339
+ if (nextToken?.type === 'link-url') {
2340
+ description.push(`[${token.value}](${nextToken.value})`);
2341
+ links.push({ label: token.value, url: nextToken.value });
2342
+ // Try to detect repository URL
2343
+ if (!state.repositoryUrl && nextToken.value.includes('github.com')) {
2344
+ state.repositoryUrl = extractRepoUrl(nextToken.value);
2345
+ }
2346
+ advance(state); // skip link-text
2347
+ advance(state); // skip link-url
2348
+ continue;
2349
+ }
2350
+ }
2351
+ else if (token.type === 'newline' || token.type === 'blank-line') {
2352
+ if (description.length > 0 && description[description.length - 1] !== '') {
2353
+ description.push('');
2354
+ }
2355
+ }
2356
+ advance(state);
2357
+ }
2358
+ // Trim trailing empty lines
2359
+ while (description.length > 0 && description[description.length - 1] === '') {
2360
+ description.pop();
2361
+ }
2362
+ return { title, description, links };
2363
+ }
2364
+ /**
2365
+ * Parses all changelog entries.
2366
+ *
2367
+ * @param state - The parser state containing tokens and position
2368
+ * @returns An array of parsed ChangelogEntry objects
2369
+ */
2370
+ function parseEntries(state) {
2371
+ const entries = [];
2372
+ while (!isEOF(state)) {
2373
+ // Look for h2 heading (version entry)
2374
+ if (currentToken(state)?.type === 'heading-2') {
2375
+ const entry = parseEntry(state);
2376
+ if (entry) {
2377
+ entries.push(entry);
2378
+ }
2379
+ }
2380
+ else {
2381
+ advance(state);
2382
+ }
2383
+ }
2384
+ return entries;
2385
+ }
2386
+ /**
2387
+ * Parses a single changelog entry.
2388
+ *
2389
+ * @param state - The parser state containing tokens and position
2390
+ * @returns The parsed ChangelogEntry or null if parsing fails
2391
+ */
2392
+ function parseEntry(state) {
2393
+ const headingToken = currentToken(state);
2394
+ if (headingToken?.type !== 'heading-2') {
2395
+ return null;
2396
+ }
2397
+ const { version, date, compareUrl } = parseVersionFromHeading(headingToken.value);
2398
+ const unreleased = version.toLowerCase() === 'unreleased';
2399
+ advance(state); // skip h2
2400
+ skipNewlines(state);
2401
+ // Parse sections
2402
+ const sections = parseSections(state);
2403
+ return {
2404
+ version,
2405
+ date,
2406
+ unreleased,
2407
+ compareUrl,
2408
+ sections,
2409
+ };
2410
+ }
2411
+ /**
2412
+ * Parses sections within an entry.
2413
+ *
2414
+ * @param state - The parser state containing tokens and position
2415
+ * @returns An array of parsed ChangelogSection objects
2416
+ */
2417
+ function parseSections(state) {
2418
+ const sections = [];
2419
+ while (!isEOF(state)) {
2420
+ const token = currentToken(state);
2421
+ // Stop at next version entry (h2)
2422
+ if (token?.type === 'heading-2') {
2423
+ break;
2424
+ }
2425
+ // Parse section (h3)
2426
+ if (token?.type === 'heading-3') {
2427
+ const section = parseSection(state);
2428
+ if (section) {
2429
+ sections.push(section);
2430
+ }
2431
+ }
2432
+ else if (token?.type === 'list-item') {
2433
+ // Items without section heading - create "other" section
2434
+ const items = parseItems(state);
2435
+ if (items.length > 0) {
2436
+ sections.push({
2437
+ type: 'other',
2438
+ heading: 'Changes',
2439
+ items,
2440
+ });
2441
+ }
2442
+ }
2443
+ else {
2444
+ advance(state);
2445
+ }
2446
+ }
2447
+ return sections;
2448
+ }
2449
+ /**
2450
+ * Parses a single section.
2451
+ *
2452
+ * @param state - The parser state containing tokens and position
2453
+ * @returns The parsed ChangelogSection or null if parsing fails
2454
+ */
2455
+ function parseSection(state) {
2456
+ const headingToken = currentToken(state);
2457
+ if (headingToken?.type !== 'heading-3') {
2458
+ return null;
2459
+ }
2460
+ const heading = headingToken.value;
2461
+ const type = getSectionType(heading);
2462
+ advance(state); // skip h3
2463
+ skipNewlines(state);
2464
+ // Parse items
2465
+ const items = parseItems(state);
2466
+ return {
2467
+ type,
2468
+ heading,
2469
+ items,
2470
+ };
2471
+ }
2472
+ /**
2473
+ * Parses list items.
2474
+ *
2475
+ * @param state - The parser state containing tokens and position
2476
+ * @returns An array of parsed ChangelogItem objects
2477
+ */
2478
+ function parseItems(state) {
2479
+ const items = [];
2480
+ while (!isEOF(state)) {
2481
+ const token = currentToken(state);
2482
+ // Stop at headings
2483
+ if (token?.type === 'heading-2' || token?.type === 'heading-3') {
2484
+ break;
2485
+ }
2486
+ // Parse list item
2487
+ if (token?.type === 'list-item') {
2488
+ const item = parseItem(state);
2489
+ if (item) {
2490
+ items.push(item);
2491
+ }
2492
+ }
2493
+ else {
2494
+ advance(state);
2495
+ }
2496
+ }
2497
+ return items;
2498
+ }
2499
+ /**
2500
+ * Parses a single list item.
2501
+ *
2502
+ * @param state - The parser state containing tokens and position
2503
+ * @returns The parsed ChangelogItem or null if parsing fails
2504
+ */
2505
+ function parseItem(state) {
2506
+ const token = currentToken(state);
2507
+ if (token?.type !== 'list-item') {
2508
+ return null;
2509
+ }
2510
+ const text = token.value;
2511
+ const { scope, description } = parseScopeFromItem(text);
2512
+ const commits = parseCommitRefs(text, state.repositoryUrl);
2513
+ const references = parseIssueRefs(text, state.repositoryUrl);
2514
+ // Check for breaking change indicators
2515
+ const breaking = isBreakingItem(text);
2516
+ advance(state);
2517
+ return createChangelogItem(description, {
2518
+ scope,
2519
+ commits,
2520
+ references,
2521
+ breaking,
2522
+ });
2523
+ }
2524
+ /**
2525
+ * Checks if an item indicates a breaking change.
2526
+ *
2527
+ * @param text - The text content of the item
2528
+ * @returns True if the item indicates a breaking change
2529
+ */
2530
+ function isBreakingItem(text) {
2531
+ const lower = text.toLowerCase();
2532
+ return lower.includes('breaking change') || lower.includes('breaking:') || lower.startsWith('!') || lower.includes('[breaking]');
2533
+ }
2534
+ /**
2535
+ * Detects the changelog format.
2536
+ *
2537
+ * @param header - The parsed header of the changelog
2538
+ * @param entries - The parsed changelog entries
2539
+ * @returns The detected ChangelogFormat
2540
+ */
2541
+ function detectFormat(header, entries) {
2542
+ const descriptionText = header.description.join(' ').toLowerCase();
2543
+ // Check for Keep a Changelog
2544
+ if (descriptionText.includes('keep a changelog') || descriptionText.includes('keepachangelog')) {
2545
+ return 'keep-a-changelog';
2546
+ }
2547
+ // Check for conventional changelog patterns
2548
+ const hasConventionalSections = entries.some((entry) => entry.sections.some((section) => ['features', 'fixes', 'performance'].includes(section.type)));
2549
+ if (hasConventionalSections) {
2550
+ return 'conventional';
2551
+ }
2552
+ // Check if we have entries with structured sections
2553
+ if (entries.some((entry) => entry.sections.length > 0)) {
2554
+ return 'custom';
2555
+ }
2556
+ return 'unknown';
2557
+ }
2558
+ /**
2559
+ * Extracts repository URL from a GitHub URL.
2560
+ *
2561
+ * @param url - The URL to extract the repository from
2562
+ * @returns The repository URL or undefined if not found
2563
+ */
2564
+ function extractRepoUrl(url) {
2565
+ // Try to extract base repo URL from various GitHub URL patterns
2566
+ const githubIndex = url.indexOf('github.com/');
2567
+ if (githubIndex !== -1) {
2568
+ const afterGithub = url.slice(githubIndex + 11);
2569
+ const parts = afterGithub.split('/');
2570
+ if (parts.length >= 2) {
2571
+ return `https://github.com/${parts[0]}/${parts[1]}`;
2572
+ }
2573
+ }
2574
+ return undefined;
2575
+ }
2576
+ // ============================================================================
2577
+ // Parser utilities
2578
+ // ============================================================================
2579
+ /**
2580
+ * Gets the current token at the parser position.
2581
+ *
2582
+ * @param state - The parser state
2583
+ * @returns The current token or undefined if at end
2584
+ */
2585
+ function currentToken(state) {
2586
+ return state.tokens[state.pos];
2587
+ }
2588
+ /**
2589
+ * Peeks at a token at an offset from the current position.
2590
+ *
2591
+ * @param state - The parser state
2592
+ * @param offset - The offset from current position
2593
+ * @returns The token at the offset or undefined if out of bounds
2594
+ */
2595
+ function peek(state, offset) {
2596
+ return state.tokens[state.pos + offset];
2597
+ }
2598
+ /**
2599
+ * Advances the parser position by one token.
2600
+ *
2601
+ * @param state - The parser state to advance
2602
+ */
2603
+ function advance(state) {
2604
+ state.pos++;
2605
+ }
2606
+ /**
2607
+ * Checks if the parser has reached the end of the token stream.
2608
+ *
2609
+ * @param state - The parser state
2610
+ * @returns True if at end of file
2611
+ */
2612
+ function isEOF(state) {
2613
+ const token = currentToken(state);
2614
+ return !token || token.type === 'eof';
2615
+ }
2616
+ /**
2617
+ * Skips newline tokens until a non-newline token is found.
2618
+ *
2619
+ * @param state - The parser state
2620
+ */
2621
+ function skipNewlines(state) {
2622
+ while (!isEOF(state)) {
2623
+ const token = currentToken(state);
2624
+ if (token?.type !== 'newline' && token?.type !== 'blank-line') {
2625
+ break;
2626
+ }
2627
+ advance(state);
2628
+ }
2629
+ }
2630
+
2631
+ /**
2632
+ * Changelog Serialization Templates
2633
+ *
2634
+ * Output templates and formatting helpers for changelog serialization.
2635
+ * Provides configurable options for different changelog styles.
2636
+ */
2637
+ /**
2638
+ * Default serialization options.
2639
+ */
2640
+ const DEFAULT_SERIALIZE_OPTIONS = {
2641
+ includeDescription: true,
2642
+ includeLinks: true,
2643
+ includeCompareUrls: true,
2644
+ includeCommits: true,
2645
+ includeReferences: true,
2646
+ includeScope: true,
2647
+ includeRawContent: false,
2648
+ sectionOrder: [
2649
+ 'breaking',
2650
+ 'features',
2651
+ 'fixes',
2652
+ 'performance',
2653
+ 'documentation',
2654
+ 'deprecations',
2655
+ 'refactoring',
2656
+ 'tests',
2657
+ 'build',
2658
+ 'ci',
2659
+ 'chores',
2660
+ 'other',
2661
+ ],
2662
+ sectionHeadings: {},
2663
+ lineEnding: '\n',
2664
+ entrySpacing: 1,
2665
+ sectionSpacing: 1,
2666
+ useAsterisks: false,
2667
+ };
2668
+ /**
2669
+ * Merges user options with defaults.
2670
+ *
2671
+ * @param options - User-provided options
2672
+ * @returns Complete options with defaults applied
2673
+ */
2674
+ function resolveOptions(options) {
2675
+ {
2676
+ return DEFAULT_SERIALIZE_OPTIONS;
2677
+ }
2678
+ }
2679
+ /**
2680
+ * Gets the heading for a section type, respecting custom headings.
2681
+ *
2682
+ * @param type - The changelog section type to get the heading for
2683
+ * @param customHeadings - Optional map of custom section type to heading overrides
2684
+ * @returns The heading string to use
2685
+ */
2686
+ function getSectionHeading(type, customHeadings) {
2687
+ return customHeadings?.[type] ?? SECTION_HEADINGS[type];
2688
+ }
2689
+ /**
2690
+ * Creates a markdown link.
2691
+ *
2692
+ * @param text - The display text for the link
2693
+ * @param url - The destination URL for the link
2694
+ * @returns Formatted markdown link
2695
+ */
2696
+ function formatLink(text, url) {
2697
+ return `[${text}](${url})`;
2698
+ }
2699
+ /**
2700
+ * Creates a list item marker.
2701
+ *
2702
+ * @param useAsterisks - Whether to use * instead of -
2703
+ * @returns The list item marker ('- ' or '* ')
2704
+ */
2705
+ function getListMarker(useAsterisks) {
2706
+ return useAsterisks ? '* ' : '- ';
2707
+ }
2708
+ /**
2709
+ * Creates blank lines for spacing.
2710
+ *
2711
+ * @param count - Number of blank lines
2712
+ * @param lineEnding - Line ending style
2713
+ * @returns String with specified number of blank lines
2714
+ */
2715
+ function createSpacing(count, lineEnding) {
2716
+ if (count <= 0)
2717
+ return '';
2718
+ return lineEnding.repeat(count);
2719
+ }
2720
+
2721
+ /**
2722
+ * Changelog Serialization to String
2723
+ *
2724
+ * Converts a Changelog object back to markdown format.
2725
+ * Supports configurable output formatting for different changelog styles.
2726
+ */
2727
+ /**
2728
+ * Serializes a Changelog object to markdown string.
2729
+ *
2730
+ * @param changelog - The changelog to serialize
2731
+ * @param options - Optional serialization options
2732
+ * @returns The markdown string representation
2733
+ *
2734
+ * @example
2735
+ * ```ts
2736
+ * const markdown = serializeChangelog(changelog)
2737
+ * ```
2738
+ *
2739
+ * @example
2740
+ * ```ts
2741
+ * const markdown = serializeChangelog(changelog, {
2742
+ * includeCommits: false,
2743
+ * useAsterisks: true,
2744
+ * })
2745
+ * ```
2746
+ */
2747
+ function serializeChangelog(changelog, options) {
2748
+ const opts = resolveOptions();
2749
+ const parts = [];
2750
+ // Serialize header
2751
+ parts.push(serializeHeader(changelog.header, opts));
2752
+ // Serialize entries
2753
+ for (let i = 0; i < changelog.entries.length; i++) {
2754
+ const entry = changelog.entries[i];
2755
+ parts.push(serializeEntry(entry, opts));
2756
+ // Add spacing between entries (except after the last one)
2757
+ if (i < changelog.entries.length - 1) {
2758
+ parts.push(createSpacing(opts.entrySpacing, opts.lineEnding));
2759
+ }
2760
+ }
2761
+ return parts.join('');
2762
+ }
2763
+ /**
2764
+ * Serializes the changelog header.
2765
+ *
2766
+ * @param header - The header to serialize
2767
+ * @param opts - Resolved options
2768
+ * @returns The serialized header string
2769
+ */
2770
+ function serializeHeader(header, opts) {
2771
+ const parts = [];
2772
+ const nl = opts.lineEnding;
2773
+ // Title
2774
+ parts.push(header.title + nl);
2775
+ parts.push(nl);
2776
+ // Description
2777
+ if (opts.includeDescription && header.description.length > 0) {
2778
+ for (const line of header.description) {
2779
+ parts.push(line + nl);
2780
+ }
2781
+ parts.push(nl);
2782
+ }
2783
+ // Links section
2784
+ if (opts.includeLinks && header.links.length > 0) {
2785
+ for (const link of header.links) {
2786
+ parts.push(serializeLink(link) + nl);
2787
+ }
2788
+ parts.push(nl);
2789
+ }
2790
+ return parts.join('');
2791
+ }
2792
+ /**
2793
+ * Serializes a changelog link.
2794
+ *
2795
+ * @param link - The link to serialize
2796
+ * @returns The serialized link
2797
+ */
2798
+ function serializeLink(link) {
2799
+ return `[${link.label}]: ${link.url}`;
2800
+ }
2801
+ /**
2802
+ * Serializes a changelog entry.
2803
+ *
2804
+ * @param entry - The entry to serialize
2805
+ * @param opts - Resolved options
2806
+ * @returns The serialized entry string
2807
+ */
2808
+ function serializeEntry(entry, opts) {
2809
+ const parts = [];
2810
+ const nl = opts.lineEnding;
2811
+ // Entry heading
2812
+ parts.push(serializeEntryHeading(entry, opts) + nl);
2813
+ parts.push(nl);
2814
+ // Raw content fallback
2815
+ if (opts.includeRawContent && entry.rawContent) {
2816
+ parts.push(entry.rawContent + nl);
2817
+ return parts.join('');
2818
+ }
2819
+ // Sort sections by specified order
2820
+ const sortedSections = sortSections(entry.sections, opts.sectionOrder);
2821
+ // Serialize sections
2822
+ for (let i = 0; i < sortedSections.length; i++) {
2823
+ const section = sortedSections[i];
2824
+ parts.push(serializeSection(section, opts));
2825
+ // Add spacing between sections (except after the last one)
2826
+ if (i < sortedSections.length - 1) {
2827
+ parts.push(createSpacing(opts.sectionSpacing, nl));
2828
+ }
2829
+ }
2830
+ return parts.join('');
2831
+ }
2832
+ /**
2833
+ * Serializes an entry heading.
2834
+ *
2835
+ * @param entry - The changelog entry to serialize the heading for
2836
+ * @param opts - Resolved serialization options
2837
+ * @returns The heading line
2838
+ */
2839
+ function serializeEntryHeading(entry, opts) {
2840
+ const parts = ['## '];
2841
+ // Version with optional compare URL
2842
+ if (opts.includeCompareUrls && entry.compareUrl) {
2843
+ parts.push(formatLink(entry.version, entry.compareUrl));
2844
+ }
2845
+ else {
2846
+ parts.push(entry.version);
2847
+ }
2848
+ // Date
2849
+ if (entry.date) {
2850
+ parts.push(` - ${entry.date}`);
2851
+ }
2852
+ return parts.join('');
2853
+ }
2854
+ /**
2855
+ * Sorts sections by the specified order.
2856
+ *
2857
+ * @param sections - The sections to sort
2858
+ * @param order - The desired section order
2859
+ * @returns Sorted sections
2860
+ */
2861
+ function sortSections(sections, order) {
2862
+ const orderMap = createMap();
2863
+ order.forEach((type, index) => orderMap.set(type, index));
2864
+ return [...sections].sort((a, b) => {
2865
+ const orderA = orderMap.get(a.type) ?? Number.MAX_SAFE_INTEGER;
2866
+ const orderB = orderMap.get(b.type) ?? Number.MAX_SAFE_INTEGER;
2867
+ return orderA - orderB;
2868
+ });
2869
+ }
2870
+ /**
2871
+ * Serializes a changelog section.
2872
+ *
2873
+ * @param section - The section to serialize
2874
+ * @param opts - Resolved options
2875
+ * @returns The serialized section string
2876
+ */
2877
+ function serializeSection(section, opts) {
2878
+ const parts = [];
2879
+ const nl = opts.lineEnding;
2880
+ // Section heading - use original heading if available, otherwise generate
2881
+ const heading = section.heading || getSectionHeading(section.type, opts.sectionHeadings);
2882
+ parts.push(`### ${heading}${nl}`);
2883
+ parts.push(nl);
2884
+ // Items
2885
+ for (const item of section.items) {
2886
+ parts.push(serializeItem(item, opts) + nl);
2887
+ }
2888
+ return parts.join('');
2889
+ }
2890
+ /**
2891
+ * Serializes a changelog item.
2892
+ *
2893
+ * @param item - The item to serialize
2894
+ * @param opts - Resolved options
2895
+ * @returns The serialized item string
2896
+ */
2897
+ function serializeItem(item, opts) {
2898
+ const parts = [];
2899
+ const marker = getListMarker(opts.useAsterisks);
2900
+ parts.push(marker);
2901
+ // Breaking change indicator
2902
+ if (item.breaking) {
2903
+ parts.push('**BREAKING** ');
2904
+ }
2905
+ // Scope
2906
+ if (opts.includeScope && item.scope) {
2907
+ parts.push(`**${item.scope}:** `);
2908
+ }
2909
+ // Description
2910
+ parts.push(item.description);
2911
+ // Commit references
2912
+ if (opts.includeCommits && item.commits.length > 0) {
2913
+ parts.push(' (');
2914
+ parts.push(item.commits.map(serializeCommitRef).join(', '));
2915
+ parts.push(')');
2916
+ }
2917
+ // Issue/PR references
2918
+ if (opts.includeReferences && item.references.length > 0) {
2919
+ const refs = item.references.map(serializeIssueRef).join(', ');
2920
+ parts.push(` ${refs}`);
2921
+ }
2922
+ return parts.join('');
2923
+ }
2924
+ /**
2925
+ * Serializes a commit reference.
2926
+ *
2927
+ * @param ref - The commit reference
2928
+ * @returns The serialized reference
2929
+ */
2930
+ function serializeCommitRef(ref) {
2931
+ if (ref.url) {
2932
+ return formatLink(ref.shortHash, ref.url);
2933
+ }
2934
+ return ref.shortHash;
2935
+ }
2936
+ /**
2937
+ * Serializes an issue/PR reference.
2938
+ *
2939
+ * @param ref - The issue reference
2940
+ * @returns The serialized reference
2941
+ */
2942
+ function serializeIssueRef(ref) {
2943
+ const text = `#${ref.number}`;
2944
+ if (ref.url) {
2945
+ return formatLink(text, ref.url);
2946
+ }
2947
+ return text;
2948
+ }
2949
+
2950
+ /**
2951
+ * Changelog Entry Addition
2952
+ *
2953
+ * Functions for adding new entries to a changelog.
2954
+ */
2955
+ /**
2956
+ * Adds a new entry to a changelog.
2957
+ *
2958
+ * @param changelog - The changelog to add to
2959
+ * @param entry - The entry to add
2960
+ * @param options - Optional add options
2961
+ * @returns A new changelog with the entry added
2962
+ *
2963
+ * @example
2964
+ * ```ts
2965
+ * const newChangelog = addEntry(changelog, {
2966
+ * version: '1.2.0',
2967
+ * date: '2024-01-15',
2968
+ * unreleased: false,
2969
+ * sections: [...]
2970
+ * })
2971
+ * ```
2972
+ */
2973
+ function addEntry(changelog, entry, options) {
2974
+ // Check for existing entry
2975
+ const existingIndex = changelog.entries.findIndex((e) => e.version === entry.version);
2976
+ if (existingIndex !== -1 && true) {
2977
+ throw createError(`Entry with version "${entry.version}" already exists. Use replaceExisting: true to replace.`);
2978
+ }
2979
+ let newEntries;
2980
+ {
2981
+ // Add new entry
2982
+ const insertIndex = 0 ;
2983
+ newEntries = [...changelog.entries];
2984
+ newEntries.splice(insertIndex, 0, entry);
2985
+ }
2986
+ // Build new metadata if requested
2987
+ const metadata = changelog.metadata;
2988
+ return {
2989
+ ...changelog,
2990
+ entries: newEntries,
2991
+ metadata,
2992
+ };
2993
+ }
2994
+
2995
+ const GENERATE_CHANGELOG_STEP_ID = 'generate-changelog';
2996
+ /**
2997
+ * Maps conventional commit types to changelog section types.
2998
+ */
2999
+ const COMMIT_TYPE_TO_SECTION = {
3000
+ feat: 'features',
3001
+ fix: 'fixes',
3002
+ perf: 'performance',
3003
+ docs: 'documentation',
3004
+ refactor: 'refactoring',
3005
+ revert: 'other',
3006
+ build: 'build',
3007
+ ci: 'ci',
3008
+ test: 'tests',
3009
+ chore: 'chores',
3010
+ style: 'other',
3011
+ };
3012
+ /**
3013
+ * Groups commits by their section type.
3014
+ *
3015
+ * @param commits - Array of conventional commits
3016
+ * @returns Record of section type to commits
3017
+ */
3018
+ function groupCommitsBySection(commits) {
3019
+ const groups = {};
3020
+ for (const commit of commits) {
3021
+ const sectionType = COMMIT_TYPE_TO_SECTION[commit.type ?? 'chore'] ?? 'chores';
3022
+ if (!groups[sectionType]) {
3023
+ groups[sectionType] = [];
3024
+ }
3025
+ groups[sectionType].push(commit);
3026
+ }
3027
+ return groups;
3028
+ }
3029
+ /**
3030
+ * Creates a changelog item from a conventional commit.
3031
+ *
3032
+ * @param commit - The conventional commit
3033
+ * @returns A changelog item
3034
+ */
3035
+ function commitToItem(commit) {
3036
+ let text = commit.subject;
3037
+ // Add scope prefix if present
3038
+ if (commit.scope) {
3039
+ text = `**${commit.scope}:** ${text}`;
3040
+ }
3041
+ // Add breaking change indicator
3042
+ if (commit.breaking) {
3043
+ text = `⚠️ BREAKING: ${text}`;
3044
+ }
3045
+ return createChangelogItem(text);
3046
+ }
3047
+ /**
3048
+ * Creates the generate-changelog step.
3049
+ *
3050
+ * This step:
3051
+ * 1. Groups commits by type/section
3052
+ * 2. Creates changelog items from commits
3053
+ * 3. Assembles a complete changelog entry
3054
+ *
3055
+ * State updates:
3056
+ * - changelogEntry: The generated ChangelogEntry
3057
+ *
3058
+ * @returns A FlowStep that generates changelog
3059
+ */
3060
+ function createGenerateChangelogStep() {
3061
+ return createStep(GENERATE_CHANGELOG_STEP_ID, 'Generate Changelog Entry', async (ctx) => {
3062
+ const { config, state } = ctx;
3063
+ const { commits, nextVersion, bumpType } = state;
3064
+ // Skip if no bump needed
3065
+ if (!nextVersion || bumpType === 'none') {
3066
+ return createSkippedResult('No version bump, skipping changelog generation');
3067
+ }
3068
+ // Skip if changelog disabled
3069
+ if (config.skipChangelog) {
3070
+ return createSkippedResult('Changelog generation disabled');
3071
+ }
3072
+ // Handle case with no commits (e.g., first release)
3073
+ if (!commits || commits.length === 0) {
3074
+ const entry = createChangelogEntry(nextVersion, {
3075
+ date: createDate().toISOString().split('T')[0],
3076
+ sections: [createChangelogSection('features', 'Features', [createChangelogItem('Initial release')])],
3077
+ });
3078
+ return {
3079
+ status: 'success',
3080
+ stateUpdates: { changelogEntry: entry },
3081
+ message: 'Generated initial release changelog entry',
3082
+ };
3083
+ }
3084
+ // Group commits by section
3085
+ const grouped = groupCommitsBySection(commits);
3086
+ // Create sections
3087
+ const sections = [];
3088
+ // Add breaking changes section first if any
3089
+ const breakingCommits = commits.filter((c) => c.breaking);
3090
+ if (breakingCommits.length > 0) {
3091
+ sections.push(createChangelogSection('breaking', 'Breaking Changes', breakingCommits.map((c) => {
3092
+ const text = c.breakingDescription ?? c.subject;
3093
+ return createChangelogItem(c.scope ? `**${c.scope}:** ${text}` : text);
3094
+ })));
3095
+ }
3096
+ // Add other sections in conventional order
3097
+ const sectionOrder = [
3098
+ { type: 'features', heading: 'Features' },
3099
+ { type: 'fixes', heading: 'Bug Fixes' },
3100
+ { type: 'performance', heading: 'Performance' },
3101
+ { type: 'documentation', heading: 'Documentation' },
3102
+ { type: 'refactoring', heading: 'Code Refactoring' },
3103
+ { type: 'build', heading: 'Build' },
3104
+ { type: 'ci', heading: 'Continuous Integration' },
3105
+ { type: 'tests', heading: 'Tests' },
3106
+ { type: 'chores', heading: 'Chores' },
3107
+ { type: 'other', heading: 'Other' },
3108
+ ];
3109
+ for (const { type: sectionType, heading } of sectionOrder) {
3110
+ const sectionCommits = grouped[sectionType];
3111
+ if (sectionCommits && sectionCommits.length > 0) {
3112
+ sections.push(createChangelogSection(sectionType, heading, sectionCommits.map(commitToItem)));
3113
+ }
3114
+ }
3115
+ // Create the entry
3116
+ const entry = createChangelogEntry(nextVersion, {
3117
+ date: createDate().toISOString().split('T')[0],
3118
+ sections,
3119
+ });
3120
+ return {
3121
+ status: 'success',
3122
+ stateUpdates: { changelogEntry: entry },
3123
+ message: `Generated changelog with ${sections.length} section(s), ${commits.length} commit(s)`,
3124
+ };
3125
+ }, {
3126
+ dependsOn: ['check-idempotency'],
3127
+ });
3128
+ }
3129
+ /**
3130
+ * Creates the write-changelog step.
3131
+ *
3132
+ * This step writes the generated changelog entry to CHANGELOG.md.
3133
+ *
3134
+ * @returns A FlowStep that writes changelog to file
3135
+ */
3136
+ function createWriteChangelogStep() {
3137
+ return createStep('write-changelog', 'Write Changelog', async (ctx) => {
3138
+ const { tree, projectRoot, config, state, logger } = ctx;
3139
+ const { changelogEntry, nextVersion, bumpType } = state;
3140
+ // Skip if no bump or no changelog
3141
+ if (!nextVersion || bumpType === 'none' || !changelogEntry || config.skipChangelog) {
3142
+ return createSkippedResult('No changelog to write');
3143
+ }
3144
+ const changelogPath = `${projectRoot}/CHANGELOG.md`;
3145
+ let existingContent = '';
3146
+ // Read existing changelog
3147
+ try {
3148
+ existingContent = tree.read(changelogPath, 'utf-8') ?? '';
3149
+ }
3150
+ catch {
3151
+ logger.debug('No existing CHANGELOG.md found');
3152
+ }
3153
+ // If no existing content, create new changelog
3154
+ if (!existingContent.trim()) {
3155
+ const newChangelog = {
3156
+ header: {
3157
+ title: '# Changelog',
3158
+ description: ['All notable changes to this project will be documented in this file.'],
3159
+ links: [],
3160
+ },
3161
+ entries: [changelogEntry]};
3162
+ const serialized = serializeChangelog(newChangelog);
3163
+ tree.write(changelogPath, serialized);
3164
+ return {
3165
+ status: 'success',
3166
+ stateUpdates: {
3167
+ modifiedFiles: [...(state.modifiedFiles ?? []), changelogPath],
3168
+ },
3169
+ message: `Created CHANGELOG.md with version ${nextVersion}`,
3170
+ };
3171
+ }
3172
+ // Parse existing and add entry
3173
+ const existing = parseChangelog(existingContent);
3174
+ const updated = addEntry(existing, changelogEntry);
3175
+ const serialized = serializeChangelog(updated);
3176
+ tree.write(changelogPath, serialized);
3177
+ return {
3178
+ status: 'success',
3179
+ stateUpdates: {
3180
+ modifiedFiles: [...(state.modifiedFiles ?? []), changelogPath],
3181
+ },
3182
+ message: `Updated CHANGELOG.md with version ${nextVersion}`,
3183
+ };
3184
+ }, {
3185
+ dependsOn: ['generate-changelog'],
3186
+ });
3187
+ }
3188
+
3189
+ const UPDATE_PACKAGES_STEP_ID = 'update-packages';
3190
+ /**
3191
+ * Creates the update-packages step.
3192
+ *
3193
+ * This step:
3194
+ * 1. Updates the version field in package.json
3195
+ * 2. Tracks the modified files
3196
+ *
3197
+ * State updates:
3198
+ * - modifiedFiles: Adds package.json to list
3199
+ *
3200
+ * @returns A FlowStep that updates package.json
3201
+ */
3202
+ function createUpdatePackageStep() {
3203
+ return createStep(UPDATE_PACKAGES_STEP_ID, 'Update Package Version', async (ctx) => {
3204
+ const { tree, projectRoot, state, logger } = ctx;
3205
+ const { nextVersion, bumpType, currentVersion } = state;
3206
+ // Skip if no bump needed
3207
+ if (!nextVersion || bumpType === 'none') {
3208
+ return createSkippedResult('No version bump needed');
3209
+ }
3210
+ const packageJsonPath = `${projectRoot}/package.json`;
3211
+ // Read package.json
3212
+ let content;
3213
+ try {
3214
+ content = tree.read(packageJsonPath, 'utf-8') ?? '';
3215
+ if (!content) {
3216
+ return {
3217
+ status: 'failed',
3218
+ error: createError('package.json not found'),
3219
+ message: 'Could not read package.json',
3220
+ };
3221
+ }
3222
+ }
3223
+ catch (error) {
3224
+ return {
3225
+ status: 'failed',
3226
+ error: error instanceof Error ? error : createError(String(error)),
3227
+ message: 'Failed to read package.json',
3228
+ };
3229
+ }
3230
+ // Parse and update version
3231
+ let pkg;
3232
+ try {
3233
+ pkg = parse(content);
3234
+ }
3235
+ catch (error) {
3236
+ return {
3237
+ status: 'failed',
3238
+ error: error instanceof Error ? error : createError(String(error)),
3239
+ message: 'Failed to parse package.json',
3240
+ };
3241
+ }
3242
+ pkg['version'] = nextVersion;
3243
+ // Write back with preserved formatting
3244
+ const updated = stringify(pkg, null, 2) + '\n';
3245
+ tree.write(packageJsonPath, updated);
3246
+ logger.info(`Updated package.json: ${currentVersion} → ${nextVersion}`);
3247
+ return {
3248
+ status: 'success',
3249
+ stateUpdates: {
3250
+ modifiedFiles: [...(state.modifiedFiles ?? []), packageJsonPath],
3251
+ },
3252
+ message: `Updated version to ${nextVersion}`,
3253
+ };
3254
+ }, {
3255
+ dependsOn: ['calculate-bump'],
3256
+ });
3257
+ }
3258
+ /**
3259
+ * Creates a step that updates dependent packages in a monorepo.
3260
+ *
3261
+ * This step cascades version updates to packages that depend
3262
+ * on the updated package.
3263
+ *
3264
+ * @returns A FlowStep that cascades dependency updates
3265
+ */
3266
+ function createCascadeDependenciesStep() {
3267
+ return createStep('cascade-dependencies', 'Cascade Dependency Updates', async (ctx) => {
3268
+ const { config, state, logger } = ctx;
3269
+ const { nextVersion, bumpType } = state;
3270
+ // Skip if dependency tracking not enabled
3271
+ if (!config.trackDeps) {
3272
+ return createSkippedResult('Dependency tracking not enabled');
3273
+ }
3274
+ // Skip if no bump needed
3275
+ if (!nextVersion || bumpType === 'none') {
3276
+ return createSkippedResult('No version bump to cascade');
3277
+ }
3278
+ // In a full implementation, this would:
3279
+ // 1. Use workspace discovery to find dependent packages
3280
+ // 2. Update their dependency references
3281
+ // 3. Track the modified files
3282
+ logger.warn('Cascade dependencies step is not fully implemented yet');
3283
+ return {
3284
+ status: 'success',
3285
+ message: 'Dependency cascade skipped (not fully implemented)',
3286
+ };
3287
+ }, {
3288
+ dependsOn: ['update-packages'],
3289
+ });
3290
+ }
3291
+
3292
+ /**
3293
+ * Interpolates template variables in a string.
3294
+ *
3295
+ * Supports: ${projectName}, ${packageName}, ${version}
3296
+ *
3297
+ * @param template - Template string with ${var} placeholders
3298
+ * @param vars - Variable values
3299
+ * @returns Interpolated string
3300
+ */
3301
+ function interpolate(template, vars) {
3302
+ let result = template;
3303
+ for (const [key, value] of entries(vars)) {
3304
+ const placeholder = '${' + key + '}';
3305
+ let index = result.indexOf(placeholder);
3306
+ while (index !== -1) {
3307
+ result = result.slice(0, index) + value + result.slice(index + placeholder.length);
3308
+ index = result.indexOf(placeholder, index + value.length);
3309
+ }
3310
+ }
3311
+ return result;
3312
+ }
3313
+
3314
+ const CREATE_COMMIT_STEP_ID = 'create-commit';
3315
+ /**
3316
+ * Creates the create-commit step.
3317
+ *
3318
+ * This step:
3319
+ * 1. Stages modified files
3320
+ * 2. Creates a commit with the configured message
3321
+ *
3322
+ * State updates:
3323
+ * - commitHash: Hash of the created commit
3324
+ *
3325
+ * @returns A FlowStep that creates a git commit
3326
+ */
3327
+ function createGitCommitStep() {
3328
+ return createStep(CREATE_COMMIT_STEP_ID, 'Create Version Commit', async (ctx) => {
3329
+ const { git, config, state, projectName, packageName, logger } = ctx;
3330
+ const { nextVersion, bumpType, modifiedFiles } = state;
3331
+ // Skip if git operations disabled
3332
+ if (config.skipGit) {
3333
+ return createSkippedResult('Git operations disabled');
3334
+ }
3335
+ // Skip if no bump needed
3336
+ if (!nextVersion || bumpType === 'none') {
3337
+ return createSkippedResult('No version bump, no commit needed');
3338
+ }
3339
+ // Skip if dry run
3340
+ if (config.dryRun) {
3341
+ const message = interpolate(config.commitMessage ?? 'chore(${projectName}): release version ${version}', {
3342
+ projectName,
3343
+ packageName,
3344
+ version: nextVersion,
3345
+ });
3346
+ return {
3347
+ status: 'success',
3348
+ message: `[DRY RUN] Would commit: "${message}"`,
3349
+ };
3350
+ }
3351
+ // Stage files
3352
+ if (modifiedFiles && modifiedFiles.length > 0) {
3353
+ try {
3354
+ git.stage(modifiedFiles);
3355
+ logger.debug(`Staged ${modifiedFiles.length} file(s)`);
3356
+ }
3357
+ catch (error) {
3358
+ return {
3359
+ status: 'failed',
3360
+ error: error instanceof Error ? error : createError(String(error)),
3361
+ message: 'Failed to stage files',
3362
+ };
3363
+ }
3364
+ }
3365
+ else {
3366
+ // Stage all changes
3367
+ try {
3368
+ git.stageAll();
3369
+ logger.debug('Staged all changes');
3370
+ }
3371
+ catch (error) {
3372
+ return {
3373
+ status: 'failed',
3374
+ error: error instanceof Error ? error : createError(String(error)),
3375
+ message: 'Failed to stage files',
3376
+ };
3377
+ }
3378
+ }
3379
+ // Create commit message
3380
+ const message = interpolate(config.commitMessage ?? 'chore(${projectName}): release version ${version}', {
3381
+ projectName,
3382
+ packageName,
3383
+ version: nextVersion,
3384
+ });
3385
+ // Create commit
3386
+ try {
3387
+ const commit = git.createCommit(message);
3388
+ logger.info(`Created commit: ${commit.hash.slice(0, 7)}`);
3389
+ return {
3390
+ status: 'success',
3391
+ stateUpdates: {
3392
+ commitHash: commit.hash,
3393
+ },
3394
+ message: `Created commit ${commit.hash.slice(0, 7)}: ${message}`,
3395
+ };
3396
+ }
3397
+ catch (error) {
3398
+ return {
3399
+ status: 'failed',
3400
+ error: error instanceof Error ? error : createError(String(error)),
3401
+ message: 'Failed to create commit',
3402
+ };
3403
+ }
3404
+ }, {
3405
+ dependsOn: ['update-packages', 'write-changelog'],
3406
+ });
3407
+ }
3408
+
3409
+ const CREATE_TAG_STEP_ID = 'create-tag';
3410
+ /**
3411
+ * Creates the create-tag step.
3412
+ *
3413
+ * This step:
3414
+ * 1. Creates an annotated git tag
3415
+ * 2. Uses the configured tag format
3416
+ *
3417
+ * State updates:
3418
+ * - tagName: Name of the created tag
3419
+ *
3420
+ * @returns A FlowStep that creates a git tag
3421
+ */
3422
+ function createTagStep() {
3423
+ return createStep(CREATE_TAG_STEP_ID, 'Create Git Tag', async (ctx) => {
3424
+ const { git, config, state, projectName, packageName, logger } = ctx;
3425
+ const { nextVersion, bumpType, changelogEntry } = state;
3426
+ // Skip if git operations disabled
3427
+ if (config.skipGit) {
3428
+ return createSkippedResult('Git operations disabled');
3429
+ }
3430
+ // Skip if tags disabled
3431
+ if (config.skipTag) {
3432
+ return createSkippedResult('Tag creation disabled');
3433
+ }
3434
+ // Skip if no bump needed
3435
+ if (!nextVersion || bumpType === 'none') {
3436
+ return createSkippedResult('No version bump, no tag needed');
3437
+ }
3438
+ // Generate tag name
3439
+ const tagName = interpolate(config.tagFormat ?? '${projectName}@${version}', {
3440
+ projectName,
3441
+ packageName,
3442
+ version: nextVersion,
3443
+ });
3444
+ // Skip if dry run
3445
+ if (config.dryRun) {
3446
+ return {
3447
+ status: 'success',
3448
+ stateUpdates: { tagName },
3449
+ message: `[DRY RUN] Would create tag: ${tagName}`,
3450
+ };
3451
+ }
3452
+ // Create tag message from changelog entry if available
3453
+ let tagMessage = `Release ${nextVersion}`;
3454
+ if (changelogEntry && changelogEntry.sections.length > 0) {
3455
+ const highlights = [];
3456
+ for (const section of changelogEntry.sections.slice(0, 3)) {
3457
+ const itemCount = section.items.length;
3458
+ highlights.push(`${section.type}: ${itemCount} change${itemCount !== 1 ? 's' : ''}`);
3459
+ }
3460
+ tagMessage = `Release ${nextVersion}\n\n${highlights.join('\n')}`;
3461
+ }
3462
+ // Create tag
3463
+ try {
3464
+ const tag = git.createTag(tagName, {
3465
+ message: tagMessage,
3466
+ });
3467
+ logger.info(`Created tag: ${tag.name}`);
3468
+ return {
3469
+ status: 'success',
3470
+ stateUpdates: {
3471
+ tagName: tag.name,
3472
+ },
3473
+ message: `Created tag: ${tagName}`,
3474
+ };
3475
+ }
3476
+ catch (error) {
3477
+ return {
3478
+ status: 'failed',
3479
+ error: error instanceof Error ? error : createError(String(error)),
3480
+ message: `Failed to create tag: ${tagName}`,
3481
+ };
3482
+ }
3483
+ }, {
3484
+ dependsOn: ['create-commit'],
3485
+ });
3486
+ }
3487
+
3488
+ /**
3489
+ * Default configuration for conventional flow.
3490
+ */
3491
+ const CONVENTIONAL_FLOW_CONFIG = {
3492
+ preset: 'conventional',
3493
+ releaseTypes: ['feat', 'fix', 'perf', 'revert'],
3494
+ minorTypes: ['feat'],
3495
+ patchTypes: ['fix', 'perf', 'revert'],
3496
+ skipGit: false,
3497
+ skipTag: true, // Tags typically created after publish
3498
+ skipChangelog: false,
3499
+ dryRun: false,
3500
+ commitMessage: 'chore(${projectName}): release version ${version}',
3501
+ tagFormat: '${projectName}@${version}',
3502
+ trackDeps: false,
3503
+ firstReleaseVersion: '0.1.0',
3504
+ };
3505
+ /**
3506
+ * Creates a conventional flow.
3507
+ *
3508
+ * This flow follows the standard conventional commits workflow:
3509
+ * 1. Fetch published version from registry
3510
+ * 2. Analyze commits since last release
3511
+ * 3. Calculate version bump based on commit types
3512
+ * 4. Check if version already published (idempotency)
3513
+ * 5. Generate changelog entry
3514
+ * 6. Update package.json version
3515
+ * 7. Write changelog to file
3516
+ * 8. Create git commit (optional)
3517
+ * 9. Create git tag (optional, typically after publish)
3518
+ *
3519
+ * @param config - Optional configuration overrides
3520
+ * @returns A VersionFlow configured for conventional commits
3521
+ *
3522
+ * @example
3523
+ * ```typescript
3524
+ * import { createConventionalFlow, executeFlow } from '@hyperfrontend/versioning'
3525
+ *
3526
+ * // Use defaults
3527
+ * const flow = createConventionalFlow()
3528
+ *
3529
+ * // With overrides
3530
+ * const customFlow = createConventionalFlow({
3531
+ * skipTag: false,
3532
+ * releaseTypes: ['feat', 'fix'],
3533
+ * })
3534
+ *
3535
+ * const result = await executeFlow(flow, 'lib-utils', '/workspace')
3536
+ * ```
3537
+ */
3538
+ function createConventionalFlow(config) {
3539
+ const mergedConfig = { ...CONVENTIONAL_FLOW_CONFIG, ...config };
3540
+ return createFlow('conventional', 'Conventional Commits Flow', [
3541
+ createFetchRegistryStep(),
3542
+ createAnalyzeCommitsStep(),
3543
+ createCalculateBumpStep(),
3544
+ createCheckIdempotencyStep(),
3545
+ createGenerateChangelogStep(),
3546
+ createUpdatePackageStep(),
3547
+ createWriteChangelogStep(),
3548
+ createGitCommitStep(),
3549
+ createTagStep(),
3550
+ ], {
3551
+ description: 'Standard versioning using conventional commits specification',
3552
+ config: mergedConfig,
3553
+ });
3554
+ }
3555
+ /**
3556
+ * Creates a minimal flow for quick releases.
3557
+ *
3558
+ * Skips changelog and tag creation.
3559
+ *
3560
+ * @param config - Optional configuration overrides
3561
+ * @returns A minimal VersionFlow
3562
+ */
3563
+ function createMinimalFlow(config) {
3564
+ return createConventionalFlow({
3565
+ skipChangelog: true,
3566
+ skipTag: true,
3567
+ ...config,
3568
+ });
3569
+ }
3570
+ /**
3571
+ * Creates a changelog-only flow.
3572
+ *
3573
+ * Only generates changelog, no version bumps or git operations.
3574
+ *
3575
+ * @param config - Optional configuration overrides
3576
+ * @returns A VersionFlow that only updates changelog
3577
+ */
3578
+ function createChangelogOnlyFlow(config) {
3579
+ return createFlow('changelog-only', 'Changelog Only Flow', [
3580
+ createFetchRegistryStep(),
3581
+ createAnalyzeCommitsStep(),
3582
+ createCalculateBumpStep(),
3583
+ createGenerateChangelogStep(),
3584
+ createWriteChangelogStep(),
3585
+ ], {
3586
+ description: 'Generate changelog without version bump or git operations',
3587
+ config: {
3588
+ ...CONVENTIONAL_FLOW_CONFIG,
3589
+ skipGit: true,
3590
+ skipTag: true,
3591
+ ...config,
3592
+ },
3593
+ });
3594
+ }
3595
+
3596
+ /**
3597
+ * Default configuration for independent flow.
3598
+ */
3599
+ const INDEPENDENT_FLOW_CONFIG = {
3600
+ ...CONVENTIONAL_FLOW_CONFIG,
3601
+ preset: 'independent',
3602
+ trackDeps: true,
3603
+ };
3604
+ /**
3605
+ * Creates a step that checks for dependent package bumps.
3606
+ *
3607
+ * In independent versioning, when a dependency is bumped,
3608
+ * dependents may also need version bumps.
3609
+ *
3610
+ * @returns A FlowStep that checks dependent bumps
3611
+ */
3612
+ function createCheckDependentBumpsStep() {
3613
+ return createStep('check-dependent-bumps', 'Check Dependent Bumps', async (ctx) => {
3614
+ const { config, state, logger } = ctx;
3615
+ const { bumpType, nextVersion } = state;
3616
+ // Skip if dependency tracking not enabled
3617
+ if (!config.trackDeps) {
3618
+ return createSkippedResult('Dependency tracking not enabled');
3619
+ }
3620
+ // Skip if no bump needed for this package
3621
+ if (!nextVersion || bumpType === 'none') {
3622
+ return createSkippedResult('No bump to propagate');
3623
+ }
3624
+ // In a full implementation, this would:
3625
+ // 1. Load workspace dependency graph
3626
+ // 2. Find packages that depend on this one
3627
+ // 3. Mark them for potential bumps
3628
+ logger.debug('Dependent bump checking not fully implemented');
3629
+ return {
3630
+ status: 'success',
3631
+ message: 'Dependent bump check complete (basic implementation)',
3632
+ };
3633
+ }, {
3634
+ dependsOn: ['calculate-bump'],
3635
+ });
3636
+ }
3637
+ /**
3638
+ * Creates an independent versioning flow.
3639
+ *
3640
+ * This flow is designed for monorepos where each package
3641
+ * is versioned independently:
3642
+ *
3643
+ * 1. Fetch published version from registry
3644
+ * 2. Analyze commits since last release
3645
+ * 3. Calculate version bump based on commit types
3646
+ * 4. Check for dependent package bumps (cascade)
3647
+ * 5. Check if version already published (idempotency)
3648
+ * 6. Generate changelog entry
3649
+ * 7. Update package.json version
3650
+ * 8. Cascade dependency updates
3651
+ * 9. Write changelog to file
3652
+ * 10. Create git commit
3653
+ * 11. Create git tag
3654
+ *
3655
+ * @param config - Optional configuration overrides
3656
+ * @returns A VersionFlow configured for independent versioning
3657
+ *
3658
+ * @example
3659
+ * ```typescript
3660
+ * import { createIndependentFlow, executeFlow } from '@hyperfrontend/versioning'
3661
+ *
3662
+ * const flow = createIndependentFlow()
3663
+ * const result = await executeFlow(flow, 'lib-utils', '/workspace')
3664
+ *
3665
+ * // Check which dependents were bumped
3666
+ * console.log(result.state.cascadedBumps)
3667
+ * ```
3668
+ */
3669
+ function createIndependentFlow(config) {
3670
+ const mergedConfig = { ...INDEPENDENT_FLOW_CONFIG, ...config };
3671
+ return createFlow('independent', 'Independent Versioning Flow', [
3672
+ createFetchRegistryStep(),
3673
+ createAnalyzeCommitsStep(),
3674
+ createCalculateBumpStep(),
3675
+ createCheckDependentBumpsStep(),
3676
+ createCheckIdempotencyStep(),
3677
+ createGenerateChangelogStep(),
3678
+ createUpdatePackageStep(),
3679
+ createCascadeDependenciesStep(),
3680
+ createWriteChangelogStep(),
3681
+ createGitCommitStep(),
3682
+ createTagStep(),
3683
+ ], {
3684
+ description: 'Version packages independently with dependency tracking',
3685
+ config: mergedConfig,
3686
+ });
3687
+ }
3688
+ /**
3689
+ * Creates a flow for releasing multiple packages independently.
3690
+ *
3691
+ * This is a variant that skips commit/tag creation, intended
3692
+ * to be used when releasing multiple packages in sequence
3693
+ * with a single commit at the end.
3694
+ *
3695
+ * @param config - Optional configuration overrides
3696
+ * @returns A VersionFlow for batch independent releases
3697
+ */
3698
+ function createBatchReleaseFlow(config) {
3699
+ return createFlow('batch-release', 'Batch Release Flow', [
3700
+ createFetchRegistryStep(),
3701
+ createAnalyzeCommitsStep(),
3702
+ createCalculateBumpStep(),
3703
+ createCheckIdempotencyStep(),
3704
+ createGenerateChangelogStep(),
3705
+ createUpdatePackageStep(),
3706
+ createWriteChangelogStep(),
3707
+ ], {
3708
+ description: 'Prepare release without committing (for batch releases)',
3709
+ config: {
3710
+ ...INDEPENDENT_FLOW_CONFIG,
3711
+ skipGit: true,
3712
+ skipTag: true,
3713
+ ...config,
3714
+ },
3715
+ });
3716
+ }
3717
+
3718
+ /**
3719
+ * Default configuration for synced flow.
3720
+ */
3721
+ const SYNCED_FLOW_CONFIG = {
3722
+ ...CONVENTIONAL_FLOW_CONFIG,
3723
+ preset: 'synced',
3724
+ tagFormat: 'v${version}', // Single tag for all packages
3725
+ commitMessage: 'chore: release version ${version}',
3726
+ };
3727
+ /**
3728
+ * Creates a step that updates all workspace packages to the same version.
3729
+ *
3730
+ * @returns A FlowStep that syncs all package versions
3731
+ */
3732
+ function createSyncAllPackagesStep() {
3733
+ return createStep('sync-all-packages', 'Sync All Package Versions', async (ctx) => {
3734
+ const { tree, workspaceRoot, state, logger } = ctx;
3735
+ const { nextVersion, bumpType } = state;
3736
+ // Skip if no bump needed
3737
+ if (!nextVersion || bumpType === 'none') {
3738
+ return createSkippedResult('No version bump needed');
3739
+ }
3740
+ // In a full implementation, this would:
3741
+ // 1. Discover all workspace packages
3742
+ // 2. Update each package.json to the same version
3743
+ // 3. Update cross-references between packages
3744
+ // For now, just update the root package.json as a demo
3745
+ const rootPackageJson = `${workspaceRoot}/package.json`;
3746
+ const modifiedFiles = [];
3747
+ try {
3748
+ const content = tree.read(rootPackageJson, 'utf-8');
3749
+ if (content) {
3750
+ const pkg = parse(content);
3751
+ pkg['version'] = nextVersion;
3752
+ tree.write(rootPackageJson, stringify(pkg, null, 2) + '\n');
3753
+ modifiedFiles.push(rootPackageJson);
3754
+ logger.info(`Updated root package.json to ${nextVersion}`);
3755
+ }
3756
+ }
3757
+ catch (error) {
3758
+ logger.warn(`Could not update root package.json: ${error}`);
3759
+ }
3760
+ return {
3761
+ status: 'success',
3762
+ stateUpdates: {
3763
+ modifiedFiles: [...(state.modifiedFiles ?? []), ...modifiedFiles],
3764
+ },
3765
+ message: `Synced ${modifiedFiles.length} package(s) to version ${nextVersion}`,
3766
+ };
3767
+ }, {
3768
+ dependsOn: ['calculate-bump'],
3769
+ });
3770
+ }
3771
+ /**
3772
+ * Creates a step that generates a combined changelog for all packages.
3773
+ *
3774
+ * @returns A FlowStep that creates a combined changelog
3775
+ */
3776
+ function createCombinedChangelogStep() {
3777
+ return createStep('combined-changelog', 'Generate Combined Changelog', async (ctx) => {
3778
+ const { config, state, logger } = ctx;
3779
+ const { nextVersion, bumpType } = state;
3780
+ // Skip if no bump or changelog disabled
3781
+ if (!nextVersion || bumpType === 'none' || config.skipChangelog) {
3782
+ return createSkippedResult('No changelog to generate');
3783
+ }
3784
+ // In a synced flow, we generate a single changelog at the workspace root
3785
+ // This is a simplified version - full implementation would aggregate
3786
+ // commits from all packages
3787
+ logger.info('Generating combined changelog for workspace');
3788
+ // Reuse the standard changelog generation
3789
+ // The actual entry is generated by createGenerateChangelogStep
3790
+ return {
3791
+ status: 'success',
3792
+ message: 'Combined changelog preparation complete',
3793
+ };
3794
+ }, {
3795
+ dependsOn: ['check-idempotency'],
3796
+ });
3797
+ }
3798
+ /**
3799
+ * Creates a synced versioning flow.
3800
+ *
3801
+ * This flow maintains the same version across all packages
3802
+ * in a monorepo. When any package changes, all packages
3803
+ * get the same new version.
3804
+ *
3805
+ * Flow steps:
3806
+ * 1. Fetch published version from registry
3807
+ * 2. Analyze commits across all packages
3808
+ * 3. Calculate version bump (highest needed)
3809
+ * 4. Check if version already published
3810
+ * 5. Sync all package versions
3811
+ * 6. Generate combined changelog
3812
+ * 7. Write changelog to root
3813
+ * 8. Create git commit
3814
+ * 9. Create single git tag
3815
+ *
3816
+ * @param config - Optional configuration overrides
3817
+ * @returns A VersionFlow configured for synced versioning
3818
+ *
3819
+ * @example
3820
+ * ```typescript
3821
+ * import { createSyncedFlow, executeFlow } from '@hyperfrontend/versioning'
3822
+ *
3823
+ * const flow = createSyncedFlow()
3824
+ * const result = await executeFlow(flow, 'workspace', '/workspace')
3825
+ *
3826
+ * // All packages now share the same version
3827
+ * console.log(`Released v${result.state.nextVersion}`)
3828
+ * ```
3829
+ */
3830
+ function createSyncedFlow(config) {
3831
+ const mergedConfig = { ...SYNCED_FLOW_CONFIG, ...config };
3832
+ return createFlow('synced', 'Synced Versioning Flow', [
3833
+ createFetchRegistryStep(),
3834
+ createAnalyzeCommitsStep(),
3835
+ createCalculateBumpStep(),
3836
+ createCheckIdempotencyStep(),
3837
+ createSyncAllPackagesStep(),
3838
+ createCombinedChangelogStep(),
3839
+ createGenerateChangelogStep(),
3840
+ createWriteChangelogStep(),
3841
+ createGitCommitStep(),
3842
+ createTagStep(),
3843
+ ], {
3844
+ description: 'Keep all packages at the same version',
3845
+ config: mergedConfig,
3846
+ });
3847
+ }
3848
+ /**
3849
+ * Creates a fixed versioning flow.
3850
+ *
3851
+ * Similar to synced, but uses a fixed version scheme
3852
+ * where the version is explicitly provided rather than
3853
+ * calculated from commits.
3854
+ *
3855
+ * @param version - The fixed version to use
3856
+ * @param config - Optional configuration overrides
3857
+ * @returns A VersionFlow with a fixed version
3858
+ */
3859
+ function createFixedVersionFlow(version, config) {
3860
+ // Override the calculate-bump step to use fixed version
3861
+ const fixedBumpStep = createStep('calculate-bump', 'Use Fixed Version', async () => ({
3862
+ status: 'success',
3863
+ stateUpdates: {
3864
+ bumpType: 'minor', // Arbitrary, version is fixed
3865
+ nextVersion: version,
3866
+ },
3867
+ message: `Using fixed version: ${version}`,
3868
+ }), {
3869
+ dependsOn: ['analyze-commits'],
3870
+ });
3871
+ return createFlow('fixed', 'Fixed Version Flow', [
3872
+ createFetchRegistryStep(),
3873
+ createAnalyzeCommitsStep(),
3874
+ fixedBumpStep,
3875
+ createCheckIdempotencyStep(),
3876
+ createSyncAllPackagesStep(),
3877
+ createCombinedChangelogStep(),
3878
+ createGenerateChangelogStep(),
3879
+ createWriteChangelogStep(),
3880
+ createGitCommitStep(),
3881
+ createTagStep(),
3882
+ ], {
3883
+ description: `Release with fixed version ${version}`,
3884
+ config: { ...SYNCED_FLOW_CONFIG, ...config },
3885
+ });
3886
+ }
3887
+
3888
+ export { CONVENTIONAL_FLOW_CONFIG, INDEPENDENT_FLOW_CONFIG, SYNCED_FLOW_CONFIG, createBatchReleaseFlow, createChangelogOnlyFlow, createCheckDependentBumpsStep, createCombinedChangelogStep, createConventionalFlow, createFixedVersionFlow, createIndependentFlow, createMinimalFlow, createSyncAllPackagesStep, createSyncedFlow };
3889
+ //# sourceMappingURL=index.esm.js.map