Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/deno-adapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@pracht/adapter-deno": minor
"@pracht/cli": patch
---

Add a Deno adapter that generates a native `Deno.serve` production entry, serves
static assets from `dist/client`, and delegates SSR, route-state, loaders,
middleware, and API routes to Pracht's Web Request/Response runtime.

`pracht preview` now detects Deno targets and runs the built server through
`deno run` with the required read, net, and env permissions.
2 changes: 2 additions & 0 deletions VISION_MVP.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ Platform adapters export a request handler shaped for their runtime:
| `adapter-node` | Node.js `http` | Static file serving, ISG mtime check |
| `adapter-cloudflare` | Workers `fetch` | `env.ASSETS`, bindings, no runtime ISG yet |
| `adapter-vercel` | Serverless / Edge | Build Output API v3 + edge handler |
| `adapter-deno` | Deno `Deno.serve` | Native Web Request/Response handler, no runtime ISG yet |

Each adapter:

Expand Down Expand Up @@ -186,6 +187,7 @@ pracht/
adapter-node/ # Node.js server adapter
adapter-cloudflare/ # Cloudflare Workers adapter
adapter-vercel/ # Vercel Edge adapter
adapter-deno/ # Deno server adapter
cli/ # Dev/build/generate/inspect/verify/doctor commands
create-pracht/ # (Phase 2) Starter scaffolding
example/ # Working example app
Expand Down
86 changes: 86 additions & 0 deletions docs/ADAPTERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,92 @@ export default {

---

## Deno Adapter

### `createDenoRequestHandler(options)`

| Option | Type | Description |
| --------------- | ----------------------------------- | --------------------------------------------------------------- |
| `app` | `PrachtApp` | The resolved app from `defineApp()` |
| `registry` | `ModuleRegistry` | Lazy module importers |
| `staticDir` | `string \| URL` | File URL or path for `dist/client/` |
| `createContext` | `(args) => TContext` | App-level context factory |
| `apiRoutes` | `ResolvedApiRoute[]` | Resolved API route metadata |
| `clientEntryUrl` | `string` | Client entry script URL |
| `cssManifest` | `Record<string, string[]>` | Route/shell CSS asset manifest |
| `jsManifest` | `Record<string, string[]>` | Route/shell JS asset manifest |
| `headersManifest` | `Record<string, Record<string, string>>` | Prerendered document headers |

### Features

- **Native Deno server**: generated entries call `Deno.serve()` and pass Web
`Request` objects directly to pracht. No Node request/response bridge is
involved.
- **Static file serving**: reads from `dist/client/` with proper content-type
headers. Hashed assets under `/assets/` get `Cache-Control: public,
max-age=31536000, immutable`; HTML and other files get `public, max-age=0,
must-revalidate`. Clean URLs resolve to `index.html` files. Static responses
apply the same baseline security headers as dynamic responses, and
prerendered HTML receives route and shell document headers from
`dist/server/headers-manifest.json`.
- **Route-state bypass**: route-state requests (`x-pracht-route-state-request:
1` or `?_data=1`) and Markdown negotiation requests bypass static files and
reach `handlePrachtRequest()`.
- **ISG caveat**: runtime ISG revalidation is not implemented for Deno yet. ISG
routes are prerendered at build time and served as static files. `pracht
build` warns when a Deno build contains ISG routes.
- **Local preview**: `pracht preview` runs `pracht build` and then executes the
built server with `deno run --allow-net --allow-read=dist --allow-env=PORT`.

### Generated entry options

When using `denoAdapter()` in `vite.config.ts`, generated entries can import a context factory:

```typescript
denoAdapter({
createContextFrom: "/src/server/context.ts",
port: 8787,
});
```

The context module must export `createContext(args)`. Deno passes `{ request }`.

### Entry module

```javascript
// virtual:pracht/server (generated in deno mode)
import { createDenoRequestHandler } from "@pracht/adapter-deno";

const staticDir = new URL("../client/", import.meta.url);
const denoHandler = createDenoRequestHandler({
app: resolvedApp,
registry,
staticDir,
apiRoutes,
clientEntryUrl,
cssManifest,
jsManifest,
});

export async function handler(request) {
return denoHandler(request);
}

if (import.meta.main) {
const port = Number(Deno.env.get("PORT") ?? 3000);
Deno.serve({ port }, handler);
}
```

Running `pracht build` for a Deno target emits `dist/server/server.js`, which is
the executable production server entry. Run it manually with:

```bash
deno run --allow-net --allow-read=dist --allow-env=PORT dist/server/server.js
```

---

## Vercel Adapter (Phase 2)

### `createVercelEdgeHandler(options)`
Expand Down
5 changes: 5 additions & 0 deletions docs/RENDERING_MODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ stale, it serves the stale HTML immediately and triggers regeneration.
> prerenders ISG routes at build time and routes ISG paths through the Edge
> Function rather than relying on process-local cache state. Use SSG for static
> output or SSR for per-request freshness on Vercel.
>
> **Deno note:** The Deno adapter currently does not implement runtime ISG
> revalidation. ISG routes are prerendered at build time and served as static
> files. Use SSG/SSR on Deno, or deploy ISG routes to Node until a persistent
> Deno cache/storage design lands.

### Webhook-based revalidation

Expand Down
22 changes: 15 additions & 7 deletions docs/WORKSPACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ described in `VISION_MVP.md`.
| `packages/adapter-node` | `@pracht/adapter-node` | Node `IncomingMessage`/`ServerResponse` bridge, ISG stale-while-revalidate |
| `packages/adapter-cloudflare` | `@pracht/adapter-cloudflare` | Cloudflare Workers fetch handler, generated worker entry source, and static asset handoff (no runtime ISG revalidation yet) |
| `packages/adapter-vercel` | `@pracht/adapter-vercel` | Vercel Edge handler and Build Output API entry source |
| `packages/adapter-deno` | `@pracht/adapter-deno` | Deno `Deno.serve` handler, generated server entry source, and static asset handoff (no runtime ISG revalidation yet) |
| `packages/preact-worker-facets` | `@pracht/preact-worker-facets` | Experimental Cloudflare Dynamic Worker + Durable Object facets runtime for inert, stateful Preact components |
| `packages/cli` | `@pracht/cli` | `pracht dev`, `build`, `verify`, the `generate` subcommands, and `doctor` |
| `examples/cloudflare` | `@pracht/example-cloudflare` | Cloudflare-targeted example app with SSG, ISG, SSR, SPA routes, auth middleware, and API routes |
Expand Down Expand Up @@ -67,11 +68,12 @@ described in `VISION_MVP.md`.
`hydrate()` from Preact.
- **CLI** — `pracht dev` starts a Vite dev server with SSR, `pracht build` runs
client + server builds (with Vite manifest generation, SSG/ISG prerendering,
ISG manifest output, executable Node server output in `dist/server/server.js`,
and Vercel `.vercel/output/` generation when the app targets those adapters),
`pracht preview` builds and serves the production output locally (Node runs
`dist/server/server.js`, Cloudflare delegates to `wrangler dev`, and Vercel
points at `vercel build`/`vercel dev`),
ISG manifest output, executable Node/Deno server output in
`dist/server/server.js`, and Vercel `.vercel/output/` generation when the app
targets those adapters), `pracht preview` builds and serves the production
output locally (Node runs `dist/server/server.js`, Deno runs it through
`deno run`, Cloudflare delegates to `wrangler dev`, and Vercel points at
`vercel build`/`vercel dev`),
`pracht verify` runs fast framework-aware checks with optional `--changed`
and `--json` output, `pracht inspect [routes|api|build] --json` emits the
resolved route graph, API handlers, and build metadata for agents/tools,
Expand All @@ -81,8 +83,9 @@ described in `VISION_MVP.md`.
files, and `pracht doctor` validates app wiring across the whole project.
- **Package builds** — `tsdown` compiles `pracht`, `@pracht/vite-plugin`,
`@pracht/preact-ssr-precompile`, `@pracht/adapter-node`,
`@pracht/adapter-cloudflare`, and `@pracht/adapter-vercel` from TypeScript to
ESM (`dist/index.mjs` + `.d.mts`). `@pracht/core` also publishes browser,
`@pracht/adapter-cloudflare`, `@pracht/adapter-vercel`, and
`@pracht/adapter-deno` from TypeScript to ESM (`dist/index.mjs` + `.d.mts`).
`@pracht/core` also publishes browser,
client, manifest, and server subpath entries so the Vite plugin can keep
server-only runtime code and route-only browser helpers out of the critical
client bootstrap graph while generated server modules avoid the browser export
Expand All @@ -99,6 +102,11 @@ described in `VISION_MVP.md`.
`.vercel/output/static` and `.vercel/output/functions/render.func`, rewrites
clean SSG URLs to static HTML, and routes ISG plus dynamic requests to the
generated edge function.
- **Deno adapter** — Emits a native `Deno.serve` entry that accepts Web
`Request` objects directly, serves static files from `dist/client`, and falls
back to `handlePrachtRequest()` for SSR, SPA route-state requests, loaders,
middleware, and API routes. Runtime ISG revalidation is intentionally not
implemented yet; Deno builds warn when ISG routes are present.
- **Preact Worker facets prototype** — `@pracht/preact-worker-facets` provides
experimental helpers for running Preact-style component modules inside
Cloudflare Dynamic Workers. A supervisor Durable Object owns auth, source
Expand Down
48 changes: 48 additions & 0 deletions e2e/deno-build.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync, rmSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";

