From 87dac4e28a61339f1343a9d2bddaf6a7c133917c Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Thu, 13 Aug 2026 10:29:21 +0000 Subject: [PATCH 1/5] feat: extract viteDevBridge into a standalone @devframes/vite package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move packages/devframe/src/helpers/vite.ts into its own package, @devframes/vite, mirroring the existing @devframes/next/@devframes/nuxt integration-package pattern. Being a separate package, it can now depend on vite directly — viteDevBridge's return type and configureServer hook are typed against vite's real Plugin/ViteDevServer surface instead of a hand-rolled duck-typed interface, while devframe core stays free of any vite dependency. - devframe/internal gains diagnostics/resolveBasePath/normalizeBasePath re-exports so the new package reuses core's DF0033 diagnostic and mount-base logic instead of duplicating it. - The 8 built-in plugins with a vite.ts (inspect, messages, terminals, code-server, og, assets, data-inspector, a11y) and @devframes/nuxt now import viteDevBridge from @devframes/vite. - Updates turbo.json's build graph, alias.ts/tsconfig.base.json, vitest.config.ts projects, knip.jsonc, docs, and the tsnapi API snapshots accordingly. This PR was created with the help of an agent. --- alias.ts | 2 +- docs/errors/DF0033.md | 4 +- docs/helpers/index.md | 2 +- docs/helpers/vite-bridge.md | 2 +- knip.jsonc | 1 - packages/devframe/package.json | 1 - .../devframe/scripts/check-client-dist.ts | 1 - packages/devframe/src/internal/index.ts | 8 + packages/devframe/tsdown.config.ts | 1 - packages/nuxt/package.json | 3 + packages/nuxt/src/module.ts | 2 +- packages/vite/package.json | 57 ++++++ .../src/helpers/vite.ts => vite/src/index.ts} | 51 +++-- .../vite.test.ts => vite/test/index.test.ts} | 6 +- packages/vite/tsconfig.json | 9 + packages/vite/tsdown.config.ts | 26 +++ plugins/a11y/package.json | 1 + plugins/a11y/src/vite.ts | 4 +- plugins/assets/package.json | 1 + plugins/assets/src/vite.ts | 4 +- plugins/code-server/package.json | 1 + plugins/code-server/src/spa/vite.config.ts | 2 +- plugins/code-server/src/vite.ts | 4 +- plugins/data-inspector/package.json | 1 + plugins/data-inspector/src/vite.ts | 4 +- plugins/inspect/package.json | 1 + plugins/inspect/src/vite.ts | 4 +- plugins/messages/package.json | 1 + plugins/messages/src/spa/vite.config.ts | 2 +- plugins/messages/src/vite.ts | 4 +- plugins/og/package.json | 1 + plugins/og/src/vite.ts | 4 +- plugins/terminals/package.json | 1 + plugins/terminals/src/vite.ts | 4 +- pnpm-lock.yaml | 94 +++++++-- skills/devframe/SKILL.md | 2 +- .../vite/index.snapshot.d.ts} | 16 +- .../vite/index.snapshot.js} | 2 +- .../tsnapi/devframe/internal.snapshot.d.ts | 192 ++++++++++++++++++ .../tsnapi/devframe/internal.snapshot.js | 3 + tsconfig.base.json | 6 +- turbo.json | 22 +- vitest.config.ts | 1 + 43 files changed, 466 insertions(+), 92 deletions(-) create mode 100644 packages/vite/package.json rename packages/{devframe/src/helpers/vite.ts => vite/src/index.ts} (78%) rename packages/{devframe/src/helpers/__tests__/vite.test.ts => vite/test/index.test.ts} (97%) create mode 100644 packages/vite/tsconfig.json create mode 100644 packages/vite/tsdown.config.ts rename tests/__snapshots__/tsnapi/{devframe/helpers/vite.snapshot.d.ts => @devframes/vite/index.snapshot.d.ts} (70%) rename tests/__snapshots__/tsnapi/{devframe/helpers/vite.snapshot.js => @devframes/vite/index.snapshot.js} (52%) diff --git a/alias.ts b/alias.ts index 4b33ee2f..94a6caa5 100644 --- a/alias.ts +++ b/alias.ts @@ -39,7 +39,6 @@ export const alias = { 'devframe/adapters/cac': r('devframe/src/adapters/cac.ts'), 'devframe/adapters/dev': r('devframe/src/adapters/dev.ts'), 'devframe/adapters/build': r('devframe/src/adapters/build.ts'), - 'devframe/helpers/vite': r('devframe/src/helpers/vite.ts'), 'devframe/adapters/embedded': r('devframe/src/adapters/embedded.ts'), 'devframe/initiate': r('devframe/src/adapters/initiate.ts'), 'devframe/adapters/mcp': r('devframe/src/adapters/mcp/index.ts'), @@ -54,6 +53,7 @@ export const alias = { '@devframes/nuxt': r('nuxt/src/index.ts'), '@devframes/next/client': r('next/src/client.tsx'), '@devframes/next': r('next/src/index.ts'), + '@devframes/vite': r('vite/src/index.ts'), '@devframes/json-render/core': r('json-render/src/core.ts'), '@devframes/json-render/hub': r('json-render/src/hub.ts'), '@devframes/json-render/node': r('json-render/src/node/index.ts'), diff --git a/docs/errors/DF0033.md b/docs/errors/DF0033.md index 865363a0..48bd0057 100644 --- a/docs/errors/DF0033.md +++ b/docs/errors/DF0033.md @@ -10,7 +10,7 @@ outline: deep ## Cause -`viteDevBridge({ devMiddleware })` could not bring up the bridge dev server that pairs a host-served SPA (Vite, Nuxt, Astro, etc.) with devframe's RPC backend. Common reasons: +`viteDevBridge({ devMiddleware })` (from `@devframes/vite`) could not bring up the bridge dev server that pairs a host-served SPA (Vite, Nuxt, Astro, etc.) with devframe's RPC backend. Common reasons: - The preferred port is in use and no fallback range was configured. - Calling `def.setup(ctx)` threw — the devframe's own setup logic surfaced an error. @@ -26,4 +26,4 @@ This is a soft warning — the surrounding Vite dev server keeps running, but th ## Source -- [`packages/devframe/src/helpers/vite.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/helpers/vite.ts) — `viteDevBridge({ devMiddleware })` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`. +- [`packages/vite/src/index.ts`](https://github.com/devframes/devframe/blob/main/packages/vite/src/index.ts) — `viteDevBridge({ devMiddleware })` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`. diff --git a/docs/helpers/index.md b/docs/helpers/index.md index 90a967b0..0ab49654 100644 --- a/docs/helpers/index.md +++ b/docs/helpers/index.md @@ -9,7 +9,7 @@ Helpers are the optional, opt-in surface around the core `defineDevframe` API: s | Helper | Entry | What it does | |--------|-------|--------------| | [Utilities](./utilities) | `devframe/utils/*` | Bundled small utilities — terminal colors, hashing, editor launch, structured-clone serialization, and more. | -| [Vite Bridge](./vite-bridge) | `devframe/helpers/vite` | Vite plugin for mounting a devframe inside any Vite-based host (Astro, SolidStart, plain Vite). | +| [Vite Bridge](./vite-bridge) | `@devframes/vite` | Vite plugin for mounting a devframe inside any Vite-based host (Astro, SolidStart, plain Vite). | | [Nuxt Module](./nuxt) | `@devframes/nuxt` | Nuxt module that wires a Nuxt SPA as a devframe client and serves the dev-time RPC bridge. | | [Next Helper](./next) | `@devframes/next` | Route-handler host + React client for mounting devframes inside a Next.js App Router app (experimental). | | [Common RPC Functions](./common-rpc-functions) | `devframe/recipes/common-rpc-functions` | Prebuilt RPC actions for "open in editor" and "reveal in Finder". | diff --git a/docs/helpers/vite-bridge.md b/docs/helpers/vite-bridge.md index 87b0aae5..99715e19 100644 --- a/docs/helpers/vite-bridge.md +++ b/docs/helpers/vite-bridge.md @@ -9,7 +9,7 @@ A thin Vite plugin for mounting a devframe inside an existing Vite dev server. U This sits below the [`vite` adapter](/adapters/vite) on the abstraction ladder: the adapter targets the full Vite DevTools dock; the bridge is the lower-level Vite plugin you reach for when you want a devframe to ride along with an existing app's dev server without the DevTools dock. ```ts -import { viteDevBridge } from 'devframe/helpers/vite' +import { viteDevBridge } from '@devframes/vite' import { defineConfig } from 'vite' import devframe from './devframe' diff --git a/knip.jsonc b/knip.jsonc index c036adf2..8c8e7573 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -90,7 +90,6 @@ // listed explicitly instead. Keep this in sync with the `exports` map. "entry": [ "src/{index,constants}.ts", - "src/helpers/vite.ts", "src/adapters/{build,cac,dev,embedded,initiate}.ts", "src/adapters/mcp/index.ts", "src/client/index.ts", diff --git a/packages/devframe/package.json b/packages/devframe/package.json index c8de7ddb..1c5203a2 100644 --- a/packages/devframe/package.json +++ b/packages/devframe/package.json @@ -27,7 +27,6 @@ "./adapters/mcp": "./dist/adapters/mcp.mjs", "./client": "./dist/client/index.mjs", "./constants": "./dist/constants.mjs", - "./helpers/vite": "./dist/helpers/vite.mjs", "./initiate": "./dist/adapters/initiate.mjs", "./internal": "./dist/internal/index.mjs", "./node": "./dist/node/index.mjs", diff --git a/packages/devframe/scripts/check-client-dist.ts b/packages/devframe/scripts/check-client-dist.ts index 61f7bbe7..917f88a9 100644 --- a/packages/devframe/scripts/check-client-dist.ts +++ b/packages/devframe/scripts/check-client-dist.ts @@ -14,7 +14,6 @@ const FORBIDDEN: ForbiddenRule[] = [ { name: 'devframe/rpc/transports/*', match: id => id.startsWith('devframe/rpc/transports/') }, { name: 'devframe/node*', match: id => id === 'devframe/node' || id.startsWith('devframe/node/') }, { name: 'devframe/adapters/*', match: id => id.startsWith('devframe/adapters/') }, - { name: 'devframe/helpers/*', match: id => id.startsWith('devframe/helpers/') }, { name: 'devframe/recipes/*', match: id => id.startsWith('devframe/recipes/') }, { name: 'devframe/utils/launch-editor', match: id => id === 'devframe/utils/launch-editor' }, { name: 'devframe/utils/open', match: id => id === 'devframe/utils/open' }, diff --git a/packages/devframe/src/internal/index.ts b/packages/devframe/src/internal/index.ts index d2522846..bdcf5f5a 100644 --- a/packages/devframe/src/internal/index.ts +++ b/packages/devframe/src/internal/index.ts @@ -26,8 +26,16 @@ // transport"), the fetch/connect handler pair, and teardown. `StartedServer` // is the live handle its bound tiers produce and `createDevServer` re-exposes. // - `normalizeHttpServerUrl` — a small host-side URL helper. +// - `resolveBasePath` / `normalizeBasePath` — the mount-base resolution +// `initDevframe` itself uses; a bridge (`@devframes/vite`) that mounts a +// devframe onto a host it doesn't own reuses the exact same defaulting. +// - `diagnostics` — devframe core's structured diagnostics instance +// (`DF00xx`), so a first-party integration built outside this package can +// report against the same registered codes instead of minting its own. +export { normalizeBasePath, resolveBasePath } from '../adapters/_shared' export { coerceAgentPositionalArgs } from '../node/agent-args' export type { AgentArgsFallback } from '../node/agent-args' +export { diagnostics } from '../node/diagnostics' export { DevframeAgentHost } from '../node/host-agent' export * from '../node/host-h3' export { listLiveDevframeInstances, registerDevframeInstance } from '../node/instance-registry' diff --git a/packages/devframe/tsdown.config.ts b/packages/devframe/tsdown.config.ts index 5dce3ab4..45329bae 100644 --- a/packages/devframe/tsdown.config.ts +++ b/packages/devframe/tsdown.config.ts @@ -110,7 +110,6 @@ const serverEntries = { 'adapters/initiate': 'src/adapters/initiate.ts', 'adapters/mcp': 'src/adapters/mcp/index.ts', 'cli/main': 'src/cli/main.ts', - 'helpers/vite': 'src/helpers/vite.ts', 'recipes/common-rpc-functions': 'src/recipes/common-rpc-functions.ts', 'recipes/interactive-auth': 'src/recipes/interactive-auth.ts', } diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 42e07ec3..fcdfcf96 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -39,6 +39,9 @@ "@nuxt/kit": "^3.0.0 || ^4.0.0 || ^5.0.0-0", "devframe": "workspace:*" }, + "dependencies": { + "@devframes/vite": "workspace:*" + }, "devDependencies": { "@nuxt/kit": "catalog:build", "@types/node": "catalog:types", diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 8315d588..0d0e1507 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -1,6 +1,6 @@ import type { DevframeDefinition } from 'devframe' +import { viteDevBridge } from '@devframes/vite' import { addPlugin, addVitePlugin, createResolver, defineNuxtModule } from '@nuxt/kit' -import { viteDevBridge } from 'devframe/helpers/vite' export interface DevframeNuxtModuleOptions { /** diff --git a/packages/vite/package.json b/packages/vite/package.json new file mode 100644 index 00000000..f327c1c0 --- /dev/null +++ b/packages/vite/package.json @@ -0,0 +1,57 @@ +{ + "name": "@devframes/vite", + "type": "module", + "version": "0.9.0-beta.1", + "description": "Vite plugin for mounting a devframe inside any Vite-based host (Astro, SolidStart, Nuxt, plain Vite apps).", + "author": "Anthony Fu ", + "license": "MIT", + "homepage": "https://github.com/devframes/devframe#readme", + "repository": { + "directory": "packages/vite", + "type": "git", + "url": "git+https://github.com/devframes/devframe.git" + }, + "bugs": "https://github.com/devframes/devframe/issues", + "keywords": [ + "devframe", + "vite", + "devtools" + ], + "sideEffects": false, + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, + "types": "./dist/index.d.mts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsdown", + "watch": "tsdown --watch", + "typecheck": "tsc --noEmit", + "prepack": "pnpm run build", + "test": "vitest run" + }, + "peerDependencies": { + "devframe": "workspace:*", + "vite": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + }, + "dependencies": { + "pathe": "catalog:deps" + }, + "devDependencies": { + "@modelcontextprotocol/client": "catalog:deps", + "@types/node": "catalog:types", + "devframe": "workspace:*", + "get-port-please": "catalog:deps", + "tsdown": "catalog:build", + "vite": "catalog:build", + "vitest": "catalog:testing" + } +} diff --git a/packages/devframe/src/helpers/vite.ts b/packages/vite/src/index.ts similarity index 78% rename from packages/devframe/src/helpers/vite.ts rename to packages/vite/src/index.ts index c3937d93..4571f229 100644 --- a/packages/devframe/src/helpers/vite.ts +++ b/packages/vite/src/index.ts @@ -1,12 +1,12 @@ +import type { DevframeDefinition, McpRouteOptions } from 'devframe' +import type { DevframeInstance } from 'devframe/initiate' +import type { DevframeAuthHandler } from 'devframe/node/auth' import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http' -import type { DevframeInstance } from '../adapters/initiate' -import type { DevframeAuthHandler } from '../node/auth/handler' -import type { DevframeDefinition, McpRouteOptions } from '../types/devframe' +import type { Plugin } from 'vite' +import { initDevframe } from 'devframe/initiate' +import { diagnostics, normalizeBasePath, resolveBasePath } from 'devframe/internal' import { serveStaticNodeMiddleware } from 'devframe/utils/serve-static' import { resolve } from 'pathe' -import { normalizeBasePath, resolveBasePath } from '../adapters/_shared' -import { initDevframe } from '../adapters/initiate' -import { diagnostics } from '../node/diagnostics' export interface ViteDevBridgeOptions { /** @@ -70,21 +70,31 @@ export interface ViteDevBridgeOptions { mcp?: boolean | McpRouteOptions } -/** The slice of a Vite dev server the bridge plugin touches. */ +/** + * The slice of a Vite dev server the bridge plugin touches — deliberately + * narrower than Vite's real `ViteDevServer` (which carries the module + * graph, watcher, transform pipeline, …) so a host can hand the bridge + * anything shaped like this, and a test double only needs to fake two + * fields. A real `ViteDevServer` satisfies this structurally, so + * `viteDevBridge`'s returned `configureServer(server: ViteDevServer)` hook + * (typed against the real `Plugin` below) is still fully type-safe. + */ export interface DevframeViteDevServerLike { middlewares: { use: ((path: string, handler: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void) => void) & ((handler: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void) => void) } - httpServer?: NodeHttpServer | null + /** + * Deliberately structural (just the one event the bridge listens for) + * rather than `NodeHttpServer` — Vite's real `ViteDevServer.httpServer` + * is `http.Server | Http2SecureServer | null`, and `Http2SecureServer` + * doesn't satisfy `http.Server`'s full shape. + */ + httpServer?: { once: (event: 'close', listener: () => void) => unknown } | null } -export interface DevframeVitePlugin { - name: string - apply: 'serve' - configureServer: (server: DevframeViteDevServerLike) => void | Promise - closeBundle?: () => void | Promise -} +/** A `viteDevBridge` plugin — a real Vite `Plugin`, scoped to its `serve`-only hooks. */ +export type DevframeVitePlugin = Plugin /** * Bridge a devframe into an existing Vite dev server. Returns a Vite @@ -108,7 +118,8 @@ export interface DevframeVitePlugin { * * Use bridge mode when integrating with frameworks that own the SPA * (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one - * `dev` / `build` / `mcp` shell, reach for {@link createCac} instead. + * `dev` / `build` / `mcp` shell, reach for `createCac` (`devframe/adapters/cac`) + * instead. */ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptions = {}): DevframeVitePlugin { const base = normalizeMountBase(options.base ?? resolveBasePath(d, 'hosted')) @@ -118,7 +129,7 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio return { name: `devframe:${d.id}`, apply: 'serve', - configureServer(server) { + configureServer(server: DevframeViteDevServerLike) { if (!distDir) return server.middlewares.use(base, serveStaticNodeMiddleware(resolve(distDir))) @@ -132,7 +143,7 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio return { name: `devframe:${d.id}`, apply: 'serve', - async configureServer(server) { + async configureServer(server: DevframeViteDevServerLike) { // Vite re-invokes `configureServer` on each restart cycle; close // the prior handle so we don't leak the WS transport. Silent catch — // a stale handle's close failure shouldn't block a fresh start. @@ -153,7 +164,11 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio ...(mw.port != null ? { ws: { port: mw.port } } : server.httpServer - ? { server: server.httpServer } + // `initDevframe`'s `server` option shares a real + // `node:http` server's WS upgrade listener — Vite's dev + // server is always one in practice (never the HTTP/2 + // variant `ViteDevServer['httpServer']` also allows for). + ? { server: server.httpServer as NodeHttpServer } : { ws: { sidecar: true } }), // Gate by default: an unset `auth` defers to the handler // (devframe's interactive OTP unless `cli.auth` opts out) rather diff --git a/packages/devframe/src/helpers/__tests__/vite.test.ts b/packages/vite/test/index.test.ts similarity index 97% rename from packages/devframe/src/helpers/__tests__/vite.test.ts rename to packages/vite/test/index.test.ts index fb040fe9..ba9a5e92 100644 --- a/packages/devframe/src/helpers/__tests__/vite.test.ts +++ b/packages/vite/test/index.test.ts @@ -1,13 +1,13 @@ +import type { DevframeDefinition } from 'devframe' import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http' -import type { DevframeDefinition } from '../../types/devframe' -import type { DevframeViteDevServerLike } from '../vite' +import type { DevframeViteDevServerLike } from '../src/index' import { createServer } from 'node:http' import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client' import { createRpcClient } from 'devframe/rpc/client' import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' import { getPort } from 'get-port-please' import { afterEach, describe, expect, it } from 'vitest' -import { viteDevBridge } from '../vite' +import { viteDevBridge } from '../src/index' function defineTestDef(): DevframeDefinition { return { diff --git a/packages/vite/tsconfig.json b/packages/vite/tsconfig.json new file mode 100644 index 00000000..b7803680 --- /dev/null +++ b/packages/vite/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "lib": ["esnext"], + "types": ["node"] + }, + "include": ["src", "tsdown.config.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/vite/tsdown.config.ts b/packages/vite/tsdown.config.ts new file mode 100644 index 00000000..23fc3af4 --- /dev/null +++ b/packages/vite/tsdown.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'tsdown' + +const tsconfig = '../../tsconfig.base.json' + +export default defineConfig({ + entry: { index: 'src/index.ts' }, + platform: 'node', + tsconfig, + clean: true, + dts: true, + outExtensions: () => ({ dts: '.d.mts' }), + // `vite`'s own type graph re-exports `esbuild`/`postcss`/`rolldown` types + // in a way that trips up rolldown's dts bundler (dozens of + // MISSING_EXPORT errors) — keep it external and let consumers resolve + // `Plugin`/`ViteDevServer` from their own installed `vite`. + deps: { + neverBundle: [ + 'vite', + 'esbuild', + 'postcss', + 'rolldown', + /^@rolldown\//, + /^@oxc-project\//, + ], + }, +}) diff --git a/plugins/a11y/package.json b/plugins/a11y/package.json index 7f1bbd18..06c60270 100644 --- a/plugins/a11y/package.json +++ b/plugins/a11y/package.json @@ -62,6 +62,7 @@ } }, "dependencies": { + "@devframes/vite": "workspace:*", "axe-core": "catalog:frontend", "cac": "catalog:deps", "solid-js": "catalog:frontend" diff --git a/plugins/a11y/src/vite.ts b/plugins/a11y/src/vite.ts index 421bad0e..0b8f19d9 100644 --- a/plugins/a11y/src/vite.ts +++ b/plugins/a11y/src/vite.ts @@ -1,5 +1,5 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite' -import { viteDevBridge } from 'devframe/helpers/vite' +import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' +import { viteDevBridge } from '@devframes/vite' import a11yDevframe from './index.ts' export type { ViteDevBridgeOptions } diff --git a/plugins/assets/package.json b/plugins/assets/package.json index 135d0160..4b4809f0 100644 --- a/plugins/assets/package.json +++ b/plugins/assets/package.json @@ -57,6 +57,7 @@ } }, "dependencies": { + "@devframes/vite": "workspace:*", "cac": "catalog:deps", "chokidar": "catalog:deps", "image-meta": "catalog:deps", diff --git a/plugins/assets/src/vite.ts b/plugins/assets/src/vite.ts index 0a1545b5..0921edb6 100644 --- a/plugins/assets/src/vite.ts +++ b/plugins/assets/src/vite.ts @@ -1,5 +1,5 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite' -import { viteDevBridge } from 'devframe/helpers/vite' +import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' +import { viteDevBridge } from '@devframes/vite' import assetsDevframe from './index' export type { ViteDevBridgeOptions } diff --git a/plugins/code-server/package.json b/plugins/code-server/package.json index de630bf2..27bff7a1 100644 --- a/plugins/code-server/package.json +++ b/plugins/code-server/package.json @@ -59,6 +59,7 @@ } }, "dependencies": { + "@devframes/vite": "workspace:*", "cac": "catalog:deps", "get-port-please": "catalog:deps", "nostics": "catalog:deps" diff --git a/plugins/code-server/src/spa/vite.config.ts b/plugins/code-server/src/spa/vite.config.ts index e47afa6b..7dcbfeec 100644 --- a/plugins/code-server/src/spa/vite.config.ts +++ b/plugins/code-server/src/spa/vite.config.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url' +import { viteDevBridge } from '@devframes/vite' import vue from '@vitejs/plugin-vue' -import { viteDevBridge } from 'devframe/helpers/vite' import UnoCSS from 'unocss/vite' import { defineConfig } from 'vite' import { alias } from '../../../../alias' diff --git a/plugins/code-server/src/vite.ts b/plugins/code-server/src/vite.ts index 0d85b302..ffa56bc3 100644 --- a/plugins/code-server/src/vite.ts +++ b/plugins/code-server/src/vite.ts @@ -1,6 +1,6 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite' +import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' import type { CodeServerOptions } from './types' -import { viteDevBridge } from 'devframe/helpers/vite' +import { viteDevBridge } from '@devframes/vite' import { createCodeServerDevframe } from './index' export interface CodeServerViteOptions extends CodeServerOptions { diff --git a/plugins/data-inspector/package.json b/plugins/data-inspector/package.json index 66e1dbfe..43bbba6d 100644 --- a/plugins/data-inspector/package.json +++ b/plugins/data-inspector/package.json @@ -59,6 +59,7 @@ } }, "dependencies": { + "@devframes/vite": "workspace:*", "cac": "catalog:deps", "get-port-please": "catalog:deps", "jora": "catalog:deps", diff --git a/plugins/data-inspector/src/vite.ts b/plugins/data-inspector/src/vite.ts index ad0d13ac..c7962637 100644 --- a/plugins/data-inspector/src/vite.ts +++ b/plugins/data-inspector/src/vite.ts @@ -1,5 +1,5 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite' -import { viteDevBridge } from 'devframe/helpers/vite' +import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' +import { viteDevBridge } from '@devframes/vite' import dataInspectorDevframe from './index' export type { ViteDevBridgeOptions } diff --git a/plugins/inspect/package.json b/plugins/inspect/package.json index 21d44e2e..8e119849 100644 --- a/plugins/inspect/package.json +++ b/plugins/inspect/package.json @@ -56,6 +56,7 @@ } }, "dependencies": { + "@devframes/vite": "workspace:*", "cac": "catalog:deps", "nostics": "catalog:deps" }, diff --git a/plugins/inspect/src/vite.ts b/plugins/inspect/src/vite.ts index 9a25e2a2..0a1c5e52 100644 --- a/plugins/inspect/src/vite.ts +++ b/plugins/inspect/src/vite.ts @@ -1,5 +1,5 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite' -import { viteDevBridge } from 'devframe/helpers/vite' +import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' +import { viteDevBridge } from '@devframes/vite' import inspectDevframe from './index' export type { ViteDevBridgeOptions } diff --git a/plugins/messages/package.json b/plugins/messages/package.json index a4a72459..474e2362 100644 --- a/plugins/messages/package.json +++ b/plugins/messages/package.json @@ -60,6 +60,7 @@ } }, "dependencies": { + "@devframes/vite": "workspace:*", "cac": "catalog:deps", "nostics": "catalog:deps" }, diff --git a/plugins/messages/src/spa/vite.config.ts b/plugins/messages/src/spa/vite.config.ts index 81bb681b..c4097f54 100644 --- a/plugins/messages/src/spa/vite.config.ts +++ b/plugins/messages/src/spa/vite.config.ts @@ -1,6 +1,6 @@ import { fileURLToPath } from 'node:url' +import { viteDevBridge } from '@devframes/vite' import vue from '@vitejs/plugin-vue' -import { viteDevBridge } from 'devframe/helpers/vite' import UnoCSS from 'unocss/vite' import { defineConfig } from 'vite' import { alias } from '../../../../alias' diff --git a/plugins/messages/src/vite.ts b/plugins/messages/src/vite.ts index 873feab3..1300f05c 100644 --- a/plugins/messages/src/vite.ts +++ b/plugins/messages/src/vite.ts @@ -1,5 +1,5 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite' -import { viteDevBridge } from 'devframe/helpers/vite' +import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' +import { viteDevBridge } from '@devframes/vite' import messagesDevframe from './index' export type { ViteDevBridgeOptions } diff --git a/plugins/og/package.json b/plugins/og/package.json index 64a483df..ccb8ffad 100644 --- a/plugins/og/package.json +++ b/plugins/og/package.json @@ -58,6 +58,7 @@ } }, "dependencies": { + "@devframes/vite": "workspace:*", "cac": "catalog:deps", "nostics": "catalog:deps", "parse5": "catalog:deps" diff --git a/plugins/og/src/vite.ts b/plugins/og/src/vite.ts index e609509c..5bb73c1f 100644 --- a/plugins/og/src/vite.ts +++ b/plugins/og/src/vite.ts @@ -1,5 +1,5 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite' -import { viteDevBridge } from 'devframe/helpers/vite' +import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' +import { viteDevBridge } from '@devframes/vite' import ogDevframe from './index' export type { ViteDevBridgeOptions } diff --git a/plugins/terminals/package.json b/plugins/terminals/package.json index 36571f3a..fda5ebde 100644 --- a/plugins/terminals/package.json +++ b/plugins/terminals/package.json @@ -59,6 +59,7 @@ } }, "dependencies": { + "@devframes/vite": "workspace:*", "cac": "catalog:deps", "nostics": "catalog:deps", "zigpty": "catalog:deps" diff --git a/plugins/terminals/src/vite.ts b/plugins/terminals/src/vite.ts index 76664c87..c31d58cf 100644 --- a/plugins/terminals/src/vite.ts +++ b/plugins/terminals/src/vite.ts @@ -1,6 +1,6 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from 'devframe/helpers/vite' +import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' import type { TerminalsOptions } from './types' -import { viteDevBridge } from 'devframe/helpers/vite' +import { viteDevBridge } from '@devframes/vite' import { createTerminalsDevframe } from './index' export interface TerminalsViteOptions extends TerminalsOptions { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e5d2958..06e45977 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1427,6 +1427,10 @@ importers: version: 0.22.14(@volar/typescript@2.4.28(typescript@6.0.3))(oxc-resolver@11.24.2)(tsx@4.23.12)(typescript@6.0.3) packages/nuxt: + dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../vite devDependencies: '@nuxt/kit': specifier: catalog:build @@ -1444,8 +1448,39 @@ importers: specifier: catalog:build version: 0.22.14(@volar/typescript@2.4.28(typescript@6.0.3))(oxc-resolver@11.24.2)(tsx@4.23.12)(typescript@6.0.3) + packages/vite: + dependencies: + pathe: + specifier: catalog:deps + version: 2.0.3 + devDependencies: + '@modelcontextprotocol/client': + specifier: catalog:deps + version: 2.0.0 + '@types/node': + specifier: catalog:types + version: 26.2.0 + devframe: + specifier: workspace:* + version: link:../devframe + get-port-please: + specifier: catalog:deps + version: 3.2.0 + tsdown: + specifier: catalog:build + version: 0.22.14(@volar/typescript@2.4.28(typescript@6.0.3))(oxc-resolver@11.24.2)(tsx@4.23.12)(typescript@6.0.3) + vite: + specifier: catalog:build + version: 8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) + vitest: + specifier: catalog:testing + version: 4.1.10(@types/node@26.2.0)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)) + plugins/a11y: dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../../packages/vite axe-core: specifier: catalog:frontend version: 4.13.0 @@ -1498,6 +1533,9 @@ importers: plugins/assets: dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../../packages/vite cac: specifier: catalog:deps version: 7.0.0 @@ -1583,6 +1621,9 @@ importers: plugins/code-server: dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../../packages/vite cac: specifier: catalog:deps version: 7.0.0 @@ -1638,6 +1679,9 @@ importers: plugins/data-inspector: dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../../packages/vite cac: specifier: catalog:deps version: 7.0.0 @@ -1802,6 +1846,9 @@ importers: plugins/inspect: dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../../packages/vite cac: specifier: catalog:deps version: 7.0.0 @@ -1875,6 +1922,9 @@ importers: plugins/messages: dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../../packages/vite cac: specifier: catalog:deps version: 7.0.0 @@ -1948,6 +1998,9 @@ importers: plugins/og: dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../../packages/vite cac: specifier: catalog:deps version: 7.0.0 @@ -2009,6 +2062,9 @@ importers: plugins/terminals: dependencies: + '@devframes/vite': + specifier: workspace:* + version: link:../../packages/vite cac: specifier: catalog:deps version: 7.0.0 @@ -12105,8 +12161,8 @@ snapshots: '@nuxt/vite-builder@4.5.2(f6d473b51fb6fe0df9e3fd2c0f41e758)': dependencies: '@nuxt/kit': 4.5.2(magic-string@1.1.1)(magicast@0.5.2)(oxc-parser@0.143.0)(rolldown@1.2.3)(unplugin@3.3.0(@rspack/core@2.1.9(@swc/helpers@0.5.23))(esbuild@0.28.0)(rolldown@1.2.3)(rollup@4.60.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))) - '@vitejs/plugin-vue': 6.0.8(vite@8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) - '@vitejs/plugin-vue-jsx': 5.1.6(supports-color@10.2.2)(vite@8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) + '@vitejs/plugin-vue': 6.0.8(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) + '@vitejs/plugin-vue-jsx': 5.1.6(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3)) autoprefixer: 10.5.4(postcss@8.5.26) consola: 3.4.2 cssnano: 8.0.2(postcss@8.5.26) @@ -12130,10 +12186,10 @@ snapshots: std-env: 4.2.0 ufo: 1.6.4 unenv: 2.0.0-rc.24 - unplugin: 3.3.0(@rspack/core@2.1.9(@swc/helpers@0.5.23))(esbuild@0.28.0)(rolldown@1.2.3)(rollup@4.60.3)(vite@8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)) - vite: 8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) + unplugin: 3.3.0(@rspack/core@2.1.9(@swc/helpers@0.5.23))(esbuild@0.28.0)(rolldown@1.2.3)(rollup@4.60.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) vite-node: 6.0.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) - vite-plugin-checker: 0.14.5(eslint@10.8.1(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)) + vite-plugin-checker: 0.14.5(eslint@10.8.1(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)) vue: 3.5.40(typescript@6.0.3) vue-bundle-renderer: 2.3.1 optionalDependencies: @@ -14034,14 +14090,14 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.47 vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) - '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.2.2)(vite@8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3))': + '@vitejs/plugin-vue-jsx@5.1.6(supports-color@10.2.2)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3))': dependencies: '@babel/core': 7.29.0(supports-color@10.2.2) '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.0(supports-color@10.2.2)) '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.0(supports-color@10.2.2))(supports-color@10.2.2) '@rolldown/pluginutils': 1.0.1 '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0(supports-color@10.2.2))(supports-color@10.2.2) - vite: 8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) vue: 3.5.40(typescript@6.0.3) transitivePeerDependencies: - supports-color @@ -14052,6 +14108,12 @@ snapshots: vite: 8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) vue: 3.5.40(typescript@6.0.3) + '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.40(typescript@6.0.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) + vue: 3.5.40(typescript@6.0.3) + '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0))(vue@3.5.41(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -19119,18 +19181,6 @@ snapshots: picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 - unplugin@3.3.0(@rspack/core@2.1.9(@swc/helpers@0.5.23))(esbuild@0.28.0)(rolldown@1.2.3)(rollup@4.60.3)(vite@8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)): - dependencies: - '@jridgewell/remapping': 2.3.5 - picomatch: 4.0.5 - webpack-virtual-modules: 0.6.2 - optionalDependencies: - '@rspack/core': 2.1.9(@swc/helpers@0.5.23) - esbuild: 0.28.0 - rolldown: 1.2.3 - rollup: 4.60.3 - vite: 8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) - unplugin@3.3.0(@rspack/core@2.1.9(@swc/helpers@0.5.23))(esbuild@0.28.0)(rolldown@1.2.3)(rollup@4.60.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)): dependencies: '@jridgewell/remapping': 2.3.5 @@ -19251,7 +19301,7 @@ snapshots: es-module-lexer: 2.1.0 obug: 2.1.4 pathe: 2.0.3 - vite: 8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - '@vitejs/devtools' @@ -19266,7 +19316,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.14.5(eslint@10.8.1(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)): + vite-plugin-checker@0.14.5(eslint@10.8.1(jiti@2.7.0)(supports-color@10.2.2))(optionator@0.9.4)(typescript@6.0.3)(vite@8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0)): dependencies: '@babel/code-frame': 7.29.7 chokidar: 5.0.0 @@ -19275,7 +19325,7 @@ snapshots: picomatch: 4.0.5 proper-lockfile: 4.1.2 tiny-invariant: 1.3.3 - vite: 8.2.0(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) + vite: 8.2.1(@types/node@26.2.0)(esbuild@0.28.0)(jiti@2.7.0)(terser@5.47.1)(tsx@4.23.12)(yaml@2.9.0) optionalDependencies: eslint: 10.8.1(jiti@2.7.0)(supports-color@10.2.2) optionator: 0.9.4 diff --git a/skills/devframe/SKILL.md b/skills/devframe/SKILL.md index b00298b8..73064247 100644 --- a/skills/devframe/SKILL.md +++ b/skills/devframe/SKILL.md @@ -40,7 +40,7 @@ All adapter factories share the shape `createXxx(devframeDef, options?)`. The same `DevframeDefinition` runs under every adapter - pick based on deployment, not on what the tool does. -For Vite-based hosts that don't use the kit (Nuxt, Astro, SolidStart, plain Vite apps), `devframe/helpers/vite` exports `viteDevBridge(def, options?)` - a Vite plugin that mounts the SPA (static mode) or starts the RPC + WS bridge alongside the host's dev server (`devMiddleware: true`). Not an adapter; just a Vite integration helper. +For Vite-based hosts that don't use the kit (Nuxt, Astro, SolidStart, plain Vite apps), `@devframes/vite` exports `viteDevBridge(def, options?)` - a Vite plugin that mounts the SPA (static mode) or starts the RPC + WS bridge alongside the host's dev server (`devMiddleware: true`). Not an adapter; just a Vite integration helper. ## Minimum viable devframe diff --git a/tests/__snapshots__/tsnapi/devframe/helpers/vite.snapshot.d.ts b/tests/__snapshots__/tsnapi/@devframes/vite/index.snapshot.d.ts similarity index 70% rename from tests/__snapshots__/tsnapi/devframe/helpers/vite.snapshot.d.ts rename to tests/__snapshots__/tsnapi/@devframes/vite/index.snapshot.d.ts index 541603bf..d8e84c88 100644 --- a/tests/__snapshots__/tsnapi/devframe/helpers/vite.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/@devframes/vite/index.snapshot.d.ts @@ -1,18 +1,14 @@ /** - * Generated by tsnapi — public API snapshot of `devframe/helpers/vite` + * Generated by tsnapi — public API snapshot of `@devframes/vite` */ // #region Interfaces export interface DevframeViteDevServerLike { middlewares: { use: ((_: string, _: (_: IncomingMessage, _: ServerResponse, _?: (_?: unknown) => void) => void) => void) & ((_: (_: IncomingMessage, _: ServerResponse, _?: (_?: unknown) => void) => void) => void); }; - httpServer?: Server | null; -} -export interface DevframeVitePlugin { - name: string; - apply: 'serve'; - configureServer: (_: DevframeViteDevServerLike) => void | Promise; - closeBundle?: () => void | Promise; + httpServer?: { + once: (_: 'close', _: () => void) => unknown; + } | null; } export interface ViteDevBridgeOptions { base?: string; @@ -26,6 +22,10 @@ export interface ViteDevBridgeOptions { } // #endregion +// #region Types +export type DevframeVitePlugin = Plugin; +// #endregion + // #region Functions export declare function viteDevBridge(_: DevframeDefinition, _?: ViteDevBridgeOptions): DevframeVitePlugin; // #endregion \ No newline at end of file diff --git a/tests/__snapshots__/tsnapi/devframe/helpers/vite.snapshot.js b/tests/__snapshots__/tsnapi/@devframes/vite/index.snapshot.js similarity index 52% rename from tests/__snapshots__/tsnapi/devframe/helpers/vite.snapshot.js rename to tests/__snapshots__/tsnapi/@devframes/vite/index.snapshot.js index e4c04245..4d00e851 100644 --- a/tests/__snapshots__/tsnapi/devframe/helpers/vite.snapshot.js +++ b/tests/__snapshots__/tsnapi/@devframes/vite/index.snapshot.js @@ -1,5 +1,5 @@ /** - * Generated by tsnapi — public API snapshot of `devframe/helpers/vite` + * Generated by tsnapi — public API snapshot of `@devframes/vite` */ // #region Functions export function viteDevBridge(_, _) {} diff --git a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts index b36c1f01..a3bbd7b1 100644 --- a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts +++ b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.d.ts @@ -58,6 +58,196 @@ export declare function peekRpcWireFrame(_: string): { }; // #endregion +// #region Variables +export declare const diagnostics: import("nostics").Diagnostics<{ + readonly DF0006: { + readonly why: (p: { + name: string; + }) => string; + }; + readonly DF0007: { + readonly why: "AsyncLocalStorage is not set, it likely to be an internal bug of the Devframe foundation"; + }; + readonly DF0008: { + readonly why: (p: { + distDir: string; + }) => string; + }; + readonly DF0012: { + readonly why: (p: { + filepath: string; + }) => string; + }; + readonly DF0013: { + readonly why: (p: { + key: string; + }) => string; + }; + readonly DF0014: { + readonly why: (p: { + name: string; + }) => string; + readonly fix: "Provide a short description (~1–3 sentences) explaining what the tool does and when agents should invoke it."; + }; + readonly DF0015: { + readonly why: (p: { + id: string; + }) => string; + readonly fix: "Tool ids must be unique across RPC functions with an `agent` field and tools registered via `ctx.agent.registerTool()`."; + }; + readonly DF0016: { + readonly why: (p: { + id: string; + }) => string; + }; + readonly DF0017: { + readonly why: (p: { + transport: string; + reason: string; + }) => string; + }; + readonly DF0029: { + readonly why: (p: { + channel: string; + id: string; + dropped: number; + }) => string; + readonly fix: "The consumer is too slow for the producer. Raise `highWaterMark` on the subscription, slow the producer, or batch chunks."; + }; + readonly DF0030: { + readonly why: (p: { + channel: string; + id: string; + }) => string; + readonly fix: "Ensure the server-side producer is running before clients subscribe, or check for typos in the stream id."; + }; + readonly DF0031: { + readonly why: (p: { + channel: string; + id: string; + }) => string; + readonly fix: "Track the producer lifecycle — guard writes with the `stream.signal.aborted` flag."; + }; + readonly DF0032: { + readonly why: (p: { + channel: string; + }) => string; + readonly fix: "Each channel name must be unique within a context. Pick a different name or reuse the existing channel handle."; + }; + readonly DF0033: { + readonly why: (p: { + id: string; + reason: string; + }) => string; + readonly fix: "Verify the bridge port is free and the devframe setup function does not throw. Pin a port via `cli.port` / `cli.portRange` on the definition, or via `devMiddleware.port` on `viteDevBridge`."; + }; + readonly DF0034: { + readonly why: (p: { + namespace: string; + name: string; + }) => string; + readonly fix: "A scoped context auto-namespaces ids. Pass a bare name without a \":\" separator (e.g. `register({ name: \"get-cwd\" })`), or use the unscoped `ctx.base.rpc.register` for a fully-qualified name."; + }; + readonly DF0035: { + readonly why: (p: { + filepath: string; + }) => string; + readonly fix: "Check that the storage directory is writable and has free space."; + }; + readonly DF0036: { + readonly why: (p: { + name: string; + }) => string; + readonly fix: "Complete the auth handshake (or connect with a static/pre-shared token) before calling a trusted method. Untrusted callers may only call `anonymous:`-prefixed methods — see `isAnonymousRpcMethod`."; + }; + readonly DF0037: { + readonly why: (p: { + id: string; + }) => string; + readonly fix: "Service ids are unique per context. Revoke the existing provider first (the `provide()` call returns a revoke function), or namespace the id with your plugin id to avoid collisions."; + }; + readonly DF0042: { + readonly why: (p: { + id: string; + }) => string; + readonly fix: "Pass `{ force: true }` to `createBuild()` if the degraded export is still useful to you, or drop `capabilities.build: false` on the definition."; + }; + readonly DF0045: { + readonly why: (p: { + file: string; + reason: string; + }) => string; + readonly fix: "Discovery tooling (`devframe connect`) will not see this instance. Check that the registry directory is writable, point `DEVFRAME_INSTANCES_DIR` at a writable directory, or set `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` to opt out of registration."; + }; + readonly DF0046: { + readonly why: (p: { + reason: string; + }) => string; + readonly fix: "Install it next to devframe (e.g. `npm install @modelcontextprotocol/server`) and run `devframe connect` again."; + }; + readonly DF0047: { + readonly why: (p: { + name: string; + id: string; + existing: string; + }) => string; + readonly fix: "Wire names derive from tool ids (characters outside [a-zA-Z0-9_-] become \"_\"). Rename one of the two ids so they sanitize to distinct names."; + }; + readonly DF0048: { + readonly why: (p: { + key: string; + }) => string; + readonly fix: "Call the devframe_state_read tool without arguments to list the available keys, then retry with one of them."; + }; + readonly DF0049: { + readonly why: "The devframe_connect_call-tool tool requires { port: number, tool: string }."; + readonly fix: "Call devframe_connect_list-instances to get the port and tool names, then retry."; + }; + readonly DF0050: { + readonly why: (p: { + port: number; + }) => string; + readonly fix: "Call devframe_connect_list-instances for the current instance list — the instance may have stopped or changed port."; + }; + readonly DF0051: { + readonly why: (p: { + port: number; + }) => string; + readonly fix: "Restart the instance with the --mcp flag (or set `cli.mcp: true` on its definition) to expose its tools, then list instances again."; + }; + readonly DF0052: { + readonly why: (p: { + host: string; + port: number; + reason: string; + }) => string; + readonly fix: "The port is likely already taken by another process (often a previous devframe instance). Free it, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `devMiddleware.port` on `viteDevBridge`. The original node error is available as `error.cause`."; + }; + readonly DF0054: { + readonly why: (p: { + id: string; + }) => string; + readonly fix: "Await `instance.ready` (or any request through `instance.handler`) before reading `connectionMeta()` — the WebSocket binding it describes is only known once initialization completes."; + }; + readonly DF0055: { + readonly why: (p: { + tier: string; + }) => string; + readonly fix: "Drop `handleUpgrade`/`attach` and let the configured transport serve the socket, or remove `server` / `ws.port` / `ws.sidecar` from the options so the instance leaves the binding to you."; + }; + readonly DF0056: { + readonly why: (p: { + url: string; + }) => string; + readonly fix: "The server behind `ws.url` owns the transport (and its auth). Drop `ws.url` to have the instance serve the socket, or pair it with `server` / `ws.port` / `ws.sidecar` for the tunnel pattern, where a local binding is advertised through the relay."; + }; + readonly DF0057: { + readonly why: () => string; + readonly fix: "Clients connect over the SSE endpoint instead — no upgrade wiring is needed. Remove `ws: false` if the instance should serve a WebSocket after all."; + }; +}, readonly [typeof devframeReporter]>; +// #endregion + // #region Other export { ContextRpcServer } export { createContextRpcServer } @@ -73,7 +263,9 @@ export { InstanceShellInit } export { InstanceShellInternals } export { InstanceWsTier } export { listLiveDevframeInstances } +export { normalizeBasePath } export { registerDevframeInstance } +export { resolveBasePath } export { resolveInstanceRegister } export { samePath } export { StartedServer } diff --git a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js index 1864f32c..861bcf1e 100644 --- a/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js +++ b/tests/__snapshots__/tsnapi/devframe/internal.snapshot.js @@ -8,10 +8,13 @@ export { createH3DevframeHost } export { createInstanceShell } export { createRpcWireCodec } export { DevframeAgentHost } +export { diagnostics } export { listLiveDevframeInstances } +export { normalizeBasePath } export { normalizeHttpServerUrl } export { peekRpcWireFrame } export { registerDevframeInstance } +export { resolveBasePath } export { resolveInstanceRegister } export { samePath } // #endregion \ No newline at end of file diff --git a/tsconfig.base.json b/tsconfig.base.json index bd981482..0e5b5e4b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -103,9 +103,6 @@ "devframe/adapters/build": [ "./packages/devframe/src/adapters/build.ts" ], - "devframe/helpers/vite": [ - "./packages/devframe/src/helpers/vite.ts" - ], "devframe/adapters/embedded": [ "./packages/devframe/src/adapters/embedded.ts" ], @@ -148,6 +145,9 @@ "@devframes/next": [ "./packages/next/src/index.ts" ], + "@devframes/vite": [ + "./packages/vite/src/index.ts" + ], "@devframes/json-render/core": [ "./packages/json-render/src/core.ts" ], diff --git a/turbo.json b/turbo.json index 5d642c27..ccbc23e6 100644 --- a/turbo.json +++ b/turbo.json @@ -16,12 +16,18 @@ }, "@devframes/nuxt#build": { "outputLogs": "new-only", + "dependsOn": ["devframe#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "@devframes/next#build": { "outputLogs": "new-only", "outputs": ["dist/**"] }, + "@devframes/vite#build": { + "outputLogs": "new-only", + "dependsOn": ["devframe#build"], + "outputs": ["dist/**"] + }, "@devframes/json-render#build": { "outputLogs": "new-only", "dependsOn": ["devframe#build", "@devframes/hub#build"], @@ -39,37 +45,37 @@ }, "@devframes/plugin-code-server#build": { "outputLogs": "new-only", - "dependsOn": ["devframe#build"], + "dependsOn": ["devframe#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "@devframes/plugin-terminals#build": { "outputLogs": "new-only", - "dependsOn": ["devframe#build"], + "dependsOn": ["devframe#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "@devframes/plugin-inspect#build": { "outputLogs": "new-only", - "dependsOn": ["devframe#build"], + "dependsOn": ["devframe#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "@devframes/plugin-og#build": { "outputLogs": "new-only", - "dependsOn": ["devframe#build"], + "dependsOn": ["devframe#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "@devframes/plugin-data-inspector#build": { "outputLogs": "new-only", - "dependsOn": ["devframe#build"], + "dependsOn": ["devframe#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "@devframes/plugin-messages#build": { "outputLogs": "new-only", - "dependsOn": ["devframe#build", "@devframes/hub#build"], + "dependsOn": ["devframe#build", "@devframes/hub#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "@devframes/plugin-assets#build": { "outputLogs": "new-only", - "dependsOn": ["devframe#build"], + "dependsOn": ["devframe#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "hub-vite#build": { @@ -134,7 +140,7 @@ }, "@devframes/plugin-a11y#build": { "outputLogs": "new-only", - "dependsOn": ["devframe#build"], + "dependsOn": ["devframe#build", "@devframes/vite#build"], "outputs": ["dist/**"] }, "files-inspector-example#cli:build": { diff --git a/vitest.config.ts b/vitest.config.ts index ec3495f0..fe703b86 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -31,6 +31,7 @@ export default defineConfig({ 'plugins/assets', 'examples/hub-next', 'packages/next', + 'packages/vite', { test: { name: 'tests', From 275e08db44f2522820e5671f572b0f8907f97605 Mon Sep 17 00:00:00 2001 From: "Anthony Fu (via agent)" Date: Fri, 14 Aug 2026 01:05:14 +0000 Subject: [PATCH 2/5] refactor(vite): rename viteDevBridge, split static-mount vs RPC-bridge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit viteDevBridge conflated two different Vite plugins behind one devMiddleware option. Split it into two purpose-named exports plus a convenience wrapper, all served as real Vite Plugin objects: - devframeVitePlugin(def, { base? }) — statically mounts the built SPA, no RPC server. - devframeViteBridge(def, { base?, port?, host?, flags?, auth?, mcp? }) — the RPC/WS bridge, options flattened (no more nested devMiddleware: boolean | {...}). - devframeVite(def, { bridge?, ...bridgeOptions }) — dispatches to one of the two above, the direct (renamed) successor to the old combined function. Updates every consumer: the 6 single-purpose plugin wrappers (inspect, messages, og, assets, data-inspector, a11y) switch to devframeVite; terminals/code-server (which always run both plugins together) call devframeViteBridge + devframeVitePlugin explicitly — this also fixes a latent bug where their bridge plugin silently dropped any port/host/flags override. @devframes/nuxt's always-bridge call site uses devframeViteBridge directly. Refreshes docs, JSDoc cross-references, and tsnapi API snapshots (allowing the intentional breaking rename — this package has not shipped yet). This PR was created with the help of an agent. --- docs/.vitepress/config.ts | 2 +- docs/adapters/initiate.md | 2 +- docs/adapters/mcp.md | 6 +- docs/errors/DF0033.md | 6 +- docs/errors/DF0052.md | 2 +- docs/guide/security.md | 2 +- docs/helpers/index.md | 2 +- docs/helpers/vite-bridge.md | 41 +++- packages/devframe/src/adapters/dev.ts | 6 +- packages/devframe/src/node/diagnostics.ts | 4 +- packages/devframe/src/types/context.ts | 2 +- packages/next/src/handler.ts | 2 +- packages/next/src/host.ts | 2 +- packages/nuxt/src/module.ts | 22 +- packages/vite/src/index.ts | 213 ++++++++++-------- packages/vite/test/index.test.ts | 95 ++++++-- plugins/a11y/src/vite.ts | 16 +- plugins/assets/src/spa/vite.config.ts | 2 +- plugins/assets/src/vite.ts | 10 +- plugins/code-server/src/spa/vite.config.ts | 10 +- plugins/code-server/src/vite.ts | 12 +- plugins/data-inspector/src/spa/vite.config.ts | 4 +- plugins/data-inspector/src/vite.ts | 14 +- plugins/inspect/src/spa/vite.config.ts | 2 +- plugins/inspect/src/vite.ts | 14 +- plugins/messages/src/spa/vite.config.ts | 4 +- plugins/messages/src/vite.ts | 14 +- plugins/og/src/spa/vite.config.ts | 2 +- plugins/og/src/vite.ts | 10 +- plugins/terminals/src/vite.ts | 12 +- skills/devframe/SKILL.md | 4 +- .../@devframes/plugin-a11y/vite.snapshot.d.ts | 4 +- .../plugin-assets/vite.snapshot.d.ts | 4 +- .../plugin-code-server/vite.snapshot.d.ts | 2 +- .../plugin-data-inspector/vite.snapshot.d.ts | 4 +- .../plugin-inspect/vite.snapshot.d.ts | 4 +- .../plugin-messages/vite.snapshot.d.ts | 4 +- .../@devframes/plugin-og/vite.snapshot.d.ts | 4 +- .../plugin-terminals/vite.snapshot.d.ts | 2 +- .../@devframes/vite/index.snapshot.d.ts | 24 +- .../tsnapi/@devframes/vite/index.snapshot.js | 4 +- .../tsnapi/devframe/internal.snapshot.d.ts | 4 +- 42 files changed, 357 insertions(+), 242 deletions(-) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index e82430c7..53852ba5 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -59,7 +59,7 @@ function helpersItems(prefix: string) { return [ { text: 'Overview', link: `${prefix}/helpers/` }, { text: 'Utilities', link: `${prefix}/helpers/utilities` }, - { text: 'Vite Bridge', link: `${prefix}/helpers/vite-bridge` }, + { text: 'Vite Plugin', link: `${prefix}/helpers/vite-bridge` }, { text: 'Nuxt Module', link: `${prefix}/helpers/nuxt` }, { text: 'Next Helper', link: `${prefix}/helpers/next` }, { text: 'Common RPC Functions', link: `${prefix}/helpers/common-rpc-functions` }, diff --git a/docs/adapters/initiate.md b/docs/adapters/initiate.md index c7c8b211..43993b70 100644 --- a/docs/adapters/initiate.md +++ b/docs/adapters/initiate.md @@ -126,4 +126,4 @@ The instance **gates by default** — a handler mounted inside an app server is ## Relation to the other adapters -`createDevServer`, `viteDevBridge`, and `@devframes/next` are assembled from this instance internally — the handler is the one wiring underneath every serving path. To host **many** devframes behind one namespace with shared transport and docks, use the hub's counterpart: [`initHub`](../guide/hub-initiate). +`createDevServer`, `devframeViteBridge` (`@devframes/vite`), and `@devframes/next` are assembled from this instance internally — the handler is the one wiring underneath every serving path. To host **many** devframes behind one namespace with shared transport and docks, use the hub's counterpart: [`initHub`](../guide/hub-initiate). diff --git a/docs/adapters/mcp.md b/docs/adapters/mcp.md index 1ff6e65f..29b5b73f 100644 --- a/docs/adapters/mcp.md +++ b/docs/adapters/mcp.md @@ -51,8 +51,8 @@ defineDevframe({ Both hosted bridges forward the same option to their side-car dev server and advertise the endpoint (with its port) in the `__connection.json` they serve: ```ts -// Vite -viteDevBridge(devframe, { devMiddleware: true, mcp: true }) +// Vite (@devframes/vite) +devframeViteBridge(devframe, { mcp: true }) // Next.js (@devframes/next) createDevframeNextHandler(devframe, { mcp: true }) @@ -90,6 +90,6 @@ It exposes two gateway tools (the wire names of the `devframe:connect:*` ids — - **`devframe_connect_list-instances`** — discover running devframe dev servers and list each one's MCP tools. Instances running without an MCP route are listed with a hint to restart with `--mcp`. - **`devframe_connect_call-tool`** — invoke one tool on one instance (`{ port, tool, args }`) over its Streamable-HTTP endpoint. -Discovery reads the **instance registry**: every `createDevServer` (CLI `dev`, `viteDevBridge`, `@devframes/next`'s handler) writes a record to `~/.devframe/instances/-.json` on boot and removes it on close; readers prune records whose liveness probe fails. The connector dials each instance's endpoint with the instance's own loopback origin, so it clears the route's origin gate without any configuration. In-process hosts register explicitly with `registerDevframeInstance` from `devframe/node` — see `createDevframeNextHost().mountMcp` for serving MCP on a Next app's own origin. `--port ` probes an explicit port besides the registry; `DEVFRAME_INSTANCES_DIR` relocates the registry and `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` opts a server out. +Discovery reads the **instance registry**: every `createDevServer` (CLI `dev`, `devframeViteBridge`, `@devframes/next`'s handler) writes a record to `~/.devframe/instances/-.json` on boot and removes it on close; readers prune records whose liveness probe fails. The connector dials each instance's endpoint with the instance's own loopback origin, so it clears the route's origin gate without any configuration. In-process hosts register explicitly with `registerDevframeInstance` from `devframe/node` — see `createDevframeNextHost().mountMcp` for serving MCP on a Next app's own origin. `--port ` probes an explicit port besides the registry; `DEVFRAME_INSTANCES_DIR` relocates the registry and `DEVFRAME_DISABLE_INSTANCE_REGISTRY=1` opts a server out. See the [Agent-Native](/guide/agent-native) page for the full API, safety model, and Claude Desktop integration example. diff --git a/docs/errors/DF0033.md b/docs/errors/DF0033.md index 48bd0057..e8268d94 100644 --- a/docs/errors/DF0033.md +++ b/docs/errors/DF0033.md @@ -10,7 +10,7 @@ outline: deep ## Cause -`viteDevBridge({ devMiddleware })` (from `@devframes/vite`) could not bring up the bridge dev server that pairs a host-served SPA (Vite, Nuxt, Astro, etc.) with devframe's RPC backend. Common reasons: +`devframeViteBridge()` (from `@devframes/vite`) could not bring up the bridge dev server that pairs a host-served SPA (Vite, Nuxt, Astro, etc.) with devframe's RPC backend. Common reasons: - The preferred port is in use and no fallback range was configured. - Calling `def.setup(ctx)` threw — the devframe's own setup logic surfaced an error. @@ -20,10 +20,10 @@ This is a soft warning — the surrounding Vite dev server keeps running, but th ## Fix -- Pin a port via `cli.port` / `cli.portRange` on the devframe definition, or via `devMiddleware.port` on `viteDevBridge`. +- Pin a port via `cli.port` / `cli.portRange` on the devframe definition, or via `port` on `devframeViteBridge`. - Inspect the `reason` (or the attached `cause`) for the underlying error — fix the setup function or free the port. - For Nuxt: pass `devMiddleware: { port: }` to the `@devframes/nuxt` module. ## Source -- [`packages/vite/src/index.ts`](https://github.com/devframes/devframe/blob/main/packages/vite/src/index.ts) — `viteDevBridge({ devMiddleware })` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`. +- [`packages/vite/src/index.ts`](https://github.com/devframes/devframe/blob/main/packages/vite/src/index.ts) — `devframeViteBridge()` logs `DF0033` when port resolution or `createDevServer` throws during `configureServer`. diff --git a/docs/errors/DF0052.md b/docs/errors/DF0052.md index 16b19f85..88f34352 100644 --- a/docs/errors/DF0052.md +++ b/docs/errors/DF0052.md @@ -21,7 +21,7 @@ The instance's side-car / shared-server transport binding tried to bind the HTTP ## Fix -- Free the port, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `devMiddleware.port` on `viteDevBridge`. +- Free the port, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `port` on `devframeViteBridge` (`@devframes/vite`). - The original node error is available as `error.cause` — check `error.cause.code` (e.g. `'EADDRINUSE'`) to branch on the failure kind programmatically. ## Source diff --git a/docs/guide/security.md b/docs/guide/security.md index ed098d49..114fa246 100644 --- a/docs/guide/security.md +++ b/docs/guide/security.md @@ -92,7 +92,7 @@ Higher-level integrations can drive their own authentication UI instead: disable ## Practices for tools built on devframe - **Stay on loopback.** The default bind host is `localhost`. Bind to a routable address only when you intend to, and require authentication when you do. -- **Keep `auth: false` local.** Reach for it only for single-user localhost tools; leave the default in place anywhere a connection could originate elsewhere. The hosted bridges (`viteDevBridge`, `@devframes/next`'s handler) gate their side-car by default too — a host that owns the trust boundary another way opts out with `auth: false` explicitly. +- **Keep `auth: false` local.** Reach for it only for single-user localhost tools; leave the default in place anywhere a connection could originate elsewhere. The hosted bridges (`devframeViteBridge`, `@devframes/next`'s handler) gate their side-car by default too — a host that owns the trust boundary another way opts out with `auth: false` explicitly. - **The MCP route requires an origin.** Unlike the WS transport, the route-based MCP server rejects `Origin`-less requests (a request must carry a loopback or allow-listed `Origin`), so a route-based endpoint isn't reachable by an arbitrary local process — see [MCP](/adapters/mcp). - **Treat tokens as secrets.** Never log the bearer token or the one-time code, and never bake either into build output. - **Authorize every handler.** A registered function is callable by any trusted client. Validate inputs, and mark state-changing functions `type: 'destructive'` so MCP and agent clients prompt before invoking them. diff --git a/docs/helpers/index.md b/docs/helpers/index.md index 0ab49654..eede9412 100644 --- a/docs/helpers/index.md +++ b/docs/helpers/index.md @@ -9,7 +9,7 @@ Helpers are the optional, opt-in surface around the core `defineDevframe` API: s | Helper | Entry | What it does | |--------|-------|--------------| | [Utilities](./utilities) | `devframe/utils/*` | Bundled small utilities — terminal colors, hashing, editor launch, structured-clone serialization, and more. | -| [Vite Bridge](./vite-bridge) | `@devframes/vite` | Vite plugin for mounting a devframe inside any Vite-based host (Astro, SolidStart, plain Vite). | +| [Vite Plugin](./vite-bridge) | `@devframes/vite` | Vite plugins for mounting a devframe inside any Vite-based host (Astro, SolidStart, plain Vite) — a static mount, an RPC bridge, or a convenience wrapper over both. | | [Nuxt Module](./nuxt) | `@devframes/nuxt` | Nuxt module that wires a Nuxt SPA as a devframe client and serves the dev-time RPC bridge. | | [Next Helper](./next) | `@devframes/next` | Route-handler host + React client for mounting devframes inside a Next.js App Router app (experimental). | | [Common RPC Functions](./common-rpc-functions) | `devframe/recipes/common-rpc-functions` | Prebuilt RPC actions for "open in editor" and "reveal in Finder". | diff --git a/docs/helpers/vite-bridge.md b/docs/helpers/vite-bridge.md index 99715e19..6a0000fa 100644 --- a/docs/helpers/vite-bridge.md +++ b/docs/helpers/vite-bridge.md @@ -2,34 +2,51 @@ outline: deep --- -# Vite Bridge +# @devframes/vite -A thin Vite plugin for mounting a devframe inside an existing Vite dev server. Used by [`@devframes/nuxt`](./nuxt) and available for any Vite-based host (Astro, SolidStart, plain Vite apps). +Two Vite plugins for mounting a devframe inside an existing Vite dev server — `devframeVitePlugin` (static mount) and `devframeViteBridge` (RPC bridge) — plus `devframeVite`, a convenience wrapper that picks between them. Used by [`@devframes/nuxt`](./nuxt) and available for any Vite-based host (Astro, SolidStart, plain Vite apps). -This sits below the [`vite` adapter](/adapters/vite) on the abstraction ladder: the adapter targets the full Vite DevTools dock; the bridge is the lower-level Vite plugin you reach for when you want a devframe to ride along with an existing app's dev server without the DevTools dock. +This sits below the [`vite` adapter](/adapters/vite) on the abstraction ladder: the adapter targets the full Vite DevTools dock; these are the lower-level Vite plugins you reach for when you want a devframe to ride along with an existing app's dev server without the DevTools dock. ```ts -import { viteDevBridge } from '@devframes/vite' +import { devframeViteBridge, devframeVitePlugin } from '@devframes/vite' import { defineConfig } from 'vite' import devframe from './devframe' export default defineConfig({ - plugins: [viteDevBridge(devframe)], + // Statically mounts the built SPA at `/__/` — no RPC server: + plugins: [devframeVitePlugin(devframe)], + // Or bridge the RPC/WS backend into this dev server instead — the + // host app owns the SPA: + // plugins: [devframeViteBridge(devframe)], }) ``` -## Modes +## `devframeVitePlugin` — static mount -- **Static mount** (default) — mounts `def.cli.distDir` at `options.base` (`/__/` by default). No RPC server. Useful when you only need the SPA bundle served from a known path. -- **Bridge mode** (`devMiddleware: true | {…}`) — skips the static mount; the host app owns the SPA. Devframe spawns a separate RPC + WS server and registers Vite middleware at `__connection.json` so the host-served SPA can discover the WS endpoint. The side-car listens on its own port, so the descriptor carries that port alongside the `/__ws` route. +Mounts `def.cli.distDir` at `options.base` (`/__/` by default) with SPA fallback. No RPC server is started — useful when you only need the SPA bundle served from a known path. -To mount the RPC socket onto the Vite server's own port instead of a side-car — so it shares the origin with the app and rides through a proxy — pass Vite's HTTP server to [`initDevframe`](/adapters/initiate) / `initHub` via the `server` option. Devframe binds only its own `__ws` upgrade route and leaves the rest (Vite's HMR socket included) untouched. +| Option | Default | Description | +|--------|---------|-------------| +| `base` | `def.basePath ?? '/__/'` | Mount path inside the Vite dev server. | + +## `devframeViteBridge` — RPC bridge -## Options +Skips the static mount — the host app owns the SPA. Devframe spawns a separate RPC + WS server and registers Vite middleware at `__connection.json` so the host-served SPA can discover the WS endpoint. The side-car listens on its own port unless it can share Vite's own HTTP server, so the descriptor carries that port alongside the `/__ws` route. + +To mount the RPC socket onto the Vite server's own port instead of a side-car — so it shares the origin with the app and rides through a proxy — pass Vite's HTTP server to [`initDevframe`](/adapters/initiate) / `initHub` via the `server` option. Devframe binds only its own `__ws` upgrade route and leaves the rest (Vite's HMR socket included) untouched. | Option | Default | Description | |--------|---------|-------------| | `base` | `def.basePath ?? '/__/'` | Mount path inside the Vite dev server. | -| `devMiddleware` | `false` | `true` or `{ port?, host?, flags? }` to enable bridge mode. | +| `port` | share Vite's HTTP server | Pin a side-car port for the RPC socket instead. | +| `host` | `def.cli?.host ?? 'localhost'` | Bind host for a pinned side-car. | +| `flags` | — | Forwarded to `def.setup(ctx, { flags })`. | +| `auth` | gated (interactive OTP) | `false` to opt out for a single-user localhost host, or a `DevframeAuthHandler` for a custom scheme. | +| `mcp` | `def.cli?.mcp` | `true` or `McpRouteOptions` to expose the route-based MCP server at `__mcp`. | + +`port` / `host` / `flags` mirror [`createDevServer`](/adapters/dev)'s options of the same name. + +## `devframeVite` — convenience wrapper -When `devMiddleware` is an object, the inner fields mirror [`createDevServer`](/adapters/dev) — `port` pins the WS server port, `host` sets the bind host, and `flags` is forwarded to `def.setup(ctx, { flags })`. +`devframeVite(def, { bridge, ...bridgeOptions })` forwards to `devframeViteBridge` when `bridge: true`, or `devframeVitePlugin` otherwise — handy when a single call site needs to switch between the two modes. Reach for the two plugins directly when a devframe needs both mounted at once (e.g. a bridge for RPC alongside a static mount serving its own bundled UI, as the built-in `terminals`/`code-server` plugins do). diff --git a/packages/devframe/src/adapters/dev.ts b/packages/devframe/src/adapters/dev.ts index 6f4cea8b..aa549b9c 100644 --- a/packages/devframe/src/adapters/dev.ts +++ b/packages/devframe/src/adapters/dev.ts @@ -35,7 +35,7 @@ export interface CreateDevServerOptions { * `def.cli?.distDir` is set, the dev server runs in **bridge mode** — * only `__connection.json` and the WS endpoint are mounted; the SPA * is expected to be hosted elsewhere (e.g. by a parent Vite/Nuxt - * dev server via `viteDevBridge({ devMiddleware })`). + * dev server via `devframeViteBridge` from `@devframes/vite`). */ distDir?: string /** @@ -74,7 +74,7 @@ export interface CreateDevServerOptions { * Override how authentication resolves, taking precedence over * `def.cli?.auth`. Pass `false` to skip the gate entirely (the standard * choice for a **hosted** deployment where the host manages auth — see - * {@link viteDevBridge}); a {@link DevframeAuthHandler} to install a custom + * {@link devframeViteBridge} from `@devframes/vite`); a {@link DevframeAuthHandler} to install a custom * scheme; or `true` to force devframe's interactive OTP gate on. When * omitted, auth resolves from `flags.auth` / `def.cli?.auth` (the standalone * default: gated). The `--no-auth` flag (`flags.auth === false`) still forces @@ -115,7 +115,7 @@ export interface CreateDevServerOptions { * server runs in **bridge mode**: only `__connection.json` and the WS * endpoint are mounted, with no SPA mount. The SPA is expected to be * hosted elsewhere (e.g. by a parent Vite/Nuxt dev server) — see - * `viteDevBridge({ devMiddleware })`. + * `devframeViteBridge` from `@devframes/vite`. * * Returns the underlying {@link StartedServer} handle so callers can * close it gracefully (SIGINT, hot-reload, test teardown). diff --git a/packages/devframe/src/node/diagnostics.ts b/packages/devframe/src/node/diagnostics.ts index 450f7459..657c6dfc 100644 --- a/packages/devframe/src/node/diagnostics.ts +++ b/packages/devframe/src/node/diagnostics.ts @@ -60,7 +60,7 @@ export const diagnostics = defineDiagnostics({ DF0033: { why: (p: { id: string, reason: string }) => `Failed to start dev RPC bridge for "${p.id}": ${p.reason}`, - fix: 'Verify the bridge port is free and the devframe setup function does not throw. Pin a port via `cli.port` / `cli.portRange` on the definition, or via `devMiddleware.port` on `viteDevBridge`.', + fix: 'Verify the bridge port is free and the devframe setup function does not throw. Pin a port via `cli.port` / `cli.portRange` on the definition, or via `port` on `devframeViteBridge` (`@devframes/vite`).', }, DF0034: { why: (p: { namespace: string, name: string }) => @@ -114,7 +114,7 @@ export const diagnostics = defineDiagnostics({ }, DF0052: { why: (p: { host: string, port: number, reason: string }) => `Failed to listen on ${p.host}:${p.port}: ${p.reason}`, - fix: 'The port is likely already taken by another process (often a previous devframe instance). Free it, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `devMiddleware.port` on `viteDevBridge`. The original node error is available as `error.cause`.', + fix: 'The port is likely already taken by another process (often a previous devframe instance). Free it, or pick another via `--port`, `cli.port` / `cli.portRange` on the definition, or `port` on `devframeViteBridge` (`@devframes/vite`). The original node error is available as `error.cause`.', }, DF0054: { why: (p: { id: string }) => `connectionMeta() was called before initDevframe("${p.id}") finished initializing.`, diff --git a/packages/devframe/src/types/context.ts b/packages/devframe/src/types/context.ts index 6ce1af13..c322c80a 100644 --- a/packages/devframe/src/types/context.ts +++ b/packages/devframe/src/types/context.ts @@ -157,7 +157,7 @@ export interface ConnectionMeta { * tooling (e.g. an MCP inspector) can discover it without guessing the * path. `path` is relative to `__connection.json`'s location, like the * WebSocket `path`. `port` is set when the endpoint lives on a side-car - * server on its own port (bridge mode — `viteDevBridge`, + * server on its own port (bridge mode — `devframeViteBridge`, * `@devframes/next`): the client combines the page hostname with `port` * and resolves `path` against that origin, mirroring * {@link ConnectionMetaWebsocket.port}. diff --git a/packages/next/src/handler.ts b/packages/next/src/handler.ts index d84c7d89..bcdd2ff3 100644 --- a/packages/next/src/handler.ts +++ b/packages/next/src/handler.ts @@ -91,7 +91,7 @@ function defaultGetStorageDir(scope: DevframeStorageScope): string { /** * Host a **single** devframe from a Next.js App Router app — the Next - * counterpart to `viteDevBridge`, reduced to memoization + defaults over + * counterpart to `devframeViteBridge`, reduced to memoization + defaults over * `initDevframe` (Next's route handlers can't accept WS upgrades, so the * RPC socket lives on the instance's side-car port, advertised at * `__connection.json`). diff --git a/packages/next/src/host.ts b/packages/next/src/host.ts index 7e869428..9becb11c 100644 --- a/packages/next/src/host.ts +++ b/packages/next/src/host.ts @@ -96,7 +96,7 @@ function stripTrailingSlash(base: string): string { * hosts one or more devframes, plus the single `fetch` handler its catch-all * route delegates to. * - * This is the hosted-adapter counterpart to `viteDevBridge` for the Next + * This is the hosted-adapter counterpart to `devframeViteBridge` for the Next * runtime, which — being webpack/Turbopack rather than Vite — can't reuse the * Vite middleware path. Instead of hand-rolling static serving in a route * handler, static mounts are registered on an internal h3 app and served diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 0d0e1507..471de506 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -1,5 +1,5 @@ import type { DevframeDefinition } from 'devframe' -import { viteDevBridge } from '@devframes/vite' +import { devframeViteBridge } from '@devframes/vite' import { addPlugin, addVitePlugin, createResolver, defineNuxtModule } from '@nuxt/kit' export interface DevframeNuxtModuleOptions { @@ -26,8 +26,8 @@ export interface DevframeNuxtModuleOptions { */ devframe?: DevframeDefinition /** - * Dev-time middleware mode. Mirrors `viteDevBridge`'s option of - * the same name. + * Dev-time middleware mode — whether to start `@devframes/vite`'s + * RPC bridge (`devframeViteBridge`) alongside `nuxt dev`. * * - `true` (default) — when `devframe` is set and Nuxt is in dev * mode, start the RPC bridge with all defaults. @@ -61,9 +61,9 @@ export type ModuleOptions = DevframeNuxtModuleOptions * - Injects a client plugin that calls {@link connectDevframe} once on * page load and exposes the RPC client via `useNuxtApp().$rpc`. * - When `devframe` is provided and Nuxt is in dev mode, registers a - * Vite plugin (via `addVitePlugin(viteDevBridge(devframe, { - * devMiddleware: ... }))`) that starts the RPC + WS bridge and - * serves `${baseURL}__connection.json`. + * Vite plugin (via `addVitePlugin(devframeViteBridge(devframe, { + * ... }))`) that starts the RPC + WS bridge and serves + * `${baseURL}__connection.json`. * * ```ts [nuxt.config.ts] * import devframe from './src/devframe' // defineDevframe(...) export @@ -139,13 +139,11 @@ export default defineNuxtModule({ ?? (nuxt.options.devServer as any)?.host ?? options.devframe.cli?.host - addVitePlugin(viteDevBridge(options.devframe, { + addVitePlugin(devframeViteBridge(options.devframe, { base: options.baseURL ?? './', - devMiddleware: { - port: mw.port, - host, - flags: mw.flags, - }, + port: mw.port, + host, + flags: mw.flags, }) as any) } }, diff --git a/packages/vite/src/index.ts b/packages/vite/src/index.ts index 4571f229..b4e60209 100644 --- a/packages/vite/src/index.ts +++ b/packages/vite/src/index.ts @@ -8,7 +8,33 @@ import { diagnostics, normalizeBasePath, resolveBasePath } from 'devframe/intern import { serveStaticNodeMiddleware } from 'devframe/utils/serve-static' import { resolve } from 'pathe' -export interface ViteDevBridgeOptions { +/** + * The slice of a Vite dev server these plugins touch — deliberately + * narrower than Vite's real `ViteDevServer` (which carries the module + * graph, watcher, transform pipeline, …) so a host can hand a plugin + * anything shaped like this, and a test double only needs to fake two + * fields. A real `ViteDevServer` satisfies this structurally, so each + * plugin's `configureServer(server: ViteDevServer)` hook (typed against + * the real `Plugin` below) is still fully type-safe. + */ +export interface DevframeViteDevServerLike { + middlewares: { + use: ((path: string, handler: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void) => void) + & ((handler: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void) => void) + } + /** + * Deliberately structural (just the one event these plugins listen for) + * rather than `NodeHttpServer` — Vite's real `ViteDevServer.httpServer` + * is `http.Server | Http2SecureServer | null`, and `Http2SecureServer` + * doesn't satisfy `http.Server`'s full shape. + */ + httpServer?: { once: (event: 'close', listener: () => void) => unknown } | null +} + +/** A devframe Vite plugin — a real Vite `Plugin`, scoped to its `serve`-only hooks. */ +export type DevframeVitePlugin = Plugin + +export interface DevframeVitePluginOptions { /** * Mount base. Defaults to `def.basePath ?? '/__/'` for this hosted * adapter — the devframe shares the origin with the host Vite app. @@ -18,32 +44,54 @@ export interface ViteDevBridgeOptions { * router. */ base?: string +} + +/** + * Statically mount a devframe's built SPA (`def.cli.distDir`) at + * `options.base` inside an existing Vite dev server. No RPC server is + * started — reach for {@link devframeViteBridge} when the mounted UI + * needs a live RPC/WebSocket connection back to the devframe. + * + * Use this when the devframe ships its own pre-built UI and the host + * only needs to serve it alongside its own app (e.g. a devtools dock + * whose backend runs elsewhere, or a hub that only wants the static + * assets). + */ +export function devframeVitePlugin(d: DevframeDefinition, options: DevframeVitePluginOptions = {}): DevframeVitePlugin { + const base = normalizeMountBase(options.base ?? resolveBasePath(d, 'hosted')) + const distDir = d.cli?.distDir + + return { + name: `devframe:${d.id}`, + apply: 'serve', + configureServer(server: DevframeViteDevServerLike) { + if (!distDir) + return + server.middlewares.use(base, serveStaticNodeMiddleware(resolve(distDir))) + }, + } +} + +export interface DevframeViteBridgeOptions { /** - * Dev-time middleware mode. When set, the host app owns the SPA and - * devframe serves the RPC surface through the Vite dev server itself — - * `__connection.json` for discovery and the WebSocket upgrade at - * `__ws` on Vite's own HTTP server (zero extra ports, proxy/HTTPS - * friendly). When Vite runs in middleware mode (no `httpServer`) — or a - * `port` is pinned — the socket falls back to a side-car server on its - * own port instead. + * Mount base. Defaults to `def.basePath ?? '/__/'` for this hosted + * adapter — the devframe shares the origin with the host Vite app. * - * - `false` (default) — static-mount the SPA at `base` with SPA - * fallback. No RPC server is started. - * - `true` — bridge mode with all defaults. - * - object — bridge mode with explicit overrides. + * Relative spellings like `'./'` (common for base-agnostic Nuxt builds) + * are normalized to absolute paths so they compose with Vite's connect + * router. */ - devMiddleware?: boolean | { - /** - * Pin a side-car port for the RPC socket instead of sharing Vite's - * server. Default: share Vite's HTTP server (side-car only when Vite - * has none). - */ - port?: number - /** Override the side-car bind host. Default: `def.cli?.host ?? 'localhost'`. */ - host?: string - /** Flag bag forwarded to `def.setup(ctx, { flags })`. */ - flags?: Record - } + base?: string + /** + * Pin a side-car port for the RPC socket instead of sharing Vite's + * server. Default: share Vite's HTTP server (side-car only when Vite + * has none). + */ + port?: number + /** Override the side-car bind host. Default: `def.cli?.host ?? 'localhost'`. */ + host?: string + /** Flag bag forwarded to `def.setup(ctx, { flags })`. */ + flags?: Record /** * Whether the bridged devframe runs its own auth gate. The RPC endpoint is * reachable by anything that can open its socket, so it **gates by @@ -51,9 +99,8 @@ export interface ViteDevBridgeOptions { * interactive OTP gate (unless the definition's `cli.auth` opts out), and * the bridge prints its code/link banner to stdout. Pass a * {@link DevframeAuthHandler} to install a custom scheme, or `false` to opt - * out for a single-user localhost host that owns the trust boundary another - * way. Only applies in bridge mode (`devMiddleware`); the static-mount mode - * starts no RPC server. + * out for a single-user localhost host that owns the trust boundary + * another way. * * @default gated (devframe's interactive OTP, unless `cli.auth` opts out) */ @@ -62,8 +109,7 @@ export interface ViteDevBridgeOptions { * Expose the bridge's route-based MCP server (Streamable-HTTP) at * `__mcp` — on the Vite app's own origin — and advertise it in the * bridge's `__connection.json`. Overrides `def.cli?.mcp`, `undefined` - * falls through to it, `false` disables the route regardless. Only applies - * in bridge mode (`devMiddleware`); the static-mount mode starts no server. + * falls through to it, `false` disables the route regardless. * * @experimental */ @@ -71,73 +117,28 @@ export interface ViteDevBridgeOptions { } /** - * The slice of a Vite dev server the bridge plugin touches — deliberately - * narrower than Vite's real `ViteDevServer` (which carries the module - * graph, watcher, transform pipeline, …) so a host can hand the bridge - * anything shaped like this, and a test double only needs to fake two - * fields. A real `ViteDevServer` satisfies this structurally, so - * `viteDevBridge`'s returned `configureServer(server: ViteDevServer)` hook - * (typed against the real `Plugin` below) is still fully type-safe. - */ -export interface DevframeViteDevServerLike { - middlewares: { - use: ((path: string, handler: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void) => void) - & ((handler: (req: IncomingMessage, res: ServerResponse, next?: (err?: unknown) => void) => void) => void) - } - /** - * Deliberately structural (just the one event the bridge listens for) - * rather than `NodeHttpServer` — Vite's real `ViteDevServer.httpServer` - * is `http.Server | Http2SecureServer | null`, and `Http2SecureServer` - * doesn't satisfy `http.Server`'s full shape. - */ - httpServer?: { once: (event: 'close', listener: () => void) => unknown } | null -} - -/** A `viteDevBridge` plugin — a real Vite `Plugin`, scoped to its `serve`-only hooks. */ -export type DevframeVitePlugin = Plugin - -/** - * Bridge a devframe into an existing Vite dev server. Returns a Vite - * plugin with two modes, picked via `options.devMiddleware`: - * - * - **static-mount mode** (default) — mounts `def.cli.distDir` at - * `options.base` with SPA fallback enabled. No RPC server is started. - * - * - **bridge mode** (`devMiddleware: true | {…}`) — skips the static - * mount; the host app owns the SPA. Devframe serves discovery - * (`__connection.json`), the WebSocket RPC upgrade - * (`__ws`, shared on Vite's own HTTP server), and the optional - * MCP route through {@link initDevframe}'s node middleware, so the - * host-served SPA can discover the endpoint via {@link connectDevframe}. + * Bridge a devframe's RPC + WebSocket backend into an existing Vite dev + * server: the host app owns the SPA (`distDir` is never mounted), and this + * plugin serves discovery (`__connection.json`), the WebSocket RPC + * upgrade (`__ws`, shared on Vite's own HTTP server), and the + * optional MCP route through {@link initDevframe}'s node middleware — so + * the host-served SPA can discover the endpoint via `connectDevframe`. * * The bridge **gates by default** (devframe's interactive OTP unless the - * definition's `cli.auth` opts out), printing its code/link banner to stdout, - * so a bridged devframe isn't silently reachable by anything that can open - * its socket. Pass `options.auth: false` to opt out for a single-user - * localhost host, or a {@link DevframeAuthHandler} for a custom scheme. + * definition's `cli.auth` opts out), printing its code/link banner to + * stdout, so a bridged devframe isn't silently reachable by anything that + * can open its socket. Pass `options.auth: false` to opt out for a + * single-user localhost host, or a {@link DevframeAuthHandler} for a + * custom scheme. * - * Use bridge mode when integrating with frameworks that own the SPA - * (Nuxt, Astro, SolidStart, plain Vite apps). For the all-in-one - * `dev` / `build` / `mcp` shell, reach for `createCac` (`devframe/adapters/cac`) - * instead. + * Use this when integrating with frameworks that own the SPA (Nuxt, Astro, + * SolidStart, plain Vite apps). Reach for {@link devframeVitePlugin} instead + * when the devframe just needs to serve its own pre-built UI with no live + * backend, or `createCac` (`devframe/adapters/cac`) for the all-in-one + * `dev` / `build` / `mcp` shell. */ -export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptions = {}): DevframeVitePlugin { +export function devframeViteBridge(d: DevframeDefinition, options: DevframeViteBridgeOptions = {}): DevframeVitePlugin { const base = normalizeMountBase(options.base ?? resolveBasePath(d, 'hosted')) - - if (!options.devMiddleware) { - const distDir = d.cli?.distDir - return { - name: `devframe:${d.id}`, - apply: 'serve', - configureServer(server: DevframeViteDevServerLike) { - if (!distDir) - return - server.middlewares.use(base, serveStaticNodeMiddleware(resolve(distDir))) - }, - } - } - - const mw = options.devMiddleware === true ? {} : options.devMiddleware let instance: DevframeInstance | undefined return { @@ -156,13 +157,13 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio // The host app owns the SPA in bridge mode — never mount the // definition's own distDir here. distDir: false, - flags: mw.flags, - host: mw.host, + flags: options.flags, + host: options.host, // Pinned port → explicit side-car. Otherwise share Vite's own // HTTP server; a middleware-mode Vite (no httpServer) has no // upgrade to share, so ask for an auto-port side-car instead. - ...(mw.port != null - ? { ws: { port: mw.port } } + ...(options.port != null + ? { ws: { port: options.port } } : server.httpServer // `initDevframe`'s `server` option shares a real // `node:http` server's WS upgrade listener — Vite's dev @@ -197,6 +198,28 @@ export function viteDevBridge(d: DevframeDefinition, options: ViteDevBridgeOptio } } +export interface DevframeViteOptions extends DevframeViteBridgeOptions { + /** + * Start devframe's RPC/WS bridge ({@link devframeViteBridge}) instead of + * statically mounting the built SPA ({@link devframeVitePlugin}). + * + * @default false (static mount, no server) + */ + bridge?: boolean +} + +/** + * Convenience wrapper around {@link devframeVitePlugin} / + * {@link devframeViteBridge}, picked via `options.bridge`. Reach for the + * two underlying plugins directly when a devframe needs both mounted at + * once (e.g. a bridge for RPC alongside a static mount serving its own + * bundled UI). + */ +export function devframeVite(d: DevframeDefinition, options: DevframeViteOptions = {}): DevframeVitePlugin { + const { bridge, ...rest } = options + return bridge ? devframeViteBridge(d, rest) : devframeVitePlugin(d, { base: rest.base }) +} + /** * Make `base` safe for `server.middlewares.use(path, …)`. Vite's connect * router matches by absolute URL prefix, so relative spellings like diff --git a/packages/vite/test/index.test.ts b/packages/vite/test/index.test.ts index ba9a5e92..fb758a60 100644 --- a/packages/vite/test/index.test.ts +++ b/packages/vite/test/index.test.ts @@ -1,15 +1,18 @@ import type { DevframeDefinition } from 'devframe' import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http' -import type { DevframeViteDevServerLike } from '../src/index' +import type { DevframeViteDevServerLike, DevframeVitePlugin } from '../src/index' +import { mkdtempSync, writeFileSync } from 'node:fs' import { createServer } from 'node:http' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client' import { createRpcClient } from 'devframe/rpc/client' import { createWsRpcChannel } from 'devframe/rpc/transports/ws-client' import { getPort } from 'get-port-please' import { afterEach, describe, expect, it } from 'vitest' -import { viteDevBridge } from '../src/index' +import { devframeVite, devframeViteBridge, devframeVitePlugin } from '../src/index' -function defineTestDef(): DevframeDefinition { +function defineTestDef(overrides: Partial = {}): DevframeDefinition { return { id: 'vite-bridge-test', name: 'Vite Bridge Test', @@ -25,6 +28,7 @@ function defineTestDef(): DevframeDefinition { handler: () => ({ greeting: 'hi' }), }) }, + ...overrides, } } @@ -79,8 +83,37 @@ function fakeViteServer(): FakeViteServer { } } -describe('viteDevBridge (bridge mode mcp)', () => { - let bridge: ReturnType | undefined +describe('devframeVitePlugin (static mount)', () => { + let vite: FakeViteServer | undefined + + afterEach(() => { + vite?.close() + vite = undefined + }) + + it('serves the built distDir at the mount base', async () => { + const host = '127.0.0.1' + const vitePort = await getPort({ port: 19700, host }) + const distDir = mkdtempSync(join(tmpdir(), 'devframe-vite-plugin-')) + writeFileSync(join(distDir, 'index.html'), '

hi

') + + const plugin = devframeVitePlugin(defineTestDef({ cli: { distDir } }), { base: '/__static-test/' }) + vite = fakeViteServer() + await vite.listen(vitePort, host) + await plugin.configureServer?.(vite as any) + + const res = await fetch(`http://${host}:${vitePort}/__static-test/`) + expect(await res.text()).toBe('

hi

') + }) + + it('starts no RPC server — no closeBundle handle to close', () => { + const plugin = devframeVitePlugin(defineTestDef()) + expect(plugin.closeBundle).toBeUndefined() + }) +}) + +describe('devframeViteBridge (bridge mode mcp)', () => { + let bridge: ReturnType | undefined let vite: FakeViteServer | undefined afterEach(async () => { @@ -94,8 +127,9 @@ describe('viteDevBridge (bridge mode mcp)', () => { const host = '127.0.0.1' const vitePort = await getPort({ port: 19705, host }) const wsPort = await getPort({ port: 19710, host }) - bridge = viteDevBridge(defineTestDef(), { - devMiddleware: { port: wsPort, host }, + bridge = devframeViteBridge(defineTestDef(), { + port: wsPort, + host, mcp: true, // The bridge gates by default; opt out here so this test can dial // the WS side-car and MCP route directly. @@ -132,8 +166,8 @@ describe('viteDevBridge (bridge mode mcp)', () => { it('shares the Vite http server for the WS endpoint when no port is pinned', async () => { const host = '127.0.0.1' const vitePort = await getPort({ port: 19715, host }) - bridge = viteDevBridge(defineTestDef(), { - devMiddleware: { host }, + bridge = devframeViteBridge(defineTestDef(), { + host, auth: false, }) @@ -159,8 +193,8 @@ describe('viteDevBridge (bridge mode mcp)', () => { }) }) -describe('viteDevBridge (auth default)', () => { - let bridge: ReturnType | undefined +describe('devframeViteBridge (auth default)', () => { + let bridge: ReturnType | undefined let vite: FakeViteServer | undefined afterEach(async () => { @@ -186,7 +220,7 @@ describe('viteDevBridge (auth default)', () => { it('gates the side-car by default (unset auth → untrusted handshake)', async () => { const port = await getPort({ port: 19730, host: '127.0.0.1' }) - bridge = viteDevBridge(defineTestDef(), { devMiddleware: { port, host: '127.0.0.1' } }) + bridge = devframeViteBridge(defineTestDef(), { port, host: '127.0.0.1' }) vite = fakeViteServer() await bridge.configureServer(vite) @@ -197,10 +231,45 @@ describe('viteDevBridge (auth default)', () => { it('opts out when auth: false is passed explicitly (auto-trust handshake)', async () => { const port = await getPort({ port: 19740, host: '127.0.0.1' }) - bridge = viteDevBridge(defineTestDef(), { devMiddleware: { port, host: '127.0.0.1' }, auth: false }) + bridge = devframeViteBridge(defineTestDef(), { port, host: '127.0.0.1', auth: false }) vite = fakeViteServer() await bridge.configureServer(vite) expect(await handshakeIsTrusted(port)).toBe(true) }) }) + +describe('devframeVite (dispatcher)', () => { + let vite: FakeViteServer | undefined + let plugin: DevframeVitePlugin | undefined + + afterEach(async () => { + await plugin?.closeBundle?.() + plugin = undefined + vite?.close() + vite = undefined + }) + + it('static-mounts by default (no bridge, no RPC server)', () => { + plugin = devframeVite(defineTestDef()) + expect(plugin.closeBundle).toBeUndefined() + }) + + it('starts the RPC bridge when bridge: true', async () => { + const port = await getPort({ port: 19750, host: '127.0.0.1' }) + plugin = devframeVite(defineTestDef(), { bridge: true, port, host: '127.0.0.1', auth: false }) + vite = fakeViteServer() + await plugin.configureServer?.(vite as any) + + const rpc = createRpcClient({}, { + channel: createWsRpcChannel({ url: `ws://127.0.0.1:${port}/__ws` }), + }) + try { + const res = await rpc.$call('anonymous:devframe:auth', { authToken: '', ua: 'test', origin: 'http://localhost' }) as { isTrusted: boolean } + expect(res.isTrusted).toBe(true) + } + finally { + rpc.$close?.() + } + }) +}) diff --git a/plugins/a11y/src/vite.ts b/plugins/a11y/src/vite.ts index 0b8f19d9..603ed719 100644 --- a/plugins/a11y/src/vite.ts +++ b/plugins/a11y/src/vite.ts @@ -1,20 +1,20 @@ -import type { DevframeVitePlugin, ViteDevBridgeOptions } from '@devframes/vite' -import { viteDevBridge } from '@devframes/vite' +import type { DevframeViteOptions, DevframeVitePlugin } from '@devframes/vite' +import { devframeVite } from '@devframes/vite' import a11yDevframe from './index.ts' -export type { ViteDevBridgeOptions } +export type { DevframeViteOptions } /** * Mount the a11y inspector panel into an existing Vite dev server. In the * default static-mount mode it serves the built panel at - * `/__devframes_plugin_a11y/`; pass `{ devMiddleware: true }` for the - * bridge mode where the host owns the SPA and devframe runs a side-car - * RPC + WS server. + * `/__devframes_plugin_a11y/`; pass `{ bridge: true }` for the bridge mode + * where the host owns the SPA and devframe runs a side-car RPC + WS + * server. * * The in-page agent that scans the host is loaded separately: a hub loads it * as this dock's client script (see {@link a11yAgentBundlePath}); a standalone * host adds `