Skip to content

Latest commit

 

History

History
253 lines (198 loc) · 11.6 KB

File metadata and controls

253 lines (198 loc) · 11.6 KB

Pracht — Vision & MVP

Pracht is a full-stack Preact framework built on Vite. It draws routing and rendering


Core Principles

  1. Preact-first — lightweight by default; ship less JavaScript to the client.
  2. Vite-native — leverage Vite's dev server, HMR, and multi-environment builds.
  3. Explicit over magic — hybrid routing (file modules + manifest config) so developers always know what runs where.
  4. Deploy anywhere — platform adapters isolate runtime differences; one codebase targets Cloudflare Workers, Vercel, Node, etc.
  5. AI-assisted — Claude Code skills for scaffolding, debugging, and operating the framework.
  6. Proven by tests — thorough E2E testing (Playwright) to prove SSR, SSG, ISG, SPA, and client navigation actually work in production scenarios.
  7. Instant local DX — one command to start, instant HMR, zero config. Hit a button and you're rolling.

Feature Overview

Routing

  • Hybrid file-based routing: route modules live in src/routes/, wired via an explicit src/routes.ts manifest using defineApp(), route(), group().
  • Dynamic segments: :param syntax, catch-all segments.
  • Typed route links: pracht typegen emits route id/param types and href helpers for <Link>, useNavigate(), and code outside components.
  • Shells: named layout wrappers (e.g. public, app) decoupled from URL structure; assigned per route or group.
  • Middleware: named middleware defined in src/middleware/, applied per route or group; runs server-side before loaders.
  • Route groups: inherit shell, middleware, render mode, and path prefix.
  • Navigation UX: automatic scroll restoration, link prefetching (intent/viewport/render), and opt-in View Transitions.

Rendering Modes

Per-route render mode via { render: "spa" | "ssr" | "ssg" | "isg" }:

Mode When HTML is generated Use case
SPA Client only Dashboards, auth-gated UI
SSR Every request Personalized / dynamic pages
SSG Build time Marketing, docs, blog
ISG Build + revalidation Pricing, catalog, semi-static

ISG supports time-based and webhook-based revalidation policies.

Hydration Modes (Islands)

Orthogonal to the render mode, every route has a hydration mode via { hydration: "full" | "islands" | "none" } (default "full", zero change for existing apps):

  • "islands" — partial hydration inspired by Deno Fresh and Astro: only components from src/islands/ hydrate, each as its own code-split chunk loaded by a tiny bootstrap instead of the full client runtime. Per-usage strategies via the client prop: load (default), idle, visible. Island props are JSON-serialized into the HTML with clear dev errors for non-serializable values. Navigation is MPA-style full-document in v1.
  • "none" — fully static output, zero JavaScript shipped.

Works in both the manifest router and the pages router (export const HYDRATION = "islands"). See docs/ISLANDS.md.

Data Loading

Two styles, both fully supported — pick whichever fits your mental model:

Inline (co-located in the route file):

  • Loaders: export function loader(args) — runs at build (SSG), request (SSR), or client navigation time. Returns typed, serializable data.
  • Form: A component that allows posting to one of our API routes

Separate files (manifest-wired):

  • Loader logic can live in a dedicated server file (e.g. src/server/).
  • Wired via the route config object in routes.ts:
    route("/dashboard", {
      component: "./routes/dashboard.tsx",
      loader: "./server/dashboard-loader.ts",
      render: "ssr",
    });
  • Route files become pure components — no server code mixed in.

Both styles can coexist in the same app. When a separate loader file is specified in the config, it takes precedence over inline exports.

  • Head: export function head(args) — per-route <head> metadata merged with shell-level head.
  • Client hooks: useRouteData(), useRevalidate(), useNavigation() (pending navigation/submission state for progress bars and optimistic UI), useNavigate(), useLocation(), useParams(), <Form> component, <Link> (with prefetch, preserveScroll, viewTransition props), and imperative prefetch().

API Routes

Standalone server endpoints independent of the page rendering pipeline:

  • Defined in src/api/ with file-based path mapping (e.g. src/api/health.ts/api/health).
  • Export named HTTP method handlers (export function GET(args), POST(args), etc.) or one default handler that branches on args.request.method.
  • Receive the same LoaderArgs-style context (request, params, context, signal).
  • Return Response objects directly — full control over status, headers, body.
  • API routes are independent of page-route middleware by default. Shared API policy can be attached explicitly via defineApp({ api: { middleware: [...] } }).
  • Opt-in request validation via defineApi() with any Standard Schema validator (body, query, params), standardized 422 issue responses, and JSON-value handler returns.
  • pracht typegen registers each API route's request/response types so the apiFetch() client checks paths, methods, bodies, queries, and params at compile time and returns typed responses. See docs/API_VALIDATION.md.

Deployment Adapters

Platform adapters export a request handler shaped for their runtime:

Adapter Runtime Notes
adapter-node Node.js http Static file serving, ISG mtime check
adapter-cloudflare Workers fetch env.ASSETS, Cache API-backed ISG, opt-in Workers Caching
adapter-vercel Serverless / Edge Build Output API v3 + native ISR

