Skip to content

Commit d2660f1

Browse files
committed
add deno adapter
1 parent ef62c46 commit d2660f1

22 files changed

Lines changed: 891 additions & 12 deletions

.changeset/deno-adapter.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@pracht/adapter-deno": minor
3+
"@pracht/cli": patch
4+
---
5+
6+
Add a Deno adapter that generates a native `Deno.serve` production entry, serves
7+
static assets from `dist/client`, and delegates SSR, route-state, loaders,
8+
middleware, and API routes to Pracht's Web Request/Response runtime.
9+
10+
`pracht preview` now detects Deno targets and runs the built server through
11+
`deno run` with the required read, net, and env permissions.

VISION_MVP.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Platform adapters export a request handler shaped for their runtime:
9999
| `adapter-node` | Node.js `http` | Static file serving, ISG mtime check |
100100
| `adapter-cloudflare` | Workers `fetch` | `env.ASSETS`, bindings, no runtime ISG yet |
101101
| `adapter-vercel` | Serverless / Edge | Build Output API v3 + edge handler |
102+
| `adapter-deno` | Deno `Deno.serve` | Native Web Request/Response handler, no runtime ISG yet |
102103

103104
Each adapter:
104105

@@ -181,6 +182,7 @@ pracht/
181182
adapter-node/ # Node.js server adapter
182183
adapter-cloudflare/ # Cloudflare Workers adapter
183184
adapter-vercel/ # Vercel Edge adapter
185+
adapter-deno/ # Deno server adapter
184186
cli/ # Dev/build/generate/inspect/verify/doctor commands
185187
create-pracht/ # (Phase 2) Starter scaffolding
186188
example/ # Working example app

