Skip to content
Open
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 docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ function examplesItems(prefix: string) {
{ text: 'next-runtime-snapshot', link: `${prefix}/examples/next-runtime-snapshot` },
{ text: 'vite-devframe-hub', link: `${prefix}/examples/vite-devframe-hub` },
{ text: 'next-devframe-hub', link: `${prefix}/examples/next-devframe-hub` },
{ text: 'rsbuild-devframe-hub', link: `${prefix}/examples/rsbuild-devframe-hub` },
] satisfies DefaultTheme.NavItemWithLink[]
}

Expand Down
1 change: 1 addition & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ End-to-end examples that exercise the full adapter surface, each a runnable app
| [next-runtime-snapshot](./next-runtime-snapshot) | React (Next.js) | A Next.js App Router SPA over RPC, surfacing the host Node runtime. |
| [vite-devframe-hub](./vite-devframe-hub) | Vanilla TypeScript (Vite) | A ~120-line Vite host wiring `@devframes/hub` end to end. |
| [next-devframe-hub](./next-devframe-hub) | React (Next.js) | The same hub protocol, hosted from a Next.js route handler. |
| [rsbuild-devframe-hub](./rsbuild-devframe-hub) | React (Rsbuild) | The same hub protocol, hosted from an Rsbuild (Rspack) dev server. |

## Run any example

Expand Down
33 changes: 33 additions & 0 deletions docs/examples/rsbuild-devframe-hub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
outline: deep
---

# rsbuild-devframe-hub

The same hub protocol as the [Vite host](./vite-devframe-hub), hosted from an **Rsbuild** (Rspack) dev server with a **React** browser UI. It wires [`@devframes/hub`](/guide/hub) through a small Rsbuild plugin that registers connect middleware and starts a side-car RPC / WebSocket server — proof that the hub is host-runtime-agnostic.

Package: `rsbuild-devframe-hub` · framework: **React (Rsbuild)**

## What it proves

