Skip to content

Commit 00d0729

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feat/auth-otp-token-exchange
# Conflicts: # tests/__snapshots__/tsnapi/devframe/client.snapshot.d.ts # tests/__snapshots__/tsnapi/devframe/client.snapshot.js
2 parents cff5b16 + 09f382a commit 00d0729

122 files changed

Lines changed: 3001 additions & 465 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ pnpm install # requires pnpm@11.x
2222
pnpm build # tsdown
2323
pnpm dev # tsdown --watch
2424
pnpm test # pnpm build && vitest (api snapshot guards against stale dist)
25-
pnpm typecheck # tsc --noEmit
25+
pnpm typecheck # turbo run typecheck (per-package tsc --noEmit)
2626
pnpm lint --fix # ESLint via @antfu/eslint-config
2727
pnpm start # tsx src/index.ts
2828
```
2929

3030
The `pnpm test` script intentionally runs `build` first so `tsnapi` snapshots compare against fresh `dist/`. `tsdown-stale-guard` enforces this in `test/api-snapshot.test.ts`.
3131

32+
`pnpm typecheck` fans out through Turbo: every workspace package owns a `"typecheck": "tsc --noEmit"` script and its own `tsconfig.json` (extending `tsconfig.base.json` with an explicit `include`). Cross-package imports resolve to source through the `paths` aliases in `tsconfig.base.json`, so no prior build is needed. Any package added under `packages/*` or `plugins/*` is typechecked automatically once it ships that `typecheck` script — add one to every new package so it can't silently skip type errors.
33+
3234
## Conventions
3335

3436
- RPC functions must use `defineRpcFunction`; always namespace IDs (`my-plugin:fn-name`).

alias.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const alias = {
2525
'devframe/utils/nanoid': r('devframe/src/utils/nanoid.ts'),
2626
'devframe/utils/open': r('devframe/src/utils/open.ts'),
2727
'devframe/utils/promise': r('devframe/src/utils/promise.ts'),
28+
'devframe/utils/scope': r('devframe/src/utils/scope.ts'),
2829
'devframe/utils/serve-static': r('devframe/src/utils/serve-static.ts'),
2930
'devframe/utils/shared-state': r('devframe/src/utils/shared-state.ts'),
3031
'devframe/utils/streaming-channel': r('devframe/src/utils/streaming-channel.ts'),

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function guideItems(prefix: string): DefaultTheme.NavItemWithLink[] {
2121
{ text: 'Introduction', link: `${prefix}/guide/` },
2222
{ text: 'Built with Devframe', link: `${prefix}/guide/built-with` },
2323
{ text: 'Devframe Definition', link: `${prefix}/guide/devframe-definition` },
24+
{ text: 'Scoped Context', link: `${prefix}/guide/scoped-context` },
2425
{ text: 'RPC', link: `${prefix}/guide/rpc` },
2526
{ text: 'Shared State', link: `${prefix}/guide/shared-state` },
2627
{ text: 'Streaming', link: `${prefix}/guide/streaming` },

docs/errors/DF0034.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF0034: Already-Namespaced Scoped Registration
6+
7+
## Message
8+
9+
> Scoped RPC registration for namespace "`{namespace}`" received an already-namespaced function name "`{name}`".
10+
11+
## Cause
12+
13+
A [scoped context](../guide/scoped-context) auto-namespaces the ids you pass it. `ctx.scope('my-plugin').rpc.register(...)` therefore expects a **bare** function name and stores it as `my-plugin:<name>`. Passing a name that already contains a `:` separator would produce a double-prefixed id, so registration throws instead.
14+
15+
## Example
16+
17+
```ts
18+
const my = ctx.scope('my-plugin')
19+
20+
// ✗ Bad — already namespaced
21+
my.rpc.register(defineRpcFunction({ name: 'my-plugin:get-cwd', type: 'query', handler }))
22+
23+
// ✓ Good — bare name, stored as `my-plugin:get-cwd`
24+
my.rpc.register(defineRpcFunction({ name: 'get-cwd', type: 'query', handler }))
25+
```
26+
27+
## Fix
28+
29+
- Pass a bare name (no `:` separator) to the scoped `register`.
30+
- To register a fully-qualified name on purpose, use the unscoped host: `ctx.base.rpc.register(...)` (server) or `client.scope(...).base.client.register(...)` (browser).
31+
32+
## Source
33+
34+
- [`packages/devframe/src/node/scope.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/scope.ts)`register()` / `update()` throw this when the supplied definition name is already namespaced.

docs/errors/DF8101.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ outline: deep
66

77
## Message
88

9-
> Cannot change the id of a dock. Use register() to add new docks.
9+
> Cannot change the id of dock "`{id}`" to "`{attempted}`". Dock ids are immutable once registered
1010
1111
## Cause
1212

docs/errors/DF8102.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ outline: deep
66

77
## Message
88

9-
> Dock with id "`{id}`" is not registered. Use register() to add new docks.
9+
> Dock with id "`{id}`" is not registered and cannot be updated
1010
1111
## Cause
1212

docs/errors/DF8105.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
outline: deep
3+
---
4+
5+
# DF8105: Devframe Already Mounted
6+
7+
## Message
8+
9+
> Devframe "`{name}`" (id "`{id}`") is already mounted on this hub
10+
11+
## Cause
12+
13+
`mountDevframe(ctx, def)` was called with a devframe whose `id` already belongs to another devframe mounted on the same hub. Devframes are deduplicated by `id`, and the definition's `duplicationStrategy` is `'warn'` (the default) or `'throw'`.
14+
15+
## Fix
16+
17+
Set `duplicationStrategy` on the definition to choose how duplicates are handled:
18+
19+
- `'warn'` (default) — keep the first registration, drop the later one, and emit this warning.
20+
- `'silent'` — drop the later one without warning.
21+
- `'throw'` — surface duplicates as a thrown error.
22+
- `'duplicate'` — let every instance coexist under a disambiguated dock id (`my-tool`, `my-tool-2`, …).
23+
24+
Otherwise, remove the redundant `mountDevframe` call so each devframe is mounted once.
25+
26+
## Source
27+
28+
- [`packages/hub/src/node/mount-devframe.ts`](https://github.com/devframes/devframe/blob/main/packages/hub/src/node/mount-devframe.ts)`mountDevframe()` emits this when a devframe sharing an already-mounted `id` is mounted and the strategy is not `'duplicate'`.

docs/guide/client.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -105,30 +105,32 @@ const ok = await rpc.requestTrustWithToken('a1b2c3…')
105105

106106
## Calling functions
107107

108+
Derive a [scoped client](./scoped-context) so ids are namespaced for you:
109+
108110
```ts
109-
const rpc = await connectDevframe()
111+
const my = (await connectDevframe()).scope('my-devframe')
110112

111113
// Standard call — awaits a response or throws.
112-
const modules = await rpc.call('my-devframe:get-modules', { limit: 10 })
114+
const modules = await my.rpc.call('get-modules', { limit: 10 })
113115

114116
// Optional — returns undefined when no handler responds (useful while HMR is restarting).
115-
const maybe = await rpc.callOptional('my-devframe:get-modules', { limit: 10 })
117+
const maybe = await my.rpc.callOptional('get-modules', { limit: 10 })
116118

117119
// Event — fire-and-forget, no response expected.
118-
rpc.callEvent('my-devframe:notify', { message: 'hello' })
120+
my.rpc.callEvent('notify', { message: 'hello' })
119121
```
120122

121-
TypeScript types flow through from the server's `defineRpcFunction` definitions, so argument and return shapes are known at the call site.
123+
The unscoped `rpc.call('my-devframe:get-modules', ...)` works too. Either way, TypeScript types flow through from the server's `defineRpcFunction` definitions, so argument and return shapes are known at the call site.
122124

123125
## Registering client functions
124126

125-
The client can register functions that the server calls via `ctx.rpc.broadcast`:
127+
The client can register functions that the server calls via `rpc.broadcast`:
126128

127129
```ts
128130
import { defineRpcFunction } from 'devframe'
129131

130-
rpc.client.register(defineRpcFunction({
131-
name: 'my-devframe:on-file-changed',
132+
my.rpc.register(defineRpcFunction({
133+
name: 'on-file-changed', // -> my-devframe:on-file-changed
132134
type: 'event',
133135
setup: () => ({
134136
handler: async ({ file }: { file: string }) => {
@@ -143,7 +145,7 @@ That's how the server pushes live updates into the UI — file-watcher events, s
143145
## Shared state
144146

145147
```ts
146-
const state = await rpc.sharedState.get('my-devframe:state')
148+
const state = await my.rpc.sharedState('state') // -> my-devframe:state
147149

148150
console.log(state.value())
149151

@@ -158,6 +160,17 @@ state.on('updated', (next) => {
158160

159161
Client-side mutations round-trip through the server before reappearing locally. See [Shared State](./shared-state) for the full API.
160162

163+
## Settings
164+
165+
A scoped client also exposes a top-level persisted `settings` store, synced from the server. Read and write per-user (`global`) or per-workspace (`project`) values:
166+
167+
```ts
168+
await my.settings.project.set('theme', 'dark')
169+
const theme = await my.settings.project.get('theme')
170+
```
171+
172+
See [Scoped Context](./scoped-context#settings) for the full API.
173+
161174
## Caching
162175

163176
Set `cacheOptions: true` (or an options object) when constructing the client:

docs/guide/devframe-definition.md

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@ import * as v from 'valibot'
1515
export default defineDevframe({
1616
id: 'my-devframe',
1717
name: 'My Devframe',
18+
version: '1.0.0',
19+
packageName: 'my-devframe',
20+
homepage: 'https://github.com/me/my-devframe',
21+
description: 'A one-line summary of what the tool does.',
1822
icon: 'ph:gauge-duotone',
1923
setup(ctx) {
24+
// A scoped context auto-namespaces ids with your devframe `id`.
25+
const my = ctx.scope('my-devframe')
26+
2027
// Register your RPC functions, shared state, etc. here.
21-
ctx.rpc.register(defineRpcFunction({
22-
name: 'my-devframe:hello',
28+
my.rpc.register(defineRpcFunction({
29+
name: 'hello', // stored as `my-devframe:hello`
2330
type: 'static',
2431
jsonSerializable: true,
2532
handler: () => ({ message: 'hello' }),
@@ -28,23 +35,47 @@ export default defineDevframe({
2835
})
2936
```
3037

31-
Host adapters (such as the [`vite` adapter](/adapters/vite) for Vite DevTools) derive their mount entry from `id`, `name`, `icon`, and `basePath` automatically.
38+
`ctx.scope(id)` is the preferred way to consume the context — see [Scoped Context](./scoped-context). Host adapters (such as the [`vite` adapter](/adapters/vite) for Vite DevTools) derive their mount entry from `id`, `name`, `icon`, and `basePath` automatically.
3239

3340
## Definition fields
3441

3542
| Field | Type | Description |
3643
|-------|------|-------------|
3744
| `id` | `string` | **Required.** Unique, namespaced identifier (kebab-case). Used as a prefix for RPC names, dock IDs, and MCP tool names. |
3845
| `name` | `string` | **Required.** Display name shown in the dock and agent manifests. |
46+
| `version` | `string` | **Required.** Semver of the tool, surfaced in hub UIs and diagnostics. |
47+
| `packageName` | `string` | **Required.** npm package name the devframe ships in (e.g. `@scope/my-tool`). |
48+
| `homepage` | `string` | **Required.** Project homepage or documentation URL. |
49+
| `description` | `string` | **Required.** One-line summary of what the tool does. |
3950
| `icon` | `string \| { light, dark }` | Optional Iconify name or URL; supports light/dark pairs. |
40-
| `version` | `string` | Optional version string surfaced to clients. |
4151
| `basePath` | `string` | Optional mount path override. Defaults depend on the adapter: `/` for standalone (`cli` / `spa` / `build`), `/.<id>/` for hosted (`vite` / `embedded`). |
52+
| `duplicationStrategy` | `'warn' \| 'silent' \| 'throw' \| 'duplicate'` | How a hub reacts when another devframe sharing this `id` is mounted onto the same hub. Defaults to `'warn'`. See [Hub](./hub). Hub adapters consult it; standalone adapters ignore it. |
4253
| `capabilities` | `{ dev?, build?, spa? }` | Per-runtime feature flags. A `boolean` applies to the runtime as a whole; an object enables individual features. |
4354
| `setup` | `(ctx, info?) => void \| Promise<void>` | **Required.** Server-side entry point. Runs in every runtime. The optional second argument carries runtime metadata — most notably the parsed CLI `flags` when running under `createCli`. |
4455
| `setupBrowser` | `(ctx) => void \| Promise<void>` | Browser-only entry used by the SPA adapter. |
4556
| `cli` | `DevframeCliOptions` | Defaults for the CLI adapter. See [CLI options](#cli-options) below. |
4657
| `spa` | `DevframeSpaOptions` | Defaults for the SPA adapter (`base`, `loader`). |
4758

59+
### Sourcing metadata from `package.json`
60+
61+
Keep `version`, `packageName`, `homepage`, and `description` in sync with the package you publish by importing them straight from its `package.json`. Note that the package's `name` field maps to `packageName` — the devframe `name` is a separate display label.
62+
63+
```ts
64+
import pkg from '../package.json' with { type: 'json' }
65+
66+
export default defineDevframe({
67+
id: 'my-devframe',
68+
name: 'My Devframe', // display label
69+
version: pkg.version,
70+
packageName: pkg.name,
71+
homepage: pkg.homepage,
72+
description: pkg.description,
73+
setup(ctx) { /**/ },
74+
})
75+
```
76+
77+
The default import with a `with { type: 'json' }` attribute resolves under both bundlers and Node's native TypeScript execution. Bundlers also support the destructured `import { version } from '../package.json'` form when the devframe is always bundled before it runs.
78+
4879
### Runtime flags
4980

5081
The `ctx.mode` field is either `'dev'` or `'build'`. Use it to gate work that should only run in one runtime:
@@ -81,12 +112,17 @@ interface DevframeNodeContext {
81112
views: DevframeViewHost // static file hosting (`hostStatic`)
82113
diagnostics: DevframeDiagnosticsHost
83114
agent: DevframeAgentHost // experimental
115+
116+
scope: (id) => DevframeScopedNodeContext // namespaced view (preferred)
84117
}
85118
```
86119

