Skip to content

Commit 3e5e573

Browse files
authored
feat: per-package per-version changelog + /changelog page + auto-enforce on version bumps (#49)
* feat(changelog): per-package per-version changelog + backfill from git Introduce a changelog/ directory at the repo root with one md file per (package, version) tuple: changelog/ README.md architecture + conventions core/0.6.0.md one file per release server/0.7.1.md cli/0.7.0.md ts-plugin/0.4.0.md ui/0.2.0.md … The shape (per-package per-version, not per-commit) matches the way we publish: every change of a packages/<pkg>/package.json version field becomes a release entry. Each entry frontmatters package, version, date, commit_count; the body groups qualifying commits (feat / fix / breaking / perf) under Breaking / Features / Performance / Fixes headings with PR links and commit SHAs. scripts/backfill-changelog.js walks every packages/<pkg>/package.json in git history, finds the commits that changed the version field, and writes <pkg>/<version>.md for any (package, version) tuple that does not yet have an entry. Existing files are left alone so hand-curation survives re-runs. Backfilled 43 files across the 5 packages from project inception through 0.6.0 (core), 0.7.1 (server), 0.7.0 (cli), 0.4.0 (ts-plugin), and 0.2.0 (ui). * feat(website): /changelog page + Changelog nav link, responsive nav Add website/app/changelog/page.ts that reads changelog/<pkg>/*.md at SSR time, parses YAML-ish frontmatter and the minimal markdown shape the backfill generator emits (h1, h2, bulleted lists, links, inline code, bold), sorts entries by date desc, and renders one card per release with a package-coloured badge, version, date, and commit count. Add the Changelog link to the layout nav. The header now uses flex-wrap with gap-y-3 and the nav itself uses flex-wrap with gap-x-3 sm:gap-x-4 + gap-y-2 so the now-six chrome elements (Docs, UI, Changelog, Blog Demo, GitHub, theme-toggle) wrap into a second row on narrow viewports instead of overflowing. Verified locally: GET /changelog returns 200 with 156 KB of rendered HTML containing every backfilled version across all five packages. * feat(changelog): auto-enforce changelog when a package version bumps Add the "version bump triggers changelog" rule to the framework AGENTS.md and back it with two gates: 1. .hooks/pre-commit refuses any commit whose staged diff bumps a packages/<pkg>/package.json version field without a matching changelog/<pkg>/<version>.md file. Prints the exact command the author needs (node scripts/backfill-changelog.js && git add changelog/). 2. .claude/hooks/changelog-nudge.sh is a PreToolUse hook that fires on Bash tool calls whose command matches git commit. It runs the same check and emits a permissionDecision: deny JSON payload, so Claude Code refuses the commit before it leaves the agent's shell. Wired into .claude/settings.json. Both hooks are bypassable with --no-verify for emergencies. The AI agent rule in AGENTS.md instructs agents to run the generator in the same commit as the version bump, review the generated file, and edit it in place for clarity before pushing. * fix(hooks): correct off-by-one in awk slice that picked up trailing slash in pkg name * feat(website): mobile hamburger menu using native <details>/<summary> The previous flex-wrap nav let the items wrap below the logo on narrow viewports, but on iPhone XR (414px) the 5 links + theme toggle still felt congested. Replace it with a real mobile menu. Below md (768px): the logo, theme-toggle, and a hamburger button sit on one row; the hamburger is a native <details>/<summary> that pops the same 5 links into a dropdown panel. At md and up: hide the mobile cluster, show the inline nav as before. CSS strips the default disclosure triangle and swaps the hamburger / close icons via .mobile-menu[open]. Progressive enhancement: native <details> works without JS, so the menu opens / closes on every viewport even when scripts have not yet hydrated. * fix(website): close mobile menu when a link inside it is clicked Native <details> stays open on inner-anchor activation, so tapping Changelog (or any other link) on mobile kept the panel visible. Add a delegated click listener that strips the open attribute from the parent <details> when any link inside .mobile-menu is activated. Covers both regular target=_blank links and the same-origin client- router-intercepted ones. * feat(website,blog,ui): unify mobile-menu pattern across the three apps webjs.dev, example-blog, and ui.webjs.dev now share the same mobile header layout: hamburger LEFT, theme-toggle RIGHT, with a native <details>/<summary> dropdown panel anchored to the hamburger. - webjs website: swap the order so the hamburger sits LEFT of the theme-toggle (was the reverse). - example blog: drop the old fixed-drawer + body[data-menu-open] pattern in favour of the dropdown. Removes ~30 lines of CSS and matches the rest of the chrome surfaces. The mobile cluster is sm:hidden because the blog's existing inline nav is sm:flex. - ui.webjs.dev: introduce the same dropdown pattern from scratch. The "Webjs" cross-link, previously hidden on small screens via hidden sm:inline, now lives in the dropdown panel where it's reachable on phones. Docs site keeps its existing left-rail-drawer pattern (intentionally untouched, since the docs UI needs a full sidebar, not a dropdown). Same delegated click handler in every layout: any anchor activation inside .mobile-menu strips the open attribute on the parent <details>, so the panel auto-closes on navigation. * fix(website,blog,ui): close mobile menu on outside click too Tapping outside the open dropdown should dismiss it on a phone; previously only the close icon (the toggled summary) closed it, which felt off because the user has nothing visually grabbing their attention to that one tap target. Extend the existing delegated click handler in all three layouts to walk `.mobile-menu[open]` and strip the open attribute on any that don't contain the click target. The link-click branch is unchanged: links inside the panel still close the panel directly. Docs site keeps its own drawer pattern; this change does not touch it.
1 parent 25452ab commit 3e5e573

53 files changed

Lines changed: 1930 additions & 67 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/hooks/changelog-nudge.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/bin/bash
2+
#
3+
# Claude Code PreToolUse hook.
4+
#
5+
# Catches the case where the agent is ABOUT to commit a change that
6+
# bumps a packages/<pkg>/package.json version but has not also
7+
# generated a matching changelog/<pkg>/<version>.md file. Fires on
8+
# Bash tool calls whose command starts with `git commit`.
9+
#
10+
# Hard block: writes a JSON hookSpecificOutput with
11+
# "permissionDecision": "deny" so the tool call is refused and the
12+
# agent reads the included reason. The agent should then run:
13+
#
14+
# node scripts/backfill-changelog.js && git add changelog/
15+
#
16+
# and retry the commit.
17+
#
18+
# Skipped outside a git work tree and when there are no staged
19+
# version bumps.
20+
21+
set -e
22+
23+
# Read the hook payload from stdin so we can inspect the command
24+
# the model is about to run.
25+
INPUT=$(cat)
26+
27+
# Only act on Bash tool calls whose command is `git commit ...`.
28+
COMMAND=$(printf '%s' "$INPUT" | grep -oE '"command"\s*:\s*"[^"]+"' | head -1 | sed 's/.*: *"\(.*\)".*/\1/')
29+
case "$COMMAND" in
30+
*git*commit*) ;;
31+
*) exit 0 ;;
32+
esac
33+
34+
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
35+
exit 0
36+
fi
37+
38+
STAGED_PKG_BUMPS=$(git diff --cached --unified=0 -- 'packages/*/package.json' 2>/dev/null \
39+
| awk '
40+
/^diff --git/ { match($0, /packages\/[^\/]+\/package.json/); pkg = substr($0, RSTART+9, RLENGTH-22); next }
41+
pkg && /^\+\s*"version":\s*"[^"]+"/ {
42+
match($0, /"[0-9]+\.[0-9]+\.[0-9]+[^"]*"/); v = substr($0, RSTART+1, RLENGTH-2);
43+
print pkg "@" v
44+
}')
45+
46+
if [ -z "$STAGED_PKG_BUMPS" ]; then
47+
exit 0
48+
fi
49+
50+
MISSING=""
51+
for bump in $STAGED_PKG_BUMPS; do
52+
pkg="${bump%@*}"; ver="${bump#*@}"
53+
if [ ! -f "changelog/$pkg/$ver.md" ]; then
54+
MISSING="$MISSING changelog/$pkg/$ver.md"
55+
fi
56+
done
57+
58+
if [ -z "$MISSING" ]; then
59+
exit 0
60+
fi
61+
62+
# Emit a JSON denial so Claude Code refuses this tool call and
63+
# surfaces the reason to the agent.
64+
cat <<EOF
65+
{
66+
"hookSpecificOutput": {
67+
"permissionDecision": "deny",
68+
"permissionDecisionReason": "[changelog-nudge] You are committing a packages/<pkg>/package.json version bump but the matching changelog file(s) are missing:$MISSING\n\nRun: node scripts/backfill-changelog.js && git add changelog/\nThen retry the commit. To bypass (rare; emergencies only): git commit --no-verify"
69+
}
70+
}
71+
EOF
72+
exit 0