import { expect, test } from "@playwright/test";

test("pracht build emits a Deno server entry", async () => {
test.setTimeout(120_000);

const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
const exampleDir = resolve(repoRoot, "examples/basic");
const distDir = resolve(exampleDir, "dist");
const serverEntryPath = resolve(exampleDir, "dist/server/server.js");
const staticIndexPath = resolve(exampleDir, "dist/client/index.html");
const headersManifestPath = resolve(exampleDir, "dist/server/headers-manifest.json");

rmSync(distDir, { force: true, recursive: true });

const result = spawnSync(process.execPath, ["../../packages/cli/bin/pracht.js", "build"], {
cwd: exampleDir,
encoding: "utf-8",
env: {
...process.env,
NODE_OPTIONS: "--experimental-strip-types",
PRACHT_ADAPTER: "deno",
},
stdio: ["ignore", "pipe", "pipe"],
});

if (result.status !== 0) {
throw new Error(result.stderr || result.stdout || "Deno example build failed");
}

expect(existsSync(serverEntryPath)).toBe(true);
expect(existsSync(staticIndexPath)).toBe(true);
expect(existsSync(headersManifestPath)).toBe(true);

const output = `${result.stdout}${result.stderr}`;
expect(output).toContain("Deno adapter currently serves prerendered ISG HTML");

const serverSource = readFileSync(serverEntryPath, "utf-8");
expect(serverSource).toContain('buildTarget = "deno"');
expect(serverSource).toContain("createDenoRequestHandler");
expect(serverSource).toContain('new URL("../client/", import.meta.url)');
expect(serverSource).toContain("Deno.serve({ port }, handler)");
expect(serverSource).toContain("Deno.readTextFile");
});
1 change: 1 addition & 0 deletions examples/basic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
"@pracht/adapter-deno": "workspace:*",
"@pracht/adapter-node": "workspace:*",
"@pracht/adapter-vercel": "workspace:*",
"@pracht/core": "workspace:*"
Expand Down
5 changes: 5 additions & 0 deletions examples/basic/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ async function resolveAdapter() {
return cloudflareAdapter();
}

