Skip to content
Merged
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
1 change: 1 addition & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const alias = {
'devframe/rpc/transports/sse-client': r('devframe/src/rpc/transports/sse-client.ts'),
'devframe/rpc/transports/sse-server': r('devframe/src/rpc/transports/sse-server.ts'),
'devframe/rpc/transports/ws-bun': r('devframe/src/rpc/transports/ws-bun.ts'),
'devframe/rpc/transports/ws-deno': r('devframe/src/rpc/transports/ws-deno.ts'),
'devframe/rpc/transports/ws-server': r('devframe/src/rpc/transports/ws-server.ts'),
'devframe/rpc/transports/ws-client': r('devframe/src/rpc/transports/ws-client.ts'),
'devframe/rpc/client': r('devframe/src/rpc/client.ts'),
Expand Down
3 changes: 3 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ function examplesItems(prefix: string) {
{ text: 'hub-next-minimal', link: `${prefix}/examples/hub-next-minimal` },
{ text: 'hub-nitro-minimal', link: `${prefix}/examples/hub-nitro-minimal` },
{ text: 'hub-hono-minimal', link: `${prefix}/examples/hub-hono-minimal` },
{ text: 'hub-fastify-minimal', link: `${prefix}/examples/hub-fastify-minimal` },
{ text: 'hub-sveltekit-minimal', link: `${prefix}/examples/hub-sveltekit-minimal` },
{ text: 'hub-deno-minimal', link: `${prefix}/examples/hub-deno-minimal` },
{ text: 'hub-rsbuild-minimal', link: `${prefix}/examples/hub-rsbuild-minimal` },
] satisfies DefaultTheme.NavItemWithLink[]
}
Expand Down
26 changes: 26 additions & 0 deletions docs/examples/hub-deno-minimal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
outline: deep
---

# hub-deno-minimal

The minimal [Deno](https://deno.com) host for [`@devframes/hub`](/guide/hub): one `initHub()` call served through `Deno.serve`, the UI supplied by `@devframes/hub-ui`.

Package: `hub-deno-minimal` · framework: **Deno**

## What it shows

- `initHub({ base, devframes, ui: createUi() })` in `src/hub.ts`, memoized on `globalThis`. No transport option, so the entry wires the socket itself.
- `Deno.serve(options, handler)` serves HTTP (web `Request` → `Response`), and the whole namespace flows through `hub.handler(request)`.
- WebSockets arrive as fetch upgrades, so `src/server.ts` binds Deno's transport with `createContextRpcServer` + `attachDenoWsTransport` (crossws' Deno adapter) and answers `${hub.base}__ws` on the app's own origin. crossws attaches the socket to the `Response` its `handleUpgrade` returns, so there is no separate `websocket` handler object.

## Run it

```sh
pnpm install
pnpm --filter hub-deno-minimal dev
```

## Source

