Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/package-e2e/versioning/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"pack-install": "cd ../../../dist/libs/versioning && npm pack && cd - && npm install ../../../dist/libs/versioning/*.tgz"
},
"dependencies": {
"@hyperfrontend/versioning": "file:../../../tmp/e2e-packs/hyperfrontend-versioning-0.6.2.tgz"
"@hyperfrontend/versioning": "file:../../../tmp/e2e-packs/hyperfrontend-versioning-0.6.3.tgz"
},
"devDependencies": {
"@types/jest": "29.5.14",
Expand Down
2 changes: 1 addition & 1 deletion libs/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"@hyperfrontend/logging": "0.1.1",
"@hyperfrontend/immutable-api-utils": "0.1.4",
"@hyperfrontend/project-scope": "0.2.4",
"@hyperfrontend/versioning": "0.6.2",
"@hyperfrontend/versioning": "0.6.3",
"@rollup/plugin-commonjs": "29.0.0",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "16.0.3",
Expand Down
14 changes: 7 additions & 7 deletions libs/features/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to this project will be documented in this file.

## [0.7.1](https://github.com/AndrewRedican/hyperfrontend/compare/ee192ea7bb541593e6590243a08a29b68d69a1ac...4943d02ebe5bc10fc471f4d6a96c6b87812d0b76) - 2026-08-19
## [0.7.1](https://github.com/AndrewRedican/hyperfrontend/compare/ee192ea7bb541593e6590243a08a29b68d69a1ac...514941cca7b348b15dbc663174874f6237fc4f06) - 2026-08-20

### Bug Fixes

Expand Down Expand Up @@ -66,25 +66,25 @@ All notable changes to this project will be documented in this file.

### Breaking Changes

- **BREAKING** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** coordinate presentation host-side with contract-declared display modes
- **BREAKING** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** throw when registering v2 security without a shared key
- **BREAKING** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** is Open stays false after open() until the handshake completes; ready() rejects when no host answers
- **BREAKING** coordinate presentation host-side with contract-declared display modes
- **BREAKING** throw when registering v2 security without a shared key
- **BREAKING** is Open stays false after open() until the handshake completes; ready() rejects when no host answers

### Features

- adopt a reloaded feature on its existing mount
- **BREAKING** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** ⚠️ BREAKING: coordinate presentation host-side with contract-declared display modes
- **BREAKING** coordinate presentation host-side with contract-declared display modes
- delegate frame permissions and add host-only sandbox containment
- add four-state liveness, closing flush window, and dirty state
- validate action payloads on send and receive
- announce contract versions and gate compatibility
- add hostee security options and shared registration
- add allow-open option to the nx build executor
- **BREAKING** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** ⚠️ BREAKING: adopt asynchronous wire-gated open with origin pinning and timeout errors
- **BREAKING** adopt asynchronous wire-gated open with origin pinning and timeout errors

### Bug Fixes

- **BREAKING** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** **BREAKING:** ⚠️ BREAKING: throw when registering v2 security without a shared key
- **BREAKING** throw when registering v2 security without a shared key

## [0.2.0](https://github.com/AndrewRedican/hyperfrontend/compare/6b5a02be62850b0509b9fd71ad9232655cf5fbbf...47a37497608ac765af3efb30f0b5e01950bae425) - 2026-07-05

Expand Down
2 changes: 1 addition & 1 deletion libs/features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@hyperfrontend/project-scope": "0.2.4",
"@hyperfrontend/questions": "0.3.0",
"@hyperfrontend/ui-utils": "0.0.6",
"@hyperfrontend/versioning": "0.6.2",
"@hyperfrontend/versioning": "0.6.3",
"tslib": "2.8.1",
"typescript": "5.9.3"
},
Expand Down
2 changes: 2 additions & 0 deletions libs/features/src/host/lifecycle.liveness.browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const TARGET = <Window>(<unknown>{ name: 'target' })