if (process.env.PRACHT_ADAPTER === "deno") {
const { denoAdapter } = await import("@pracht/adapter-deno");
return denoAdapter();
}

const { nodeAdapter } = await import("@pracht/adapter-node");
return nodeAdapter();
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"license": "MIT",
"type": "module",
"scripts": {
"build": "pnpm -r --filter @pracht/core --filter @pracht/vite-plugin --filter @pracht/preact-ssr-precompile --filter @pracht/adapter-node --filter @pracht/adapter-cloudflare --filter @pracht/adapter-vercel --filter @pracht/cli --filter create-pracht run build",
"build": "pnpm -r --filter @pracht/core --filter @pracht/vite-plugin --filter @pracht/preact-ssr-precompile --filter @pracht/adapter-node --filter @pracht/adapter-cloudflare --filter @pracht/adapter-vercel --filter @pracht/adapter-deno --filter @pracht/cli --filter create-pracht run build",
"check": "pnpm build && pnpm typecheck && pnpm test",
"typecheck": "tsc --noEmit",
"test": "vitest run",
Expand Down
21 changes: 21 additions & 0 deletions packages/adapter-deno/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Pracht contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
21 changes: 21 additions & 0 deletions packages/adapter-deno/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# @pracht/adapter-deno

Deno adapter for Pracht apps.

```ts
import { denoAdapter } from "@pracht/adapter-deno";
import { pracht } from "@pracht/vite-plugin";

export default {
plugins: [pracht({ adapter: denoAdapter() })],
};
```

Build and run with Deno:

```sh
pnpm pracht build
deno run --allow-net --allow-read=dist --allow-env=PORT dist/server/server.js
```

`pracht preview` runs the same server entry with the required Deno permissions.
46 changes: 46 additions & 0 deletions packages/adapter-deno/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@pracht/adapter-deno",
"version": "0.1.0",
"description": "Deno adapter for serving Pracht apps with Deno.serve, static assets, loaders, and API routes.",
"keywords": [
"pracht",
"preact",
"deno",
"adapter",
"server",
"ssr",
"ssg",
"api-routes",
"vite"
],
"license": "MIT",
"homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/adapter-deno",
"bugs": {
"url": "https://github.com/JoviDeCroock/pracht/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/JoviDeCroock/pracht",
"directory": "packages/adapter-deno"
},
"files": [
"dist"
],
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"publishConfig": {
"provenance": true
},
"scripts": {
"build": "tsdown",
"prepublishOnly": "npm run build"
},
"dependencies": {
"@pracht/core": "workspace:*"
}
}
Loading