Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions alias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const r = (path: string) => fileURLToPath(new URL(`./packages/${path}`, import.m
const p = (path: string) => fileURLToPath(new URL(`./plugins/${path}`, import.meta.url))

export const alias = {
'devframe/rpc/transports/ws-bun': r('devframe/src/rpc/transports/ws-bun.ts'),
'devframe/rpc/transports/ws-server': r('devframe/src/rpc/transports/ws-server.ts'),
'devframe/rpc/transports/ws-client': r('devframe/src/rpc/transports/ws-client.ts'),
'devframe/rpc/client': r('devframe/src/rpc/client.ts'),
Expand Down Expand Up @@ -44,6 +45,7 @@ export const alias = {
'devframe/adapters/mcp': r('devframe/src/adapters/mcp/index.ts'),
'@devframes/hub/client': r('hub/src/client/index.ts'),
'@devframes/hub/constants': r('hub/src/constants.ts'),
'@devframes/hub/initiate': r('hub/src/node/initiate.ts'),
'@devframes/hub/node': r('hub/src/node/index.ts'),
'@devframes/hub/types': r('hub/src/types/index.ts'),
'@devframes/hub': r('hub/src/index.ts'),
Expand Down
31 changes: 31 additions & 0 deletions docs/errors/DF8000.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
outline: deep
---

# DF8000: Devframe Id Collides With a Reserved Hub Path

## Message

> Devframe id "`{id}`" collides with a reserved hub path — it cannot be mounted directly under the hub base.

## Cause

`initHub` mounts every devframe at `<base><id>/`, directly under the hub base. The filenames that live at that same level — `__connection.json`, `__ws`, `__index.json`, `__client-imports.js`, `__mcp`, and `embedded.js` — are the hub protocol's own endpoints, so a devframe id equal to one of them would shadow the endpoint.

## Example

```ts
import { initHub } from '@devframes/hub/initiate'

initHub({
devframes: [defineDevframe({ id: '__mcp', /* … */ })], // ✗ throws DF8000
})
```

## Fix

Rename the devframe id, or mount it at a non-colliding path via `basePath` on the definition.

## Source

- [`packages/hub/src/node/initiate.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/initiate.ts) — `initHub` throws this while mounting the `devframes` list.
33 changes: 33 additions & 0 deletions docs/errors/DF8001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
outline: deep
---

# DF8001: Memoized Hub Instance Replaced

## Message

> initHub replaced the live hub instance memoized under key "`{key}`": its options changed since the previous call.

## Cause

`initHub` was called with a `key` that already maps to a live instance, but the option fingerprint differs from the memoized one's. Dev servers that re-evaluate modules on the fly (Next.js, Nitro, SvelteKit HMR) re-run `initHub` on every reload; the `key` memoization normally returns the live instance, but when the options genuinely changed the old instance — including its side-car WebSocket server — is closed and a fresh one starts.

## Example

```ts
import { initHub } from '@devframes/hub/initiate'

// First evaluation:
initHub({ key: 'devtools', devframes: [git] })

// A later reload with a different frame list replaces the live instance:
initHub({ key: 'devtools', devframes: [git, terminals] }) // ⚠ DF8001
```

## Fix

This is informational when you edited the options on purpose — the replacement is the intended behavior. If it fires without an intentional change, keep the options stable across reloads (module-level constants rather than values recomputed per evaluation), or give genuinely different hubs distinct keys.

## Source

- [`packages/hub/src/node/initiate.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/initiate.ts) — `initHub` warns this before closing and replacing a memoized instance whose options fingerprint changed.
36 changes: 36 additions & 0 deletions docs/errors/DF8002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
outline: deep
---

# DF8002: Both devframes and context Passed to initHub

## Message

> initHub received both `devframes` and `context` — the two assembly modes are mutually exclusive.

## Cause

`initHub` assembles a hub in one of two ways: **declaratively** (`devframes: [...]` — the instance creates the hub context with its own host and mounts each frame under `<base><id>/`), or **from a pre-built context** (`context: ctx` — your host already created the context and mounted the frames; the instance serves only the hub-level endpoints and transport). A `devframes` list cannot be mounted into a context whose host the instance doesn't own, so passing both is a contradiction.

## Example

```ts
// ✗ Bad
initHub({ devframes: [git], context: myCtx })

// ✓ Good — declarative:
initHub({ devframes: [git] })

// ✓ Good — bring your own context:
const ctx = await createHubContext({ host: myHost, cwd })
await mountDevframe(ctx, git)
initHub({ context: ctx })
```

## Fix

Pick one mode. Use `configure(ctx)` on the declarative mode when you need post-mount registrations (docks, commands, terminals) on the instance-created context.

## Source

- [`packages/hub/src/node/initiate.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/initiate.ts) — `initHub` throws this during initialization when both options are present.
33 changes: 33 additions & 0 deletions docs/errors/DF8003.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
outline: deep
---

# DF8003: connectionMeta() Before Hub Instance Ready

## Message

> connectionMeta() was called before initHub finished initializing.

## Cause

`initHub` is a synchronous factory that kicks off asynchronous initialization eagerly — creating the hub context, mounting every frame, and binding the WebSocket tier. `connectionMeta()` describes the WebSocket binding, which only exists once that initialization completes; calling it earlier has nothing correct to return.

## Example

```ts
import { initHub } from '@devframes/hub/initiate'

const hub = initHub({ devframes: [git] })
hub.connectionMeta() // ✗ throws DF8003 — init is still in flight

await hub.ready
hub.connectionMeta() // ✓ { backend: 'websocket', websocket: { … } }
```

## Fix

Await `instance.ready` (or any request through `instance.handler` — it awaits readiness internally) before reading `connectionMeta()`.

## Source

- [`packages/hub/src/node/initiate.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/initiate.ts) — `initHub`'s `connectionMeta()` throws this while initialization is still pending.
4 changes: 2 additions & 2 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@
"src/recipes/{common-rpc-functions,interactive-auth,open-helpers}.ts",
"src/rpc/{index,client,server}.ts",
"src/rpc/dump/index.ts",
"src/rpc/transports/{ws-client,ws-server}.ts",
"src/rpc/transports/{ws-bun,ws-client,ws-server}.ts",
"src/types/index.ts",
"src/utils/*.ts"
]
},
"packages/hub": {
"entry": ["src/{index,constants}.ts", "src/{client,node,types}/index.ts"]
"entry": ["src/{index,constants}.ts", "src/{client,node,types}/index.ts", "src/node/initiate.ts"]
},
"packages/json-render": {
// `src/node/index.ts` is already picked up via `tsdown.config.ts`
Expand Down
1 change: 1 addition & 0 deletions packages/devframe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"./rpc/client": "./dist/rpc/client.mjs",
"./rpc/dump": "./dist/rpc/dump.mjs",
"./rpc/server": "./dist/rpc/server.mjs",
"./rpc/transports/ws-bun": "./dist/rpc/transports/ws-bun.mjs",
"./rpc/transports/ws-client": "./dist/rpc/transports/ws-client.mjs",
"./rpc/transports/ws-server": "./dist/rpc/transports/ws-server.mjs",
"./types": "./dist/types/index.mjs",
Expand Down
4 changes: 2 additions & 2 deletions packages/devframe/src/adapters/initiate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import type { ConnectionMeta, DevframeNodeContext, DevframeNodeRpcSession, Devfr
import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http'
import type { DevframeAuthHandler } from '../node/auth/handler'
import type { StartedServer } from '../node/server'
import type { BunWsTier } from '../rpc/transports/ws-bun'
import type { DevframeDefinition, DevframeSetupInfo, DevframeWsOptions, McpRouteOptions } from '../types/devframe'
import type { BunWsTier } from './initiate-bun'
import process from 'node:process'
import { mountStaticHandler } from 'devframe/utils/serve-static'
import { H3, toNodeHandler } from 'h3'
Expand Down Expand Up @@ -456,7 +456,7 @@ function instantiateDevframe(
else {
// Bun fetch-upgrade — same-origin upgrades completed through
// `handler(request, server)`, hooks exposed via `websocket`.
const { attachBunWsTransport } = await import('./initiate-bun')
const { attachBunWsTransport } = await import('../rpc/transports/ws-bun')
const { createContextRpcServer } = await import('../node/rpc-core')
const core = createContextRpcServer({
context: ctx,
Expand Down
6 changes: 6 additions & 0 deletions packages/devframe/src/adapters/mcp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,9 @@ export {
type CreateMcpFetchHandlerOptions,
type McpFetchHandler,
} from './fetch'

export {
type MountedMcpHttp,
mountMcpHttp,
type MountMcpHttpOptions,
} from './http'
4 changes: 4 additions & 0 deletions packages/devframe/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export * from './host-views'
// lower-level read/probe/prune helpers stay internal to the connector.
export { listLiveDevframeInstances, registerDevframeInstance } from './instance-registry'
export type { DevframeInstanceRecord, DevframeInstanceRegistration } from './instance-registry'
// The transport-agnostic RPC core is public so hosts that bind their own
// transports (a Bun fetch-upgrade route, a custom relay) reuse the exact
// session/auth wiring `startHttpAndWs` uses — see `createContextRpcServer`.
export * from './rpc-core'
export * from './rpc-shared-state'
export * from './rpc-streaming'
export * from './scope'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { WsOriginRegistry } from 'devframe/rpc/transports/ws-server'
import type { ContextRpcServer } from '../node/rpc-core'
import { createWsRpcPeerHooks, isAllowedOrigin } from 'devframe/rpc/transports/ws-server'
import type { ContextRpcServer } from '../../node/rpc-core'
import type { WsOriginRegistry } from './ws-server'
import { createWsRpcPeerHooks, isAllowedOrigin } from './ws-server'

export interface AttachBunWsTransportOptions {
/** Same contract as `WsRpcTransportOptions.allowedOrigins`. */
Expand All @@ -12,7 +12,7 @@ export interface AttachBunWsTransportOptions {
* crossws Bun adapter produces — typed loosely so devframe carries no
* dependency on Bun's own types.
*/
interface BunWsTierWebSocket {
export interface BunWsTierWebSocket {
open?: (ws: unknown) => unknown
message: (ws: unknown, message: unknown) => unknown
close?: (ws: unknown, code?: number, reason?: string) => unknown
Expand All @@ -28,11 +28,11 @@ export interface BunWsTier {
}

/**
* The Bun fetch-upgrade WebSocket tier for `createHandler` — the same RPC
* peer wiring as `attachWsRpcTransport`, driven by crossws's Bun adapter so
* upgrades complete through `fetch(request, server)` on the app's own
* origin, with no side-car server. Loaded dynamically so the Bun adapter
* never enters a Node-only bundle path.
* The Bun fetch-upgrade WebSocket tier for `initDevframe` / `initHub` — the
* same RPC peer wiring as `attachWsRpcTransport`, driven by crossws's Bun
* adapter so upgrades complete through `handler(request, server)` on the
* app's own origin, with no side-car server. Load it dynamically so the Bun
* adapter never enters a Node-only bundle path.
*/
export async function attachBunWsTransport(
core: ContextRpcServer,
Expand Down
1 change: 1 addition & 0 deletions packages/devframe/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ const serverEntries = {
'rpc/client': 'src/rpc/client.ts',
'rpc/dump': 'src/rpc/dump/index.ts',
'rpc/server': 'src/rpc/server.ts',
'rpc/transports/ws-bun': 'src/rpc/transports/ws-bun.ts',
'rpc/transports/ws-client': 'src/rpc/transports/ws-client.ts',
'rpc/transports/ws-server': 'src/rpc/transports/ws-server.ts',
'node/index': 'src/node/index.ts',
Expand Down
4 changes: 4 additions & 0 deletions packages/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
".": "./dist/index.mjs",
"./client": "./dist/client/index.mjs",
"./constants": "./dist/constants.mjs",
"./initiate": "./dist/node/initiate.mjs",
"./node": "./dist/node/index.mjs",
"./types": "./dist/types/index.mjs",
"./package.json": "./package.json"
Expand All @@ -44,15 +45,18 @@
"dependencies": {
"@standard-schema/spec": "catalog:deps",
"destr": "catalog:deps",
"h3": "catalog:deps",
"nostics": "catalog:deps",
"pathe": "catalog:deps",
"perfect-debounce": "catalog:deps",
"tinyexec": "catalog:deps",
"ufo": "catalog:deps",
"zigpty": "catalog:deps"
},
"devDependencies": {
"@types/node": "catalog:types",
"devframe": "workspace:*",
"get-port-please": "catalog:deps",
"mlly": "catalog:build",
"tsdown": "catalog:build",
"valibot": "catalog:deps"
Expand Down
Loading
Loading