docs/ADAPTERS.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,92 @@ export default {
369369

370370
---
371371

372+
## Deno Adapter
373+
374+
### `createDenoRequestHandler(options)`
375+
376+
| Option | Type | Description |
377+
| --------------- | ----------------------------------- | --------------------------------------------------------------- |
378+
| `app` | `PrachtApp` | The resolved app from `defineApp()` |
379+
| `registry` | `ModuleRegistry` | Lazy module importers |
380+
| `staticDir` | `string \| URL` | File URL or path for `dist/client/` |
381+
| `createContext` | `(args) => TContext` | App-level context factory |
382+
| `apiRoutes` | `ResolvedApiRoute[]` | Resolved API route metadata |
383+
| `clientEntryUrl` | `string` | Client entry script URL |
384+
| `cssManifest` | `Record<string, string[]>` | Route/shell CSS asset manifest |
385+
| `jsManifest` | `Record<string, string[]>` | Route/shell JS asset manifest |
386+
| `headersManifest` | `Record<string, Record<string, string>>` | Prerendered document headers |
387+
388+
### Features
389+
390+
- **Native Deno server**: generated entries call `Deno.serve()` and pass Web
391+
`Request` objects directly to pracht. No Node request/response bridge is
392+
involved.
393+
- **Static file serving**: reads from `dist/client/` with proper content-type
394+
headers. Hashed assets under `/assets/` get `Cache-Control: public,
395+
max-age=31536000, immutable`; HTML and other files get `public, max-age=0,
396+
must-revalidate`. Clean URLs resolve to `index.html` files. Static responses
397+
apply the same baseline security headers as dynamic responses, and
398+
prerendered HTML receives route and shell document headers from
399+
`dist/server/headers-manifest.json`.
400+
- **Route-state bypass**: route-state requests (`x-pracht-route-state-request:
401+
1` or `?_data=1`) and Markdown negotiation requests bypass static files and
402+
reach `handlePrachtRequest()`.
403+
- **ISG caveat**: runtime ISG revalidation is not implemented for Deno yet. ISG
404+
routes are prerendered at build time and served as static files. `pracht
405+
build` warns when a Deno build contains ISG routes.
406+
- **Local preview**: `pracht preview` runs `pracht build` and then executes the
407+
built server with `deno run --allow-net --allow-read=dist --allow-env=PORT`.
408+
409+
### Generated entry options
410+
411+
When using `denoAdapter()` in `vite.config.ts`, generated entries can import a context factory:
412+
413+
```typescript
414+
denoAdapter({
415+
createContextFrom: "/src/server/context.ts",
416+
port: 8787,
417+
});
418+
```
419+
420+
The context module must export `createContext(args)`. Deno passes `{ request }`.
421+
422+
### Entry module
423+
424+
```javascript
425+
// virtual:pracht/server (generated in deno mode)
426+
import { createDenoRequestHandler } from "@pracht/adapter-deno";
427+
428+
const staticDir = new URL("../client/", import.meta.url);
429+
const denoHandler = createDenoRequestHandler({
430+
app: resolvedApp,
431+
registry,
432+
staticDir,
433+
apiRoutes,
434+
clientEntryUrl,
435+
cssManifest,
436+
jsManifest,
437+
});
438+
439+
export async function handler(request) {
440+
return denoHandler(request);
441+
}
442+
443+
if (import.meta.main) {
444+
const port = Number(Deno.env.get("PORT") ?? 3000);
445+
Deno.serve({ port }, handler);
446+
}
447+
```
448+
449+
Running `pracht build` for a Deno target emits `dist/server/server.js`, which is
450+
the executable production server entry. Run it manually with:
451+
452+
```bash
453+
deno run --allow-net --allow-read=dist --allow-env=PORT dist/server/server.js
454+
```
455+
456+
---
457+
372458
## Vercel Adapter (Phase 2)
373459
374460
### `createVercelEdgeHandler(options)`

docs/RENDERING_MODES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ stale, it serves the stale HTML immediately and triggers regeneration.
113113
> prerenders ISG routes at build time and routes ISG paths through the Edge
114114
> Function rather than relying on process-local cache state. Use SSG for static
115115
> output or SSR for per-request freshness on Vercel.
116+
>
117+
> **Deno note:** The Deno adapter currently does not implement runtime ISG
118+
> revalidation. ISG routes are prerendered at build time and served as static
119+
> files. Use SSG/SSR on Deno, or deploy ISG routes to Node until a persistent
120+
> Deno cache/storage design lands.
116121
117122
### Webhook-based revalidation
118123

docs/WORKSPACE.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ described in `VISION_MVP.md`.
1313
| `packages/adapter-node` | `@pracht/adapter-node` | Node `IncomingMessage`/`ServerResponse` bridge, ISG stale-while-revalidate |
1414
| `packages/adapter-cloudflare` | `@pracht/adapter-cloudflare` | Cloudflare Workers fetch handler, generated worker entry source, and static asset handoff (no runtime ISG revalidation yet) |
1515
| `packages/adapter-vercel` | `@pracht/adapter-vercel` | Vercel Edge handler and Build Output API entry source |
16+
| `packages/adapter-deno` | `@pracht/adapter-deno` | Deno `Deno.serve` handler, generated server entry source, and static asset handoff (no runtime ISG revalidation yet) |
1617
| `packages/preact-worker-facets` | `@pracht/preact-worker-facets` | Experimental Cloudflare Dynamic Worker + Durable Object facets runtime for inert, stateful Preact components |
1718
| `packages/cli` | `@pracht/cli` | `pracht dev`, `build`, `verify`, the `generate` subcommands, and `doctor` |
1819
| `examples/cloudflare` | `@pracht/example-cloudflare` | Cloudflare-targeted example app with SSG, ISG, SSR, SPA routes, auth middleware, and API routes |
@@ -67,11 +68,12 @@ described in `VISION_MVP.md`.
6768
`hydrate()` from Preact.
6869
- **CLI**`pracht dev` starts a Vite dev server with SSR, `pracht build` runs
6970
client + server builds (with Vite manifest generation, SSG/ISG prerendering,
70-
ISG manifest output, executable Node server output in `dist/server/server.js`,
71-
and Vercel `.vercel/output/` generation when the app targets those adapters),
72-
`pracht preview` builds and serves the production output locally (Node runs
73-
`dist/server/server.js`, Cloudflare delegates to `wrangler dev`, and Vercel
74-
points at `vercel build`/`vercel dev`),
71+
ISG manifest output, executable Node/Deno server output in
72+
`dist/server/server.js`, and Vercel `.vercel/output/` generation when the app
73+
targets those adapters), `pracht preview` builds and serves the production
74+
output locally (Node runs `dist/server/server.js`, Deno runs it through
75+
`deno run`, Cloudflare delegates to `wrangler dev`, and Vercel points at
76+
`vercel build`/`vercel dev`),
7577
`pracht verify` runs fast framework-aware checks with optional `--changed`
7678
and `--json` output, `pracht inspect [routes|api|build] --json` emits the
7779
resolved route graph, API handlers, and build metadata for agents/tools,
@@ -81,8 +83,9 @@ described in `VISION_MVP.md`.
8183
files, and `pracht doctor` validates app wiring across the whole project.
8284
- **Package builds**`tsdown` compiles `pracht`, `@pracht/vite-plugin`,
8385
`@pracht/preact-ssr-precompile`, `@pracht/adapter-node`,
84-
`@pracht/adapter-cloudflare`, and `@pracht/adapter-vercel` from TypeScript to
85-
ESM (`dist/index.mjs` + `.d.mts`). `@pracht/core` also publishes browser,
86+
`@pracht/adapter-cloudflare`, `@pracht/adapter-vercel`, and
87+
`@pracht/adapter-deno` from TypeScript to ESM (`dist/index.mjs` + `.d.mts`).
88+
`@pracht/core` also publishes browser,
8689
client, manifest, and server subpath entries so the Vite plugin can keep
8790
server-only runtime code and route-only browser helpers out of the critical
8891
client bootstrap graph while generated server modules avoid the browser export
@@ -99,6 +102,11 @@ described in `VISION_MVP.md`.
99102
`.vercel/output/static` and `.vercel/output/functions/render.func`, rewrites
100103
clean SSG URLs to static HTML, and routes ISG plus dynamic requests to the
101104
generated edge function.
105+
- **Deno adapter** — Emits a native `Deno.serve` entry that accepts Web
106+
`Request` objects directly, serves static files from `dist/client`, and falls
107+
back to `handlePrachtRequest()` for SSR, SPA route-state requests, loaders,
108+
middleware, and API routes. Runtime ISG revalidation is intentionally not
109+
implemented yet; Deno builds warn when ISG routes are present.
102110
- **Preact Worker facets prototype**`@pracht/preact-worker-facets` provides
103111
experimental helpers for running Preact-style component modules inside
104112
Cloudflare Dynamic Workers. A supervisor Durable Object owns auth, source

