yggdrasil/AGENTS.md
xfy a4331bd813 feat(build): add FreeBSD x86_64 cross-compile support
方案 A(clang + lld + 本地 sysroot)。rustc 为 freebsd target 链接的系统库
(libc/libexecinfo/libgcc_s/libthr)及 sysinfo 依赖的 freebsd 专有库
(libkvm/libmemstat/libprocstat/libdevstat)需 FreeBSD 运行时,无法纯靠
clang+lld 无 sysroot 完成;故从 FreeBSD 15.1 base.txz 解出 crt 对象 + 系统库
到 .freebsd-sysroot/(gitignored,machine-local)。

- .cargo/config.toml: freebsd target 配置作为注释模板(sysroot 路径机器相关,
  不硬编码进仓库);linker 经 rustflags 的 -C linker=clang 设置,使 Makefile
  能用 CARGO_TARGET_*_RUSTFLAGS 环境变量整体注入。
- Makefile: 新增 freebsd-sysroot(幂等下载+解压 base.txz)与 build-freebsd
  (release server 二进制交叉编译)target。server 用 cargo 直出而非 dx CLI,
  因 dx 对该 target 的支持未经验证。
- .gitignore: 忽略 .freebsd-sysroot/。
- AGENTS.md: 补充构建命令说明。

实测 release 二进制 16MB,stripped ELF for FreeBSD 15.1,动态依赖均为
FreeBSD base 自带库(libc.so.7 libthr.so.3 libkvm.so.7 等)。
2026-07-01 13:40:00 +08:00

17 KiB
Raw Blame History

AGENTS.md

Workflow

  • 每完成一个功能点立即提交。Agent 自主判断提交时机——当一个逻辑完整的改动通过验证(编译通过 / 测试通过)后,无需等待用户指令,直接 git add + git commit
  • 提交粒度按"功能点"而非"文件":相关联的多文件改动合并为一个提交,不相关的改动拆成多个提交。
  • 提交信息遵循现有风格:type(scope): 简述,正文(可选)说明动机与关键改动。常见 type:feat / fix / docs / refactor / chore / perf
  • 只在用户明确要求时才 git push。提交到本地即可,不主动推送。

Development Commands

make dev           # tailwindcss watch + dx serve (needs PostgreSQL)
make build         # build-editor → highlight-css → tailwindcss → doc → dx build --release
make build-linux   # same as build but targets x86_64-unknown-linux-musl
make build-freebsd # cross-compile FreeBSD x86_64 server binary (clang + lld + sysroot)
make freebsd-sysroot # download/extract FreeBSD base.txz → .freebsd-sysroot/ (idempotent)
make css           # one-shot CSS
make css-watch     # watch mode
make test          # cargo test + vitest (tiptap-editor + lightbox + yggdrasil-core libs)
make doc           # cargo doc (ayu 主题) → 拷贝到 public/doc/,随 build 发布
make doc-open      # 同 doc生成后自动用浏览器打开本地预览不拷贝
make clean         # cargo clean + rm public/style.css

Build order matters: make build runs build-editorhighlight-css (cargo run --bin generate_highlight_css) → tailwindcss --minifydoc (cargo doc + 拷贝到 public/doc/) → dx build --release. Do not run dx build --release alone.

Prerequisites

  • Rust 1.95+ with wasm32-unknown-unknown target
  • dx CLI (cargo install dioxus-cli)
  • tailwindcss CLI v4 — install via npm install -g @tailwindcss/cli (v4 splits the CLI into its own package; the tailwindcss core package has no bin), or use the standalone binary
  • PostgreSQL running locally

Environment

Create .env (not committed):

DATABASE_URL=postgres://postgres:postgres@localhost:5432/yggdrasil
RUST_LOG=info

Optional tuning via env vars (all have sane defaults):

WEBP_QUALITY=85.0           # 0.0100.0, clamped
WEBP_METHOD=2               # 06, clamped
MAX_IMAGE_DIMENSION=8192     # max single side in px, min 512, no upper limit
MAX_IMAGE_PIXELS=50000000   # max total pixels (~7k×7k), min 1M, no upper limit
RATE_LIMIT_STRICT_PER_SEC=1
RATE_LIMIT_STRICT_BURST=5
RATE_LIMIT_UPLOAD_PER_SEC=2
RATE_LIMIT_UPLOAD_BURST=15
RATE_LIMIT_IMAGE_PER_SEC=10
RATE_LIMIT_IMAGE_BURST=50
RATE_LIMIT_COMMENT_PER_SEC=1          # comment posting
RATE_LIMIT_COMMENT_BURST=5
RATE_LIMIT_UNKNOWN_PER_SEC=30         # fallback bucket when real client IP can't be determined
RATE_LIMIT_UNKNOWN_BURST=100
DB_POOL_SIZE=20             # database connection pool size
MIGRATE_STARTUP_TIMEOUT_SECS=30  # how long startup waits for PostgreSQL before giving up
STATEMENT_TIMEOUT_SECS=30   # per-query timeout; slow queries are canceled to protect the pool
SSR_CACHE_SECS=3600         # incremental SSR cache TTL