/**
* Drives this page's reported visibility, the way a phone backgrounding the tab does.
*
* @param value - The state `document.visibilityState` should report.
*/
function setPageVisibility(value: 'visible' | 'hidden'): void {
Expand All @@ -50,6 +51,7 @@ function setPageVisibility(value: 'visible' | 'hidden'): void {
/**
* Builds a shell wired to the real watchdog and the real visibility observer,
* mounted as an in-document frame.
*
* @returns The handle, its channel double, and the errors it emitted.
*/
function setup() {
Expand Down
4 changes: 2 additions & 2 deletions libs/nexus/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file.

### Breaking Changes

- **BREAKING:** open fires only after a counterpr completes the handshake; unanswered connects time out instead of opening
- **BREAKING** open fires only after a counterpr completes the handshake; unanswered connects time out instead of opening

### Features

Expand All @@ -22,7 +22,7 @@ All notable changes to this project will be documented in this file.
- gate the handshake on a contract compatibility rule
- activate security negotiation and transport attachment
- rebuild the security transport on the real wire protocol and export it
- **BREAKING** **BREAKING:** ⚠️ BREAKING: replace channel self-activation with the wire handshake, pinned origins, and connect deadline
- **BREAKING** replace channel self-activation with the wire handshake, pinned origins, and connect deadline

### Bug Fixes

Expand Down
8 changes: 8 additions & 0 deletions libs/versioning/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to this project will be documented in this file.

## [0.6.3](https://github.com/AndrewRedican/hyperfrontend/compare/74110dd15151bfc8360ef4edcdb1086cb003c909...514941cca7b348b15dbc663174874f6237fc4f06) - 2026-08-20

### Bug Fixes

- skip references the description already carries
- stop duplicating the breaking marker in item text
- lift breaking markers off changelog item text

## [0.6.2](https://github.com/AndrewRedican/hyperfrontend/compare/5f116abb8ba6355dfb283fa03b7481e5eb029480...47a37497608ac765af3efb30f0b5e01950bae425) - 2026-07-05

### Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion libs/versioning/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperfrontend/versioning",
"version": "0.6.2",
"version": "0.6.3",
"description": "Versioning library with changelog parsing, conventional commits, and semver flow orchestration.",
"dependencies": {
"@hyperfrontend/immutable-api-utils": "0.1.4",
Expand Down
2 changes: 1 addition & 1 deletion libs/versioning/src/changelog/parse/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

Tokenizer and parser for converting Keep-a-Changelog markdown into structured `Changelog` objects.

`tokenize` walks markdown character-by-character to emit a typed `Token[]` stream: headings, list items, links, and section markers. `parseChangelog` consumes that stream and assembles entries, sections, and items in their declared order. Per-line helpers (`parseVersionFromHeading`, `parseCommitRefs`, `parseIssueRefs`, `parseScopeFromItem`) extract the structured fragments embedded in entry headers and bullet items. No regex is used anywhere in the parsing pipeline, keeping behavior linear in input length and free of catastrophic backtracking risk.
`tokenize` walks markdown character-by-character to emit a typed `Token[]` stream: headings, list items, links, and section markers. `parseChangelog` consumes that stream and assembles entries, sections, and items in their declared order. Per-line helpers (`parseVersionFromHeading`, `parseCommitRefs`, `parseIssueRefs`, `parseBreakingFromItem`, `parseScopeFromItem`) extract the structured fragments embedded in entry headers and bullet items. `parseBreakingFromItem` lifts the breaking-change markers an item opens with onto the item's `breaking` flag, which is what keeps a parse then serialize round trip a fixpoint: the serializer re-emits exactly one marker, and any run of markers left by an earlier round trip collapses back to that one. No regex is used anywhere in the parsing pipeline, keeping behavior linear in input length and free of catastrophic backtracking risk.
97 changes: 97 additions & 0 deletions libs/versioning/src/changelog/parse/breaking.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { parseBreakingFromItem } from './breaking'

describe('parseBreakingFromItem', () => {
it('lifts the bold marker off the text', () => {
expect(parseBreakingFromItem('**BREAKING** drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a bold marker with the colon inside', () => {
expect(parseBreakingFromItem('**BREAKING:** drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a bold marker with the colon outside', () => {
expect(parseBreakingFromItem('**BREAKING**: drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a bracketed marker', () => {
expect(parseBreakingFromItem('[BREAKING] drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a bracketed marker with a trailing colon', () => {
expect(parseBreakingFromItem('[BREAKING]: drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a warning-sign marker', () => {
expect(parseBreakingFromItem('⚠️ BREAKING: drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a warning sign written without its variation selector', () => {
expect(parseBreakingFromItem('⚠ BREAKING: drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a warning sign written without a following space', () => {
expect(parseBreakingFromItem('⚠️BREAKING: drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a marker that runs straight into the description', () => {
expect(parseBreakingFromItem('**BREAKING:**drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts a marker that is the whole item', () => {
expect(parseBreakingFromItem('**BREAKING**')).toEqual({ breaking: true, text: '' })
})

it('lifts a bare marker', () => {
expect(parseBreakingFromItem('BREAKING: drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts the BREAKING CHANGE spelling', () => {
expect(parseBreakingFromItem('BREAKING CHANGE: drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('lifts the BREAKING CHANGES spelling', () => {
expect(parseBreakingFromItem('**BREAKING CHANGES:** drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('matches the marker case-insensitively', () => {
expect(parseBreakingFromItem('**breaking:** drop the sync open')).toEqual({ breaking: true, text: 'drop the sync open' })
})

it('collapses a run of markers left by earlier round trips', () => {
expect(parseBreakingFromItem('**BREAKING** **BREAKING:** **BREAKING:** ⚠️ BREAKING: drop the sync open')).toEqual({
breaking: true,
text: 'drop the sync open',
})
})

it('leaves an unmarked item alone', () => {
expect(parseBreakingFromItem('add a retry budget')).toEqual({ breaking: false, text: 'add a retry budget' })
})

it('leaves prose that merely opens with the word alone', () => {
expect(parseBreakingFromItem('Breaking apart the parser')).toEqual({ breaking: false, text: 'Breaking apart the parser' })
})

it('leaves a scope prefix alone', () => {
expect(parseBreakingFromItem('**api:** add an endpoint')).toEqual({ breaking: false, text: '**api:** add an endpoint' })
})

it('leaves an unterminated bold marker alone', () => {
expect(parseBreakingFromItem('**BREAKING drop the sync open')).toEqual({ breaking: false, text: '**BREAKING drop the sync open' })
})

it('leaves a longer word that merely starts with the marker alone', () => {
expect(parseBreakingFromItem('**BREAKINGS** drop the sync open')).toEqual({
breaking: false,
text: '**BREAKINGS** drop the sync open',
})
})

it('leaves text shorter than the marker alone', () => {
expect(parseBreakingFromItem('BRE')).toEqual({ breaking: false, text: 'BRE' })
})

it('trims surrounding whitespace', () => {
expect(parseBreakingFromItem(' **BREAKING** drop the sync open ')).toEqual({ breaking: true, text: 'drop the sync open' })
})
})
133 changes: 133 additions & 0 deletions libs/versioning/src/changelog/parse/breaking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/**
* Result of {@link parseBreakingFromItem}: the breaking flag plus the item
* text left once its leading markers are removed.
*/
export type BreakingFromItem = {
/** Whether the item opened with at least one breaking marker */
breaking: boolean
/** The item text with every leading breaking marker stripped */
text: string
}

/**
* Parses the breaking-change markers a changelog item can open with, lifting
* them out of the text and onto a flag. Serializing re-emits exactly one
* marker, so an item survives a parse then serialize round trip unchanged; a
* run of markers left behind by earlier round trips collapses back to a single
* flag.
*
* Recognized openers, case-insensitive and repeatable: `**BREAKING**`,
* `**BREAKING:**`, `[BREAKING]`, `⚠️ BREAKING:`, a bare `BREAKING:`, and the
* `BREAKING CHANGE`/`BREAKING CHANGES` spelling of each. A bare mention needs
* its colon to count, so prose such as `Breaking apart the parser` is left
* alone. Scanning is character-by-character to stay ReDoS-safe.
*
* @param text - The changelog item text, without its list marker
* @returns The breaking flag and the text that follows the markers
*
* @example Lifting markers out of item text
* ```typescript
* parseBreakingFromItem('**BREAKING** **BREAKING:** drop the sync open')
* // => { breaking: true, text: 'drop the sync open' }
*
* parseBreakingFromItem('add a retry budget')
* // => { breaking: false, text: 'add a retry budget' }
* ```
*/
export function parseBreakingFromItem(text: string): BreakingFromItem {
const trimmed = text.trim()
let pos = 0
let breaking = false

while (pos < trimmed.length) {
const next = matchBreakingMarker(trimmed, pos)
if (next === -1) break
breaking = true
pos = next
}

return { breaking, text: trimmed.slice(pos) }
}

/**
* Matches a single breaking marker, tolerating the bold, bracketed,
* warning-sign, and bare spellings plus any spaces that trail it.
*
* @param text - The text being scanned
* @param start - Index the marker has to start at
* @returns The index just past the marker, or -1 when none matches
*/
function matchBreakingMarker(text: string, start: number): number {
let pos = start
let closer = ''

if (text.startsWith('**', pos)) {
closer = '**'
pos += 2
} else if (text[pos] === '[') {
closer = ']'
pos += 1
} else if (text[pos] === '⚠') {
pos += 1
// why: the warning sign is usually written with an emoji variation selector
if (text[pos] === '\uFE0F') pos += 1
while (text[pos] === ' ') pos++
}

pos = matchBreakingWord(text, pos)
if (pos === -1) return -1

const colon = text[pos] === ':'
if (colon) pos += 1

if (closer.length > 0) {
if (!text.startsWith(closer, pos)) return -1
pos += closer.length
if (text[pos] === ':') pos += 1
} else if (!colon) {
return -1
}

while (text[pos] === ' ') pos++

return pos
}

/**
* Matches the literal `BREAKING`, optionally continued as `BREAKING CHANGE`
* or `BREAKING CHANGES`.
*
* @param text - The text being scanned
* @param start - Index the word has to start at
* @returns The index just past the word, or -1 when it does not match
*/
function matchBreakingWord(text: string, start: number): number {
const afterWord = matchLiteral(text, start, 'breaking')
if (afterWord === -1) return -1

const afterChanges = matchLiteral(text, afterWord, ' changes')
if (afterChanges !== -1) return afterChanges

const afterChange = matchLiteral(text, afterWord, ' change')
if (afterChange !== -1) return afterChange

return afterWord
}

/**
* Matches a lowercase literal at a position, ignoring case.
*
* @param text - The text being scanned
* @param start - Index the literal has to start at
* @param literal - The lowercase literal to match against
* @returns The index just past the literal, or -1 when it does not match
*/
function matchLiteral(text: string, start: number, literal: string): number {
if (start + literal.length > text.length) return -1

for (let i = 0; i < literal.length; i++) {
if (text[start + i]?.toLowerCase() !== literal[i]) return -1
}

return start + literal.length
}
2 changes: 2 additions & 0 deletions libs/versioning/src/changelog/parse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
*
* @module @hyperfrontend/versioning/changelog/parse
*/
export type { BreakingFromItem } from './breaking'
export type { Token, TokenType } from './tokenizer'
export { parseBreakingFromItem } from './breaking'
export { parseVersionFromHeading, parseCommitRefs, parseIssueRefs, parseScopeFromItem } from './line'
export { parseChangelog } from './parser'
export { tokenize } from './tokenizer'
14 changes: 14 additions & 0 deletions libs/versioning/src/changelog/parse/parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,20 @@ Based on [Keep a Changelog](https://keepachangelog.com).
expect(items?.[1]?.breaking).toBe(false)
})

it('reads the serialized breaking marker back off an item', () => {
const content = `## [1.0.0]

### Breaking Changes
- **BREAKING** Changed API
`

const changelog = parseChangelog(content)

expect(changelog.entries[0]?.sections[0]?.items[0]).toEqual(
expect.objectContaining({ description: 'Changed API', scope: undefined, breaking: true })
)
})

it('detects breaking change with ! prefix', () => {
const content = `## [1.0.0]

Expand Down
Loading
Loading