.claude/settings.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,15 @@
99
"command": ".claude/hooks/block-prose-punctuation.sh"
1010
}
1111
]
12+
},
13+
{
14+
"matcher": "Bash",
15+
"hooks": [
16+
{
17+
"type": "command",
18+
"command": ".claude/hooks/changelog-nudge.sh"
19+
}
20+
]
1221
}
1322
],
1423
"PostToolUse": [

.hooks/pre-commit

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,41 @@ if ! npm test --silent; then
3232
exit 1
3333
fi
3434

35+
# Gate 3: when a packages/<pkg>/package.json version bumped, require
36+
# a matching changelog/<pkg>/<version>.md to land in the same commit.
37+
# The "diff --cached" check pulls the new version line from the
38+
# staged package.json; the directory existence test catches the
39+
# matching changelog entry. Re-runs `scripts/backfill-changelog.js`
40+
# so an author who forgets only loses time, not the commit.
41+
STAGED_PKG_BUMPS=$(git diff --cached --unified=0 -- 'packages/*/package.json' 2>/dev/null \
42+
| awk '
43+
/^diff --git/ { match($0, /packages\/[^\/]+\/package.json/); pkg = substr($0, RSTART+9, RLENGTH-22); next }
44+
pkg && /^\+\s*"version":\s*"[^"]+"/ {
45+
match($0, /"[0-9]+\.[0-9]+\.[0-9]+[^"]*"/); v = substr($0, RSTART+1, RLENGTH-2);
46+
print pkg "@" v
47+
}')
48+
49+
if [ -n "$STAGED_PKG_BUMPS" ]; then
50+
MISSING=""
51+
for bump in $STAGED_PKG_BUMPS; do
52+
pkg="${bump%@*}"; ver="${bump#*@}"
53+
if [ ! -f "changelog/$pkg/$ver.md" ]; then
54+
MISSING="$MISSING\n - changelog/$pkg/$ver.md (for @webjskit/$pkg $ver)"
55+
fi
56+
done
57+
if [ -n "$MISSING" ]; then
58+
echo ""
59+
echo "ERROR: detected version bumps in staged changes but matching changelog files are missing:"
60+
echo -e "$MISSING"
61+
echo ""
62+
echo "Run:"
63+
echo " node scripts/backfill-changelog.js"
64+
echo " git add changelog/"
65+
echo ""
66+
echo "Then re-commit. To bypass (emergencies only): git commit --no-verify"
67+
echo ""
68+
exit 1
69+
fi
70+
fi
71+
3572
exit 0

