|
| 1 | +--- |
| 2 | +outline: deep |
| 3 | +--- |
| 4 | + |
| 5 | +# Next Helper |
| 6 | + |
| 7 | +> [!WARNING] |
| 8 | +> Experimental. `@devframes/next` ships as an in-repo spike ([plan 027](https://github.com/devframes/devframe/blob/main/plans/notes/devframes-next-proposal.md)) and isn't published to npm yet. The API described here is the proposed surface; expect changes before a stable release. |
| 9 | +
|
| 10 | +The `@devframes/next` package hosts one or more devframes from a Next.js App Router app. Next runs on webpack/Turbopack rather than Vite, so it hosts through a route handler instead of the [Vite Bridge](./vite-bridge): the package hands you a `DevframeHost` plus a single `fetch` handler your catch-all route delegates to. |
| 11 | + |
| 12 | +It handles the two things a Next.js host needs: |
| 13 | + |
| 14 | +1. **Static + connection serving.** `createDevframeNextHost()` returns a `DevframeHost` whose `mountStatic` / `mountConnectionMeta` calls accumulate into one WHATWG-`fetch` handler that serves every mounted SPA — reusing devframe's own [`serveStaticHandler`](/adapters/dev) for SPA fallback, content types, and path-traversal guarding — and answers each `<base>/__connection.json`. |
| 15 | +2. **Host-mode Next config.** `withDevframe()` applies the one Next setting a devframe host requires. |
| 16 | + |
| 17 | +## Install |
| 18 | + |
| 19 | +```ts [next.config.mjs] |
| 20 | +import { withDevframe } from '@devframes/next' |
| 21 | + |
| 22 | +export default withDevframe({ |
| 23 | + transpilePackages: ['@antfu/design'], |
| 24 | +}) |
| 25 | +``` |
| 26 | + |
| 27 | +`withDevframe` sets `skipTrailingSlashRedirect: true` and preserves the rest of your config. Mounted SPAs are served at `/__<id>/` and reference their assets relatively (`./_next/…`); Next's default trailing-slash redirect (`/__git/` → `/__git`) would re-root those paths and 404 every asset, so a host serves the base verbatim. |
| 28 | + |
| 29 | +## Hosting a devframe |
| 30 | + |
| 31 | +Build the host once (a module-level singleton, since App Router invokes route handlers per request), then delegate both routes to its `fetch`: |
| 32 | + |
| 33 | +```ts [devframe/host.ts] |
| 34 | +import { createHubContext, mountDevframe } from '@devframes/hub/node' |
| 35 | +import { createDevframeNextHost } from '@devframes/next' |
| 36 | +import { startHttpAndWs } from 'devframe/node' |
| 37 | + |
| 38 | +const nextHost = createDevframeNextHost({ |
| 39 | + resolveOrigin: () => 'http://localhost:3000', |
| 40 | + getStorageDir: scope => resolveStorageDir(scope), |
| 41 | +}) |
| 42 | + |
| 43 | +const context = await createHubContext({ host: nextHost.host, mode: 'dev' }) |
| 44 | +await mountDevframe(context, myDevframe) |
| 45 | +nextHost.host.mountConnectionMeta('/__hub') // the hub's own connection base |
| 46 | + |
| 47 | +const started = await startHttpAndWs({ context, port, auth: false }) |
| 48 | +nextHost.setConnectionMeta({ backend: 'websocket', websocket: started.port }) |
| 49 | + |
| 50 | +export const hub = { fetch: nextHost.fetch } |
| 51 | +``` |
| 52 | + |
| 53 | +```ts [app/__[id]/[[...path]]/route.ts] |
| 54 | +export const runtime = 'nodejs' |
| 55 | +export const dynamic = 'force-dynamic' |
| 56 | + |
| 57 | +export async function GET(request: Request): Promise<Response> { |
| 58 | + return hub.fetch(request) // serves every mounted SPA + connection meta |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +The same `fetch` answers the hub's own `__hub/__connection.json` — register `/__hub` with `mountConnectionMeta` and one route body covers both. |
| 63 | + |
| 64 | +## API |
| 65 | + |
| 66 | +### `createDevframeNextHost(options)` |
| 67 | + |
| 68 | +| Option | Description | |
| 69 | +|--------|-------------| |
| 70 | +| `resolveOrigin` | Returns the public origin the app is reachable at, for docks needing an absolute iframe URL. | |
| 71 | +| `getStorageDir` | Resolves a directory for persisted state per `scope` (`workspace` / `project` / `global`). | |
| 72 | +| `connectionMeta` | Optional initial meta; usually published later via `setConnectionMeta`. | |
| 73 | + |
| 74 | +Returns `{ host, fetch, setConnectionMeta }`: |
| 75 | + |
| 76 | +- **`host`** — the [`DevframeHost`](/guide/hub) to pass to `createHubContext` / `createHostContext`. |
| 77 | +- **`fetch(request)`** — the WHATWG-`fetch` handler your route delegates to. Connection meta is matched before the static handler, so an SPA fallback never swallows a `<base>/__connection.json` discovery fetch. |
| 78 | +- **`setConnectionMeta(meta)`** — publish the live meta once the RPC/WS port is known. Until then, meta requests answer `503` so a racing client retries rather than caching a wrong endpoint. |
| 79 | + |
| 80 | +### `withDevframe(nextConfig)` |
| 81 | + |
| 82 | +Returns a Next config with `skipTrailingSlashRedirect: true` applied, preserving everything else. |
| 83 | + |
| 84 | +## Runtime |
| 85 | + |
| 86 | +Route handlers that call `fetch` pin `export const runtime = 'nodejs'`: the static handler streams built SPA files from disk, and the side-car RPC/WS server the hub starts is a Node process. |
| 87 | + |
| 88 | +## See also |
| 89 | + |
| 90 | +- [Vite Bridge](./vite-bridge) — the equivalent for Vite-based hosts |
| 91 | +- [Hub](/guide/hub) — `createHubContext`, `mountDevframe`, and `DevframeHost` |
| 92 | +- [minimal-next-devframe-hub](/examples/minimal-next-devframe-hub) — a full working host |
0 commit comments