# Ramonyx — llms-full (public)
> Full public corpus for agents: public machine layer (JSON) + public documentation.
> Source: repo RamonyxJS/ramonyx (https://ramonyx.devmoon.net). Internal docs are never included.
## docs/public/machine/features.json
```json
{
"schemaVersion": 1,
"features": [
{
"id": "routing-file-based",
"name": "File-based routing halaman (statis, dinamis, catch-all)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/compiler",
"@ramonyx/ssr"
],
"evidence": [
"packages/compiler/src/pages.ts",
"packages/compiler/test/pages.test.ts"
],
"limitations": [
"Route groups `(grup)` belum ada",
"Middleware per-folder belum ada"
],
"docs": "docs/public/routing.md"
},
{
"id": "routing-api-methods",
"name": "File-based routing API (satu file boleh mengekspor beberapa method HTTP)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/compiler",
"@ramonyx/server"
],
"evidence": [
"packages/compiler/src/manifest.ts",
"packages/compiler/test/manifest.test.ts"
],
"limitations": [],
"docs": "docs/public/routing.md"
},
{
"id": "routing-params",
"name": "Pencocokan rute berparameter + catch-all + prioritas statis",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/router"
],
"evidence": [
"packages/router/src/match.ts",
"packages/router/test/match.test.ts",
"packages/router/test/match.fuzz.test.ts"
],
"limitations": [],
"docs": "docs/public/routing.md"
},
{
"id": "layouts-nested",
"name": "Layout bersarang (layout.tsx per folder, terdekat menang)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/compiler"
],
"evidence": [
"packages/compiler/src/pages.ts",
"packages/compiler/test/pages.test.ts"
],
"limitations": [],
"docs": "docs/public/routing.md"
},
{
"id": "error-boundaries",
"name": "Error boundary per folder (error.tsx, terdekat menang; halaman error tidak di-hydrate)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/compiler",
"@ramonyx/ssr"
],
"evidence": [
"packages/compiler/src/pages.ts",
"packages/compiler/test/pages.test.ts",
"apps/playground/e2e/errors.spec.ts"
],
"limitations": [],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "middleware-global",
"name": "Middleware global (pre-routing: aset statis, logging) di createHandler",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/server"
],
"evidence": [
"packages/server/src/handler.ts",
"packages/server/test/pipeline.test.ts"
],
"limitations": [
"Tanpa matcher per-path (semua request)"
],
"docs": "docs/public/routing.md"
},
{
"id": "middleware-per-folder",
"name": "Middleware per-folder (middleware.ts, root→terdalam, otomatis via manifest)",
"status": "implemented",
"since": "0.2.0",
"priority": "p1",
"packages": [
"@ramonyx/compiler",
"@ramonyx/server"
],
"evidence": [
"packages/compiler/src/pages.ts",
"packages/server/src/handler.ts",
"packages/compiler/test/playground-pages.test.ts",
"apps/playground/src/routes/users/middleware.ts"
],
"limitations": [],
"docs": "docs/public/routing.md"
},
{
"id": "ssr-sync",
"name": "SSR React sinkron (renderToString) + dokumen HTML",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/ssr",
"@ramonyx/react"
],
"evidence": [
"packages/ssr/src/pages.ts",
"packages/ssr/src/document.ts",
"packages/ssr/test/pages.test.ts"
],
"limitations": [],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "ssr-streaming",
"name": "SSR streaming (opt-in `streaming: true`, react-dom/server.edge)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/ssr",
"@ramonyx/react"
],
"evidence": [
"packages/ssr/src/pages.ts",
"docs/public/engineering/adr/0047-ssr-streaming.md"
],
"limitations": [
"Error setelah byte pertama = koneksi diputus (status/error page hanya berlaku sebelum byte pertama)"
],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "hydration",
"name": "Hydration (payload inline `window.__RAMONYX__` + satu bundle client)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/react",
"@ramonyx/compiler"
],
"evidence": [
"packages/react/src/client.ts",
"apps/playground/e2e/ssr.spec.ts",
"docs/public/engineering/adr/0007-hydration.md"
],
"limitations": [],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "soft-navigation",
"name": "Soft navigation (fetch HTML → swap → re-hydrate; title + scroll direset)",
"status": "implemented",
"since": "0.2.0",
"priority": "p1",
"packages": [
"@ramonyx/react"
],
"evidence": [
"packages/react/src/client.ts",
"apps/playground/e2e/navigation.spec.ts",
"apps/playground/e2e/prefetch.spec.ts",
"docs/public/engineering/adr/0007-hydration.md"
],
"limitations": [],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "data-loaders",
"name": "Data loader per halaman (GET, paralel dengan layout, JSON-serializable)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/core",
"@ramonyx/ssr"
],
"evidence": [
"packages/core/src/page.ts",
"packages/ssr/src/pages.ts",
"packages/ssr/test/pages.test.ts"
],
"limitations": [
"Hanya GET; belum ada loader dependensi antar-route",
"Cache opsional per halaman (ADR-0066) — data loader bisa disajikan dari cache"
],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "mutations-server-actions",
"name": "Server actions (mutasi form per halaman: FormData, actionSchema, PRG, progressive enhancement)",
"status": "implemented",
"since": "0.2.0",
"priority": "p0",
"packages": [
"@ramonyx/core",
"@ramonyx/ssr",
"@ramonyx/react"
],
"evidence": [
"packages/ssr/src/pages.ts",
"packages/ssr/test/pages.test.ts",
"packages/react/src/client.ts",
"apps/playground/e2e/form.spec.ts",
"docs/public/engineering/adr/0057-server-actions.md"
],
"limitations": [
"Tanpa pending state klien/optimistic UI",
"Revalidasi antar-halaman: manual via `revalidate({ paths })` (tanpa deklarasi otomatis)"
],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "redirect-helper",
"name": "Helper redirect(location, status?) pada konteks rute",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/core"
],
"evidence": [
"packages/core/src/types.ts",
"packages/core/test/route.test.ts"
],
"limitations": [],
"docs": "docs/public/routing.md"
},
{
"id": "head-metadata",
"name": "Metadata halaman (title, description, canonical, OG/Twitter, robots) + injeksi
",
"status": "implemented",
"since": "0.2.0",
"priority": "p1",
"packages": [
"@ramonyx/core",
"@ramonyx/ssr"
],
"evidence": [
"packages/core/src/page.ts",
"packages/ssr/src/pages.ts",
"packages/ssr/test/pages.test.ts"
],
"limitations": [
"Origin kanonik dari request (di belakang proxy: pastikan header benar)"
],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "schema-validation",
"name": "Validasi schema runtime di trust boundary (Standard Schema)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/schema"
],
"evidence": [
"packages/schema/src/validate.ts",
"packages/schema/test/validate.test.ts",
"packages/schema/test/validate.fuzz.test.ts"
],
"limitations": [],
"docs": "docs/public/routing.md"
},
{
"id": "config",
"name": "Konfigurasi aplikasi (createApp/createHandler + pass-through key tak dikenal)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/core"
],
"evidence": [
"packages/core/src/config.ts",
"packages/core/test/config.test.ts"
],
"limitations": [],
"docs": "docs/public/cli.md"
},
{
"id": "security-headers",
"name": "Security headers (CSP, X-Frame-Options, nosniff, Referrer-Policy, dst.)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/security"
],
"evidence": [
"packages/security/src/headers.ts",
"packages/security/test/headers.test.ts"
],
"limitations": [],
"docs": "docs/public/security/overview.md"
},
{
"id": "csp-nonce",
"name": "CSP nonce per request (inline script diizinkan tanpa unsafe-inline)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/security"
],
"evidence": [
"packages/security/src/csp.ts",
"packages/security/test/csp.test.ts"
],
"limitations": [],
"docs": "docs/public/security/overview.md"
},
{
"id": "csrf",
"name": "Proteksi CSRF (double-submit cookie + validasi origin)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/security"
],
"evidence": [
"packages/security/src/csrf.ts",
"packages/security/test/csrf.test.ts"
],
"limitations": [],
"docs": "docs/public/sessions-and-auth.md"
},
{
"id": "ssrf-guard",
"name": "Guard SSRF (guardedFetch: validasi host/IP, IP-pinning anti DNS-rebinding, redirect per-hop)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/security"
],
"evidence": [
"packages/security/src/ssrf.ts",
"packages/security/test/ssrf.test.ts"
],
"limitations": [
"Node-only: impor via @ramonyx/security/ssrf (index edge-safe — ADR-0058)",
"Body FormData/multipart belum didukung"
],
"docs": "docs/public/security/overview.md"
},
{
"id": "rate-limit",
"name": "Rate limit per klien (token bucket, 429 + Retry-After)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/security"
],
"evidence": [
"packages/security/src/rate-limit.ts",
"packages/security/test/rate-limit.test.ts"
],
"limitations": [
"In-memory per proses (tanpa store terdistribusi)"
],
"docs": "docs/public/security/overview.md"
},
{
"id": "resource-limits",
"name": "Resource limits secure-by-default (body 1 MiB→413, timeout 10s→503, concurrency 100→503)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/server"
],
"evidence": [
"packages/server/src/handler.ts",
"tests/security/server-hardening.test.ts"
],
"limitations": [],
"docs": "docs/public/security/overview.md"
},
{
"id": "output-limit",
"name": "Batas ukuran output SSR",
"status": "implemented",
"since": "0.1.0",
"priority": "p2",
"packages": [
"@ramonyx/ssr"
],
"evidence": [
"packages/ssr/src/pages.ts"
],
"limitations": [],
"docs": "docs/public/security/overview.md"
},
{
"id": "dev-server-hmr",
"name": "Dev server + HMR (Vite → SSR) + panel devtools (rute, timing, hydration, bundel)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/bundler"
],
"evidence": [
"packages/bundler/src/dev.ts",
"packages/bundler/src/devtools.ts",
"packages/bundler/test/devtools.test.ts",
"apps/create-app/test/create-app.test.ts"
],
"limitations": [
"Panel dev-only; korelasi hook→request aproksimasi (request terbuka terakhir dengan path sama)",
"Daftar rute halaman saja (rute API tidak discan)"
],
"docs": "docs/public/cli.md"
},
{
"id": "client-bundle",
"name": "Build bundle client produksi (Vite) + route manifest + metadata build",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/bundler",
"@ramonyx/compiler"
],
"evidence": [
"packages/bundler/src/build.ts"
],
"limitations": [
"Belum ada code splitting per rute"
],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "source-maps",
"name": "Source map produksi (client + server)",
"status": "implemented",
"since": "0.1.1",
"priority": "p2",
"packages": [
"@ramonyx/bundler"
],
"evidence": [
"packages/bundler/src/index.ts"
],
"limitations": [],
"docs": "docs/public/deployment.md"
},
{
"id": "jsx-loader",
"name": "Loader dev/test JSX+TS (esbuild sendiri, dipakai app & test)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/compiler"
],
"evidence": [
"packages/compiler/src/register.mjs",
"packages/compiler/src/esbuild-hooks.mjs"
],
"limitations": [],
"docs": null
},
{
"id": "cli-devtools",
"name": "CLI `ramonyx` (dev, build --static/--target edge/--watch, start, doctor, konvensi src/server.ts)",
"status": "implemented",
"since": "0.1.1",
"priority": "p0",
"packages": [
"@ramonyx/devtools"
],
"evidence": [
"packages/devtools/src/commands.ts",
"packages/devtools/src/load.ts",
"apps/create-app/test/create-app.test.ts"
],
"limitations": [
"`start --watch` (restart server) belum",
"Plugin hooks butuh createApp meneruskan opsi hooks (template & aplikasi repo sudah)"
],
"docs": "docs/public/cli.md"
},
{
"id": "scaffolder",
"name": "Scaffolder aplikasi (`pnpm create:app` / `create-ramonyx-app`)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"create-ramonyx-app"
],
"evidence": [
"apps/create-app/test/create-app.test.ts"
],
"limitations": [],
"docs": "docs/public/cli.md"
},
{
"id": "typed-routes",
"name": "Typed routes (modul generate .ramonyx/routes.ts: RouteParamsMap + routeHref + Link)",
"status": "implemented",
"since": "0.2.0",
"priority": "p2",
"packages": [
"@ramonyx/router",
"@ramonyx/compiler",
"@ramonyx/devtools",
"@ramonyx/react"
],
"evidence": [
"packages/router/src/routes.ts",
"packages/compiler/src/pages.ts",
"packages/react/src/link.ts",
"packages/router/test/typed-routes.test.ts"
],
"limitations": [
"Impor via path relatif ke .ramonyx/routes.ts (belum ada alias)",
"href Link tetap string (URL konkret rute dinamis)"
],
"docs": "docs/public/typed-routes.md"
},
{
"id": "error-messages-id",
"name": "Pesan error: kode stabil (RMX-…) + tautan troubleshooting + overlay dev (stack/sumber)",
"status": "implemented",
"since": "0.2.0",
"priority": "p3",
"packages": [
"@ramonyx/core",
"@ramonyx/ssr",
"@ramonyx/compiler",
"@ramonyx/bundler"
],
"evidence": [
"packages/core/src/errors-docs.ts",
"packages/ssr/src/pages.ts"
],
"limitations": [
"Kode hanya untuk error framework (HttpError aplikasi bebas)",
"Overlay berupa halaman error ber-detail (bukan panel JS)"
],
"docs": "docs/public/troubleshooting.md"
},
{
"id": "static-export",
"name": "Ekspor statis situs docs (dogfood SSG)",
"status": "implemented",
"since": "0.2.0",
"priority": "p2",
"packages": [
"@ramonyx/bundler"
],
"evidence": [
"apps/docs/export.ts",
"apps/docs/test/docs.test.ts"
],
"limitations": [],
"docs": "docs/public/deployment.md"
},
{
"id": "ssg-first-class",
"name": "Static generation (SSG): prerenderRoutes + `ramonyx build --static`",
"status": "implemented",
"since": "0.2.0",
"priority": "p0",
"packages": [
"@ramonyx/bundler",
"@ramonyx/devtools"
],
"evidence": [
"packages/bundler/src/prerender.ts",
"packages/bundler/test/prerender.test.ts",
"packages/devtools/src/commands.ts"
],
"limitations": [
"Revalidasi/ISR belum (p1-revalidate)"
],
"docs": "docs/public/deployment.md"
},
{
"id": "http-cache-helpers",
"name": "Helper cache HTTP (cacheControl: maxAge/sMaxAge/SWR/immutable/private)",
"status": "implemented",
"since": "0.2.0",
"priority": "p2",
"packages": [
"@ramonyx/core"
],
"evidence": [
"packages/core/src/cache.ts",
"packages/core/test/cache.test.ts"
],
"limitations": [
"Helper untuk header respons — bukan cache runtime (cache data loader terpisah: ADR-0066)"
],
"docs": "docs/public/data-and-caching.md"
},
{
"id": "asset-fingerprinting",
"name": "Fingerprinting aset (client-.js) + manifest + cache immutable",
"status": "implemented",
"since": "0.2.0",
"priority": "p0",
"packages": [
"@ramonyx/bundler"
],
"evidence": [
"packages/bundler/src/build.ts",
"packages/bundler/test/build.test.ts",
"apps/create-app/template/src/routes/middleware.ts"
],
"limitations": [],
"docs": "docs/public/assets-and-images.md"
},
{
"id": "adapter-node",
"name": "Adapter runtime Node (http server, keep-alive, streaming)",
"status": "implemented",
"since": "0.1.0",
"priority": "p0",
"packages": [
"@ramonyx/adapter-runtime"
],
"evidence": [
"packages/adapter-runtime/src/node.ts",
"packages/adapter-runtime/test/node.test.ts"
],
"limitations": [
"Range request belum"
],
"docs": "docs/public/deployment.md"
},
{
"id": "adapter-memory",
"name": "Adapter memory (test in-process tanpa port)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [
"@ramonyx/adapter-runtime"
],
"evidence": [
"packages/adapter-runtime/src/memory.ts",
"packages/adapter-runtime/test/memory.test.ts"
],
"limitations": [],
"docs": "docs/public/testing.md"
},
{
"id": "adapter-serverless-edge",
"name": "Adapter edge/serverless (--target edge, kontrak fetch handler, binding env, smoke workerd)",
"status": "implemented",
"since": "0.2.0",
"priority": "p0",
"packages": [
"@ramonyx/bundler",
"@ramonyx/compiler",
"@ramonyx/devtools",
"@ramonyx/server"
],
"evidence": [
"packages/bundler/src/edge.ts",
"packages/compiler/src/server-entry.ts",
"scripts/edge-smoke.ts",
"apps/create-app/test/create-app.test.ts",
"docs/public/deployment.md",
"docs/public/engineering/adr/0058-edge-target.md"
],
"limitations": [
"Preset Vercel/Deno Deploy belum",
"Deploy ke akun platform tetap langkah pengguna (tanpa kredensial di CI)"
],
"docs": "docs/public/deployment.md"
},
{
"id": "compression-etag",
"name": "Kompresi (brotli/gzip) di server standalone + vary + flush streaming",
"status": "implemented",
"since": "0.2.0",
"priority": "p0",
"packages": [
"@ramonyx/adapter-runtime"
],
"evidence": [
"packages/adapter-runtime/src/node.ts",
"packages/adapter-runtime/test/node.test.ts"
],
"limitations": [
"Range request belum",
"Aset pra-kompresi (.br/.gz) belum"
],
"docs": "docs/public/deployment.md"
},
{
"id": "image-optimization",
"name": "Aset: komponen Image (anti-CLS, lazy) — tanpa resize/format modern (M1)",
"status": "implemented",
"since": "0.3.0",
"priority": "p3",
"packages": [
"@ramonyx/plugin-image",
"@ramonyx/bundler",
"@ramonyx/react"
],
"evidence": [
"packages/plugin-image/src/index.ts",
"packages/plugin-image/test/image.test.ts",
"packages/bundler/src/assets.ts",
"packages/react/src/assets.ts",
"apps/playground/e2e/image.spec.ts",
"benchmarks/results/image-pipeline-vitals.json",
"docs/public/assets-and-images.md",
"docs/public/engineering/adr/0069-assets.md"
],
"limitations": [
"Optimasi butuh sharp (peer opsional, tidak diinstal otomatis); dev tanpa varian",
"avif opt-in (jauh lebih lambat saat build); tanpa placeholder blur"
],
"docs": "docs/public/assets-and-images.md"
},
{
"id": "i18n",
"name": "Internasionalisasi: routing locale (prefix) + helper terjemahan",
"status": "implemented",
"since": "0.4.0",
"priority": "p3",
"packages": [
"@ramonyx/core",
"@ramonyx/ssr"
],
"evidence": [
"packages/core/src/i18n.ts",
"packages/ssr/src/pages.ts",
"packages/core/test/i18n.test.ts",
"packages/ssr/test/i18n.test.ts",
"apps/playground/e2e/i18n.spec.ts",
"docs/public/engineering/adr/0081-i18n.md"
],
"limitations": [
"Tanpa ICU MessageFormat (interpolasi + plural Intl saja)",
"Tanpa deteksi/redirect otomatis — pola middleware + cookie (docs/i18n.md)",
"Rute didaftarkan per locale (linear); lokalisasi per-rute belum didukung"
],
"docs": "docs/public/i18n.md"
},
{
"id": "auth-session",
"name": "Primitif autentikasi: cookie bertanda tangan + session (HMAC, edge-safe)",
"status": "partial",
"since": "0.2.0",
"priority": "p1",
"packages": [
"@ramonyx/security"
],
"evidence": [
"packages/security/src/cookies.ts",
"packages/security/src/session.ts",
"packages/security/test/session.test.ts",
"apps/playground/src/routes/sesi/page.tsx"
],
"limitations": [
"Ditandatangani, bukan dienkripsi",
"Tanpa rotasi secret otomatis / store bawaan / UI login"
],
"docs": "docs/public/sessions-and-auth.md"
},
{
"id": "forms",
"name": "Form handling (action + FormData + actionSchema + progressive enhancement)",
"status": "implemented",
"since": "0.2.0",
"priority": "p2",
"packages": [
"@ramonyx/ssr"
],
"evidence": [
"packages/ssr/src/pages.ts",
"packages/core/src/page.ts",
"packages/core/test/page.test.ts",
"docs/public/engineering/adr/0057-server-actions.md"
],
"limitations": [
"Upload file besar belum dioptimasi (multipart di-buffer FormData)"
],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "observability-logger",
"name": "Observability: trace-id + request logging terstruktur + metrik dasar (hook)",
"status": "implemented",
"since": "0.2.0",
"priority": "p2",
"packages": [
"@ramonyx/server"
],
"evidence": [
"packages/server/src/observability.ts",
"packages/server/test/observability.test.ts",
"apps/playground/src/app.ts"
],
"limitations": [
"In-memory per proses (tanpa agregasi multi-instance)",
"Tanpa tracing OTel native/histogram"
],
"docs": "docs/public/observability.md"
},
{
"id": "observability-metrics",
"name": "Metrik operasi runtime (timings, fase startup) — runtime Rust",
"status": "partial",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/metrics.rs"
],
"limitations": [
"Hanya di runtime eksperimental; versi Node kini punya createMetrics (ADR-0071)"
],
"docs": "docs/public/observability.md"
},
{
"id": "benchmark-gates",
"name": "Benchmark + gate report-only (HTTP/SSR/Vitals/startup) dengan baseline per-lingkungan",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [],
"evidence": [
"benchmarks/http/bench.ts",
"benchmarks/ssr/bench.tsx",
"benchmarks/lib/env.ts"
],
"limitations": [
"Report-only di CI (runner GitHub-hosted bervariasi); --strict sahih hanya di lingkungan baseline (guard ADR-0074)",
"Baseline mesin dev noisy (ambient load) — regenerasi di mesin tenang"
],
"docs": "docs/public/benchmarks.md"
},
{
"id": "e2e-playwright",
"name": "E2E browser (Playwright: SSR, hydration, navigasi, error, vitals)",
"status": "implemented",
"since": "0.1.0",
"priority": "p1",
"packages": [],
"evidence": [
"apps/playground/e2e/ssr.spec.ts",
"apps/playground/e2e/navigation.spec.ts"
],
"limitations": [],
"docs": "docs/public/testing.md"
},
{
"id": "testing-utilities",
"name": "Utilitas testing (in-process handler, request helper)",
"status": "implemented",
"since": "0.1.0",
"priority": "p2",
"packages": [
"@ramonyx/testing"
],
"evidence": [
"packages/testing/src/index.ts",
"packages/testing/test/testing.test.ts"
],
"limitations": [],
"docs": "docs/public/testing.md"
},
{
"id": "fuzz-tests",
"name": "Property-based/fuzz test (fast-check) untuk router, schema, query",
"status": "implemented",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"packages/router/test/match.fuzz.test.ts",
"packages/schema/test/validate.fuzz.test.ts",
"packages/server/test/query.fuzz.test.ts"
],
"limitations": [],
"docs": "docs/public/testing.md"
},
{
"id": "runtime-permissions",
"name": "Permission model deny-all runtime Rust (--allow-read/write/net/env, revoke)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/runtime-core/src/permissions.rs",
"runtime/crates/cli/tests/cli.rs"
],
"limitations": [
"Runtime belum produksi (Phase 4+)"
],
"docs": null
},
{
"id": "runtime-http-server",
"name": "HTTP server pull-model runtime (bind 127.0.0.1, keep-alive, cancellation)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/http.rs",
"runtime/crates/js-engine/tests/compat_playground.rs"
],
"limitations": [
"Bind localhost; production hardening belum"
],
"docs": null
},
{
"id": "runtime-streaming",
"name": "Streaming respons + request body runtime (chunked, 413 dini)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/http.rs",
"runtime/crates/js-engine/tests/compat_stream_ssr.rs"
],
"limitations": [],
"docs": null
},
{
"id": "runtime-worker-isolation",
"name": "Isolasi worker (thread + isolate V8 terpisah, pesan JSON, kill switch)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/worker.rs"
],
"limitations": [
"Kuota heap = heap V8 saja (ArrayBuffer besar tidak dihitung)"
],
"docs": null
},
{
"id": "runtime-plugin-process",
"name": "Isolasi plugin level proses (ramonyx.spawn, protokol JSON, permission irisan, kill)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/plugin.rs",
"runtime/crates/js-engine/tests/plugin_process.rs",
"runtime/crates/js-engine/tests/plugin_pentest.rs"
],
"limitations": [
"Landlock net deny hanya TCP (bukan UDP/DNS)",
"Env proses diwariskan OS-level (API JS dijaga permission; env_clear selektif kandidat)",
"Leaf-only: plugin tidak dapat spawn plugin (ADR-0075)"
],
"docs": null
},
{
"id": "runtime-plugin-supervision",
"name": "Supervisi plugin (restart + backoff eksponensial, health-check heartbeat, watchdog)",
"status": "experimental",
"since": "0.1.1",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/plugin.rs"
],
"limitations": [],
"docs": null
},
{
"id": "runtime-plugin-sandbox",
"name": "Sandbox OS plugin (Landlock: FS + TCP kernel-level)",
"status": "experimental",
"since": "0.1.1",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/sandbox.rs",
"runtime/crates/js-engine/tests/plugin_process.rs",
"runtime/crates/js-engine/tests/plugin_pentest.rs"
],
"limitations": [
"Landlock net deny hanya TCP",
"Pentest v1 selesai (5 skenario); audit eksternal belum"
],
"docs": null
},
{
"id": "runtime-quotas",
"name": "Kuota runtime (heap V8, watchdog timeout, CPU per-thread, RSS proses)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/lib.rs"
],
"limitations": [
"Heap hanya heap V8; alokasi ArrayBuffer raksasa tunggal masih bisa OOM fatal"
],
"docs": null
},
{
"id": "runtime-compat-suite",
"name": "Compatibility suite runtime (pipeline framework nyata berjalan di runtime)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/tests/compat_framework.rs"
],
"limitations": [],
"docs": null
},
{
"id": "runtime-cjs-interop",
"name": "Interop CommonJS runtime (require subset, react/react-dom server.browser)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/cjs.rs"
],
"limitations": [
"Subset — require dinamis/modul ESM lewat require ditolak dengan pesan jelas"
],
"docs": null
},
{
"id": "runtime-fetch",
"name": "fetch runtime (https + TLS diverifikasi, redirect per-hop, ReadableStream)",
"status": "experimental",
"since": "0.1.0",
"priority": "p2",
"packages": [],
"evidence": [
"runtime/crates/js-engine/src/http_client.rs",
"runtime/crates/js-engine/tests/fetch_tls.rs"
],
"limitations": [],
"docs": null
},
{
"id": "docs-site",
"name": "Situs dokumentasi (dogfood penuh: SSR + hydration + soft navigation atas docs/**)",
"status": "implemented",
"since": "0.1.1",
"priority": "p1",
"packages": [],
"evidence": [
"apps/docs/export.ts",
"apps/docs/test/docs.test.ts"
],
"limitations": [
"Di-host di GitHub Pages dengan domain kustom (CNAME)"
],
"docs": null
},
{
"id": "machine-docs",
"name": "Lapisan dokumentasi machine-readable (docs/machine: features/adrs/rules/security/glossary/stats)",
"status": "implemented",
"since": "0.2.0",
"priority": "p1",
"packages": [],
"evidence": [
"docs/public/machine/features.json",
"scripts/docs/validate.ts"
],
"limitations": [],
"docs": "docs/public/for-agents.md"
},
{
"id": "llms-txt",
"name": "llms.txt + llms-full.txt (digenerate, diterbitkan di situs)",
"status": "implemented",
"since": "0.2.0",
"priority": "p1",
"packages": [],
"evidence": [
"docs/public/machine/llms.txt",
"docs/public/machine/llms-full.txt"
],
"limitations": [
"Belum ada markdown per halaman untuk paket npm (hanya situs)"
],
"docs": "docs/public/for-agents.md"
},
{
"id": "agent-skill",
"name": "Skill agen untuk penulis aplikasi (.agents/skills/ramonyx)",
"status": "implemented",
"since": "0.2.0",
"priority": "p2",
"packages": [],
"evidence": [
".agents/skills/ramonyx/SKILL.md"
],
"limitations": [
"Satu skill; tanpa instalasi otomatis di scaffolder"
],
"docs": "docs/public/for-agents.md"
},
{
"id": "mcp-docs-server",
"name": "MCP docs server (tools untuk agen)",
"status": "implemented",
"since": "0.3.0",
"priority": "p2",
"packages": [
"@ramonyx/mcp-docs-server"
],
"evidence": [
"packages/mcp-docs-server/src/index.ts",
"packages/mcp-docs-server/src/docs.ts",
"packages/mcp-docs-server/src/cli.ts",
"packages/mcp-docs-server/test/server.test.ts",
"packages/mcp-docs-server/test/stdio.test.ts",
"docs/public/engineering/adr/0078-mcp-docs-server.md"
],
"limitations": [
"Subset protokol: stdio saja (tanpa HTTP/prompts/sampling/batching)",
"Snapshot docs = versi paket (docs repo terbaru butuh --docs dari clone)"
],
"docs": "docs/public/for-agents.md"
},
{
"id": "static-assets",
"name": "Middleware aset statis (serveStatic: ETag/304, immutable, guard traversal)",
"status": "implemented",
"since": "0.2.0",
"priority": "p0",
"packages": [
"@ramonyx/adapter-runtime"
],
"evidence": [
"packages/adapter-runtime/src/static.ts",
"packages/adapter-runtime/test/static.test.ts",
"docs/public/engineering/adr/0069-assets.md"
],
"limitations": [],
"docs": "docs/public/assets-and-images.md"
},
{
"id": "seo-helpers",
"name": "Helper SEO (sitemap.xml + robots.txt dari manifest rute)",
"status": "implemented",
"since": "0.2.0",
"priority": "p1",
"packages": [
"@ramonyx/ssr"
],
"evidence": [
"packages/ssr/src/seo.ts",
"packages/ssr/test/seo.test.ts",
"apps/docs/export.ts"
],
"limitations": [],
"docs": "docs/public/ssr-hydration.md"
},
{
"id": "routing-route-groups",
"name": "Route group `(grup)` — organisasi folder tanpa segmen URL",
"status": "implemented",
"since": "0.2.0",
"priority": "p2",
"packages": [
"@ramonyx/compiler"
],
"evidence": [
"packages/compiler/src/pages.ts",
"packages/compiler/src/manifest.ts",
"packages/compiler/test/playground-pages.test.ts"
],
"limitations": [],
"docs": "docs/public/routing.md"
},
{
"id": "boundary-enforcement",
"name": "Penegakan batas server/client (`*.server.ts` stub + tolak node:* di bundle klien)",
"status": "implemented",
"since": "0.2.0",
"priority": "p1",
"packages": [
"@ramonyx/bundler"
],
"evidence": [
"packages/bundler/src/server-only.ts",
"packages/bundler/src/build.ts",
"packages/bundler/test/build.test.ts"
],
"limitations": [
"Lint statis (Biome) untuk konvensi belum"
],
"docs": "docs/public/architecture.md"
},
{
"id": "extension-primitives",
"name": "Primitif ekstensi (AppHooks, KeyValueStore + contract test, kontrak plugin experimental)",
"status": "experimental",
"since": "0.2.0",
"priority": "p1",
"packages": [
"@ramonyx/core",
"@ramonyx/bundler"
],
"evidence": [
"packages/core/src/hooks.ts",
"packages/core/src/store.ts",
"packages/bundler/src/plugin.ts",
"tests/contracts/store.test.ts"
],
"limitations": [
"Kontrak plugin experimental sampai >=2 plugin nyata",
"Store eksternal (redis) belum ada implementasinya"
],
"docs": "docs/public/plugins.md"
},
{
"id": "revalidate",
"name": "Revalidasi data (ISR-lite): cache loader + SWR + invalidasi path/tag",
"status": "implemented",
"since": "0.2.0",
"priority": "p1",
"packages": [
"@ramonyx/core",
"@ramonyx/ssr"
],
"evidence": [
"packages/core/src/revalidate.ts",
"packages/ssr/src/pages.ts",
"packages/ssr/test/revalidate.test.ts",
"packages/core/test/revalidate.test.ts",
"docs/public/data-and-caching.md",
"docs/public/engineering/adr/0066-revalidate.md"
],
"limitations": [
"In-memory per proses (store eksternal belum ada implementasinya)",
"Query string/cookie melewati cache (disengaja)",
"Tanpa purge CDN"
],
"docs": "docs/public/data-and-caching.md"
},
{
"id": "plugin-ecosystem",
"name": "Plugin (vite passthrough + middleware pre-routing + hooks) dari ramonyx.config.ts",
"status": "partial",
"since": "0.2.0",
"priority": "p2",
"packages": [
"@ramonyx/bundler",
"@ramonyx/devtools",
"@ramonyx/server"
],
"evidence": [
"packages/bundler/src/plugin.ts",
"packages/plugin-image/src/index.ts",
"apps/playground/src/plugins/kunjungan.ts",
"docs/public/plugins.md"
],
"limitations": [
"Kontrak stabil dideklarasikan setelah satu siklus rilis tanpa perubahan aditif (ADR-0080; TODO p2-plugin-contract-freeze)",
"Tanpa registry/`ramonyx add` (instalasi manual)",
"Hook plugin butuh createApp aplikasi meneruskan opsi hooks"
],
"docs": "docs/public/plugins.md"
},
{
"id": "asset-pipeline",
"name": "Pipeline aset (src/assets/** → hash + manifest + resolver + preload font)",
"status": "implemented",
"since": "0.2.0",
"priority": "p2",
"packages": [
"@ramonyx/bundler",
"@ramonyx/core",
"@ramonyx/ssr"
],
"evidence": [
"packages/bundler/src/assets.ts",
"packages/react/src/assets.ts",
"packages/bundler/test/assets.test.ts",
"benchmarks/vitals/image-pipeline.mjs",
"benchmarks/results/image-pipeline-vitals.json",
"docs/public/assets-and-images.md",
"docs/public/engineering/adr/0069-assets.md"
],
"limitations": [
"Nama dev tanpa hash (fallbackToPath) berbeda dari produksi",
"Varian gambar hanya di build (dev menyajikan berkas asli)"
],
"docs": "docs/public/assets-and-images.md"
}
]
}
```
## docs/public/machine/adrs.json
```json
{
"schemaVersion": 1,
"adrs": [
{
"id": "ADR-0007",
"file": "0007-hydration.md",
"title": "Hydration — Inline Payload + One Client Bundle",
"status": "accepted",
"statusDetail": "Public summary (the full decision record lives in the internal log)",
"date": "2026-10-18",
"decision": "Embed a JSON hydration payload in the document and hydrate from one hashed client bundle; error pages are server-only.",
"related": ["ADR-0047"],
"features": ["soft-navigation"],
"visibility": "public"
},
{
"id": "ADR-0047",
"file": "0047-ssr-streaming.md",
"title": "SSR Streaming — Opt-in, Edge Renderer",
"status": "accepted",
"statusDetail": "Public summary (the full decision record lives in the internal log)",
"date": "2026-10-18",
"decision": "Streaming is opt-in via app.streaming/createPageRoutes; react-dom/server.edge renderers; error pages stay synchronous and errors after the first byte close the stream.",
"related": ["ADR-0007"],
"features": ["ssr-streaming"],
"visibility": "public"
},
{
"id": "ADR-0057",
"file": "0057-server-actions.md",
"title": "Server Actions — Form Mutations First-Class",
"status": "accepted",
"statusDetail": "Public summary (the full decision record lives in the internal log)",
"date": "2026-10-18",
"decision": "Pages may export action + actionSchema (POST to the same path); 303 PRG redirects; forms work without JavaScript; action result re-renders the page.",
"related": ["ADR-0004", "ADR-0066"],
"features": ["mutations-server-actions"],
"visibility": "public"
},
{
"id": "ADR-0058",
"file": "0058-edge-target.md",
"title": "Target Edge/Serverless",
"status": "accepted",
"statusDetail": "Public summary (the full decision record lives in the internal log)",
"date": "2026-10-18",
"decision": "ramonyx build --target edge emits a single { fetch } artifact with statically imported pages/API/middleware, inlined asset map, and a build guard against node:*; workerd smoke test in CI.",
"related": ["ADR-0068"],
"features": ["adapter-serverless-edge"],
"visibility": "public"
},
{
"id": "ADR-0066",
"file": "0066-revalidate.md",
"title": "Revalidation (ISR-lite) — Loader Data, Not HTML",
"status": "accepted",
"statusDetail": "Public summary (the full decision record lives in the internal log)",
"date": "2026-10-19",
"decision": "revalidate caches loader data with stale-while-revalidate; cookies/query bypass the cache; cacheTags + revalidate() invalidate on demand; store is pluggable.",
"related": ["ADR-0007", "ADR-0065"],
"features": ["revalidate"],
"visibility": "public"
},
{
"id": "ADR-0069",
"file": "0069-assets.md",
"title": "Asset Pipeline — Manifest + Resolver",
"status": "accepted",
"statusDetail": "Public summary (the full decision record lives in the internal log)",
"date": "2026-10-19",
"decision": "src/assets is hashed into a manifest; assets.url()/assets.image() feed loaders and Image; edge inlines the map; URLs map 1:1 to the client output root.",
"related": ["ADR-0055", "ADR-0079"],
"features": ["asset-pipeline", "image-optimization"],
"visibility": "public"
},
{
"id": "ADR-0078",
"file": "0078-mcp-docs-server.md",
"title": "MCP Docs Server — stdio, No Hosting",
"status": "accepted",
"statusDetail": "Public summary (the full decision record lives in the internal log)",
"date": "2026-10-19",
"decision": "Package @ramonyx/mcp-docs-server runs MCP over stdio with no external dependencies and five tools; the npm package bundles a public docs snapshot and is verified in the release pipeline.",
"related": ["ADR-0054"],
"features": ["mcp-docs-server"],
"visibility": "public"
},
{
"id": "ADR-0081",
"file": "0081-i18n.md",
"title": "i18n — Primitives + Locale Routing",
"status": "accepted",
"statusDetail": "Public summary (the full decision record lives in the internal log)",
"date": "2026-10-19",
"decision": "Edge-safe primitives (negotiation, localePath, createTranslator); per-locale literal routes; default locale unprefixed with 308 normalization; lang/dir + hreflang; locale-aware cache keys; no automatic redirects.",
"related": ["ADR-0066"],
"features": ["i18n"],
"visibility": "public"
}
]
}
```
## docs/public/machine/rules.json
```json
{
"schemaVersion": 1,
"rules": [
{
"id": "erasable-syntax",
"rule": "Tulis TypeScript yang erasable: dilarang `enum`, `namespace`, parameter property, dan `import =`. Tipe harus hilang saat transpile.",
"rationale": "Paket framework diekspor sebagai `.ts` dan dijalankan Node type stripping (tanpa transpile). Sintaks non-erasable gagal saat runtime.",
"scope": "framework",
"good": "export type Status = \"ok\" | \"error\";\nconst STATUS = { ok: \"ok\" } as const;",
"bad": "enum Status { Ok, Error }\nclass X { constructor(private y: number) {} }",
"enforcement": "tsconfig `erasableSyntaxOnly` + `verbatimModuleSyntax`; `pnpm typecheck`",
"adrs": ["ADR-0002", "ADR-0006"]
},
{
"id": "relative-imports-extension",
"rule": "Impor relatif memakai ekstensi eksplisit: `./x.ts` / `./x.tsx` (bukan `./x`).",
"rationale": "Node ESM tidak menebak ekstensi; paket diekspor sebagai sumber `.ts` dan dijalankan langsung.",
"scope": "framework",
"good": "import { createApp } from \"./app.ts\";",
"bad": "import { createApp } from \"./app\";",
"enforcement": "Konsisten di seluruh repo; test loader akan gagal bila salah",
"adrs": ["ADR-0002"]
},
{
"id": "node-builtins-boundary",
"rule": "Impor `node:*` hanya di paket Node-only (adapter-runtime, compiler, bundler, ssr, react, security, devtools, mcp-docs-server, plugin-image). Paket edge-safe (core, router, schema, server, testing, ramonyx) dilarang.",
"rationale": "Paket edge-safe harus berjalan di runtime edge/serverless tanpa `node:*` (artefak `--target edge`, ADR-0058); paket Node-only memang butuh fs/net/build.",
"scope": "framework",
"good": "// packages/security/src/ssrf.ts\nimport { lookup } from \"node:dns/promises\";",
"bad": "// packages/core/src/route.ts\nimport { readFile } from \"node:fs/promises\";",
"enforcement": "Biome `noRestrictedImports` (packages/core|router|schema|server|testing|ramonyx) + guard build edge",
"adrs": ["ADR-0002", "ADR-0015"]
},
{
"id": "no-jsx-in-framework",
"rule": "Paket framework tidak berisi JSX; JSX hanya di kode aplikasi (`.tsx`) yang dijalankan lewat loader dev/test.",
"rationale": "Node type stripping tidak memproses JSX; paket framework harus berjalan di Node murni tanpa loader.",
"scope": "framework",
"good": "// packages/react/src/renderer.ts — fungsi murni tanpa JSX\nreturn createElement(page.component, props);",
"bad": "// packages/react/src/renderer.ts\nreturn ;",
"enforcement": "Struktur repo (tidak ada `.tsx` di packages/*/src) + test",
"adrs": ["ADR-0006"]
},
{
"id": "test-server-close-finally",
"rule": "Setiap server yang dijalankan test WAJIB `close()` di blok `finally`.",
"rationale": "Assert gagal sebelum `close()` membocorkan listener → `node --test` menggantung tanpa batas (pernah terjadi di CI).",
"scope": "repo",
"good": "const server = await start();\ntry { ...assert... } finally { await server.close(); }",
"bad": "const server = await start();\nassert.equal(...); // gagal → server tidak pernah ditutup\nawait server.close();",
"enforcement": "`--test-timeout=120000` + `--test-force-exit` sebagai jaring pengaman; konvensi review",
"adrs": []
},
{
"id": "node-env-production",
"rule": "Produksi WAJIB `NODE_ENV=production` (skrip `start`/`docs`/bench dan template `create-app` sudah men-set).",
"rationale": "Tanpa itu React berjalan build dev — pernah terjadi di server produksi (ADR-0048).",
"scope": "app",
"good": "\"start\": \"NODE_ENV=production node --import ./tools/register.mjs src/server.ts\"",
"bad": "\"start\": \"node src/server.ts\"",
"enforcement": "Template + skrip repo sudah benar; CI menjalankan build produksi di e2e",
"adrs": ["ADR-0048"]
},
{
"id": "loader-data-json",
"rule": "Data loader halaman harus JSON-serializable.",
"rationale": "Payload hydration ditanam inline ke HTML (`window.__RAMONYX__`); nilai non-JSON (function/class/Date) hilang atau error.",
"scope": "app",
"good": "export function load() { return { items: [\"a\", \"b\"] }; }",
"bad": "export function load() { return { when: new Date(), fn: () => 1 }; }",
"enforcement": "Serialisasi payload saat render; test SSR",
"adrs": ["ADR-0007"]
},
{
"id": "streaming-server-edge",
"rule": "SSR streaming memakai `react-dom/server.edge`; JANGAN `react-dom/server.browser` di Node.",
"rationale": "`server.browser` membuat MessageChannel level-modul yang menahan proses Node (test/CLI menggantung).",
"scope": "framework",
"good": "import { renderToReadableStream } from \"react-dom/server.edge\";",
"bad": "import { renderToReadableStream } from \"react-dom/server.browser\";",
"enforcement": "Impor langsung di `packages/ssr`; test streaming + e2e",
"adrs": ["ADR-0047"]
},
{
"id": "error-pages-not-hydrated",
"rule": "Jangan mengandalkan interaktivitas client di halaman error — halaman error tidak di-hydrate.",
"rationale": "Payload/hydration hanya untuk halaman sukses (ADR-0007); halaman error adalah HTML statis.",
"scope": "app",
"good": "// error.tsx: tautan biasa",
"bad": "// error.tsx: useEffect(() => fetch(...)) berharap hydration jalan",
"enforcement": "Renderer tidak menyuntikkan script client di halaman error; e2e errors.spec",
"adrs": ["ADR-0007"]
},
{
"id": "docs-relative-links",
"rule": "Tautan di dalam `docs/**` (situs docs) harus relatif — jangan absolut (`/dokumen/...`).",
"rationale": "Situs di-host di root domain kustom; `` menegakkan tautan relatif (ADR-0052).",
"scope": "docs",
"good": "[arsitektur](../architecture.md)",
"bad": "[arsitektur](/dokumen/architecture)",
"enforcement": "Rewriter tautan markdown otomatis + validasi `pnpm docs:validate`",
"adrs": ["ADR-0052"]
},
{
"id": "numbers-single-source",
"rule": "Jangan mengetik angka (versi, jumlah test/ADR/paket) di dokumen; rujuk `docs/machine/stats.json`.",
"rationale": "Angka duplikat menjadi basi (audit menemukan 128/101 vs 161/111). Satu sumber = tidak bisa drift.",
"scope": "docs",
"good": "Angka aktual: `docs/machine/stats.json` (generate: `pnpm docs:stats`).",
"bad": "Repo punya 161 tes Node dan 111 tes Rust.",
"enforcement": "`pnpm docs:validate` + review",
"adrs": ["ADR-0054"]
},
{
"id": "claims-need-evidence",
"rule": "Klaim performa/keamanan wajib punya bukti yang bisa dijalankan (test, benchmark, ADR). Tanpa itu tulis \"belum\".",
"rationale": "Disiplin klaim BLUEPRINT §10 — kredibilitas proyek dibangun dari bukti, bukan klaim.",
"scope": "docs",
"good": "\"Body >1 MiB → 413 (test: tests/security/server-hardening.test.ts)\"",
"bad": "\"Ramonyx sangat aman dan cepat.\"",
"enforcement": "Review + `pnpm docs:validate` (semua bukti di features.json harus ada)",
"adrs": []
},
{
"id": "adr-for-decisions",
"rule": "Keputusan arsitektur (API publik, format, protokol, dependency baru) ditulis sebagai ADR di `docs/adr/` + indeks diperbarui.",
"rationale": "Jejak keputusan yang bisa diaudit; ADR = sumber kebenaran untuk \"kenapa\", kode untuk \"apa\".",
"scope": "repo",
"good": "docs/adr/0054-machine-readable-docs.md + baris di docs/adr/README.md",
"bad": "Mengubah format publik tanpa catatan keputusan.",
"enforcement": "Konvensi review; `pnpm docs:validate` memeriksa indeks ADR lengkap",
"adrs": []
},
{
"id": "bench-strict-local-only",
"rule": "`--strict` pada benchmark hanya sahih di mesin baseline; jangan mengaktifkan gate blocking di CI lintas-mesin.",
"rationale": "CI ±3× lebih cepat dari mesin dev; guard menolak lintas-mesin (ADR-0045).",
"scope": "repo",
"good": "pnpm bench:report # CI: report-only",
"bad": "VITALS_STRICT=1 di CI tanpa baseline per-lingkungan",
"enforcement": "Guard `cpuModel` di runner bench",
"adrs": ["ADR-0045", "ADR-0048"]
},
{
"id": "dependency-quarantine",
"rule": "Dependency baru menunggu 24 jam sebelum dipasang (`minimumReleaseAge` di `pnpm-workspace.yaml`). Jangan bypass.",
"rationale": "Kebijakan supply chain (ADR-0016) — mitigasi paket berbahaya yang baru dirilis.",
"scope": "repo",
"good": "Tunggu ≥24 jam setelah rilis versi baru sebelum menaikkan dependency.",
"bad": "pnpm add lib@latest && pnpm install --config.minimumReleaseAge=0",
"enforcement": "`pnpm-workspace.yaml` + review",
"adrs": ["ADR-0016"]
},
{
"id": "node-version",
"rule": "Butuh Node ≥24. Jalankan app/test dengan loader: `node --import ./tools/register.mjs` (atau CLI `ramonyx`).",
"rationale": "Fitur Node 24 (type stripping stabil) menjadi baseline; JSX butuh loader esbuild.",
"scope": "app",
"good": "node --import ./tools/register.mjs src/server.ts",
"bad": "node src/server.tsx # JSX tidak diproses Node murni",
"enforcement": "`engines` di package.json; loader publik `@ramonyx/compiler/register`",
"adrs": ["ADR-0002", "ADR-0006"]
},
{
"id": "client-server-boundary",
"rule": "Kode server (fs/db/rahasia) ditulis di modul `*.server.ts`; jangan mengimpor `node:*` langsung dari modul halaman/layout (bundle klien).",
"rationale": "Modul halaman masuk bundle klien (hydration); impor server langsung merusak bundle. Stub poison-pill membuat pemanggilan dari klien gagal cepat dengan pesan jelas.",
"scope": "app",
"good": "// data.server.ts (server)\nimport { readFile } from \"node:fs/promises\";\nexport async function baca() { return readFile(...); }\n// page.tsx\nimport { baca } from \"./data.server.ts\";\nexport async function loader() { return { isi: await baca() }; }",
"bad": "// page.tsx\nimport { readFile } from \"node:fs/promises\"; // → build ditolak (ADR-0063)",
"enforcement": "Build klien: pre-check modul klien + scan output (shim browser-external/node:*) → error dengan file; stub `*.server.ts` melempar saat dipanggil di browser",
"adrs": ["ADR-0063"]
},
{
"id": "mutations-via-action",
"rule": "Mutasi yang berasal dari form ditulis sebagai `export const action` di halaman (POST ke path yang sama), bukan API route + fetch manual.",
"rationale": "Action memberi validasi (actionSchema), pesan error terstruktur, dan progressive enhancement (berfungsi tanpa JS) tanpa boilerplate state client.",
"scope": "app",
"good": "export function action({ formData }: PageActionContext) {\n const nama = String(formData.get(\"nama\") ?? \"\").trim();\n if (nama.length < 2) return { errors: { nama: \"Nama minimal 2 karakter\" } };\n return { message: `Halo, ${nama}!` };\n}",
"bad": "// fetch manual + useState untuk error di setiap form\nonSubmit = async (e) => { e.preventDefault(); const res = await fetch(\"/api/simpan\", { method: \"POST\", body: JSON.stringify(...) }); ... }",
"enforcement": "Konvensi + test (ssr action tests, e2e form); API route tetap sah untuk non-form",
"adrs": ["ADR-0057"]
},
{
"id": "middleware-edge-safe",
"rule": "`src/routes/middleware.ts` harus edge-safe (tanpa `node:*`); urusan host Node (serveStatic, logger, dll.) dipasang di `src/app.ts`.",
"rationale": "Middleware ikut dibundel ke artefak edge (`ramonyx build --target edge`); impor `node:*` membuat build gagal (guard) atau runtime edge error.",
"scope": "app",
"good": "// routes/middleware.ts\nexport default [securityHeaders(), cspNonce()];",
"bad": "// routes/middleware.ts\nimport { serveStatic } from \"@ramonyx/adapter-runtime\"; // node:fs → gagal di edge",
"enforcement": "Guard build edge menolak `node:*` dengan pesan jelas",
"adrs": ["ADR-0058"]
},
{
"id": "additive-contracts",
"rule": "Perubahan kontrak publik (payload hydration, opsi createHandler/createPageRoutes, PageModule, manifest aset, config) harus ADITIF — hanya menambah field opsional; perubahan nama/bentuk butuh ADR + jalur deprecasi.",
"rationale": "Mencegah rewrite berulang dan breaking change diam-diam; freeze window 0.2.0 menetapkan titik beku kontrak (ADR-0065).",
"scope": "framework",
"good": "// menambah field opsional\nreadonly hooks?: AppHooks;",
"bad": "// mengganti nama field tanpa deprecasi\nreadonly actions?: ... // dulu: action",
"enforcement": "Review + ADR + test; fitur baru wajib memakai primitif yang ada (hooks/store/config pass-through) alih-alih mekanisme baru",
"adrs": ["ADR-0065"]
},
{
"id": "cache-session-safety",
"rule": "Jangan cache data yang bergantung sesi/cookie atau query; `revalidate` hanya untuk data publik (GET tanpa query/cookie — ditegakkan otomatis oleh framework).",
"rationale": "Cache data sesi dapat menyajikan data pengguna lain (kebocoran lintas pengguna) — kesalahan kelas keamanan paling berbahaya pada caching.",
"scope": "app",
"good": "// data publik (katalog) → boleh\nexport const revalidate = 60;",
"bad": "// data per pengguna → jangan\nexport const revalidate = 60;\nexport async function loader({ request }) { return profil(request.headers.get(\"cookie\")); }",
"enforcement": "Framework melewati cache saat ada cookie/query (test: packages/ssr/test/revalidate.test.ts)",
"adrs": ["ADR-0066"]
}
]
}
```
## docs/public/machine/glossary.json
```json
{
"schemaVersion": 1,
"terms": [
{
"term": "SSR (Server-Side Rendering)",
"definition": "Render React di server menjadi HTML; dua mode: sinkron (`renderToString`) dan streaming (opt-in `streaming: true`, `renderToReadableStream`).",
"related": [
"Hydration",
"Streaming SSR"
],
"docs": "docs/public/api/core-contract.md"
},
{
"term": "Streaming SSR",
"definition": "SSR yang mengirim HTML bertahap; error setelah byte pertama memutus koneksi (status/error page hanya berlaku sebelum byte pertama). Memakai `react-dom/server.edge`.",
"related": [
"SSR (Server-Side Rendering)",
"Suspense"
],
"docs": "docs/public/engineering/adr/0047-ssr-streaming.md"
},
{
"term": "Hydration",
"definition": "Mengaktifkan React di browser atas HTML SSR: payload inline `window.__RAMONYX__` + satu bundle client + `hydrateRoot`. Halaman error tidak di-hydrate.",
"related": [
"SSR (Server-Side Rendering)",
"Soft navigation"
],
"docs": "docs/public/engineering/adr/0007-hydration.md"
},
{
"term": "Soft navigation",
"definition": "Navigasi klien tanpa full reload: fetch HTML halaman → swap `#ramonyx-root` → re-hydrate; `document.title` + scroll direset. Belum ada prefetch.",
"related": [
"Hydration"
]
},
{
"term": "Loader",
"definition": "Fungsi per halaman yang mengambil data sebelum render (GET, boleh async); hasilnya harus JSON-serializable karena ikut payload hydration.",
"related": [
"Page module",
"Payload"
],
"docs": "docs/public/api/core-contract.md"
},
{
"term": "Page module",
"definition": "Modul `.tsx` di folder rute yang mengekspor komponen `default` + opsional `load` dan `metadata` (title/status).",
"related": [
"Loader",
"Layout",
"Route manifest"
],
"docs": "docs/public/api/core-contract.md"
},
{
"term": "Layout",
"definition": "Komponen pembungkus per folder (`layout.tsx`) yang membungkus halaman; bersarang dan layout terdekat menang.",
"related": [
"Page module",
"Error boundary"
],
"docs": "docs/public/api/core-contract.md"
},
{
"term": "Error boundary",
"definition": "Komponen `error.tsx` per folder yang dirender saat loader/render gagal; boundary terdekat menang. Halaman error tidak di-hydrate.",
"related": [
"Layout"
],
"docs": "docs/public/api/core-contract.md"
},
{
"term": "Route manifest",
"definition": "Artefak compiler yang memetakan file rute → pola URL, method, dan entry (dibuat saat build/dev; sumber kebenaran routing).",
"related": [
"Page module",
"Route entry"
]
},
{
"term": "Route entry",
"definition": "Pasangan rute halaman/API + handler yang dihasilkan compiler dan dipasang ke pipeline server.",
"related": [
"Route manifest"
]
},
{
"term": "Middleware",
"definition": "Fungsi app-level (array di `createHandler`) yang berjalan berurutan sebelum rute; dapat mengganti request (mis. menyuntik nonce). Belum ada per-folder.",
"related": [
"Handler",
"CSP nonce"
],
"docs": "docs/public/api/core-contract.md"
},
{
"term": "Handler",
"definition": "Pipeline request server: middleware → pencocokan rute → validasi → eksekusi (halaman/API) dengan limits (body/timeout/concurrency).",
"related": [
"Middleware",
"Adapter"
]
},
{
"term": "Adapter",
"definition": "Lapisan runtime yang menjalankan handler: `node` (http server), `memory` (test in-process). Adapter serverless/edge belum ada.",
"related": [
"Handler",
"RuntimeHost"
]
},
{
"term": "RuntimeHost",
"definition": "Kontrak antara framework dan host runtime (Node baseline; runtime Rust eksperimental). Lihat docs/architecture.md.",
"related": [
"Adapter"
]
},
{
"term": "Client bundle",
"definition": "Bundle JS untuk browser (Vite) berisi entry client + komponen yang di-hydrate; dibangun oleh `ramonyx build`.",
"related": [
"Hydration"
]
},
{
"term": "Capability / permission",
"definition": "Izin eksplisit di runtime Rust (fs/env/net) — default deny-all; diberikan lewat flag `--allow-*`, dapat dicabut (`revoke`).",
"related": [
"Deny-all",
"Worker",
"Plugin"
]
},
{
"term": "Deny-all",
"definition": "Default permission runtime: tanpa flag `--allow-*`, akses fs/env/net ditolak — bukan diizinkan dengan peringatan.",
"related": [
"Capability / permission"
]
},
{
"term": "Worker (runtime)",
"definition": "Eksekusi di thread + isolate V8 terpisah dengan pesan JSON, permission irisan (anti-eskalasi), dan kill switch. Bukan sandbox keamanan penuh.",
"related": [
"Capability / permission",
"Plugin"
]
},
{
"term": "Plugin (runtime)",
"definition": "Proses terpisah yang dijalankan `ramonyx.spawn` dengan protokol JSON stdin/stdout; dapat disupervisi (restart, backoff, heartbeat) dan disandbox Landlock.",
"related": [
"Worker (runtime)",
"Landlock",
"Watchdog"
]
},
{
"term": "Landlock",
"definition": "LSM kernel Linux yang membatasi filesystem + TCP proses plugin di level kernel; pelengkap permission JS. Net deny = TCP saja (bukan UDP/DNS).",
"related": [
"Plugin (runtime)",
"Deny-all"
]
},
{
"term": "Watchdog",
"definition": "Health-check otomatis plugin: heartbeat + deteksi senyap, restart dengan backoff eksponensial dan reset stabil.",
"related": [
"Plugin (runtime)"
]
},
{
"term": "Gate report-only",
"definition": "Gate benchmark yang melaporkan regresi tanpa memblokir CI; `--strict` hanya sahih di mesin baseline (guard lintas-mesin).",
"related": [
"Baseline"
]
},
{
"term": "Baseline (benchmark)",
"definition": "Hasil benchmark ter-commit di `benchmarks/results/` sebagai pembanding; median dari beberapa ulangan, bukan rata-rata.",
"related": [
"Gate report-only"
]
},
{
"term": "ADR (Architecture Decision Record)",
"definition": "Catatan keputusan arsitektur di `docs/adr/` — satu keputusan per berkas, dengan konteks/alternatif/konsekuensi; indeks machine-readable di docs/machine/adrs.json.",
"related": [
"Machine layer"
]
},
{
"term": "Machine layer",
"definition": "Lapisan dokumentasi machine-readable di `docs/machine/` (features/adrs/rules/security/glossary/stats/llms) yang divalidasi `pnpm docs:validate`.",
"related": [
"ADR (Architecture Decision Record)",
"llms.txt"
],
"docs": "docs/public/machine/README.md"
},
{
"term": "llms.txt",
"definition": "Indeks dokumentasi untuk agen AI (format llmstxt.org): H1 + ringkasan + daftar tautan; Ramonyx juga menerbitkan `llms-full.txt` (teks lengkap).",
"related": [
"Machine layer"
],
"docs": "docs/public/machine/llms.txt"
},
{
"term": "Stats",
"definition": "`docs/machine/stats.json` — satu-satunya tempat angka (versi, jumlah paket/ADR/test) berada; digenerate `pnpm docs:stats`.",
"related": [
"Machine layer"
],
"docs": "docs/public/machine/stats.json"
}
]
}
```
## docs/public/machine/api.json
```json
{
"schemaVersion": 1,
"generatedAt": "2026-10-20T09:02:43.014Z",
"packages": [
{
"name": "@ramonyx/adapter-runtime",
"version": "0.4.1",
"source": "packages/adapter-runtime/src/index.ts",
"values": [
"createConsoleLogger",
"createMemoryHost",
"createNodeHost",
"serveStatic"
],
"types": [
"FetchHandler",
"MemoryHost",
"MemoryHostOptions",
"MemoryServer",
"NodeHostOptions",
"RunningServer",
"RuntimeHost",
"ServeOptions",
"ServeStaticOptions"
],
"reexports": []
},
{
"name": "@ramonyx/bundler",
"version": "0.4.1",
"source": "packages/bundler/src/index.ts",
"values": [
"CLIENT_MANIFEST_FILE",
"DEFAULT_ASSETS_DIR",
"assetInfoMap",
"assetUrl",
"assetsDirExists",
"buildClient",
"buildEdge",
"collectAssets",
"createAssetResolver",
"createDevServer",
"definePlugin",
"fontPreloadTag",
"loadPlugins",
"prerenderRoutes",
"readAssetManifest",
"readClientScript",
"writeAssetManifest"
],
"types": [
"AssetEntry",
"AssetManifest",
"AssetMap",
"AssetProcessor",
"AssetProcessorInput",
"AssetProcessorOutput",
"AssetProcessorResult",
"AssetUrlOptions",
"BuildClientOptions",
"BuildClientResult",
"BuildEdgeOptions",
"BuildEdgeResult",
"CollectAssetsOptions",
"CollectAssetsResult",
"CreateAssetResolverOptions",
"DevServer",
"DevServerOptions",
"LoadedPlugins",
"PluginContext",
"PrerenderOptions",
"PrerenderResult",
"PrerenderRouteInfo",
"RamonyxPlugin"
],
"reexports": []
},
{
"name": "@ramonyx/compiler",
"version": "0.4.1",
"source": "packages/compiler/src/index.ts",
"values": [
"HTTP_METHODS",
"RouteManifestError",
"createRouteManifest",
"generateClientEntry",
"generateRouteTypes",
"generateServerEntry",
"listApiRoutes",
"loadPageRoutes",
"nativeImporter"
],
"types": [
"ApiRouteFile",
"CreateRouteManifestOptions",
"GenerateClientEntryOptions",
"GenerateServerEntryOptions",
"HttpMethod",
"LoadPageRoutesOptions",
"ModuleImporter"
],
"reexports": []
},
{
"name": "@ramonyx/core",
"version": "0.4.1",
"source": "packages/core/src/index.ts",
"values": [
"HttpError",
"LoadedPage",
"NONCE_HEADER",
"PageAction",
"PageActionContext",
"PageActionResult",
"PageActionState",
"PageLoader",
"PageLoaderContext",
"PageMetadata",
"PageModule",
"REMOTE_ADDRESS_HEADER",
"TROUBLESHOOTING_URL",
"cacheControl",
"createCache",
"createMemoryStore",
"defineConfig",
"docsUrl",
"errorHint",
"fillRouteParams",
"formDataToObject",
"hasDynamicSegment",
"mergeHooks",
"pathTag",
"resolveConfig",
"route",
"routeTag",
"silentLogger"
],
"types": [
"AppConfig",
"AppHooks",
"AssetResolver",
"Cache",
"CacheControlOptions",
"CreateCacheOptions",
"EnvReader",
"ErrorCode",
"FetchHandler",
"HttpErrorOptions",
"InvalidateEvent",
"KeyValueStore",
"LoaderEndEvent",
"Logger",
"MemoryStoreOptions",
"Middleware",
"MiddlewareContext",
"RamonyxConfig",
"RamonyxConfigInput",
"RenderEndEvent",
"RequestEndEvent",
"RequestStartEvent",
"RevalidateOptions",
"RouteDefinition",
"RouteEntry",
"RouteHandler",
"RouteHandlerContext",
"ServerConfig",
"StoreEntry",
"StoreSetOptions"
],
"reexports": [
"./assets.ts",
"./i18n.ts"
]
},
{
"name": "@ramonyx/devtools",
"version": "0.4.1",
"source": "packages/devtools/src/index.ts",
"values": [
"loadConfigFile",
"loadCreateApp",
"runCli"
],
"types": [
"CreateApp",
"CreateAppOptions"
],
"reexports": []
},
{
"name": "@ramonyx/mcp-docs-server",
"version": "0.4.1",
"source": "packages/mcp-docs-server/src/index.ts",
"values": [
"MCP_PROTOCOL_VERSION",
"createDocsIndex",
"createMcpServer",
"resolveDocsRoot",
"runStdioServer"
],
"types": [
"DocEntry",
"DocHit",
"DocsIndex",
"MachineName"
],
"reexports": []
},
{
"name": "@ramonyx/plugin-image",
"version": "0.4.1",
"source": "packages/plugin-image/src/index.ts",
"values": [
"imageOptimizer"
],
"types": [],
"reexports": []
},
{
"name": "ramonyx",
"version": "0.4.1",
"source": "packages/ramonyx/src/index.ts",
"values": [
"HttpError",
"cacheControl",
"createCache",
"createMemoryStore",
"createTranslator",
"defineConfig",
"formDataToObject",
"localePath",
"mergeHooks",
"negotiateLocale",
"parseAcceptLanguage",
"resolveConfig",
"route"
],
"types": [
"AppHooks",
"Cache",
"CreateTranslatorOptions",
"EnvReader",
"FetchHandler",
"I18nRoutingOptions",
"KeyValueStore",
"LanguageRange",
"Logger",
"Messages",
"Middleware",
"MiddlewareContext",
"PageAction",
"PageActionContext",
"PageActionResult",
"PageActionState",
"PageLoaderContext",
"RamonyxConfig",
"RamonyxConfigInput",
"RevalidateOptions",
"RouteDefinition",
"RouteEntry",
"RouteHandlerContext",
"Translator"
],
"reexports": []
},
{
"name": "@ramonyx/react",
"version": "0.4.1",
"source": "packages/react/src/index.ts",
"values": [
"createReactRenderer"
],
"types": [
"ErrorPageProps",
"LayoutProps",
"PageProps"
],
"reexports": []
},
{
"name": "@ramonyx/router",
"version": "0.4.1",
"source": "packages/router/src/index.ts",
"values": [
"createRouteTable",
"fillHref",
"matchRoute"
],
"types": [
"RouteMatch",
"RouteTable"
],
"reexports": []
},
{
"name": "@ramonyx/schema",
"version": "0.4.1",
"source": "packages/schema/src/index.ts",
"values": [
"validate"
],
"types": [
"InferInput",
"InferOutput",
"StandardIssue",
"StandardPathSegment",
"StandardResult",
"StandardSchema",
"StandardSchemaProps",
"ValidationIssue",
"ValidationResult"
],
"reexports": []
},
{
"name": "@ramonyx/security",
"version": "0.4.1",
"source": "packages/security/src/index.ts",
"values": [
"DEFAULT_COOKIE_OPTIONS",
"createSession",
"cspNonce",
"csrfProtection",
"defaultContentSecurityPolicy",
"expireCookie",
"parseCookies",
"rateLimit",
"securityHeaders",
"serializeCookie",
"signValue",
"verifyValue"
],
"types": [
"CookieOptions",
"CspNonceOptions",
"CsrfOptions",
"RateLimitOptions",
"SecurityHeadersOptions",
"Session",
"SessionData",
"SessionOptions"
],
"reexports": []
},
{
"name": "@ramonyx/server",
"version": "0.4.1",
"source": "packages/server/src/index.ts",
"values": [
"createHandler",
"createMetrics",
"createMetricsRoute",
"parseQuery",
"requestLogging",
"traceId",
"withMiddleware"
],
"types": [
"CreateHandlerOptions",
"Metrics",
"MetricsSnapshot",
"QueryRecord",
"QueryValue",
"RequestLoggingOptions",
"TraceIdOptions"
],
"reexports": []
},
{
"name": "@ramonyx/ssr",
"version": "0.4.1",
"source": "packages/ssr/src/index.ts",
"values": [
"PAYLOAD_SCRIPT_ID",
"RAMONYX_MOUNT_ID",
"createPageRoutes",
"createSeoRoutes",
"defaultDocument",
"escapeHtml",
"robotsTxt",
"sitemapXml"
],
"types": [
"ClientOptions",
"CreatePageRoutesOptions",
"CreateSeoRoutesOptions",
"DocumentOptions",
"DocumentRenderer",
"ElementDescriptor",
"Renderer",
"RobotsOptions",
"SitemapEntry"
],
"reexports": []
},
{
"name": "@ramonyx/testing",
"version": "0.4.1",
"source": "packages/testing/src/index.ts",
"values": [
"createTestServer",
"createTestServerFromHandler"
],
"types": [],
"reexports": []
}
]
}
```
## docs/public/machine/cli.json
```json
{
"schemaVersion": 1,
"generatedAt": "2026-10-20T09:02:44.164Z",
"usage": "ramonyx — CLI pengembangan Ramonyx\n\nPemakaian:\n ramonyx dev Vite + HMR (default :5173) → SSR (port dari config)\n ramonyx build Bundel klien → dist/client (jalankan sebelum start produksi)\n ramonyx build --static Bundel klien + prerender halaman → dist/static (hosting statis)\n ramonyx build --target edge\n Bundel server edge/serverless → dist/edge (kontrak fetch handler)\n ramonyx start Server produksi (NODE_ENV=production, SSR)\n ramonyx doctor Diagnostik konfigurasi & lingkungan\n ramonyx version Versi CLI\n ramonyx help Bantuan ini\n\nOpsi:\n --app Direktori aplikasi (default: direktori kerja saat ini)\n --config Path config (default: /ramonyx.config.ts)\n --port Override port server SSR\n --dev-port Override port Vite (default 5173)\n --static (build) Prerender rute statis + getStaticPaths → dist/static\n --target (build) Target tambahan: \"edge\" → dist/edge (serverless/edge)\n --watch (build) Bangun ulang otomatis saat berkas berubah (Ctrl+C untuk keluar)\n\nKonvensi:\n src/app.ts mengekspor createApp(options?) → FetchHandler\n src/routes halaman (page.tsx/layout.tsx/error.tsx) + API (api/*.ts)\n ramonyx.config.ts default export { app?, server? } (opsional)\n\n",
"commands": [
{
"name": "dev",
"summary": "Vite + HMR (default :5173) → SSR (port dari config)"
},
{
"name": "build",
"summary": "Bundel klien → dist/client (jalankan sebelum start produksi)"
},
{
"name": "start",
"summary": "Server produksi (NODE_ENV=production, SSR)"
},
{
"name": "doctor",
"summary": "Diagnostik konfigurasi & lingkungan"
},
{
"name": "version",
"summary": "Versi CLI"
},
{
"name": "help",
"summary": "Bantuan ini"
}
],
"flags": [
{
"flag": "--app",
"argument": "dir",
"description": "Direktori aplikasi (default: direktori kerja saat ini)"
},
{
"flag": "--config",
"argument": "path",
"description": "Path config (default: /ramonyx.config.ts)"
},
{
"flag": "--port",
"argument": "n",
"description": "Override port server SSR"
},
{
"flag": "--dev-port",
"argument": "n",
"description": "Override port Vite (default 5173)"
},
{
"flag": "--static",
"argument": null,
"description": "(build) Prerender rute statis + getStaticPaths → dist/static"
},
{
"flag": "--target",
"argument": "nama",
"description": "(build) Target tambahan: \"edge\" → dist/edge (serverless/edge)"
},
{
"flag": "--watch",
"argument": null,
"description": "(build) Bangun ulang otomatis saat berkas berubah (Ctrl+C untuk keluar)"
}
]
}
```
## docs/public/machine/stats.json
```json
{
"schemaVersion": 1,
"version": "0.4.1",
"generatedAt": "2026-10-20T04:37:41.931Z",
"commit": "a6b0d0c9ac2d8400878e363b33064e221b6c29ac",
"node": "v24.22.0",
"packages": {
"workspace": 15,
"npm": 16,
"names": [
"@ramonyx/adapter-runtime",
"@ramonyx/bundler",
"@ramonyx/compiler",
"@ramonyx/core",
"@ramonyx/devtools",
"@ramonyx/mcp-docs-server",
"@ramonyx/plugin-image",
"ramonyx",
"@ramonyx/react",
"@ramonyx/router",
"@ramonyx/schema",
"@ramonyx/security",
"@ramonyx/server",
"@ramonyx/ssr",
"@ramonyx/testing",
"create-ramonyx-app"
]
},
"adrs": 8,
"docs": 21,
"tests": {
"node": 289,
"rust": 116,
"e2eSpecFiles": 9
}
}
```
## docs/public/machine/config.schema.json
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Ramonyx application config (input)",
"description": "Bentuk input `ramonyx.config.ts` (RamonyxConfigInput). Default dari resolveConfig: app.strictValidation=true, server.port=3000, server.host=\"localhost\". Diverifikasi test tests/docs/config-schema.test.ts terhadap packages/core/src/config.ts.",
"type": "object",
"additionalProperties": false,
"properties": {
"app": {
"type": "object",
"additionalProperties": false,
"properties": {
"strictValidation": {
"type": "boolean",
"description": "Default true. Validasi schema runtime di trust boundary."
},
"streaming": {
"type": "boolean",
"description": "Default false. SSR streaming (ADR-0047) — dibaca CLI dev/start."
}
}
},
"server": {
"type": "object",
"additionalProperties": false,
"properties": {
"port": {
"type": "integer",
"minimum": 0,
"maximum": 65535,
"description": "Default 3000."
},
"host": {
"type": "string",
"minLength": 1,
"description": "Default \"localhost\"."
}
}
}
}
}
```
## docs/public/api/core-contract.md
# API Reference
The public API surface, grouped by package. Everything is ESM, TypeScript-first, and edge-safe
unless noted.
## `ramonyx` (meta package)
Re-exports the common primitives:
```ts
import {
route, createHandler, HttpError, defineConfig, resolveConfig,
createCache, createMemoryStore, mergeHooks, formDataToObject,
createTranslator, negotiateLocale, localePath, parseAcceptLanguage,
} from "ramonyx";
```
## Server — `@ramonyx/server`
```ts
createHandler(options): FetchHandler
```
| Option | Type | Default |
| --- | --- | --- |
| `routes` | `RouteEntry[]` | required |
| `middleware` | `Middleware[]` | `[]` |
| `env` | `EnvReader` | `process.env` |
| `logger` | `Logger` | console |
| `strictValidation` | `boolean` | `true` |
| `hooks` | `AppHooks` | — |
| `maxBodyBytes` | `number` | `1_048_576` |
| `timeoutMs` | `number` | `10_000` |
| `maxConcurrent` | `number` | `100` |
| `envFromRequest` | `(request) => EnvReader?` | — |
Also: `traceId()`, `requestLogging()`, `createMetrics()`, `createMetricsRoute(metrics)`.
## Core — `@ramonyx/core`
```ts
route({ handler, params?, query?, body?, output? }): RouteDefinition
```
`handler(context)` receives `{ request, params, query, body, env, log, json(), text(), redirect() }`.
Invalid schema input → `400` with `issues`; invalid output → `500`.
Other exports: `HttpError`, `defineConfig`, `resolveConfig`, `mergeHooks`, `formDataToObject`,
`createCache`, `createMemoryStore`, `createTranslator`, `negotiateLocale`, `localePath`,
`parseAcceptLanguage`.
## SSR — `@ramonyx/ssr`
```ts
createPageRoutes({
pages, renderer, document?, client?, streaming?, hooks?, cache?,
assets?, i18n?, errorDetails?, maxDocumentBytes?,
}): RouteEntry[]
```
- `client: { script, extraModules? }` enables hydration.
- `i18n: { locales, defaultLocale, prefixDefaultLocale?, directions? }` enables locale routing.
- `document` overrides the HTML shell (`DocumentOptions` includes `title, body, head, scripts,
status, lang, dir`).
Also: `defaultDocument`, `RAMONYX_MOUNT_ID`, and SEO helpers `sitemapXml`, `robotsTxt`,
`createSeoRoutes`.
## React — `@ramonyx/react`
- `createReactRenderer()` — SSR + streaming renderer.
- `Image` (`@ramonyx/react/assets`) — safe defaults + `` when variants exist.
- `Link` (`@ramonyx/react/link`) — client navigation + prefetch.
- Types: `PageProps`, `LayoutProps`, `ErrorPageProps`.
## Compiler — `@ramonyx/compiler`
- `createRouteManifest({ dir, importer? })` — API routes.
- `loadPageRoutes({ dir, apiDir?, importer? })` — page manifest.
- `generateClientEntry(pages, { from })`, `generateServerEntry(options)`,
`generateRouteTypes(pages)`.
- Loader: `import "@ramonyx/compiler/register"` (TypeScript + JSX for app code).
## Bundler — `@ramonyx/bundler`
- `buildClient(options)`, `buildEdge(options)`, `prerenderRoutes(options)`.
- `readClientScript(clientDir)`, `createAssetResolver(clientDir, { fallbackToPath? })`,
`assetUrl(manifest, path)`, `readAssetManifest`, `fontPreloadTag`.
- `definePlugin`, `loadPlugins`, `createDevServer`.
## Security — `@ramonyx/security`
`securityHeaders()`, `cspNonce()`, `csrfProtection()`, `rateLimit()`, `parseCookies()`,
`serializeCookie()`, `signValue()`, `verifyValue()`, `createSession()`. The SSRF-guarded fetch is a
Node-only subpath: `import { guardedFetch } from "@ramonyx/security/ssrf"`.
## Adapter runtime — `@ramonyx/adapter-runtime`
`createNodeHost()` (`serve`, `env`), `serveStatic({ dir, prefix, immutable?, headers? })`, and the
in-memory host used by tests. `cacheControl({ maxAge, sMaxAge?, staleWhileRevalidate?, immutable?,
private? })` lives in `@ramonyx/core` (re-exported by `ramonyx`).
## Testing — `@ramonyx/testing`
`createTestServer(options)` / `createTestServerFromHandler(handler)` — in-process requests without
binding a port.
## CLI — `@ramonyx/devtools`
`ramonyx dev | build | start | doctor | version | help`. Build flags: `--static`, `--target edge`,
`--watch`. Dev prints a devtools panel URL (`/__ramonyx/devtools`).
## Diagnostics
Framework errors carry stable `RMX-*` codes. In development, error pages include the stack and
source file; production pages never do.
## docs/public/architecture.md
# Architecture
Ramonyx is a Node.js-first framework with an experimental Rust/V8 runtime. This page describes the
public architecture: how a request flows, what the packages do, and where the boundaries are.
## Request lifecycle
```
Request
→ runtime host (Node adapter)
→ server: body/timeout/concurrency gates
→ global middleware (security headers, CSP nonce, rate limit, logging)
→ router (API routes + page routes, 404/405)
→ folder middleware (root → deepest)
→ loader (GET) / action (POST)
→ render (React: sync or stream)
→ hydration payload + HTML document
Response
```
## Layers
| Layer | Responsibility |
| --- | --- |
| **Framework** | routing, SSR, server actions, data loading, client navigation, security defaults |
| **Compiler/bundler** | route manifest, codegen (client entry, typed routes, edge entry), Vite builds |
| **Runtime** | Node.js (baseline) or the experimental Rust/V8 host |
| **System** | OS, TCP/TLS, filesystem, process |
## Packages
| Package | Role |
| --- | --- |
| `ramonyx` | meta package re-exporting common primitives |
| `@ramonyx/core` | request lifecycle, context, middleware, errors, cache, i18n, assets types |
| `@ramonyx/router` | route matching, params, 404/405 |
| `@ramonyx/server` | HTTP wiring, limits, rate limit, observability |
| `@ramonyx/schema` | Standard Schema validation + error mapping |
| `@ramonyx/adapter-runtime` | runtime contract, Node + in-memory hosts, static serving |
| `@ramonyx/ssr` | page rendering, streaming, SEO helpers, locale routing |
| `@ramonyx/react` | React renderer, `Image`, `Link` |
| `@ramonyx/compiler` | page/API manifests, codegen, dev loader |
| `@ramonyx/bundler` | Vite builds, dev server, assets, plugins |
| `@ramonyx/security` | headers, CSP, CSRF, rate limit, cookies/sessions, SSRF guard |
| `@ramonyx/testing` | in-memory test server |
| `@ramonyx/devtools` | the `ramonyx` CLI |
| `@ramonyx/mcp-docs-server` | MCP server over stdio for agents |
| `@ramonyx/plugin-image` | build-time image variants (optional `sharp`) |
## Server ↔ client boundary
- Page/layout modules are isomorphic; loaders and actions are server-only.
- `*.server.ts` modules are stubbed with a poison pill in the client bundle.
- The build rejects `node:*` imports in client modules and scans the output for leaks.
- Secrets belong in `*.server.ts` or server-only loaders; only UI data goes into the hydration
payload (JSON-serializable).
## Runtime contract
`@ramonyx/core` defines the web-standard boundary used by every host:
```ts
export type FetchHandler = (request: Request) => Promise;
export interface RuntimeHost {
readonly name: string; // "node" | "memory" | "ramonyx"
serve(handler: FetchHandler, options: { port: number; host?: string }): Promise;
readonly env: { get(name: string): string | undefined };
}
```
The Node adapter and the in-memory test host implement it; the experimental Rust/V8 runtime targets
the same contract (compatibility suite). The framework does not depend on runtime internals.
## Rendering & deployment targets
- **Node**: SSR + hydration + soft navigation; streaming opt-in.
- **Static**: prerendered pages + assets (`ramonyx build --static`).
- **Edge**: a single `{ fetch }` artifact with statically imported routes and an inlined asset map
(CI exercises it in workerd).
See [Routing](routing.md), [SSR & Hydration](ssr-hydration.md), and [Deployment](deployment.md).
## docs/public/assets-and-images.md
# Assets & Images
Files in `src/assets/**` are hashed at build time, recorded in a manifest, and resolved through the
server-side asset resolver. Node cannot `import photo.png`, so the resolver is the contract between
SSR and the client bundle.
## Add an asset
```
src/assets/
logo.svg
img/hero.png
fonts/inter.woff2
```
Wire the resolver once in `src/app.ts`:
```ts
import { createAssetResolver } from "@ramonyx/bundler";
const assets = await createAssetResolver(clientDir, { fallbackToPath: true });
createPageRoutes({ pages, renderer, assets /* ... */ });
```
Loaders receive `assets`:
```ts
export async function loader({ assets }) {
return {
logo: assets.url("logo.svg"), // "/assets/logo-3f9a1c2b7d.svg"
hero: assets.image("img/hero.png"), // { src, width, height, srcSet?, sources? }
};
}
```
## Rendering images
`Image` from `@ramonyx/react/assets` supplies safe defaults: explicit dimensions (no layout shift),
`loading="lazy"`, `decoding="async"`, and an optional `` when responsive variants exist.
```tsx
import { Image } from "@ramonyx/react/assets";
;
```
With the image plugin enabled, `assets.image()` returns `srcSet` and `` entries, so the
markup renders:
```html
```
## Responsive images (build-time variants)
```bash
pnpm add @ramonyx/plugin-image sharp
```
```ts
// ramonyx.config.ts
import { imageOptimizer } from "@ramonyx/plugin-image";
import { defineConfig } from "ramonyx";
export default defineConfig({
plugins: [imageOptimizer({ widths: [320, 640, 960, 1280], formats: ["webp"] })],
});
```
- Variants are generated at build time (resize + WebP; AVIF is opt-in because it is slower).
- `sharp` is an **optional peer** — installed by the app, not by the framework.
- Results are cached by content hash in `.ramonyx/image-cache`; unchanged images are not re-encoded.
- The original file is always copied and used as the universal fallback.
- Colors/dimensions are recorded in `ramonyx.assets.json` (`width`, `height`, `variants`).
`avif` can be added to `formats` for smaller files at the cost of build time. SVG is passed through
(vector assets do not need variants).
## Fonts
Fonts are hashed like any asset. Preload a font by putting a `` tag in page
metadata:
```ts
import { fontPreloadTag } from "@ramonyx/bundler";
export const metadata = {
title: "Home",
head: fontPreloadTag("/assets/fonts/inter-6a1b2c3d.woff2"),
};
```
## Honest limitations
- Development serves `src/assets/**` as-is (no hashes, no variants); build output is the source of
truth for production behavior.
- No blur placeholders (LQIP) or on-demand dev variants yet.
- Image optimization only runs for raster formats (PNG/JPEG/WebP/AVIF/TIFF).
## docs/public/benchmarks.md
# Benchmarks
Performance claims need measurements you can run. The benchmark protocol is fixed: warm up, repeat,
report the distribution (median, spread) — never a single run, never an average hiding a tail.
## What is measured
| Benchmark | Metric | How |
| --- | --- | --- |
| HTTP | requests/s, p50/p95/p99 | autocannon against the playground |
| SSR | render time, TTFB, output size | synchronous vs streaming, in-process |
| Security | status/error counts, RSS under abuse/soak | 6 abuse scenarios + sustained load |
| Startup | wall/startup/load/eval | Rust runtime CLI, median of N runs |
| Web Vitals | LCP, CLS, FCP, TTFB | Playwright on the production build |
| Image pipeline | before/after LCP/CLS with variants | throttled CDP + cold loads |
## Commands
```bash
pnpm bench # HTTP baseline (warm-up 3s + 5 runs, median/spread)
pnpm bench:report # compare against baseline (report-only, tolerance 25%)
pnpm bench:ssr # SSR sync vs streaming
pnpm bench:ssr:report # comparison artifact
pnpm bench:security # abuse cases + soak (RSS from /proc)
pnpm bench:image # image pipeline before/after (playground)
benchmarks/runtime-startup.sh # Rust runtime startup (needs a release build)
```
Results are written under `benchmarks/results/`. Web Vitals are measured by the e2e suite and
compared report-only against the committed baseline.
## Baselines are per environment
A baseline is only meaningful on the machine that produced it. Baselines are stored per
(platform × arch × Node major × CPU), and the strict comparison **refuses to compare across
environments** — it prints the mismatching fields instead of pretending.
- Development baselines are committed for the maintainer machine.
- CI runs benchmarks **report-only**: GitHub-hosted runners vary too much for a blocking gate.
- `VITALS_STRICT=1` turns the Web Vitals comparison into a gate; use it only on a stable runner.
## Rules
- Same hardware/OS/runtime/config for every comparison.
- Identical work per request; verify responses match.
- Report medians and spread; keep raw artifacts.
- No public performance claim without a runnable benchmark in the repo.
## docs/public/cli.md
# CLI Reference
The `ramonyx` CLI (from `@ramonyx/devtools`) runs the app: dev server, build, production start, and
diagnostics.
```bash
ramonyx dev # Vite + HMR (default :5173) → SSR (port from config)
ramonyx build # client bundle → dist/client (run before production start)
ramonyx build --static # client bundle + prerender pages → dist/static
ramonyx build --target edge # server/edge bundle → dist/edge ({ fetch } contract)
ramonyx build --watch # rebuild on file changes (covers --static/--target)
ramonyx start # production SSR (NODE_ENV=production)
ramonyx doctor # config + routes + build diagnostics
ramonyx version | help
```
## Flags
| Flag | Applies to | Meaning |
| --- | --- | --- |
| `--app ` | all | App directory (default: cwd) |
| `--config ` | all | Config path (default `/ramonyx.config.ts`) |
| `--port ` | dev/start | Override the SSR port |
| `--dev-port ` | dev | Override the Vite port (default 5173) |
| `--static` | build | Prerender static routes (`getStaticPaths` for dynamic routes) → `dist/static` |
| `--target ` | build | Extra target; `edge` → `dist/edge` |
| `--watch` | build | Rebuild on changes (debounced, safe queue) |
## Conventions
```
src/app.ts export createApp(options?) → FetchHandler
src/server.ts optional custom server entry (CLI still hosts it)
src/routes/ page.tsx / layout.tsx / error.tsx + api/*.ts
ramonyx.config.ts default export { app?, server? } (optional)
```
- `ramonyx dev|start` prefers `src/server.ts` when present, otherwise `src/app.ts`.
- Plugins from `ramonyx.config.ts` are loaded by the CLI (Vite, middleware, hooks, assets).
- `ramonyx start` sets `NODE_ENV=production` if unset; reactivity/React production build depends on it.
- `--static` and `--target edge` cannot be combined (different artifacts).
## Examples
```bash
# Local development with HMR
ramonyx dev --app examples/shop
# Production build + start on port 8080
ramonyx build --app examples/shop
ramonyx start --app examples/shop --port 8080
# Static export and edge artifact
ramonyx build --static --app examples/shop
ramonyx build --target edge --app examples/shop
ramonyx build --watch --app examples/shop
```
## Doctor
`ramonyx doctor` prints the CLI/Node versions, resolved config, whether `src/app.ts` exists, the
number of pages, and whether `dist/client` has a manifest. Use it before filing an issue.
## Devtools
`ramonyx dev` also prints the devtools panel URL (`/__ramonyx/devtools`). See
[Devtools Panel](devtools.md).
## docs/public/data-and-caching.md
# Data & Caching
Ramonyx keeps data loading in the page module: a `loader` for reads, an `action` for writes, and an
opt-in cache for expensive work.
## Loaders
```ts
export const revalidate = 60;
export const cacheTags = ["catalog"];
export async function loader({ params }) {
return { product: await db.products.find(params.id) };
}
```
Loader data is JSON-serializable and available to the component as `data` (and to the client via
the hydration payload).
## Cache semantics
| Setting | Effect |
| --- | --- |
| `revalidate: 60` | Cache loader data for 60 s; the next 60 s serves stale data and refreshes in the background |
| `cacheTags: ["catalog"]` | Tag the entry so `revalidate({ tags: ["catalog"] })` removes it |
| no `revalidate` | No caching — loader runs on every request |
Session safety: requests with **cookies** or a **query string** bypass the cache. That keeps
per-user responses out of a shared cache by construction.
The default cache is in-memory (single process). The store interface (`KeyValueStore`) is pluggable
— implement `get/set/delete/invalidateTags` for Redis or another backend and pass it to
`createCache({ store })`. The contract has a shared test suite in the repo.
## On-demand invalidation
```ts
// from an action
await revalidate({ tags: ["catalog"] });
```
```ts
// from anywhere with the cache instance
import { createCache } from "ramonyx";
const cache = createCache();
await cache.revalidate({ paths: ["/products"], tags: ["catalog"] });
```
Invalidation also fires the `onInvalidate` app hook, so observability and plugins can react.
## Server actions
Actions are POST handlers colocated with the page:
```ts
export const actionSchema = {
"~standard": {
version: 1,
vendor: "app",
validate: (form) => {
const name = String((form as Record)?.name ?? "").trim();
return name.length > 0
? { value: { name } }
: { issues: [{ message: "Name is required", path: ["name"] }] };
},
},
};
export async function action({ formData, revalidate }) {
const name = String(formData.get("name"));
await db.customers.create({ name });
await revalidate({ tags: ["customers"] });
return { message: `Created ${name}` };
}
```
`actionSchema` validates the form data before the action runs (invalid → `400` + field errors). The
page renders the last action result as `action` (`{ ok, errors?, message?, data? }`). Redirects use
`303` so a refresh does not resubmit.
## Pragmatic notes
- The cache key includes the page path, route params, and (with i18n) the active locale.
- Cached values are process-local; for multi-instance deployments use an external store.
- There is no HTML page cache (CSP nonces make HTML per-request); only **loader data** is cached.
- A CDN can cache the HTML of static/prerendered pages separately.
## docs/public/deployment.md
# Deployment
One app, three targets. The same `src/app.ts` runs on Node, as static files, or as an edge
`fetch` handler.
## Node (default)
```bash
ramonyx build # client bundle → dist/client (hashed assets + manifest)
ramonyx start # SSR on the configured port; NODE_ENV=production
```
Set the port/host in `ramonyx.config.ts`:
```ts
import { defineConfig } from "ramonyx";
export default defineConfig({ server: { port: 3000, host: "0.0.0.0" } });
```
Production notes:
- Serve `dist/client/assets/**` with `immutable` cache headers (the CLI's `start` does this for you).
- The Node adapter compresses responses with brotli/gzip and appends per-chunk for streaming.
- Keep the process behind a reverse proxy for TLS; the runtime verifies TLS for outbound `fetch`.
- Defaults: body 1 MiB, timeout 10 s, concurrency 100. Tune via `createHandler` options.
## Static
```bash
ramonyx build --static # → dist/static
```
- Static routes are prerendered; dynamic routes need `getStaticPaths`.
- A page with `metadata.status = 404` becomes `404.html`.
- Assets are copied alongside the HTML.
- Deploy the directory to any static host (GitHub Pages, S3, Netlify, …).
## Edge / serverless
```bash
ramonyx build --target edge # → dist/edge/index.js
```
The artifact exports `{ fetch }` — the standard edge contract:
```js
export const fetch = (request, env) => handler(request);
export default { fetch };
```
- Pages, API routes, actions, middleware, and the asset map are inlined into one bundle.
- Platform bindings (KV, secrets) are forwarded per request: the entry receives `env` and exposes
it to loaders/actions as `env.get(name)` / `env.getRaw(name)`.
- The build rejects `node:*` imports in the edge artifact (except where shimmed by the platform).
- CI runs a **workerd** smoke test against the edge artifact (Cloudflare's runtime), so SSR, API,
actions, env bindings, middleware, and 404s are exercised in a real edge runtime.
## Environment variables
| Context | Access |
| --- | --- |
| Node | `env.get("NAME")` (process env by default) |
| Edge | per-request `env` bindings forwarded by the platform adapter |
| Client | never — server values stay server-side |
## Headers & security in production
- `securityHeaders()` + `cspNonce()` from `@ramonyx/security` set CSP (with per-request nonce),
`X-Content-Type-Options`, `Referrer-Policy`, `X-Frame-Options`/`frame-ancestors`.
- HSTS/TLS termination belongs to your proxy or platform.
- Rate limiting and load shedding are in-process by default; use an external store for
multi-instance deployments.
## docs/public/devtools.md
# Devtools Panel
Every `ramonyx dev` session exposes a dev-only panel with routes, request/loader/render timings,
hydration status, and bundle sizes. It is never enabled in production builds.
```bash
ramonyx dev
# dev: http://localhost:5173 (Vite + HMR) → SSR http://127.0.0.1:3000
# devtools: http://localhost:5173/__ramonyx/devtools
```
## What it shows
| Section | Data source | Notes |
| --- | --- | --- |
| **Routes** | Page manifest | Updated when route files change (same watcher as client entry regeneration) |
| **Bundles & assets** | `dist/client/ramonyx.assets.json` + file sizes | Empty until the app has been built — the panel does not build for you |
| **Recent requests** | Collector middleware + `AppHooks` | Last 50 requests; method, path, status, total, loader, render, cache, hydration |
**Hydration** is detected from the response HTML: `payload` when
``,
- the hashed client bundle (`/assets/client-.js`) that hydrates the tree and wires soft
navigation, prefetch, and form interception.
Because the payload is embedded, there is no extra data request on hydration.
## Streaming (opt-in)
```ts
// ramonyx.config.ts
export default defineConfig({ app: { streaming: true } });
```
With `streaming: true`, the shell (head + layout) is sent immediately and the body is streamed with
`renderToReadableStream` (edge build of React). Errors after the first byte close the stream — the
status can no longer change. Error pages are always rendered synchronously.
## Layouts & error boundaries
- `layout.tsx` wraps pages in the folder and above; layouts receive `children`.
- `error.tsx` receives `{ error: { status, message, details? } }`. `details` (stack + source file)
is only included outside production.
- Unknown paths fall through to a catch-all page (the template ships `[...slug]` with
`metadata.status = 404`).
## Client behavior
| Feature | Behavior |
| --- | --- |
| Hydration | `hydrateRoot` on the existing markup; no re-render flash |
| Soft navigation | same-origin links fetch the next HTML, swap the container, re-hydrate, `pushState` |
| Prefetch | hover/focus/viewport prefetch with a small LRU cache (8 entries, 30 s TTL, max 2 concurrent) |
| Forms | submit intercepted when JS is available; `POST` + `303` PRG without JS |
| Scroll | `popstate` restores scroll; new navigations start at the top |
## Data loading & caching
Loaders run on the server on every request unless a page opts into caching:
```ts
export const revalidate = 60; // seconds (ISR-lite)
export const cacheTags = ["products"]; // for on-demand invalidation
export async function loader() {
return { products: await db.products.list() };
}
```
- Responses that depend on a session (cookies) or query strings are **not** cached.
- Serve stale data while refreshing in the background (`stale-while-revalidate`).
- Invalidate on demand: `revalidate({ paths: ["/products"], tags: ["products"] })` from a loader,
action (`context.revalidate`), or the cache instance.
- The default cache is in-memory per process; the store interface is pluggable for multi-instance
deployments.
## Server actions
An `action` export turns a page into a mutation endpoint (POST to the same URL). It receives
`{ params, formData, request, env, log, locale, json(), revalidate() }` and can return
`{ message, errors, data, status, headers, redirect }`. A redirect is `303` (POST/Redirect/GET).
```ts
export async function action({ formData, revalidate }) {
await db.notes.create({ text: String(formData.get("text") ?? "") });
await revalidate({ tags: ["notes"] });
return { message: "Saved" };
}
```
## Related primitives
- `AppHooks` (`onRequestStart/End`, `onLoaderEnd`, `onRenderEnd`, `onInvalidate`) for instrumentation.
- `traceId()`, `requestLogging()`, `createMetrics()` from `@ramonyx/server` for observability.
- Asset resolver + `Image` for hashed assets, responsive images, and anti-CLS dimensions.
## docs/public/testing.md
# Testing
Test the handler in-process (no ports, no runtime), then add browser tests for hydration and
progressive enhancement.
## In-process tests
`@ramonyx/testing` wraps a handler in an in-memory host:
```ts
import assert from "node:assert/strict";
import { test } from "node:test";
import { createTestServerFromHandler } from "@ramonyx/testing";
import { createApp } from "../src/app.ts";
test("GET / renders SSR", async () => {
const server = await createTestServerFromHandler(await createApp());
try {
const response = await server.fetch("/");
assert.equal(response.status, 200);
assert.match(await response.text(), /Hello/);
} finally {
await server.close();
}
});
```
- `createTestServerFromHandler(handler)` — any `FetchHandler` (e.g. from `createApp`).
- `createTestServer({ routes, middleware })` — build a handler from route entries directly.
- `server.fetch(path | Request, init?)` resolves relative URLs against the in-memory origin.
- **Always `close()` in `finally`** — a leaked listener makes `node --test` hang.
## What to test
| Layer | Approach |
| --- | --- |
| Routing / params / methods | `server.fetch` — assert `200`, `404`, `405` + `Allow` |
| Validation | invalid query/params/body → `400` with `issues` |
| Loaders & actions | assert rendered HTML and the hydration payload |
| Cache/revalidate | call twice, assert loader counts and stale-while-revalidate behavior |
| Security | body limits (`413`), timeout (`503`), concurrency (`503` + `Retry-After`), CSRF |
| Browser | Playwright: hydration, soft navigation, forms with and without JS |
## Browser tests
The repo's playground runs Playwright against a production build (`pnpm build && pnpm test:e2e`).
For a new app:
```ts
// e2e/home.spec.ts
import { expect, test } from "@playwright/test";
test("hydrates and handles a form", async ({ page }) => {
await page.goto("/");
await page.getByRole("button", { name: /Click/ }).click();
await expect(page.getByRole("button")).toHaveText(/1/);
});
```
Test **no-JS** by disabling JavaScript in the Playwright context: forms must still submit
(`POST` + `303` PRG).
## Commands
```bash
pnpm test # node --test (packages, apps, tests, benchmarks)
pnpm test:e2e # Playwright against a production build
pnpm test:docs # this site's e2e (policy + a11y), if you vendor the docs app
```
## Tips
- Loader data must be JSON-serializable — assert the payload shape when in doubt.
- Use a fresh app/server per test for isolation; keep shared fixtures read-only.
- The dev scripts run app TypeScript/JSX through `@ramonyx/compiler/register` (no build step).
## docs/public/troubleshooting.md
# Troubleshooting
Framework errors carry a stable code and a link to this page. Search the code below, or grab the
anchor from the error message.
## RMX-CONFIG-1 — invalid config
`ramonyx.config.ts` has a value with the wrong type.
| Field | Expected |
| --- | --- |
| `app.strictValidation` | boolean |
| `app.streaming` | boolean |
| `server.port` | integer 0–65535 |
| `server.host` | non-empty string |
Fix the value; unknown keys are kept (config pass-through) and are not the cause.
## RMX-ROUTES-1 — can't read the routes directory
The compiler cannot read `src/routes` (missing directory or permissions). Create
`src/routes/page.tsx` or point the CLI at the right app with `--app`.
## RMX-ROUTES-2 — missing default export
A page/layout/error module has no `default` export. Every `page.tsx`, `layout.tsx`, and `error.tsx`
must export the component as default:
```tsx
export default function Page() { /* ... */ }
```
## RMX-ROUTES-3 — invalid middleware export
A `middleware.ts` must default-export an **array of functions**:
```ts
import type { Middleware } from "ramonyx";
export default [securityHeaders(), cspNonce()] satisfies Middleware[];
```
## RMX-ROUTES-4 — API route without an HTTP method export
An `api/**/*.ts` file exports none of `GET`/`POST`/`PUT`/`PATCH`/`DELETE`/`HEAD`/`OPTIONS`. Export at
least one using `route({ handler })`.
## RMX-ROUTES-5 — invalid route definition
An exported method is not a route definition. Use the helper:
```ts
export const GET = route({
handler: ({ json }) => json({ ok: true }),
});
```
## RMX-BUILD-1 — server import in a client module
A page/layout imports `node:*` directly. The client bundle cannot contain Node APIs. Move that code
into a `*.server.ts` module and call it from a loader/action/API route; the client bundle gets a
poison-pill stub instead of your server code.
## RMX-BUILD-2 — server code leaked into the client bundle
Same problem, but through a transitive import (or a Vite browser-external shim was found). Split the
server-only code behind `*.server.ts` and import it only from the server side.
## RMX-EDGE-1 — `node:*` in the edge artifact
The edge target rejects Node built-ins. `guardedFetch` lives in a Node-only subpath; import it from
`@ramonyx/security/ssrf` **only** in Node code, and keep edge code on `fetch`.
## Common non-coded issues
| Symptom | Cause / fix |
| --- | --- |
| `413` on POST | Body over 1 MiB. Raise `maxBodyBytes` in `createHandler` or stream/upload differently. |
| `503` after 10 s | Handler/loader/render timeout. Optimize, or raise `timeoutMs`. `request.signal` is aborted. |
| `503` + `Retry-After` under load | Concurrency limit (default 100) shedding load. Scale out or raise `maxConcurrent`. |
| Hydration mismatch / no interactivity | Client bundle not built (`ramonyx build` before `ramonyx start`) or an error page (error pages are not hydrated). |
| Assets 404 in production | Build first; assets are hashed into `dist/client` and served by `ramonyx start`. |
| `node_modules` TypeScript fails | Node does not type-strip `node_modules`; publish compiled JS for plugins/consumers. |
| Dev server panel missing | Open the URL printed by `ramonyx dev` (`/__ramonyx/devtools`); it is dev-only. |
## Getting more detail
- Development error pages include the stack and source file; production pages never do.
- The CLI `ramonyx doctor` prints config, route count, and build status.
- See [Testing](testing.md) for reproducing issues in-process.
## docs/public/typed-routes.md
# Typed Routes
`ramonyx build` and `ramonyx dev` generate `.ramonyx/routes.ts` from your page manifest. Use it to
make route paths and params type-safe at compile time.
## Generated module
```ts
// .ramonyx/routes.ts (generated — do not edit)
export type RouteParamsMap = {
"/": undefined;
"/users": undefined;
"/users/[id]": { id: string };
"/blog/[...slug]": { slug: string };
};
export function routeHref(
path: P,
...args: RouteParamsMap[P] extends undefined ? [] : [params: RouteParamsMap[P]]
): string;
```
Import it with a **relative path** (there is no `#routes` alias yet):
```tsx
import { routeHref } from "../.ramonyx/routes.ts";
routeHref("/users"); // "/users"
routeHref("/users/[id]", { id: "42" }); // "/users/42"
```
Wrong paths or missing params fail at type-check time. The generated file is refreshed on
`ramonyx dev` and `ramonyx build` — no manual step.
## `Link` component
`Link` from `@ramonyx/react/link` renders an `` and uses the client navigation/prefetch path:
```tsx
import { Link } from "@ramonyx/react/link";
User 42;
```
`href` is a plain string; use `routeHref` to build it safely.
## Notes
- The module only exists after the first `build`/`dev` run.
- Import by relative path from your app code; a path alias is a candidate for a future release.
- The params map follows the file router: `[param]` → required single segment, `[...param]` → rest.
- Custom codegen? The manifest is the source; `generateRouteTypes(pages)` is exported from
`@ramonyx/compiler` if you need it in another tool.
## docs/public/whats-included.md
# What's Included
A map of implemented capabilities to their documentation. Status: ✅ implemented, ◐ partial,
⚗️ experimental (runtime). Everything below is exercised by the test suite.
## Core & routing
| Capability | Docs |
| --- | --- |
| File routing (pages, nested layouts, route groups) | [Routing](routing.md) |
| Dynamic params `[id]` and catch-all `[...slug]` | [Routing](routing.md) |
| API routes with method exports + 404/405 | [Routing](routing.md) |
| Global + per-folder middleware | [Routing](routing.md) |
| Error boundaries (`error.tsx`) + stable `RMX-*` codes | [Troubleshooting](troubleshooting.md) |
| Request limits (body 1 MiB, 10 s timeout, 100 concurrent) | [Routing](routing.md) |
| Config pass-through (`ramonyx.config.ts`) | [CLI](cli.md) |
## Rendering & data
| Capability | Docs |
| --- | --- |
| SSR React (sync + streaming opt-in) | [SSR & Hydration](ssr-hydration.md) |
| Hydration payload + soft navigation + prefetch | [SSR & Hydration](ssr-hydration.md) |
| Loaders (`params`, `query`, `env`, `assets`, `locale`) | [SSR & Hydration](ssr-hydration.md) |
| Server actions + schema validation + 303 PRG | [SSR & Hydration](ssr-hydration.md) |
| Loader cache with TTL + stale-while-revalidate + tags | [Data & Caching](data-and-caching.md) |
| On-demand invalidation (`revalidate`) | [Data & Caching](data-and-caching.md) |
| Metadata/SEO (sitemap, robots, canonical) | [SSR & Hydration](ssr-hydration.md) |
| i18n routing + translation helpers | [Internationalization](i18n.md) |
| Typed routes (`routeHref`) | [Typed Routes](typed-routes.md) |
| Streaming SSR | [SSR & Hydration](ssr-hydration.md) |
## Security
| Capability | Docs |
| --- | --- |
| Security headers + CSP nonce | [Security overview](security/overview.md) |
| CSRF double-submit | [Sessions & Auth](sessions-and-auth.md) |
| Rate limiting per client | [Security overview](security/overview.md) |
| SSRF guard with DNS pinning (`guardedFetch`) | [Security overview](security/overview.md) |
| Safe cookies + HMAC-signed sessions | [Sessions & Auth](sessions-and-auth.md) |
| Server/client boundary enforcement | [Architecture](architecture.md) |
| Body/timeout/concurrency/output limits | [Routing](routing.md) |
## Assets & media
| Capability | Docs |
| --- | --- |
| `src/assets/**` hashing + manifest + resolver | [Assets & Images](assets-and-images.md) |
| `Image` (anti-CLS, lazy, ``) | [Assets & Images](assets-and-images.md) |
| Build-time image variants (WebP; AVIF opt-in) | [Assets & Images](assets-and-images.md) |
| Font preload helper | [Assets & Images](assets-and-images.md) |
## Tooling & deployment
| Capability | Docs |
| --- | --- |
| CLI (`dev`, `build`, `start`, `doctor`, `watch`) | [CLI](cli.md) |
| Dev server + HMR | [CLI](cli.md) |
| Devtools panel (dev-only) | [Devtools Panel](devtools.md) |
| Scaffolder (`create-ramonyx-app`) | [Getting Started](getting-started.md) |
| Typed routes codegen | [Typed Routes](typed-routes.md) |
| Observability (trace id, logs, metrics) | [Observability](observability.md) |
| Static export / SSG | [Deployment](deployment.md) |
| Edge/serverless target | [Deployment](deployment.md) |
| Compression + ETag + immutable assets | [Deployment](deployment.md) |
| Source maps (client + packages) | [Troubleshooting](troubleshooting.md) |
| Benchmark protocol | [Benchmarks](benchmarks.md) |
| Test utilities | [Testing](testing.md) |
## Extensibility
| Capability | Docs |
| --- | --- |
| Plugins (Vite, middleware, hooks, assets) | [Plugins](plugins.md) |
| Plugin publishing conventions | [Plugins](plugins.md) |
| Image plugin (`sharp` optional peer) | [Assets & Images](assets-and-images.md) |
| MCP docs server + `llms.txt` | [For Agents](for-agents.md) |
## Experimental runtime (Rust + V8)
Permission deny-all model, HTTP server with streaming/cancellation, worker and process isolation,
CPU/RSS quotas, plugin sandbox (Landlock), TLS `fetch`, CommonJS interop. Shipped as a separate
binary; **not part of the production promise** yet — see the repository for its assessments.