AGENTS.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ When editing the framework monorepo (this repo, not a scaffolded app): **`packag
9797

9898
See `agent-docs/framework-dev.md` for monorepo commands, workspace layout, reference codebases, and per-feature update checklists.
9999

100+
### Changelog: per-package, per-version, auto-generated
101+
102+
webjs ships per-package per-version changelogs under `changelog/<pkg>/<version>.md`. The model: **a version bump is the trigger**. When any commit on `main` changes the `version` field in `packages/<pkg>/package.json`, the scripts/backfill-changelog.js generator emits a new `changelog/<pkg>/<version>.md` summarising every conventional-commit (`feat:` / `fix:` / `breaking:` / `perf:`) that landed in that package since the prior bump. The website renders the union of all packages' files at `/changelog`.
103+
104+
**Mandatory rule for AI agents working in this monorepo:**
105+
106+
1. When you bump a `packages/<pkg>/package.json` `version`, you MUST run `node scripts/backfill-changelog.js` in the same commit (or the immediately following one), so the new `changelog/<pkg>/<version>.md` lands together with the bump.
107+
2. Review the generated file. The script's body excerpts are the first lines of each commit message; if any are unclear, **edit the file in place** to add migration notes (especially for `breaking` entries) before committing.
108+
3. Subsequent re-runs are safe: the script never overwrites an existing entry, so your hand-edits survive.
109+
4. Never edit `changelog/<pkg>/<version>.md` for a version that has already been published. Edit `changelog/<pkg>/<next>.md` instead.
110+
111+
The `.hooks/pre-commit` git hook (and the Claude Code `PreToolUse` hook `.claude/hooks/changelog-nudge.sh`) catches version bumps that lack a matching changelog file and refuses the commit until both land together.
112+
100113
---
101114

102115
## What webjs is

changelog/README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Changelog
2+
3+
webjs ships **per-package, per-version** changelog files.
4+
5+
```
6+
changelog/
7+
README.md this file
8+
core/
9+
0.6.0.md
10+
0.5.0.md
11+
12+
server/
13+
0.7.1.md
14+
15+
cli/
16+
0.7.0.md
17+
18+
ts-plugin/
19+
0.4.0.md
20+
21+
ui/
22+
0.2.0.md
23+
24+
```
25+
26+
The website (`website/app/changelog/page.ts`) reads every
27+
`<pkg>/<version>.md` at SSR time, sorts by date descending, and
28+
renders the unified release feed.
29+
30+
## How a release entry gets created
31+
32+
The model is "**a version bump produces a changelog file**". When a
33+
package's `package.json` `version` field changes in a commit, the
34+
entry script:
35+
36+
1. Identifies the prior version of the same package on `main`.
37+
2. Walks the commits between the prior version's bump and the new
38+
bump that touched files under `packages/<pkg>/`.
39+
3. Filters to conventional-commit prefixes that matter to users:
40+
`feat:`, `fix:`, `breaking:`, `perf:`.
41+
4. Groups them under `Breaking` / `Features` / `Performance` /
42+
`Fixes` and writes a `<version>.md` file with frontmatter
43+
(`package`, `version`, `date`, `commit_count`) plus a bulleted
44+
list of changes (PR link + commit SHA + body excerpt).
45+
46+
`chore:`, `refactor:`, `test:`, `docs:`, `style:`, `build:`, `ci:`
47+
commits never appear in the changelog: those changes don't change the
48+
package's user-facing contract.
49+
50+
## Entry format
51+
52+
```markdown
53+
---
54+
package: "@webjskit/core"
55+
version: 0.6.0
56+
date: 2026-05-21
57+
commit_count: 4
58+
---
59+
60+
# @webjskit/core 0.6.0
61+
62+
## Breaking
63+
64+
- **Title of the change** ([#NN](https://github.com/vivek7405/webjs/pull/NN)) [`abcd123`](https://github.com/vivek7405/webjs/commit/abcd123)
65+
First four lines of the commit body, indented two spaces.
66+
67+
## Features
68+
69+
70+
## Fixes
71+
72+
```
73+
74+
Frontmatter fields:
75+
76+
| Field | Required | Meaning |
77+
|---|---|---|
78+
| `package` | yes | The fully-qualified npm name (`@webjskit/<pkg>`). |
79+
| `version` | yes | Semver string of the new release. |
80+
| `date` | yes | Date of the version-bump commit, `YYYY-MM-DD`. |
81+
| `commit_count` | yes | How many qualifying commits this version shipped. |
82+
83+
## Backfill + ongoing automation
84+
85+
A single script runs in both modes:
86+
87+
```sh
88+
node scripts/backfill-changelog.js
89+
```
90+
91+
It walks every package's `package.json` history, finds version
92+
bumps, and writes a `<pkg>/<version>.md` for any version that does
93+
not yet have one. **Files that already exist are left alone**, so
94+
hand-curated entries survive subsequent runs (CI re-runs are safe).
95+
96+
Going forward, the same script runs:
97+
98+
- on every CI build of `main`, so a forgotten version bump
99+
still produces an entry without manual intervention;
100+
- locally when an agent edits a `packages/<pkg>/package.json` to
101+
bump the version (the post-commit hook
102+
`.hooks/post-version-bump` runs the script).
103+
104+
The AGENTS.md rule that AI agents follow on every code-commit:
105+
106+
> When you bump a `packages/<pkg>/package.json` `version`, run
107+
> `node scripts/backfill-changelog.js` (or just commit; CI will
108+
> regenerate). The generated `changelog/<pkg>/<version>.md` file
109+
> should be reviewed and **edited in-place** for clarity, especially
110+
> for `breaking` entries that need migration notes. Then commit the
111+
> changelog file alongside the version bump.
112+
113+
## What if the same change ships across packages
114+
115+
A commit that touches more than one package (e.g. a refactor that
116+
moves code from `core` to `server`) appears in **every** affected
117+
package's release file when each of those packages bumps its
118+
version. That is the right shape: the same change is a user-facing
119+
event for every package that carries it.
120+
121+
## Migration to the new format
122+
123+
The pre-0.7.1 history was backfilled in one pass. The files in
124+
`changelog/<pkg>/` are auto-generated but can (and should) be
125+
hand-edited to add migration notes, examples, or links to docs.
126+
Subsequent re-runs of `scripts/backfill-changelog.js` will not
127+
overwrite hand edits because the script skips files that already
128+
exist.

0 commit comments

Comments
 (0)