Each adapter:

  • Converts platform request → Web Request
  • Loads Vite manifests for asset injection
  • Implements ISG revalidation only when the platform has appropriate persistent storage/cache semantics, and documents/warns otherwise
  • Generates a platform-specific entry module via the Vite plugin

Agent Workflow (provable authoring, reviewable changes)

  • Constraints: defineApp({ constraints }) declares invariants over the route graph (requireMiddleware, requireShell, requireRenderMode, forbidRenderMode, requireHead); pracht verify enforces them deterministically.
  • App-graph snapshot + plan: .pracht/app-graph.json is a committed route-graph lockfile; pracht plan diffs the live graph against a base git ref for an intent-level changelog (--markdown for PR comments); verify fails when the snapshot is stale.
  • Report: pracht report assembles a PR-ready markdown body from the graph diff, verify results, and bundle budgets.
  • Smoke tests: pracht generate route emits a Playwright smoke test when the app has an e2e setup.
  • Authoring guide: pracht llms [--write] and the MCP get_docs tool expose the framework's conventions to any coding agent.

See docs/AGENT_WORKFLOW.md.

Skills (Claude Code)

Agent skills for framework authors and app builders live in skills/ (one SKILL.md per skill; .claude/skills symlinks there so Claude Code picks them up in this repo). They are published with a discovery manifest at https://pracht.resynapse.dev/.well-known/agent-skills/index.json and seeded into new apps by create-pracht — see the skills catalog:

  • Scaffold: generate routes, shells, middleware, API routes with correct wiring
  • Debug: framework-aware debugging (route matching, loader errors, hydration)
  • Deploy: guided adapter setup and deployment
  • Audit: loaders, auth, CSRF, headers, secrets, bundles, SEO, a11y, and more

MVP Scope — Phase 1

Phase 1 delivers a working framework that can build and serve a Preact app with SSR and SSG, deployed to Node. Thoroughly tested with Playwright E2E tests.

Phase 1 Deliverables

  1. packages/framework — core exports

    • defineApp(), route(), group() — route manifest API
    • RouteModule type — loader, head, default/Component, errorBoundary
    • ShellModule type — layout wrapper with head contribution
    • MiddlewareModule type — server-side request interceptor
    • Router: matchAppRoute() segment-based matching
    • Server renderer: handlePrachtRequest() → full HTML with hydration state
    • Client runtime: startApp(), hydration, client-side navigation
    • Hooks: useRouteData(), useRevalidate(), <Form>
  2. packages/vite-plugin — Vite integration

    • Virtual modules: virtual:pracht/client, virtual:pracht/server
    • Multi-environment build (client + ssr)
    • import.meta.glob() module registry generation
    • SSG prerendering at build time (concurrent, configurable)
    • Dev server with HMR
  3. packages/adapter-node — Node.js adapter

    • createNodeRequestHandler() — Web Request/Response over http
    • Static file serving from dist/client/
    • Vite manifest loading for asset injection
    • ISG time-window and webhook revalidation
  4. packages/cli — developer tooling (instant local DX)

    • pracht dev — one command, instant Vite dev server with HMR
    • pracht build — production build with clear output
    • Node targets run the production build with node dist/server/server.js; Cloudflare and Vercel use their platform CLIs
    • Zero config to start — sensible defaults, override when needed
    • Fast feedback loop: save a file → see the change instantly
  5. Example app — demonstrates SSR + SSG routes, shells, loaders, and forms

  6. E2E tests — Playwright tests proving:

    • SSR renders correct HTML on the server
    • SSG generates static files at build time
    • Client-side navigation works without full page reload
    • Loaders return correct data
    • Shells wrap routes correctly
    • Hydration completes without errors

Monorepo Structure

pracht/
  packages/
    framework/        # Core: routing, rendering, runtime, types
    vite-plugin/      # Vite integration, virtual modules, build
    adapter-node/     # Node.js server adapter
    adapter-cloudflare/  # Cloudflare Workers adapter
    adapter-vercel/      # Vercel Edge adapter
    cli/              # Dev/build/generate/inspect/verify/plan/report/doctor commands
    create-pracht/     # (Phase 2) Starter scaffolding
  example/            # Working example app
  docs/               # Architecture and design docs
  e2e/                # Playwright end-to-end tests

The live scaffold for this layout is documented in docs/WORKSPACE.md.


Key Design Decisions

See docs/ARCHITECTURE.md for detailed rationale.

  1. Hybrid routing over pure file-based — explicit manifest avoids hidden conventions and enables shell/middleware grouping without directory nesting.
  2. Per-route render modes — no global default; each route declares its strategy.
  3. Server-owned navigation — client fetches route state JSON from server rather than running loaders in the browser; keeps secrets server-side.
  4. Virtual module generation — Vite plugin generates entry points from import.meta.glob(), avoiding manual registration.
  5. Adapter pattern — platform logic isolated from core; adapters are thin request/response translators.