diff --git a/website/src/_data/navigation.json b/website/src/_data/navigation.json index 5970c15..f479cad 100644 --- a/website/src/_data/navigation.json +++ b/website/src/_data/navigation.json @@ -12,6 +12,10 @@ "text": "Blog", "url": "/blog/" }, + { + "text": "Releases", + "url": "/releases/" + }, { "text": "GitHub", "url": "https://github.com/MelbourneDeveloper/dart_node", @@ -133,6 +137,10 @@ "text": "Blog", "url": "/blog/" }, + { + "text": "Releases", + "url": "/releases/" + }, { "text": "Dart Official", "url": "https://dart.dev" diff --git a/website/src/_data/navigation_zh.json b/website/src/_data/navigation_zh.json index 1e550fa..b950260 100644 --- a/website/src/_data/navigation_zh.json +++ b/website/src/_data/navigation_zh.json @@ -12,6 +12,10 @@ "text": "博客", "url": "/zh/blog/" }, + { + "text": "版本历史", + "url": "/zh/releases/" + }, { "text": "GitHub", "url": "https://github.com/MelbourneDeveloper/dart_node", @@ -133,6 +137,10 @@ "text": "博客", "url": "/zh/blog/" }, + { + "text": "版本历史", + "url": "/zh/releases/" + }, { "text": "Dart 官网", "url": "https://dart.dev" diff --git a/website/src/_data/releases.js b/website/src/_data/releases.js new file mode 100644 index 0000000..ca5c47c --- /dev/null +++ b/website/src/_data/releases.js @@ -0,0 +1,180 @@ +// Build-time release history, derived from the canonical package CHANGELOGs — +// the same release notes the publish pipeline validates per `Release/` +// tag. Single source of truth: packages//CHANGELOG.md. Versions and per- +// package notes are parsed here; publish dates come from pub.dev (baked below, +// since the Pages build checks out shallow with no git tags). Chinese notes use +// a translation overlay and fall back to English for any unmapped line. + +import { readFileSync } from "fs"; +import { dirname, resolve, join } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PACKAGES_DIR = resolve(__dirname, "..", "..", "..", "packages"); + +// Publishable packages, in tier/display order. +const PACKAGES = [ + "dart_logging", + "dart_node_core", + "reflux", + "dart_node_express", + "dart_node_ws", + "dart_node_better_sqlite3", + "dart_node_sql_js", + "dart_node_mcp", + "dart_node_react", + "dart_node_react_native", +]; + +// Release dates (ISO) sourced from pub.dev — earliest publish across packages +// for each version. Versions absent here render without a date. +const RELEASE_DATES = { + "0.13.0-beta": "2026-06-07", + "0.12.0-beta": "2026-01-29", + "0.11.0-beta": "2025-12-13", + "0.9.0-beta": "2025-12-11", + "0.8.0-beta": "2025-12-04", + "0.2.0-beta": "2025-12-02", + "0.1.0-beta": "2025-11-30", +}; + +// Chinese overlay for changelog lines. Unmapped lines fall back to English. +const ZH_NOTES = { + "Maintenance release; no functional changes": "维护版本;无功能性更改", + "Maintenance release": "维护版本", + "Maintenance release; internal test improvements": "维护版本;内部测试改进", + "Enhanced test coverage": "增强测试覆盖率", + "Early release": "早期版本", + "Initial beta release": "首个 Beta 版本", + "Version bump for upcoming release": "为即将发布的版本提升版本号", + "Updated docs": "更新文档", + "Improved documentation for core extensions": "改进核心扩展的文档", + "Pino-style structured logging framework": "Pino 风格的结构化日志框架", + "Child loggers with inherited context": "支持继承上下文的子日志记录器", + "Add filesystem (`fs`) bindings": "新增文件系统(`fs`)绑定", + "Add child process interop": "新增子进程互操作", + "Core JS interop utilities for Dart-to-JavaScript compilation": + "用于 Dart 到 JavaScript 编译的核心 JS 互操作工具", + "Foundation for dart_node package family": "dart_node 包系列的基础", + "Add WebSocket support via dart_node_ws package": + "通过 dart_node_ws 包新增 WebSocket 支持", + "Redux-style predictable state container for Dart": + "面向 Dart 的 Redux 风格可预测状态容器", + "Sealed classes for exhaustive action pattern matching": + "使用密封类实现详尽的 action 模式匹配", + "Express.js bindings for Dart": "面向 Dart 的 Express.js 绑定", + "Build Node.js HTTP servers entirely in Dart": + "完全使用 Dart 构建 Node.js HTTP 服务器", + "Add WebSocket support integration with dart_node_ws": + "新增与 dart_node_ws 的 WebSocket 集成支持", + "WebSocket bindings for Dart on Node.js": + "面向 Node.js 上 Dart 的 WebSocket 绑定", + "Build real-time WebSocket servers entirely in Dart": + "完全使用 Dart 构建实时 WebSocket 服务器", + "Typed Dart bindings for better-sqlite3 npm package": + "为 better-sqlite3 npm 包提供类型化的 Dart 绑定", + "Synchronous SQLite3 access with WAL mode": + "支持 WAL 模式的同步 SQLite3 访问", + "Typed Dart bindings for sql.js (SQLite compiled to WebAssembly)": + "为 sql.js(编译为 WebAssembly 的 SQLite)提供类型化的 Dart 绑定", + "Synchronous in-memory SQLite with file persistence on Node.js": + "在 Node.js 上支持文件持久化的同步内存 SQLite", + "`Result`-based API: open, prepare, exec, run, get, all, pragma": + "基于 `Result` 的 API:open、prepare、exec、run、get、all、pragma", + "Add streamable HTTP transport": "新增可流式传输的 HTTP 传输层", + "Additional MCP server capabilities": "新增 MCP 服务器能力", + "Typed Dart bindings for @modelcontextprotocol/sdk": + "为 @modelcontextprotocol/sdk 提供类型化的 Dart 绑定", + "Build MCP servers in Dart that run on Node.js": + "使用 Dart 构建运行于 Node.js 的 MCP 服务器", + "React bindings for Dart": "面向 Dart 的 React 绑定", + "Build React web applications entirely in Dart": + "完全使用 Dart 构建 React Web 应用", + "React Native bindings for Dart": "面向 Dart 的 React Native 绑定", + "Build mobile apps with Expo entirely in Dart": + "完全使用 Dart 与 Expo 构建移动应用", +}; + +const EN_MONTHS = [ + "January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December", +]; + +const escapeHtml = (s) => + s.replace(/&/g, "&").replace(//g, ">"); + +// Escape, then render inline `code` spans. Content is our own changelogs. +const toHtml = (s) => + escapeHtml(s).replace(/`([^`]+)`/g, "$1"); + +const formatDateEn = (iso) => { + const [y, m, d] = iso.split("-").map((n) => parseInt(n, 10)); + return `${EN_MONTHS[m - 1]} ${d}, ${y}`; +}; + +const formatDateZh = (iso) => { + const [y, m, d] = iso.split("-").map((n) => parseInt(n, 10)); + return `${y}年${m}月${d}日`; +}; + +const PRERELEASE_RANK = { alpha: 1, beta: 2 }; + +const parseVersion = (v) => { + const [core, pre] = v.split("-"); + const [major, minor, patch] = core.split(".").map((n) => parseInt(n, 10) || 0); + const rank = pre ? (PRERELEASE_RANK[pre.split(".")[0]] ?? 3) : 4; + return [major, minor, patch, rank]; +}; + +const compareDesc = (a, b) => { + const pa = parseVersion(a); + const pb = parseVersion(b); + for (let i = 0; i < pa.length; i += 1) { + if (pa[i] !== pb[i]) return pb[i] - pa[i]; + } + return 0; +}; + +const parseChangelog = (markdown) => { + const byVersion = {}; + let current = null; + for (const line of markdown.split("\n")) { + const header = line.match(/^##\s+(.+?)\s*$/); + if (header) { + current = header[1].trim(); + byVersion[current] = []; + continue; + } + if (!current) continue; + const bullet = line.match(/^[-*]\s+(.+?)\s*$/); + if (bullet) byVersion[current].push(bullet[1].trim()); + } + return byVersion; +}; + +const versions = {}; +for (const pkg of PACKAGES) { + const file = join(PACKAGES_DIR, pkg, "CHANGELOG.md"); + const parsed = parseChangelog(readFileSync(file, "utf-8")); + for (const [version, changes] of Object.entries(parsed)) { + if (changes.length === 0) continue; + versions[version] ??= []; + versions[version].push({ + name: pkg, + changes: changes.map((c) => ({ en: toHtml(c), zh: toHtml(ZH_NOTES[c] ?? c) })), + }); + } +} + +export default Object.keys(versions) + .sort(compareDesc) + .map((version) => { + const iso = RELEASE_DATES[version] ?? null; + return { + version, + dateISO: iso, + dateEn: iso ? formatDateEn(iso) : null, + dateZh: iso ? formatDateZh(iso) : null, + packages: versions[version], + }; + }); diff --git a/website/src/assets/css/styles.css b/website/src/assets/css/styles.css index 9df4eed..94f490b 100644 --- a/website/src/assets/css/styles.css +++ b/website/src/assets/css/styles.css @@ -1784,3 +1784,109 @@ a.tag.tag-secondary:hover { .mobile-menu-toggle span { transition: all 0.3s ease; } + +/* Releases page */ +.releases { + padding: var(--space-16) 0; +} + +.release-list { + list-style: none; + max-width: 820px; + margin: 0 auto; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--space-8); +} + +.release-list > li { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-sm); + padding: var(--space-6) var(--space-8); + scroll-margin-top: var(--space-20); +} + +.release-list > li > header { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: var(--space-4); + border-bottom: 1px solid var(--border-color); + padding-bottom: var(--space-4); + margin-bottom: var(--space-5); +} + +.release-list > li > header h2 { + margin: 0; + font-size: var(--text-2xl); +} + +.release-list > li > header h2 a { + color: var(--text-primary); + text-decoration: none; +} + +.release-list > li > header h2 a:hover { + color: var(--color-primary); +} + +.release-list > li > header time { + color: var(--text-secondary); + font-size: var(--text-sm); +} + +.release-list > li > header span { + margin-left: auto; + font-size: var(--text-xs); + color: var(--text-tertiary); + background: var(--bg-tertiary); + padding: var(--space-1) var(--space-3); + border-radius: var(--radius-full); +} + +.release-list dl { + display: grid; + grid-template-columns: minmax(180px, 220px) 1fr; + gap: var(--space-4) var(--space-6); + margin: 0; +} + +.release-list dt { + font-family: var(--font-mono); + font-size: var(--text-sm); + font-weight: 500; + color: var(--color-primary-light); + word-break: break-word; +} + +.release-list dd { + margin: 0; +} + +.release-list dd ul { + margin: 0; + padding-left: var(--space-5); +} + +.release-list dd li { + color: var(--text-secondary); + margin-bottom: var(--space-1); +} + +@media (max-width: 640px) { + .release-list > li { + padding: var(--space-5) var(--space-5); + } + + .release-list dl { + grid-template-columns: 1fr; + gap: var(--space-1) 0; + } + + .release-list dt { + margin-top: var(--space-3); + } +} diff --git a/website/src/releases.njk b/website/src/releases.njk new file mode 100644 index 0000000..b242350 --- /dev/null +++ b/website/src/releases.njk @@ -0,0 +1,40 @@ +--- +layout: layouts/base.njk +title: "Release History - dart_node" +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." +keywords: "dart_node releases, dart_node changelog, dart_node versions, pub.dev release history, Dart package versions" +permalink: /releases/ +--- + +
+
+
+

Release History

+

Every published version of the dart_node packages, newest first. Each release ships to pub.dev from the matching Release/<version> tag.

+
+ +
    + {% for release in releases %} +
  1. +
    +

    {{ release.version }}

    + {% if release.dateEn %}{% endif %} + {{ release.packages | length }} package{% if release.packages | length != 1 %}s{% endif %} +
    +
    + {% for pkg in release.packages %} +
    {{ pkg.name }}
    +
    +
      + {% for change in pkg.changes %} +
    • {{ change.en | safe }}
    • + {% endfor %} +
    +
    + {% endfor %} +
    +
  2. + {% endfor %} +
+
+
diff --git a/website/src/zh/releases.njk b/website/src/zh/releases.njk new file mode 100644 index 0000000..ba9f7ce --- /dev/null +++ b/website/src/zh/releases.njk @@ -0,0 +1,41 @@ +--- +layout: layouts/base.njk +title: "版本历史 - dart_node" +description: "dart_node 各个包的逐版本发布历史,包含发布日期和每个包的变更说明。在 christianfindlay.com 名下发布至 pub.dev。" +keywords: "dart_node 版本, dart_node 更新日志, dart_node 发布历史, pub.dev 版本, Dart 包版本" +lang: zh +permalink: /zh/releases/ +--- + +
+
+
+

版本历史

+

dart_node 各个包的所有已发布版本,最新版本在前。每个版本都通过对应的 Release/<version> 标签发布至 pub.dev

+
+ +
    + {% for release in releases %} +
  1. +
    +

    {{ release.version }}

    + {% if release.dateZh %}{% endif %} + {{ release.packages | length }} 个包 +
    +
    + {% for pkg in release.packages %} +
    {{ pkg.name }}
    +
    +
      + {% for change in pkg.changes %} +
    • {{ change.zh | safe }}
    • + {% endfor %} +
    +
    + {% endfor %} +
    +
  2. + {% endfor %} +
+
+