e2e/deno-build.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { spawnSync } from "node:child_process";
2+
import { existsSync, readFileSync, rmSync } from "node:fs";
3+
import { resolve } from "node:path";
4+
import { fileURLToPath } from "node:url";
5+
6+
import { expect, test } from "@playwright/test";
7+
8+
test("pracht build emits a Deno server entry", async () => {
9+
test.setTimeout(120_000);
10+
11+
const repoRoot = resolve(fileURLToPath(new URL("..", import.meta.url)));
12+
const exampleDir = resolve(repoRoot, "examples/basic");
13+
const distDir = resolve(exampleDir, "dist");
14+
const serverEntryPath = resolve(exampleDir, "dist/server/server.js");
15+
const staticIndexPath = resolve(exampleDir, "dist/client/index.html");
16+
const headersManifestPath = resolve(exampleDir, "dist/server/headers-manifest.json");
17+
18+
rmSync(distDir, { force: true, recursive: true });
19+
20+
const result = spawnSync(process.execPath, ["../../packages/cli/bin/pracht.js", "build"], {
21+
cwd: exampleDir,
22+
encoding: "utf-8",
23+
env: {
24+
...process.env,
25+
NODE_OPTIONS: "--experimental-strip-types",
26+
PRACHT_ADAPTER: "deno",
27+
},
28+
stdio: ["ignore", "pipe", "pipe"],
29+
});
30+
31+
if (result.status !== 0) {
32+
throw new Error(result.stderr || result.stdout || "Deno example build failed");
33+
}
34+
35+
expect(existsSync(serverEntryPath)).toBe(true);
36+
expect(existsSync(staticIndexPath)).toBe(true);
37+
expect(existsSync(headersManifestPath)).toBe(true);
38+
39+
const output = `${result.stdout}${result.stderr}`;
40+
expect(output).toContain("Deno adapter currently serves prerendered ISG HTML");
41+
42+
const serverSource = readFileSync(serverEntryPath, "utf-8");
43+
expect(serverSource).toContain('buildTarget = "deno"');
44+
expect(serverSource).toContain("createDenoRequestHandler");
45+
expect(serverSource).toContain('new URL("../client/", import.meta.url)');
46+
expect(serverSource).toContain("Deno.serve({ port }, handler)");
47+
expect(serverSource).toContain("Deno.readTextFile");
48+
});

examples/basic/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"private": true,
55
"type": "module",
66
"dependencies": {
7+
"@pracht/adapter-deno": "workspace:*",
78
"@pracht/adapter-node": "workspace:*",
89
"@pracht/adapter-vercel": "workspace:*",
910
"@pracht/core": "workspace:*"

examples/basic/vite.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ async function resolveAdapter() {
1212
return cloudflareAdapter();
1313
}
1414

15+
if (process.env.PRACHT_ADAPTER === "deno") {
16+
const { denoAdapter } = await import("@pracht/adapter-deno");
17+
return denoAdapter();
18+
}
19+
1520
const { nodeAdapter } = await import("@pracht/adapter-node");
1621
return nodeAdapter();
1722
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"license": "MIT",
66
"type": "module",
77
"scripts": {
8-
"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",
8+
"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",
99
"check": "pnpm build && pnpm typecheck && pnpm test",
1010
"typecheck": "tsc --noEmit",
1111
"test": "vitest run",

packages/adapter-deno/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Pracht contributors
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

0 commit comments

Comments
 (0)