Session / security tuning:

COOKIE_SECURE=false         # set true/1/yes to add Secure flag to session cookie
TRUSTED_PROXY_COUNT=0       # number of reverse proxies in front of the app; used to extract real client IP from X-Forwarded-For
APP_BASE_URL=               # e.g. https://your-domain.example — trusted origin for CSRF checks on write requests; unset falls back to Host header + X-Forwarded-Proto

Run migrations before first dev server start:

./migrate.sh       # auto-creates DB, runs migrations/ in order

Architecture: Conditional Compilation

Dioxus 0.7 fullstack project with two independent gates — the most common source of compilation errors.

Gate Applies to Used for
#[cfg(feature = "server")] Server binary only DB, env loading, background tasks, server function bodies, highlight, WebP, caching
#[cfg(target_arch = "wasm32")] WASM frontend only localStorage, DOM APIs, web_sys calls, theme detection

Critical: Both default features (web + server) are enabled in Cargo.toml. The dx CLI handles feature selection during builds.

Stub pattern: src/db/mod.rs provides a DummyPool when server feature is disabled — do not remove.

Server-only helpers: src/auth/password.rs, src/auth/session.rs, src/api/auth.rs, src/api/comments/helpers.rs, and several model helper methods are gated with #[cfg(feature = "server")] because they are only called from server function bodies, which are stripped in WASM builds.

Server-only dependencies: Crates that are only used behind #[cfg(feature = "server")] (e.g., argon2, uuid, regex, pulldown-cmark, rand, http, sha2, hex, plus the pre-existing optional server stack) are declared as optional = true in Cargo.toml and enabled only through the server feature. They are not compiled into the WASM frontend.

Dual API Architecture

