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