|
| 1 | +--- |
| 2 | +outline: deep |
| 3 | +--- |
| 4 | + |
| 5 | +# Next Helper |
| 6 | + |
| 7 | +> [!WARNING] |
| 8 | +> Experimental. `@devframes/next`'s API is still settling — expect changes before a stable release. |
| 9 | +
|
| 10 | +`@devframes/next` hosts 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 serves each devframe's SPA and its `__connection.json` from a single `fetch` handler your catch-all route delegates to, reusing devframe's own [`serveStaticHandler`](/adapters/dev) for SPA fallback, content types, and path-traversal guarding. |
| 11 | + |
| 12 | +It comes in three parts: |
| 13 | + |
| 14 | +1. **`withDevframe()`** — applies the one Next config setting a devframe host needs. |
| 15 | +2. **`createDevframeNextHandler()`** — hosts a single devframe (the common case). |
| 16 | +3. **`createDevframeNextHost()`** — the lower-level primitive for a hub mounting many devframes at once. |
| 17 | + |
| 18 | +Plus a React client surface at `@devframes/next/client`. |
| 19 | + |
| 20 | +## Config |
| 21 | + |
| 22 | +```ts [next.config.mjs] |
| 23 | +import { withDevframe } from '@devframes/next' |
| 24 | + |
| 25 | +export default withDevframe({ |
| 26 | + // ...your own Next config |
| 27 | +}) |
| 28 | +``` |
| 29 | + |
| 30 | +`withDevframe` sets `skipTrailingSlashRedirect: true` and preserves the rest. 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. |
| 31 | + |
| 32 | +## Hosting a single devframe |
| 33 | + |
| 34 | +`createDevframeNextHandler(definition)` statically serves the devframe's built SPA and starts a side-car RPC/WebSocket server, advertising it at `<base>/__connection.json`. Delegate your catch-all route to its `fetch`: |
| 35 | + |
| 36 | +```ts [app/__my-tool/[[...path]]/route.ts] |
| 37 | +import { createDevframeNextHandler } from '@devframes/next' |
| 38 | +import myDevframe from '@/devframe' |
| 39 | + |
| 40 | +export const runtime = 'nodejs' |
| 41 | +export const dynamic = 'force-dynamic' |
| 42 | + |
| 43 | +const handler = createDevframeNextHandler(myDevframe) |
| 44 | +export const GET = handler.fetch |
| 45 | +``` |
| 46 | + |
| 47 | +The base defaults to `def.basePath ?? '/__<id>/'`. `close()` shuts the side-car down; `ready` resolves once it's listening. |
| 48 | + |
| 49 | +| Option | Default | Description | |
| 50 | +|--------|---------|-------------| |
| 51 | +| `base` | `def.basePath ?? '/__<id>/'` | Mount path for the SPA. | |
| 52 | +| `host` | `def.cli?.host ?? 'localhost'` | Side-car bind host. | |
| 53 | +| `port` | resolved from `def.cli?.port` | Side-car port. | |
| 54 | +| `flags` | — | Forwarded to `def.setup(ctx, { flags })`. | |
| 55 | +| `auth` | `false` | `true` for devframe's OTP gate, or a handler. The Next app owns auth by default. | |
| 56 | + |
| 57 | +## Hosting a hub |
| 58 | + |
| 59 | +For many devframes at once, use `createDevframeNextHost()` with [`@devframes/hub`](/guide/hub). Its `host` accumulates every `mountStatic` / `mountConnectionMeta` call into one `fetch` handler: |
| 60 | + |
| 61 | +```ts [devframe/host.ts] |
| 62 | +import { createHubContext, mountDevframe } from '@devframes/hub/node' |
| 63 | +import { createDevframeNextHost } from '@devframes/next' |
| 64 | +import { startHttpAndWs } from 'devframe/node' |
| 65 | + |
| 66 | +const nextHost = createDevframeNextHost({ |
| 67 | + resolveOrigin: () => 'http://localhost:3000', |
| 68 | + getStorageDir: scope => resolveStorageDir(scope), |
| 69 | +}) |
| 70 | + |
| 71 | +const context = await createHubContext({ host: nextHost.host, mode: 'dev' }) |
| 72 | +await mountDevframe(context, myDevframe) |
| 73 | +nextHost.host.mountConnectionMeta('/__hub') // the hub's own connection base |
| 74 | + |
| 75 | +const started = await startHttpAndWs({ context, port, auth: false }) |
| 76 | +nextHost.setConnectionMeta({ backend: 'websocket', websocket: started.port }) |
| 77 | + |
| 78 | +export const hub = { fetch: nextHost.fetch } |
| 79 | +``` |
| 80 | + |
| 81 | +```ts [app/__[id]/[[...path]]/route.ts] |
| 82 | +export const runtime = 'nodejs' |
| 83 | +export const dynamic = 'force-dynamic' |
| 84 | + |
| 85 | +export async function GET(request: Request): Promise<Response> { |
| 86 | + return hub.fetch(request) // serves every mounted SPA + connection meta |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +`createDevframeNextHost` returns `{ host, fetch, setConnectionMeta }`: |
| 91 | + |
| 92 | +- **`host`** — the [`DevframeHost`](/guide/hub) to pass to `createHubContext` / `createHostContext`. |
| 93 | +- **`fetch(request)`** — the 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; a miss returns a bare `404`. |
| 94 | +- **`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. |
| 95 | + |
| 96 | +## React client |
| 97 | + |
| 98 | +`@devframes/next/client` connects to the RPC backend and provides the client to your component tree — the React counterpart to `@devframes/nuxt`'s `$rpc` plugin. Children render immediately, so your shell and a connection indicator stay visible while the client connects. |
| 99 | + |
| 100 | +```tsx [app/providers.tsx] |
| 101 | +'use client' |
| 102 | +import { RpcProvider } from '@devframes/next/client' |
| 103 | + |
| 104 | +export function Providers({ children }: { children: React.ReactNode }) { |
| 105 | + return <RpcProvider baseURL="/__my-tool/">{children}</RpcProvider> |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +`useRpc()` returns the connected `DevframeRpcClient`, or `null` while connecting; scope it to your tool's namespace. `useRpcStatus()` returns the live `{ status, error }` for a connection indicator. |
| 110 | + |
| 111 | +```tsx [app/panel.tsx] |
| 112 | +'use client' |
| 113 | +import { useRpc, useRpcStatus } from '@devframes/next/client' |
| 114 | + |
| 115 | +export function Panel() { |
| 116 | + const rpc = useRpc()?.scope('my-tool:') |
| 117 | + const { status, error } = useRpcStatus() |
| 118 | + if (!rpc) |
| 119 | + return <p>{error ? `connection failed — ${error.message}` : 'connecting…'}</p> |
| 120 | + // rpc.rpc.call('get-payload'), rpc.sharedState, … |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +Both hooks throw outside a `<RpcProvider>`. Theming and layout stay app-owned. |
| 125 | + |
| 126 | +## Runtime |
| 127 | + |
| 128 | +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 is a Node process. |
| 129 | + |
| 130 | +## See also |
| 131 | + |
| 132 | +- [Vite Bridge](./vite-bridge) — the equivalent for Vite-based hosts |
| 133 | +- [Hub](/guide/hub) — `createHubContext`, `mountDevframe`, and `DevframeHost` |
| 134 | +- [minimal-next-devframe-hub](/examples/minimal-next-devframe-hub) — a full working host |
0 commit comments