From 8a34e606c858d036879394ac84960dddc2e3a15e Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Fri, 14 Aug 2026 06:45:05 +0000 Subject: [PATCH 1/4] feat(hub-ui): add a dev-served playground with HMR Mounts a bare, headless hub instance and hub-ui's own DockStandalone/ DockEmbedded straight from source, so the client Vue components hot-reload during development instead of requiring a build. Run with `pnpm --filter @devframes/hub-ui dev`. --- packages/hub-ui/package.json | 1 + packages/hub-ui/playground/hub-plugin.ts | 70 ++++++++++++++++++++++ packages/hub-ui/playground/index.html | 55 +++++++++++++++++ packages/hub-ui/playground/main.ts | 58 ++++++++++++++++++ packages/hub-ui/playground/seed.ts | 72 +++++++++++++++++++++++ packages/hub-ui/playground/vite.config.ts | 27 +++++++++ packages/hub-ui/tsconfig.json | 2 +- 7 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 packages/hub-ui/playground/hub-plugin.ts create mode 100644 packages/hub-ui/playground/index.html create mode 100644 packages/hub-ui/playground/main.ts create mode 100644 packages/hub-ui/playground/seed.ts create mode 100644 packages/hub-ui/playground/vite.config.ts diff --git a/packages/hub-ui/package.json b/packages/hub-ui/package.json index 97c7d15d..1ee1ac3f 100644 --- a/packages/hub-ui/package.json +++ b/packages/hub-ui/package.json @@ -34,6 +34,7 @@ "build:node": "tsdown", "build:embedded": "vite build --config vite.embedded.config.ts", "build:standalone": "vite build --config src/client/standalone/vite.config.ts", + "dev": "pnpm -C ../.. exec turbo run build --filter=@devframes/hub... && vite --config playground/vite.config.ts", "typecheck": "tsc --noEmit", "prepack": "pnpm build", "storybook": "storybook dev -p 6014", diff --git a/packages/hub-ui/playground/hub-plugin.ts b/packages/hub-ui/playground/hub-plugin.ts new file mode 100644 index 00000000..bf4ae102 --- /dev/null +++ b/packages/hub-ui/playground/hub-plugin.ts @@ -0,0 +1,70 @@ +import type { HubInstance } from '@devframes/hub/initiate' +import type { Plugin, ViteDevServer } from 'vite' +import { Server as NodeHttpServer } from 'node:http' +import { DEVFRAMES_HUB_BASE, initHub } from '@devframes/hub/initiate' +import { seedPlayground } from './seed' + +/** + * Mounts a bare, headless hub instance as Vite dev-server middleware — just + * enough backend for `main.ts`'s `DockStandalone`/`DockEmbedded` to connect + * to (RPC, WebSocket, `__connection.json`). No `devframes`, no `ui` slot, no + * renderer manifest: this playground is developing hub-ui itself, not + * exercising the wider hub protocol (`examples/hub-vite` already does that). + * + * A hand-rolled slice of `@devframes/vite/hub` rather than that package + * itself — pulling it in here would make `@devframes/hub-ui` and + * `@devframes/vite` depend on each other (`@devframes/vite` already carries + * an optional peer dependency on `@devframes/hub-ui` for its own default UI + * slot), a cyclic workspace dependency for no real benefit. + */ +export function hubUiPlaygroundHub(): Plugin { + let instance: HubInstance | undefined + + const teardown = async (): Promise => { + const previous = instance + instance = undefined + await previous?.close().catch(() => {}) + } + + return { + name: 'hub-ui-playground:hub', + apply: 'serve', + + async configureServer(server: ViteDevServer) { + // Vite re-invokes `configureServer` on each restart. + await teardown() + + const httpServer = server.httpServer instanceof NodeHttpServer ? server.httpServer : undefined + const hub = initHub({ + base: DEVFRAMES_HUB_BASE, + // Storage (if anything writes to it) lands under this package's own + // `node_modules`, not the playground folder. + cwd: new URL('..', import.meta.url).pathname, + origin: () => { + const resolved = server.resolvedUrls?.local?.[0] + return resolved ? new URL(resolved).origin : '' + }, + // Frictionless local loop — no interactive OTP gate. + auth: false, + // Share Vite's own HTTP server for the WS upgrade, like + // `@devframes/vite/hub` does. + server: httpServer, + ...(httpServer ? {} : { ws: { sidecar: true } }), + configure: seedPlayground, + }) + instance = hub + + server.middlewares.use(hub.nodeMiddleware) + + server.httpServer?.once('close', () => { + if (instance !== hub) + return + void teardown() + }) + }, + + async closeBundle() { + await teardown() + }, + } +} diff --git a/packages/hub-ui/playground/index.html b/packages/hub-ui/playground/index.html new file mode 100644 index 00000000..b584c783 --- /dev/null +++ b/packages/hub-ui/playground/index.html @@ -0,0 +1,55 @@ + + + + + + hub-ui playground + + + + +
+
+

hub-ui playground

+

+ A sample host page. Load with ?embedded to see the + floating dock (DockEmbedded) over this content instead + of the full-page standalone viewer (DockStandalone). +

+
+ + + diff --git a/packages/hub-ui/playground/main.ts b/packages/hub-ui/playground/main.ts new file mode 100644 index 00000000..24db58dc --- /dev/null +++ b/packages/hub-ui/playground/main.ts @@ -0,0 +1,58 @@ +import type { DockPanelStorage } from '@devframes/hub/client' +import { getDevframeRpcClient, setDevframeClientContext } from '@devframes/hub/client' +import { useLocalStorage } from '@vueuse/core' +import { watchEffect } from 'vue' +import { isDark } from '../src/client/state/color-mode' +import { DEFAULT_DOCK_PANEL_STORE } from '../src/client/state/docks' + +/** + * The base `hub-plugin.ts` mounts the playground's hub instance at. Kept as + * an explicit constant (rather than inferred from the page's own URL, like + * the production `standalone`/`embedded` entries do) because this playground + * page is served from Vite's own root (`/`), not colocated with the hub the + * way a built `createUi()` viewer is. + */ +const HUB_BASE = '/__devframes/' + +/** + * `?embedded` mounts the floating `DockEmbedded` bootstrap over the sample + * host content in `index.html` — the same surface a host page gets from + * `