The server exposes two distinct API patterns:

  1. Dioxus server functions (#[server(Name, "/api")] in src/api/) — auto-routed, callable from both client and server Rust. Spread across src/api/auth.rs, src/api/posts/, src/api/comments/, and src/api/settings.rs.

  2. Axum routes (registered in src/main.rs) — manual axum::Router for endpoints that don't fit the server-function model:

    • POST /api/upload — image upload (multipart, auth-required, rate-limited)
    • GET /uploads/{*path} — image serving with on-the-fly resize/rotate/convert (query params: w, h, thumb, rotate, format, quality)

Server Module Structure

src/api/          — server functions + Axum handlers
  auth.rs         — login, register, session validation
  comments/       — comment CRUD + approval/spam/trash server functions
  markdown.rs     — Markdown→HTML rendering (pulldown-cmark + ammonia sanitization)
  image.rs        — image serving with processing pipeline + disk+memory cache
  upload.rs       — image upload, auto-converts to WebP
  rate_limit.rs   — governor-based rate limiting (5 tiers: strict/upload/image/comment/unknown)
  settings.rs     — site settings server functions (trash retention, etc.)
  slug.rs         — URL slug generation
  posts/          — CRUD server functions for blog posts
src/auth/         — password hashing (Argon2) + session token management
src/bin/          — generate_highlight_css (build-time CSS generation)
src/cache.rs      — moka future-based caches for posts, tags, stats
src/components/   — Dioxus UI components
src/db/           — PostgreSQL pool (deadpool-postgres, LazyLock global)
src/hooks/        — shared Dioxus hooks
src/models/       — Post, User, Tag data models
src/pages/        — route page components (frontend + admin)
src/tasks/        — background tokio tasks (session cleanup)
src/theme.rs      — light/dark theme with SSR cookie + WASM localStorage
src/webp.rs       — zenwebp encode/decode (image crate has no WebP)

Tiptap Editor Subproject

Rich-text editor in libs/tiptap-editor/, built as an IIFE library exposing window.TiptapEditor.

  • Output: public/tiptap/
  • make build runs npm ci --include=dev && npm run build inside libs/tiptap-editor; the build script is tsc --noEmit && vite build (Vite 8 / Rolldown, type-check before bundle)
  • Unit tests: npm test (Vitest 4 + happy-dom), covering UploadCoordinator counts/lifecycle, UploadImageNodeView rendering/callbacks, and isValidUrl
  • src/pages/admin/write.rs initializes via src/tiptap_bridge.rs (wasm-bindgen bindings): injects Closure callbacks (onUpdate/onReady/onUploadEvent/onImageUpload) into TiptapEditor.create, holds the instance + closures in EditorHandle (Drop calls destroy()). No js_sys::eval, no window globals, no polling.

Do not edit public/tiptap/ — they are build artifacts.

Lightbox Subproject

Image lightbox (click-to-zoom) in libs/lightbox/, built as an IIFE library. Unlike the Tiptap editor, it is not wired through wasm-bindgen; the built lightbox.js is injected globally via Dioxus.toml (script = ["/lightbox/lightbox.js"]), and src/components/post/post_content.rs sets window.__lightboxSelectors (e.g. ['.post-content', '.entry-cover']) before the script loads. The IIFE tail reads this config and self-initializes; post_content.rs also calls it directly as a fallback if the script was already loaded.

  • Output: public/lightbox/ (lightbox.js, lightbox.css, lightbox.js.map)
  • make build runs npm ci --include=dev && npm run build inside libs/lightbox; the build script is tsc --noEmit && vite build
  • Unit tests: npm test (Vitest, 23 tests), covering geometry math and lightbox rendering/lifecycle

Do not edit public/lightbox/ — they are build artifacts.

Yggdrasil-Core Subproject

Core JavaScript bundle in libs/yggdrasil-core/, built as an IIFE library exposing window.__initPostContent and window.__startThemeTransition (and future core JS entry points). This is the designated home for all new core JS — add to it rather than creating new public/js/ scripts.

  • Output: public/yggdrasil-core/ (yggdrasil-core.js, yggdrasil-core.css, yggdrasil-core.js.map)
  • make build runs npm ci --include=dev && npm run build inside libs/yggdrasil-core; the build script is tsc --noEmit && vite build (Vite 8 / Rolldown, type-check before bundle). Source is ES module, output is IIFE (formats: ['iife'] in vite.config.ts) because Dioxus [web.resource] script injects bare <script src> without type="module" support.
  • Injected globally via Dioxus.toml (script + style arrays). Rust calls the entry points via js_sys::eval("window.__xxx(...)") with an if (window.__xxx) guard to survive script-load ordering races.
  • Unit tests: npm test (Vitest 4 + happy-dom), covering post-content copy-button lifecycle and theme-transition fallback paths (happy-dom lacks startViewTransition, naturally covering the degradation path).
  • Theme reveal animation uses the View Transitions API: JS synchronously toggles the dark class inside startViewTransition's callback (so the browser snapshots the new state), CSS @keyframes tt-reveal expands ::view-transition-new(root) via clip-path: circle() from the click point; Rust's theme.set() aligns state afterward (idempotent — use_effect sets the same class again). prefers-reduced-motion and browsers without VT fall back to instant switch.

Do not edit public/yggdrasil-core/ — they are build artifacts.

CodeMirror Editor Subproject

CodeMirror 6 code editor in libs/codemirror-editor/, built as an IIFE library exposing window.CodeMirrorEditor (object literal { create(containerId, options) }) and window.EditorOptions (a class so new EditorOptions() survives TS erasure from wasm). Mirrors the tiptap-editor packaging pattern exactly.

  • Output: public/codemirror/ (editor.js, editor.js.map). No CSS file — themes are JS Extensions from @catppuccin/codemirror (Latte light / Mocha dark, matching the syntax-highlighting themes/).
  • make build runs pnpm ci --include=dev && pnpm run build inside libs/codemirror-editor; the build script is tsc --noEmit && vite build (Vite 8 / Rolldown, type-check before bundle). Output is IIFE (formats: ['iife'], exports: 'default').
  • Features: @codemirror/lang-sql with live schema autocomplete (schema injected via set_schema from get_db_schema server function), @replit/codemirror-vim keymap (injected before other keymaps), Catppuccin theme hot-swap via Compartment.reconfigure (no instance rebuild — preserves Vim state/cursor/undo).
  • Rust bridge: src/codemirror_bridge.rs mirrors src/tiptap_bridge.rsget_module() uses js_sys::Reflect::get + unchecked_into (object literal, not a constructor, so NOT an extern fn call and NOT dyn_into); EditorHandle holds the instance + all Closures; impl Drop calls destroy().
  • Shared data types SqlSchema/SqlTable (serde) compile on both targets; the wasm-bindgen externs live in a #[cfg(target_arch = "wasm32")] mod wasm.
  • Unit tests: pnpm test (Vitest 4 + happy-dom), covering getValue/setValue, setTheme (Compartment), setSchema, vim toggle, onChange.
  • Injected via Dioxus.toml script array (/codemirror/editor.js).

Do not edit public/codemirror/ — they are build artifacts.

Database Management (/admin/system)

Admin area at /admin/system (menu "系统") with 5 tabs: 数据库状态 / 服务器状态 / SQL 控制台 / 数据导出 / 备份恢复. All gated by get_current_admin_user (admin-only). Backend in src/api/database/ (status/system_status/sql_console/schema/export/backup/tasks), page in src/pages/admin/system.rs.

  • SQL 控制台 is full read-write with 4 guards: (1) sqlparser AST gates — DROP DATABASE/DROP SCHEMA/CREATE DATABASE absolutely forbidden (string pre-check); DROP/TRUNCATE/ALTER require a confirm_dangerous checkbox; (2) UPDATE/DELETE without WHERE rejected; (3) STATEMENT_TIMEOUT_SECS query timeout (pool-level GUC); (4) frontend write-confirm dialog. Multi-statement disabled by default. Results capped at 500 rows.
  • 备份恢复 uses dashmap task-progress table; create_backup/restore_backup return a task_id immediately and poll get_task_progress. Backup prefers pg_dump (full, incl. schema), falls back to per-table COPY TO STDOUT (data only) when pg_dump is unavailable. Backup files carry a -- YGGDRASIL BACKUP v1 signature header; restore rejects non-system files. backups/ is gitignored and served only via GET /api/database/backups/{filename} (admin-gated, path-allowlist).
  • 服务器状态 uses sysinfo (optional, server feature) with a background sampler (SYSINFO_SAMPLE_SECS, default 0.5s) writing to a RwLock<SystemSnapshot>; server functions read the snapshot (zero sampling cost), so frontend can poll high-frequency. src/cache.rs exposes moka hit-rate via AtomicU64 hit/miss counters per cache + cache_stats().
  • New env var: SYSINFO_SAMPLE_SECS (sysinfo sampling interval in seconds, supports decimals).
  • New deps (all optional = true, server feature): sysinfo, sqlparser, dashmap.

Syntax Highlighting Pipeline

  • themes/ contains Catppuccin Latte (light) and Mocha (dark) .tmTheme files
  • syntaxes/ has custom Sublime syntax definitions (Kotlin, Swift)
  • src/bin/generate_highlight_css.rs generates public/highlight.css with class-based rules scoped under .md-content pre code, with .dark prefix for dark mode
  • src/highlight.rs uses syntect at runtime for code block highlighting
  • All gated behind #[cfg(feature = "server")]

Auth & Session

  • Registration: first user becomes admin; subsequent registrations rejected with "Registration is closed"
  • Login: sets an HttpOnly cookie via FullstackContext::add_response_header
  • Session validation: get_current_user reads session cookie, queries sessions + users tables
  • Background cleanup: tasks::session_cleanup::run_cleanup() deletes expired sessions every hour

Caching

  • Post/tag caches (src/cache.rs): moka future-based, TTL varies by data type (60s600s). Invalidated on writes.
  • Image processing cache (src/api/image.rs): two-tier — in-memory moka cache + disk cache in uploads/.cache/. Keyed by path + query params.

Testing

make test          # cargo test (Rust, 402 tests) + vitest (tiptap-editor 46 tests, lightbox 23 tests, yggdrasil-core 9 tests)
dx check          # Dioxus type-check (catches component/Router issues)
cargo clippy      # lint

Most tests use #[cfg(all(test, feature = "server"))] — they only run when the server feature is active (which is the default). No integration tests requiring a database connection. The two libs/ subprojects (tiptap-editor, lightbox) run their own vitest suites.

Image Processing Constraints

  • The image crate is configured without WebP support (default-features = false, features = ["jpeg", "png", "gif"]). Do not add WebP to the image crate features.
  • All WebP encode/decode goes through zenwebp via src/webp.rs.
  • Upload pipeline auto-converts non-GIF/non-WebP images to WebP, keeping original format if WebP is larger.
  • Image serving supports on-the-fly resize (w, h), thumbnail (thumb=WxH), rotation (90/180/270), and format conversion.

Build Artifacts (gitignored)

  • public/style.css — Tailwind output
  • public/highlight.css — generated by generate_highlight_css binary
  • public/tiptap/ — Vite build output (editor)
  • public/lightbox/ — Vite build output (lightbox)
  • public/yggdrasil-core/ — Vite build output (core JS bundle)
  • /dist, /.dioxus, /target
  • node_modules (inside libs/tiptap-editor/, libs/lightbox/, and libs/yggdrasil-core/)
  • uploads/.cache/ — image processing disk cache

Notes

  • rand is optional and only enabled by the server feature; it is not compiled into the WASM frontend.
  • #[allow(unused_mut, unused_variables)] on Write component is intentional — mut signals are used in #[cfg(target_arch = "wasm32")] blocks stripped in server builds.
  • Server uses incremental rendering with 300s cache (IncrementalRendererConfig in src/main.rs).