120+
`ctx.scope(id)` returns a namespace-scoped view that auto-prefixes every RPC id, shared-state key, and streaming channel and adds a persisted top-level `settings` store. It's the recommended entry point from a single tool's setup code — see [Scoped Context](./scoped-context).
121+
87122
Host adapters can augment `ctx` with additional surfaces. For example, the [`vite` adapter](/adapters/vite) exposes Vite DevTools' dock, command, message, and terminal hosts via an optional `setup` hook on `createPluginFromDevframe` — consult the host's docs for those extras.
88123

89124
Each devframe-level host has a dedicated page:
125+
- [Scoped Context](./scoped-context)`ctx.scope(id)`, `settings`
90126
- [RPC](./rpc)`ctx.rpc`
91127
- [Shared State](./shared-state)`ctx.rpc.sharedState`
92128
- [Diagnostics](./diagnostics)`ctx.diagnostics`

docs/guide/hub.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ await mountDevframe(ctx, myDevframe)
4343

4444
Framework kits typically wrap this in a plugin shell. `@vitejs/devtools-kit`'s `createPluginFromDevframe` returns a Vite `Plugin` whose `devtools.setup` calls into `mountDevframe`.
4545

46+
### Duplicate devframes
47+
48+
When a devframe sharing an already-mounted `id` is mounted onto the same hub, its `duplicationStrategy` decides what happens. By default the first registration wins:
49+
50+
| Strategy | Behavior |
51+
|---|---|
52+
| `'warn'` (default) | Keep the first registration, drop the later one, and emit `DF8105`. |
53+
| `'silent'` | Drop the later one without warning. |
54+
| `'throw'` | Throw `DF8105`. |
55+
| `'duplicate'` | Let every instance coexist under a disambiguated dock id (`my-tool`, `my-tool-2`, …). |
56+
57+
```ts
58+
defineDevframe({
59+
id: 'my-tool',
60+
//
61+
duplicationStrategy: 'duplicate',
62+
})
63+
```
64+
4665
## Grouping dock entries
4766

4867
When a hub combines many integrations, related dock entries can collapse under a single dock-bar button. A `type: 'group'` entry is that button; any entry pointing its `groupId` at the group's `id` becomes a member.

0 commit comments

Comments
 (0)