- `createHubContext()` boots a hub without any bundler-specific code path.
- A `DevframeHost` implementation plugs framework specifics (static mounts, connection meta, storage, origin resolution) into the hub uniformly.
- Because `rsbuild.config.ts` runs in Node — not through Rspack — the built-in plugins are imported directly, exactly like the Vite host; no bundler-ignored dynamic `import()` is needed.
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock, serving both its SPA and its `__connection.json`.
- The built-in `hub:commands:execute` RPC dispatches any registered server command, regardless of how the host was constructed.
- The browser-side `connectDevframe({ baseURL: '/__hub/' })` discovers the WS endpoint via the plugin's `/__hub/__connection.json` middleware.
- The [JSON-render](/guide/json-render) hub integration with **registry replacement**: the host authors a view and projects it onto a `json-render` dock, and the React client renders it with the mini React registry **shared with the [Next host](./next-devframe-hub)** (rather than the Vue `@devframes/json-render-ui`).
- [Client-only docks](/guide/client-context#client-only-docks) the page registers itself with `context.docks.register()`: an iframe dock rendered from a Blob URL, and an interactive `json-render` dock whose spec is authored in the browser and carried inline in the dock entry (`view: { spec }`) — rendered by the same React registry as the server-authored view.

## Run it

```sh
pnpm install
pnpm --filter rsbuild-devframe-hub dev
```

Open the printed URL to see the docks, commands, messages, and terminals lists, plus a button that dispatches a sample command through `hub:commands:execute`.

## Source

[`examples/rsbuild-devframe-hub`](https://github.com/devframes/devframe/tree/main/examples/rsbuild-devframe-hub)
46 changes: 46 additions & 0 deletions examples/rsbuild-devframe-hub/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Rsbuild Devframe Hub

A tiny, copyable **vite-devtools-style hub on Rsbuild**. [vite-devtools](https://github.com/vitejs/devtools) is the full viewer that docks many integrations behind one icon rail on top of `@devframes/hub`; this example wears the same shape — an icon dock, an iframe stage, and a drawer of hub subsystems — but hosts it from an [Rsbuild](https://rsbuild.rs) (Rspack) dev server, with a **React** browser UI.

`src/rsbuild-devframe-hub.ts` is the entire host: a small Rsbuild plugin that wires `@devframes/hub` into an Rsbuild dev server. Because `rsbuild.config.ts` runs in Node — not through Rspack — the built-in plugins are imported directly, exactly like the Vite host; none of a bundled server's dynamic-`import()` dance is needed.

## Run it

```sh
pnpm install
pnpm --filter rsbuild-devframe-hub dev
```

Open the printed URL. The dock on the left lists every mounted tool with its icon:

- **Git**, **Terminals**, **Code Server**, **RPC & State Inspector**, **A11y Inspector** — the built-in plugins, each a published `DevframeDefinition` mounted with `mountDevframe`
- **Rsbuild Demo Tool** / **Rsbuild Demo Tool B** — two trivial static SPAs that show the bare mount path

Selecting a tool loads its SPA in the stage. The bottom drawer mirrors the hub's **Commands**, **Messages**, and **Terminals** subsystems, plus a button that dispatches a command through `hub:commands:execute`.

The A11y Inspector shows a live axe-core report of this hub's own page. Rsbuild has no Vite `/@fs/`, so the host serves the plugin's in-page agent module same-origin from its own directory (`DevframeHost.mountStatic`) and attaches it as the a11y dock's `clientScript`; the hub client runtime — `createDevframeClientHost()` booted in `src/client/index.tsx` — imports it into the host page. Panel and agent share the Rsbuild origin their BroadcastChannel rides; hover a violation to ring the offending element in the hub UI.

The **RPC & State Inspector** carries an **Instances** tab that lists every devframe dev server running on your machine. The host registers itself in the shared registry (`~/.devframe/instances/`) on startup via `registerDevframeInstance()`, so it shows up as "this instance"; start another example (`pnpm --filter vite-devframe-hub dev`, or any `node bin.mjs` CLI example) in a second terminal and it appears there too, each linking to its own SPA. Its **Data** tab also exposes the live `RsbuildDevServer` and its normalized config as a data source.

## What the example proves

- `createHubContext()` boots a hub with no bundler-specific code path; a `DevframeHost` impl plugs framework specifics (static mounts, connection meta, storage, origin) in uniformly
- `mountDevframe(ctx, def)` registers any `DevframeDefinition` as a dock and serves both its SPA and its `__connection.json`, so the embedded SPA connects straight back to the hub
- Real integrations work end to end through the mount path — the inspector lists every plugin's RPC functions live, terminals stream over the hub, and code-server launches an authenticated editor
- The browser reads `devframe:docks` / `devframe:commands` shared state and dispatches commands over RPC — no hub classes imported on the client
- `createDevframeClientHost()` boots the hub's framework-level client runtime in the host page: it publishes the shared client context and imports each dock's `clientScript` (here, the a11y agent) so plugins run code in the page being inspected
- The opt-in [JSON-render](https://devframe.dev) hub integration renders through a mini **React** registry **shared verbatim with the Next hub example** — a React host rendering a server-authored spec with its own components (the "registry replacement" path) instead of the Vue reference frontend

## Build your own

The dock UI is React in `src/client/`. To skin your own viewer, read the same shared-state keys and render them however you like. The host file is the part worth copying verbatim.

## Files

| File | Role |
|---|---|
| `src/rsbuild-devframe-hub.ts` | The Rsbuild host — hub context, static + connection-meta mounts, side-car WS, instance-registry registration |
| `rsbuild.config.ts` | Mounts the built-in plugins via the host's `devframes` option; serves + attaches the a11y agent as its dock's `clientScript`; registers the JSON-render and tabbed-tool docks |
| `src/client/index.tsx` | Boots the React app and the hub client runtime |
| `src/client/Page.tsx` | The React UI that consumes the hub protocol |
| `src/client/icons.ts` | Offline Phosphor icons for the dock |
45 changes: 45 additions & 0 deletions examples/rsbuild-devframe-hub/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "rsbuild-devframe-hub",
"type": "module",
"version": "0.8.1",
"private": true,
"description": "Protocol-witness example — a tiny Rsbuild Devframe Hub built on @devframes/hub that exercises every hub subsystem end-to-end.",
"homepage": "https://github.com/devframes/devframe/tree/main/examples/rsbuild-devframe-hub",
"scripts": {
"dev": "pnpm -C ../.. run build && rsbuild dev",
"build": "rsbuild build",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@antfu/design": "catalog:frontend",
"@devframes/hub": "workspace:*",
"@devframes/json-render": "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:*",
"@json-render/react": "catalog:frontend",
"colorjs.io": "catalog:frontend",
"devframe": "workspace:*",
"dompurify": "catalog:frontend",
"json-render": "workspace:*",
"react": "catalog:frontend",
"react-dom": "catalog:frontend"
},
"devDependencies": {
"@iconify-json/ph": "catalog:frontend",
"@rsbuild/core": "catalog:build",
"@rsbuild/plugin-react": "catalog:build",
"@types/react": "catalog:types",
"@types/react-dom": "catalog:types",
"@unocss/postcss": "catalog:frontend",
"get-port-please": "catalog:deps",
"pathe": "catalog:deps",
"unocss": "catalog:frontend"
}
}
8 changes: 8 additions & 0 deletions examples/rsbuild-devframe-hub/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import UnoCSS from '@unocss/postcss'

// Rsbuild's PostCSS loader resolves plugins eagerly, so pass the plugin
// instance directly (the string form leaves the ESM namespace unresolved —
// "[object Module] is not a PostCSS plugin").
export default {
plugins: [UnoCSS()],
}
8 changes: 8 additions & 0 deletions examples/rsbuild-devframe-hub/public/data/notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Vite hub assets

These files live in the Vite hub's `public/` directory, served by Vite at
the site root (`/`). The **Assets** dock manages this same directory, so:

- previews load from the host's real URLs (`/logo.svg`, `/images/…`),
- editing, renaming, deleting, and uploading all round-trip to disk,
- the listing refreshes live as files change.
12 changes: 12 additions & 0 deletions examples/rsbuild-devframe-hub/public/data/site.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "Devframe Assets",
"description": "Sample fixtures for the assets plugin dev server.",
"theme": {
"primary": "#3a6a45",
"surface": "#ffffff"
},
"nav": [
{ "label": "Home", "href": "/" },
{ "label": "Docs", "href": "/docs" }
]
}
4 changes: 4 additions & 0 deletions examples/rsbuild-devframe-hub/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions examples/rsbuild-devframe-hub/public/images/banner.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions examples/rsbuild-devframe-hub/public/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions examples/rsbuild-devframe-hub/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
User-agent: *
Allow: /
11 changes: 11 additions & 0 deletions examples/rsbuild-devframe-hub/public/styles/theme.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
:root {
--color-primary: #3a6a45;
--color-surface: #ffffff;
--radius: 0.5rem;
}

.button {
background: var(--color-primary);
border-radius: var(--radius);
color: #fff;
}
122 changes: 122 additions & 0 deletions examples/rsbuild-devframe-hub/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { RsbuildPlugin } from '@rsbuild/core'
import { mountDevframe } from '@devframes/hub/node'
import { toJsonRenderDockEntry } from '@devframes/json-render/hub'
import a11yDevframe, { a11yAgentBundlePath } from '@devframes/plugin-a11y'
import assetsDevframe from '@devframes/plugin-assets'
import codeServerDevframe from '@devframes/plugin-code-server'
import dataInspectorDevframe from '@devframes/plugin-data-inspector'
import { registerDataSource } from '@devframes/plugin-data-inspector/registry'
import gitDevframe from '@devframes/plugin-git'
import inspectDevframe from '@devframes/plugin-inspect'
import messagesDevframe from '@devframes/plugin-messages'
import ogDevframe from '@devframes/plugin-og'
import terminalsDevframe from '@devframes/plugin-terminals'
import { defineConfig } from '@rsbuild/core'
import { pluginReact } from '@rsbuild/plugin-react'
import { createDashboardView } from 'json-render/dashboard'
import { basename, dirname } from 'pathe'
import { alias } from '../../alias'
import demoDevframe from './src/devframe'
import demoDevframeB from './src/devframe-b'
import { rsbuildDevframeHub } from './src/rsbuild-devframe-hub'
import tabbedToolDevframe from './src/tabbed-tool'

// The host registers the live Rsbuild dev server as a data-inspector source —
// the registry is process-global, so this works from any plugin hook. Mirrors
// the Vite host's `vite:server` source.
function dataSources(): RsbuildPlugin {
return {
name: 'rsbuild-devframe-hub:data-sources',
setup(api) {
api.onBeforeStartDevServer(({ server }) => {
registerDataSource({
id: 'rsbuild:server',
title: 'Rsbuild Dev Server',
description: 'The live RsbuildDevServer + normalized config serving this hub.',
icon: 'i-ph:lightning-duotone',
data: () => ({ port: server.port, config: api.getNormalizedConfig() }),
queries: [
{ title: 'Dev server port', query: 'port' },
{ title: 'Environment names', query: 'config.environments' },
{
title: 'Resolved config (clean)',
query: 'config',
excludeFunctions: true,
excludeUnderscoreProps: true,
},
],
})
})
},
}
}

export default defineConfig({
resolve: { alias },
source: {
entry: { index: './src/client/index.tsx' },
},
html: {
template: './src/client/index.html',
},
plugins: [
pluginReact(),
dataSources(),
rsbuildDevframeHub({
devframes: [
demoDevframe,
demoDevframeB,
// Every built-in plugin, dogfooded end-to-end through the hub mount
// path — the same set a full viewer like vite-devtools would surface.
gitDevframe,
terminalsDevframe,
codeServerDevframe,
inspectDevframe,
dataInspectorDevframe,
a11yDevframe,
messagesDevframe,
ogDevframe,
assetsDevframe,
],
// Attach the a11y inspector's in-page agent as its dock's client script.
// The hub client runtime (booted in src/client/index.tsx) imports it into
// this page so the docked panel scans the host live — no bespoke injection
// plugin needed. Rsbuild has no Vite `/@fs/`, so the host serves the built
// agent module same-origin from its own directory (see the host's
// `mountStatic`), and points the client script at that URL.
clientScripts: {
[a11yDevframe.id]: { importFrom: `/__rsbuild-a11y-agent/${basename(a11yAgentBundlePath)}` },
},
// Dogfood the opt-in JSON-render hub integration: author a view on the
// hub context and project it onto a `json-render` dock. The client host
// (src/client/index.tsx) renders it with a mini React registry shared with
// the Next hub example.
onContextReady: async (context) => {
// Serve the a11y agent module same-origin so the client script above
// resolves. `mountStatic` is the same host seam the mounted SPAs use.
context.host.mountStatic('/__rsbuild-a11y-agent/', dirname(a11yAgentBundlePath))

const view = createDashboardView(context)
context.docks.register(toJsonRenderDockEntry(view, {
id: 'example:json-render',
title: 'JSON Render',
icon: 'ph:layout-duotone',
category: 'app',
}))

// Shared-iframe soft-navigation demo. mountDevframe serves the SPA and
// registers its iframe dock; the `dock` override marks it a `subTabs`
// anchor (a shared `frameId` + the postmessage protocol) so the client
// host attaches the frame-nav adapter, materializing one client-only
// dock per tab the SPA's shim reports — all sharing this one iframe.
await mountDevframe(context, tabbedToolDevframe, {
dock: {
category: 'app',
frameId: 'rsbuild-tabbed-tool',
subTabs: { protocol: 'postmessage' },
},
})
},
}),
],
})
22 changes: 22 additions & 0 deletions examples/rsbuild-devframe-hub/spa/rsbuild-demo-tool-b/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rsbuild Demo Tool B</title>
<style>
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
body { margin: 0; padding: 2rem; }
h1 { margin-top: 0; }
code { font-family: ui-monospace, monospace; }
</style>
</head>
<body>
<h1>Rsbuild Demo Tool B</h1>
<p>Served from <code id="loc"></code></p>
<p>A second demo devframe, mounted alongside the first to demonstrate dock switching.</p>
<script>
document.getElementById('loc').textContent = location.pathname
</script>
</body>
</html>
22 changes: 22 additions & 0 deletions examples/rsbuild-devframe-hub/spa/rsbuild-demo-tool/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rsbuild Demo Tool</title>
<style>
:root { color-scheme: light dark; font-family: system-ui, sans-serif; }
body { margin: 0; padding: 2rem; }
h1 { margin-top: 0; }
code { font-family: ui-monospace, monospace; }
</style>
</head>
<body>
<h1>Rsbuild Demo Tool</h1>
<p>Served from <code id="loc"></code></p>
<p>This SPA is mounted by <code>rsbuild-devframe-hub</code> via <code>DevframeHost.mountStatic()</code>.</p>
<script>
document.getElementById('loc').textContent = location.pathname
</script>
</body>
</html>
Loading
Loading