# 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"; Hero; ``` 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.