Skip to content

Commit 8acd90e

Browse files
Add Release History page (en + zh) from package CHANGELOGs (#64)
## TLDR Adds a Release History page (English + Chinese) listing every published dart_node version with dates and per-package changelog notes. ## What Was Added? - `website/src/_data/releases.js` — build-time data file that parses every publishable package's `CHANGELOG.md` into a version-by-version history (newest first). Publish dates are sourced from pub.dev (baked in, since the Pages build checks out shallow with no git tags). A Chinese translation overlay localizes the notes, falling back to English for any unmapped line. - `website/src/releases.njk` and `website/src/zh/releases.njk` — the `/releases/` and `/zh/releases/` pages. - "Releases" / "版本历史" added to the main nav and footer in both `navigation.json` and `navigation_zh.json`. - Element-scoped CSS for the release list (dark-mode aware via existing design tokens). ## What Was Changed or Deleted? Nothing removed. Two nav data files gain one entry each; the stylesheet gains one scoped block. ## How Do The Automated Tests Prove It Works? Full website Playwright suite passes (108/108). Both new pages build and appear in `sitemap.xml` (2 entries). Manually verified rendered output: 8 release versions with correct dates (e.g. `0.13.0-beta` → June 7, 2026 / 2026年6月7日), `dart_node_sql_js` listed under 0.13.0-beta, inline `` `fs` `` rendered as `<code>`, and the zh page fully translated (版本历史, 10 个包, 维护版本;无功能性更改). Playwright desktop + mobile screenshots show zero console errors. ## Spec / Doc Changes None. ## Breaking Changes - [x] None
1 parent 7316ecd commit 8acd90e

6 files changed

Lines changed: 383 additions & 0 deletions

File tree

website/src/_data/navigation.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"text": "Blog",
1313
"url": "/blog/"
1414
},
15+
{
16+
"text": "Releases",
17+
"url": "/releases/"
18+
},
1519
{
1620
"text": "GitHub",
1721
"url": "https://github.com/MelbourneDeveloper/dart_node",
@@ -133,6 +137,10 @@
133137
"text": "Blog",
134138
"url": "/blog/"
135139
},
140+
{
141+
"text": "Releases",
142+
"url": "/releases/"
143+
},
136144
{
137145
"text": "Dart Official",
138146
"url": "https://dart.dev"

website/src/_data/navigation_zh.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
"text": "博客",
1313
"url": "/zh/blog/"
1414
},
15+
{
16+
"text": "版本历史",
17+
"url": "/zh/releases/"
18+
},
1519
{
1620
"text": "GitHub",
1721
"url": "https://github.com/MelbourneDeveloper/dart_node",
@@ -133,6 +137,10 @@
133137
"text": "博客",
134138
"url": "/zh/blog/"
135139
},
140+
{
141+
"text": "版本历史",
142+
"url": "/zh/releases/"
143+
},
136144
{
137145
"text": "Dart 官网",
138146
"url": "https://dart.dev"

website/src/_data/releases.js

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Build-time release history, derived from the canonical package CHANGELOGs —
2+
// the same release notes the publish pipeline validates per `Release/<version>`
3+
// tag. Single source of truth: packages/<pkg>/CHANGELOG.md. Versions and per-
4+
// package notes are parsed here; publish dates come from pub.dev (baked below,
5+
// since the Pages build checks out shallow with no git tags). Chinese notes use
6+
// a translation overlay and fall back to English for any unmapped line.
7+
8+
import { readFileSync } from "fs";
9+
import { dirname, resolve, join } from "path";
10+
import { fileURLToPath } from "url";
11+
12+
const __dirname = dirname(fileURLToPath(import.meta.url));
13+
const PACKAGES_DIR = resolve(__dirname, "..", "..", "..", "packages");
14+
15+
// Publishable packages, in tier/display order.
16+
const PACKAGES = [
17+
"dart_logging",
18+
"dart_node_core",
19+
"reflux",
20+
"dart_node_express",
21+
"dart_node_ws",
22+
"dart_node_better_sqlite3",
23+
"dart_node_sql_js",
24+
"dart_node_mcp",
25+
"dart_node_react",
26+
"dart_node_react_native",
27+
];
28+
29+
// Release dates (ISO) sourced from pub.dev — earliest publish across packages
30+
// for each version. Versions absent here render without a date.
31+
const RELEASE_DATES = {
32+
"0.13.0-beta": "2026-06-07",
33+
"0.12.0-beta": "2026-01-29",
34+
"0.11.0-beta": "2025-12-13",
35+
"0.9.0-beta": "2025-12-11",
36+
"0.8.0-beta": "2025-12-04",
37+
"0.2.0-beta": "2025-12-02",
38+
"0.1.0-beta": "2025-11-30",
39+
};
40+
41+
// Chinese overlay for changelog lines. Unmapped lines fall back to English.
42+
const ZH_NOTES = {
43+
"Maintenance release; no functional changes": "维护版本;无功能性更改",
44+
"Maintenance release": "维护版本",
45+
"Maintenance release; internal test improvements": "维护版本;内部测试改进",
46+
"Enhanced test coverage": "增强测试覆盖率",
47+
"Early release": "早期版本",
48+
"Initial beta release": "首个 Beta 版本",
49+
"Version bump for upcoming release": "为即将发布的版本提升版本号",
50+
"Updated docs": "更新文档",
51+
"Improved documentation for core extensions": "改进核心扩展的文档",
52+
"Pino-style structured logging framework": "Pino 风格的结构化日志框架",
53+
"Child loggers with inherited context": "支持继承上下文的子日志记录器",
54+
"Add filesystem (`fs`) bindings": "新增文件系统(`fs`)绑定",
55+
"Add child process interop": "新增子进程互操作",
56+
"Core JS interop utilities for Dart-to-JavaScript compilation":
57+
"用于 Dart 到 JavaScript 编译的核心 JS 互操作工具",
58+
"Foundation for dart_node package family": "dart_node 包系列的基础",
59+
"Add WebSocket support via dart_node_ws package":
60+
"通过 dart_node_ws 包新增 WebSocket 支持",
61+
"Redux-style predictable state container for Dart":
62+
"面向 Dart 的 Redux 风格可预测状态容器",
63+
"Sealed classes for exhaustive action pattern matching":
64+
"使用密封类实现详尽的 action 模式匹配",
65+
"Express.js bindings for Dart": "面向 Dart 的 Express.js 绑定",
66+
"Build Node.js HTTP servers entirely in Dart":
67+
"完全使用 Dart 构建 Node.js HTTP 服务器",
68+
"Add WebSocket support integration with dart_node_ws":
69+
"新增与 dart_node_ws 的 WebSocket 集成支持",
70+
"WebSocket bindings for Dart on Node.js":
71+
"面向 Node.js 上 Dart 的 WebSocket 绑定",
72+
"Build real-time WebSocket servers entirely in Dart":
73+
"完全使用 Dart 构建实时 WebSocket 服务器",
74+
"Typed Dart bindings for better-sqlite3 npm package":
75+
"为 better-sqlite3 npm 包提供类型化的 Dart 绑定",
76+
"Synchronous SQLite3 access with WAL mode":
77+
"支持 WAL 模式的同步 SQLite3 访问",
78+
"Typed Dart bindings for sql.js (SQLite compiled to WebAssembly)":
79+
"为 sql.js(编译为 WebAssembly 的 SQLite)提供类型化的 Dart 绑定",
80+
"Synchronous in-memory SQLite with file persistence on Node.js":
81+
"在 Node.js 上支持文件持久化的同步内存 SQLite",
82+
"`Result`-based API: open, prepare, exec, run, get, all, pragma":
83+
"基于 `Result` 的 API:open、prepare、exec、run、get、all、pragma",
84+
"Add streamable HTTP transport": "新增可流式传输的 HTTP 传输层",
85+
"Additional MCP server capabilities": "新增 MCP 服务器能力",
86+
"Typed Dart bindings for @modelcontextprotocol/sdk":
87+
"为 @modelcontextprotocol/sdk 提供类型化的 Dart 绑定",
88+
"Build MCP servers in Dart that run on Node.js":
89+
"使用 Dart 构建运行于 Node.js 的 MCP 服务器",
90+
"React bindings for Dart": "面向 Dart 的 React 绑定",
91+
"Build React web applications entirely in Dart":
92+
"完全使用 Dart 构建 React Web 应用",
93+
"React Native bindings for Dart": "面向 Dart 的 React Native 绑定",
94+
"Build mobile apps with Expo entirely in Dart":
95+
"完全使用 Dart 与 Expo 构建移动应用",
96+
};
97+
98+
const EN_MONTHS = [
99+
"January", "February", "March", "April", "May", "June",
100+
"July", "August", "September", "October", "November", "December",
101+
];
102+
103+
const escapeHtml = (s) =>
104+
s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
105+
106+
// Escape, then render inline `code` spans. Content is our own changelogs.
107+
const toHtml = (s) =>
108+
escapeHtml(s).replace(/`([^`]+)`/g, "<code>$1</code>");
109+
110+
const formatDateEn = (iso) => {
111+
const [y, m, d] = iso.split("-").map((n) => parseInt(n, 10));
112+
return `${EN_MONTHS[m - 1]} ${d}, ${y}`;
113+
};
114+
115+
const formatDateZh = (iso) => {
116+
const [y, m, d] = iso.split("-").map((n) => parseInt(n, 10));
117+
return `${y}${m}${d}日`;
118+
};
119+
120+
const PRERELEASE_RANK = { alpha: 1, beta: 2 };
121+
122+
const parseVersion = (v) => {
123+
const [core, pre] = v.split("-");
124+
const [major, minor, patch] = core.split(".").map((n) => parseInt(n, 10) || 0);
125+
const rank = pre ? (PRERELEASE_RANK[pre.split(".")[0]] ?? 3) : 4;
126+
return [major, minor, patch, rank];
127+
};
128+
129+
const compareDesc = (a, b) => {
130+
const pa = parseVersion(a);
131+
const pb = parseVersion(b);
132+
for (let i = 0; i < pa.length; i += 1) {
133+
if (pa[i] !== pb[i]) return pb[i] - pa[i];
134+
}
135+
return 0;
136+
};
137+
138+
const parseChangelog = (markdown) => {
139+
const byVersion = {};
140+
let current = null;
141+
for (const line of markdown.split("\n")) {
142+
const header = line.match(/^##\s+(.+?)\s*$/);
143+
if (header) {
144+
current = header[1].trim();
145+
byVersion[current] = [];
146+
continue;
147+
}
148+
if (!current) continue;
149+
const bullet = line.match(/^[-*]\s+(.+?)\s*$/);
150+
if (bullet) byVersion[current].push(bullet[1].trim());
151+
}
152+
return byVersion;
153+
};
154+
155+
const versions = {};
156+
for (const pkg of PACKAGES) {
157+
const file = join(PACKAGES_DIR, pkg, "CHANGELOG.md");
158+
const parsed = parseChangelog(readFileSync(file, "utf-8"));
159+
for (const [version, changes] of Object.entries(parsed)) {
160+
if (changes.length === 0) continue;
161+
versions[version] ??= [];
162+
versions[version].push({
163+
name: pkg,
164+
changes: changes.map((c) => ({ en: toHtml(c), zh: toHtml(ZH_NOTES[c] ?? c) })),
165+
});
166+
}
167+
}
168+
169+
export default Object.keys(versions)
170+
.sort(compareDesc)
171+
.map((version) => {
172+
const iso = RELEASE_DATES[version] ?? null;
173+
return {
174+
version,
175+
dateISO: iso,
176+
dateEn: iso ? formatDateEn(iso) : null,
177+
dateZh: iso ? formatDateZh(iso) : null,
178+
packages: versions[version],
179+
};
180+
});

website/src/assets/css/styles.css

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,3 +1784,109 @@ a.tag.tag-secondary:hover {
17841784
.mobile-menu-toggle span {
17851785
transition: all 0.3s ease;
17861786
}
1787+
1788+
/* Releases page */
1789+
.releases {
1790+
padding: var(--space-16) 0;
1791+
}
1792+
1793+
.release-list {
1794+
list-style: none;
1795+
max-width: 820px;
1796+
margin: 0 auto;
1797+
padding: 0;
1798+
display: flex;
1799+
flex-direction: column;
1800+
gap: var(--space-8);
1801+
}
1802+
1803+
.release-list > li {
1804+
background: var(--bg-secondary);
1805+
border: 1px solid var(--border-color);
1806+
border-radius: var(--radius-lg);
1807+
box-shadow: var(--shadow-sm);
1808+
padding: var(--space-6) var(--space-8);
1809+
scroll-margin-top: var(--space-20);
1810+
}
1811+
1812+
.release-list > li > header {
1813+
display: flex;
1814+
align-items: baseline;
1815+
flex-wrap: wrap;
1816+
gap: var(--space-4);
1817+
border-bottom: 1px solid var(--border-color);
1818+
padding-bottom: var(--space-4);
1819+
margin-bottom: var(--space-5);
1820+
}
1821+
1822+
.release-list > li > header h2 {
1823+
margin: 0;
1824+
font-size: var(--text-2xl);
1825+
}
1826+
1827+
.release-list > li > header h2 a {
1828+
color: var(--text-primary);
1829+
text-decoration: none;
1830+
}
1831+
1832+
.release-list > li > header h2 a:hover {
1833+
color: var(--color-primary);
1834+
}
1835+
1836+
.release-list > li > header time {
1837+
color: var(--text-secondary);
1838+
font-size: var(--text-sm);
1839+
}
1840+
1841+
.release-list > li > header span {
1842+
margin-left: auto;
1843+
font-size: var(--text-xs);
1844+
color: var(--text-tertiary);
1845+
background: var(--bg-tertiary);
1846+
padding: var(--space-1) var(--space-3);
1847+
border-radius: var(--radius-full);
1848+
}
1849+
1850+
.release-list dl {
1851+
display: grid;
1852+
grid-template-columns: minmax(180px, 220px) 1fr;
1853+
gap: var(--space-4) var(--space-6);
1854+
margin: 0;
1855+
}
1856+
1857+
.release-list dt {
1858+
font-family: var(--font-mono);
1859+
font-size: var(--text-sm);
1860+
font-weight: 500;
1861+
color: var(--color-primary-light);
1862+
word-break: break-word;
1863+
}
1864+
1865+
.release-list dd {
1866+
margin: 0;
1867+
}
1868+
1869+
.release-list dd ul {
1870+
margin: 0;
1871+
padding-left: var(--space-5);
1872+
}
1873+
1874+
.release-list dd li {
1875+
color: var(--text-secondary);
1876+
margin-bottom: var(--space-1);
1877+
}
1878+
1879+
@media (max-width: 640px) {
1880+
.release-list > li {
1881+
padding: var(--space-5) var(--space-5);
1882+
}
1883+
1884+
.release-list dl {
1885+
grid-template-columns: 1fr;
1886+
gap: var(--space-1) 0;
1887+
}
1888+
1889+
.release-list dt {
1890+
margin-top: var(--space-3);
1891+
}
1892+
}

website/src/releases.njk

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
layout: layouts/base.njk
3+
title: "Release History - dart_node"
4+
description: "Version-by-version release history for every dart_node package, with publish dates and per-package changelog notes. Published to pub.dev under christianfindlay.com."
5+
keywords: "dart_node releases, dart_node changelog, dart_node versions, pub.dev release history, Dart package versions"
6+
permalink: /releases/
7+
---
8+
9+
<section class="releases">
10+
<div class="container">
11+
<div class="section-title">
12+
<h1>Release History</h1>
13+
<p>Every published version of the dart_node packages, newest first. Each release ships to <a href="https://pub.dev/publishers/christianfindlay.com/packages" target="_blank" rel="noopener noreferrer">pub.dev</a> from the matching <code>Release/&lt;version&gt;</code> tag.</p>
14+
</div>
15+
16+
<ol class="release-list" reversed>
17+
{% for release in releases %}
18+
<li id="v{{ release.version }}">
19+
<header>
20+
<h2><a href="#v{{ release.version }}">{{ release.version }}</a></h2>
21+
{% if release.dateEn %}<time datetime="{{ release.dateISO }}">{{ release.dateEn }}</time>{% endif %}
22+
<span>{{ release.packages | length }} package{% if release.packages | length != 1 %}s{% endif %}</span>
23+
</header>
24+
<dl>
25+
{% for pkg in release.packages %}
26+
<dt>{{ pkg.name }}</dt>
27+
<dd>
28+
<ul>
29+
{% for change in pkg.changes %}
30+
<li>{{ change.en | safe }}</li>
31+
{% endfor %}
32+
</ul>
33+
</dd>
34+
{% endfor %}
35+
</dl>
36+
</li>
37+
{% endfor %}
38+
</ol>
39+
</div>
40+
</section>

0 commit comments

Comments
 (0)