[`examples/hub-deno-minimal`](https://github.com/devframes/devframe/tree/main/examples/hub-deno-minimal)
26 changes: 26 additions & 0 deletions docs/examples/hub-fastify-minimal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
outline: deep
---

# hub-fastify-minimal

The minimal [Fastify](https://fastify.dev) host for [`@devframes/hub`](/guide/hub): one `initHub()` call mounted through Fastify's connect-middleware layer, the UI supplied by `@devframes/hub-ui`.

Package: `hub-fastify-minimal` · framework: **Fastify**

## What it shows

- `initHub({ base, devframes, ui: createUi() })` in `src/hub.ts`, memoized on `globalThis`. No transport option, so the socket rides Fastify's own server.
- Fastify is the `nodeMiddleware` host: `src/server.ts` registers `hub.nodeMiddleware` — the same `(req, res, next)` shape Vite's dev server consumes — through [`@fastify/middie`](https://github.com/fastify/middie). Requests under `${hub.base}` are served by the hub; the rest fall through `next()` to Fastify's routes.
- `hub.attach(fastify.server)` routes the HTTP server's upgrade events to the RPC socket at `${hub.base}__ws`, on the app's own origin — no side-car port.

## Run it

```sh
pnpm install
pnpm --filter hub-fastify-minimal dev
```

## Source

[`examples/hub-fastify-minimal`](https://github.com/devframes/devframe/tree/main/examples/hub-fastify-minimal)
26 changes: 26 additions & 0 deletions docs/examples/hub-sveltekit-minimal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
outline: deep
---

# hub-sveltekit-minimal

The minimal [SvelteKit](https://svelte.dev/docs/kit) host for [`@devframes/hub`](/guide/hub): one `initHub()` call behind a single catch-all endpoint, the UI supplied by `@devframes/hub-ui`.

Package: `hub-sveltekit-minimal` · framework: **SvelteKit**

## What it shows

- `initHub({ base, devframes, ui: createUi() })` in `src/hub.ts`, memoized on `globalThis`. The RPC socket runs on a side-car port (`ws: { sidecar: true }`) advertised via `__connection.json` — SvelteKit's `+server.ts` handlers hand over `Request`s and never see WebSocket upgrades, so the hub takes a socket of its own.
- `src/routes/__devframes/[...path]/+server.ts` mounts the whole namespace: `fallback` answers every method with `hub.handler(event.request)`, and the `[...path]` rest param matches the namespace root as well as everything beneath it.
- The endpoint exports `trailingSlash = 'ignore'` so SvelteKit serves the hub's trailing-slash URLs (the standalone viewer and each frame SPA) verbatim instead of 308-redirecting them, and `src/app.html` injects `${hub.base}embedded.js` to mount the floating dock.

## Run it

```sh
pnpm install
pnpm --filter hub-sveltekit-minimal dev
```

## Source

[`examples/hub-sveltekit-minimal`](https://github.com/devframes/devframe/tree/main/examples/hub-sveltekit-minimal)
3 changes: 3 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ The **minimal** family instead mounts one `initHub({ ui: createUi() })` handler
| [hub-next-minimal](./hub-next-minimal) | Next.js | The hub handler on an App Router catch-all route. |
| [hub-nitro-minimal](./hub-nitro-minimal) | Nitro | The hub handler on a Nitro catch-all route. |
| [hub-hono-minimal](./hub-hono-minimal) | Hono | The hub handler on Hono, running on Node and Bun. |
| [hub-fastify-minimal](./hub-fastify-minimal) | Fastify | The hub handler on Fastify via `nodeMiddleware`. |
| [hub-sveltekit-minimal](./hub-sveltekit-minimal) | SvelteKit | The hub handler on a SvelteKit catch-all endpoint. |
| [hub-deno-minimal](./hub-deno-minimal) | Deno | The hub handler on `Deno.serve`, with a Deno fetch-upgrade socket. |
| [hub-rsbuild-minimal](./hub-rsbuild-minimal) | Rsbuild | The hub handler on Rsbuild's dev middleware. |

## Run any example
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/hub-initiate.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const hub = initHub({

## The shared socket

One transport serves the whole namespace, and the hub binds nothing on its own — the same four choices `initDevframe` offers, in the same precedence: `ws.port` pins a side-car, `server` shares the host's `node:http` upgrade at `<base>__ws`, `ws: { sidecar: true }` takes a free port (for Next.js, Nitro and Rsbuild hosts, whose handlers never see upgrades), and passing none of them leaves the socket to the host:
One transport serves the whole namespace, and the hub binds nothing on its own — the same four choices `initDevframe` offers, in the same precedence: `ws.port` pins a side-car, `server` shares the host's `node:http` upgrade at `<base>__ws`, `ws: { sidecar: true }` takes a free port (for Next.js, Nitro, SvelteKit and Rsbuild hosts, whose handlers never see upgrades), and passing none of them leaves the socket to the host — a Node host hands over its server with `hub.attach(server)`, and Bun and Deno hosts complete the upgrade on their own origin with `attachBunWsTransport` / `attachDenoWsTransport`:

```ts
import { serve } from '@hono/node-server'
Expand Down Expand Up @@ -125,4 +125,4 @@ Hosts that assemble `createHubContext` + `ctx.install` themselves (with their ow
const hub = initHub({ base: DEVFRAMES_HUB_BASE, context: ctx })
```

The instance then serves the hub-level endpoints and transport only; serve each frame's meta from `hub.connectionMeta()` yourself. The two reference examples — `examples/hub-vite` and `examples/hub-next` — use the declarative mode with their own hand-built viewer UIs, while the `hub-*-minimal` family (`hub-vite-minimal`, `hub-next-minimal`, `hub-nitro-minimal`, `hub-hono-minimal`, `hub-rsbuild-minimal`) shows the minimal `createUi()` mount across frameworks (the Hono one on Node and Bun).
The instance then serves the hub-level endpoints and transport only; serve each frame's meta from `hub.connectionMeta()` yourself. The two reference examples — `examples/hub-vite` and `examples/hub-next` — use the declarative mode with their own hand-built viewer UIs, while the `hub-*-minimal` family (`hub-vite-minimal`, `hub-next-minimal`, `hub-nitro-minimal`, `hub-hono-minimal`, `hub-fastify-minimal`, `hub-sveltekit-minimal`, `hub-deno-minimal`, `hub-rsbuild-minimal`) shows the minimal `createUi()` mount across frameworks (the Hono one on Node and Bun, the Deno one on `Deno.serve`).
16 changes: 16 additions & 0 deletions examples/hub-deno-minimal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# hub-deno-minimal

The minimal [Deno](https://deno.com) host for `@devframes/hub` — one `initHub()` call served through `Deno.serve`, with the RPC socket riding a same-origin fetch upgrade.

```sh
pnpm --filter hub-deno-minimal dev
```

Open <http://localhost:5182> — the host page carries the floating dock via one script tag — or <http://localhost:5182/__devframes/> for the standalone viewer.

## How it works

- [`src/hub.ts`](./src/hub.ts) — `initHub({ devframes, ui: createUi({ branding }) })` (rebranded to Deno's own green, `#70ffaf`), memoized on `globalThis` so a dev-time reload reuses the live hub. No `ws` option is passed, so the entry wires the socket itself.
- [`src/server.ts`](./src/server.ts) — Deno serves HTTP through `Deno.serve(options, handler)` (web `Request` → `Response`), and the whole namespace flows through `hub.handler(request)`. WebSockets arrive as fetch upgrades rather than `node:http` `upgrade` events, so the entry binds Deno's own transport to the hub context with `createContextRpcServer` + `attachDenoWsTransport` (crossws' Deno adapter) and answers `/__devframes/__ws` itself — the socket rides the app's own origin with no side-car port. crossws attaches the socket to the `Response` its `handleUpgrade` returns, so there is no separate `websocket` handler object to register.

The same `initHub` instance mounts identically on Vite, Hono, Nitro, Fastify, and Next.js — see the sibling examples.
28 changes: 28 additions & 0 deletions examples/hub-deno-minimal/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "hub-deno-minimal",
"type": "module",
"version": "0.9.1",
"private": true,
"description": "Minimal Deno host that serves the devframe hub over a fetch-upgrade WebSocket.",
"scripts": {
"dev": "deno run --allow-net --allow-read --allow-env --node-modules-dir src/server.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@devframes/hub": "workspace:*",
"@devframes/hub-ui": "workspace:*",
"@devframes/plugin-a11y": "workspace:*",
"@devframes/plugin-assets": "workspace:*",
"@devframes/plugin-code-server": "workspace:*",
"@devframes/plugin-data-inspector": "workspace:*",
"@devframes/plugin-git": "workspace:*",
"@devframes/plugin-inspect": "workspace:*",
"@devframes/plugin-messages": "workspace:*",
"@devframes/plugin-og": "workspace:*",
"@devframes/plugin-terminals": "workspace:*",
"devframe": "workspace:*"
},
"devDependencies": {
"@types/node": "catalog:types"
}
}
82 changes: 82 additions & 0 deletions examples/hub-deno-minimal/src/hub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { HubInstance } from '@devframes/hub/initiate'
import { createUi } from '@devframes/hub-ui'
import { DEVFRAMES_HUB_BASE, initHub } from '@devframes/hub/initiate'
import { createA11yDevframe } from '@devframes/plugin-a11y'
import { createAssetsDevframe } from '@devframes/plugin-assets'
import { createCodeServerDevframe } from '@devframes/plugin-code-server'
import { createDataInspectorDevframe } from '@devframes/plugin-data-inspector'
import { createGitDevframe } from '@devframes/plugin-git'
import { createInspectDevframe } from '@devframes/plugin-inspect'
import { createMessagesDevframe } from '@devframes/plugin-messages'
import { createOgDevframe } from '@devframes/plugin-og'
import { createTerminalsDevframe } from '@devframes/plugin-terminals'

// One `initHub` call, memoized on globalThis so a dev-time reload returns the
// live hub instead of leaking transports. No `ws` option is passed: on Deno,
// WebSockets arrive as fetch upgrades rather than `node:http` `upgrade`
// events, so `src/server.ts` binds Deno's own transport to the hub context
// and answers the upgrade route itself — the socket rides the app's own
// origin with no side-car port.
const globalRef = globalThis as { __hubDenoMinimal?: HubInstance }

export const hub: HubInstance = globalRef.__hubDenoMinimal ??= initHub({
base: DEVFRAMES_HUB_BASE,
// Every built-in plugin, dogfooded end to end through the hub mount path.
// `data-inspector`'s default id carries `:` (a route-param marker), so it
// gets a colon-free id override to be a valid `<base><id>/` segment; the
// assets watcher is off since this host demonstrates mounting, not authoring.
devframes: [
createGitDevframe(),
createTerminalsDevframe(),
createCodeServerDevframe(),
createInspectDevframe(),
createDataInspectorDevframe({ id: 'devframes_plugin_data-inspector' }),
createA11yDevframe(),
createMessagesDevframe(),
createOgDevframe(),
createAssetsDevframe({ watch: false }),
],
// Rebrand the reference UI to Deno's own navy — one field, no CSS:
// `createUi`'s `branding` option publishes `ConnectionMeta.configs.ui.branding`,
// which the dock reads at connect time and feeds into `--devframe-primary`
// (see `@devframes/hub-ui`'s `primary-ramp.css`).
ui: createUi({ branding: { primaryColor: '#70ffaf', productName: 'Devframes on Deno' } }),
// Gate with devframe's interactive OTP (the default). The hub prints a
// 6-digit code + magic link on startup, and the reference UI's authorization
// view exchanges it for a bearer token. See docs/guide/security.md.
configure(ctx) {
ctx.commands.register({
id: 'example:hub-deno-minimal:ping',
title: 'Deno Hub · Ping',
icon: 'ph:bell-duotone',
category: 'kit',
handler: () => 'pong',
})
ctx.rpc.register({
name: 'example:hub-deno-minimal:probe',
type: 'query',
jsonSerializable: true,
handler: () => 'pong',
})
},
})

/** The host page — one script tag turns any page into a devtools host. */
export const hostPage = `<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Deno Devframe Hub</title>
</head>
<body style="font-family: system-ui; padding: 2rem">
<h1>Deno Devframe Hub</h1>
<p>This page is the host app. The devtools ride along:</p>
<ul>
<li>the floating dock (bottom of this page) is <code>/__devframes/embedded.js</code></li>
<li>the standalone viewer lives at <a href="/__devframes/">/__devframes/</a></li>
<li>discovery: <a href="/__devframes/__index.json">__index.json</a> · <a href="/__devframes/__connection.json">__connection.json</a></li>
</ul>
<script type="module" src="/__devframes/embedded.js"></script>
</body>
</html>`
70 changes: 70 additions & 0 deletions examples/hub-deno-minimal/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createContextRpcServer } from 'devframe/internal'
import { createInteractiveAuth } from 'devframe/recipes/interactive-auth'
import { attachDenoWsTransport } from 'devframe/rpc/transports/ws-deno'
import { hostPage, hub } from './hub'

// `Deno` is a global on the Deno runtime only; this is the Deno entry. Declare
// the slice this file uses so `tsc` (Node types only, no Deno lib) can check
// it — typing the handler's params here also keeps them from being `any`.
declare const Deno: {
serve: (
options: { port: number, hostname?: string },
handler: (request: Request, info: unknown) => Response | Promise<Response>,
) => { finished: Promise<void>, shutdown: () => Promise<void>, addr: { port: number } }
env: { get: (key: string) => string | undefined }
}

/**
* The Deno entry. Deno serves HTTP through `Deno.serve(options, handler)` (web
* Request → Response), but WebSockets arrive as fetch upgrades rather than
* `node:http` `upgrade` events — so this host binds Deno's own transport to
* the hub context with two public primitives: `createContextRpcServer` (the
* session/auth wiring every devframe transport shares) and
* `attachDenoWsTransport` (crossws' Deno adapter). crossws attaches the socket
* to the `Response` its `handleUpgrade` returns, so there is no separate
* websocket handler object to register.
*
* The hub advertises `/__devframes/__ws` on the app's own origin, so the
* browser client needs no per-runtime knowledge — this file only has to
* answer that route.
*/
export async function startDenoServer(port: number): Promise<{ port: number, close: () => Promise<void> }> {
await hub.ready
const context = await hub.context
// Bind the same interactive-OTP handler the gated `initHub` uses to Deno's
// own WS transport, sharing the context's auth storage and one-time code so
// a client authorizes once regardless of which surface serves it.
const core = createContextRpcServer({ context, auth: createInteractiveAuth(context) })
const tier = await attachDenoWsTransport(core)
const upgradePath = `${hub.base}__ws`
const baseNoSlash = hub.base.replace(/\/$/, '')

const server = Deno.serve({ port, hostname: '0.0.0.0' }, (request, info) => {
const { pathname } = new URL(request.url)
if (pathname === upgradePath && request.headers.get('upgrade')?.toLowerCase() === 'websocket')
return tier.handleUpgrade(request, info)
// The whole hub namespace behind one delegation — frame SPAs,
// __connection.json, __index.json, embedded.js, __client-imports.js.
if (pathname === baseNoSlash || pathname.startsWith(hub.base))
return hub.handler(request)
if (pathname === '/')
return new Response(hostPage, { headers: { 'content-type': 'text/html; charset=utf-8' } })
return new Response('Not found', { status: 404 })
})

return {
port: server.addr.port,
close: async () => {
await tier.close()
await hub.close()
await server.shutdown()
},
}
}

if (import.meta.main) {
void startDenoServer(Number(Deno.env.get('PORT') ?? 5182)).then(({ port }) => {
// eslint-disable-next-line no-console
console.log(`deno-devframe-hub on http://localhost:${port} — devtools at /__devframes/`)
})
}
19 changes: 19 additions & 0 deletions examples/hub-deno-minimal/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": [
"esnext",
"dom"
],
"types": [
"node"
]
},
"include": [
"src"
],
"exclude": [
"dist",
"node_modules"
]
}
16 changes: 16 additions & 0 deletions examples/hub-fastify-minimal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# hub-fastify-minimal

The minimal [Fastify](https://fastify.dev) host for `@devframes/hub` — one `initHub()` call mounted through Fastify's connect-middleware layer, with the RPC socket riding Fastify's own HTTP server.

```sh
pnpm --filter hub-fastify-minimal dev
```

Open <http://localhost:5183> — the host page carries the floating dock via one script tag — or <http://localhost:5183/__devframes/> for the standalone viewer.

## How it works

- [`src/hub.ts`](./src/hub.ts) — `initHub({ devframes, ui: createUi({ branding }) })` (rebranded to Fastify's own black, `#2f2f2f`), memoized on `globalThis` so a dev-time reload reuses the live hub. No `ws` option is passed, so the hub binds nothing on its own.
- [`src/server.ts`](./src/server.ts) — Fastify is the `nodeMiddleware` host: rather than bridging every request to `hub.handler`, it registers `hub.nodeMiddleware` — the same `(req, res, next)` shape a Vite dev server consumes — through [`@fastify/middie`](https://github.com/fastify/middie). Requests under `/__devframes/` are served by the hub; everything else falls through `next()` to Fastify's own routes. The RPC socket rides Fastify's own `node:http` server: `fastify.server` is that server, and `hub.attach(server)` routes its upgrade events to `/__devframes/__ws` on the app's origin — no side-car port.

The same `initHub` instance mounts identically on Vite, Hono, Nitro, and Next.js — see the sibling examples.
Loading
Loading