devframe 0.5.3 → 0.6.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_shared-CUFqO4kJ.mjs → _shared-bWRzeSa0.mjs} +2 -3
- package/dist/adapters/build.d.mts +1 -1
- package/dist/adapters/build.mjs +7 -7
- package/dist/adapters/cli.d.mts +1 -1
- package/dist/adapters/cli.mjs +2 -2
- package/dist/adapters/dev.d.mts +8 -2
- package/dist/adapters/dev.mjs +1 -1
- package/dist/adapters/embedded.d.mts +1 -1
- package/dist/adapters/mcp.d.mts +1 -1
- package/dist/adapters/mcp.mjs +4 -4
- package/dist/client/index.d.mts +148 -6
- package/dist/client/index.mjs +272 -49
- package/dist/constants.d.mts +17 -1
- package/dist/constants.mjs +17 -1
- package/dist/context-3x_bgBgZ.mjs +48 -0
- package/dist/{context-DTRcO_UH.d.mts → context-o-TwncyP.d.mts} +1 -1
- package/dist/{dev-Cv43GfqM.mjs → dev-CSpLSEVh.mjs} +39 -13
- package/dist/{devframe-BuR6n9ZD.d.mts → devframe-BwfavB78.d.mts} +365 -15
- package/dist/{diagnostics-GitRGKbr.mjs → diagnostics-BXwBQmoN.mjs} +1 -1
- package/dist/{dump-9lKIJTLh.mjs → dump-DoXkj4ku.mjs} +1 -1
- package/dist/helpers/vite.d.mts +1 -1
- package/dist/helpers/vite.mjs +11 -11
- package/dist/{host-h3-Dgpgr1Ul.mjs → host-h3-C5SZlk03.mjs} +134 -6
- package/dist/{index-C7M1hnvL.d.mts → index-CxaPKDXX.d.mts} +2 -2
- package/dist/{index-DH2sBIwd.d.mts → index-DXHWuwJk.d.mts} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +1 -1
- package/dist/node/auth.d.mts +43 -23
- package/dist/node/auth.mjs +84 -37
- package/dist/node/hub-internals.d.mts +2 -2
- package/dist/node/hub-internals.mjs +2 -2
- package/dist/node/index.d.mts +22 -6
- package/dist/node/index.mjs +4 -4
- package/dist/recipes/open-helpers.d.mts +1 -1
- package/dist/recipes/open-helpers.mjs +3 -3
- package/dist/revoke-ENfxWkFK.mjs +79 -0
- package/dist/rpc/dump.d.mts +1 -1
- package/dist/rpc/dump.mjs +1 -1
- package/dist/rpc/index.d.mts +3 -3
- package/dist/rpc/index.mjs +4 -4
- package/dist/rpc/transports/ws-client.d.mts +1 -1
- package/dist/rpc/transports/ws-client.mjs +2 -2
- package/dist/rpc/transports/ws-server.d.mts +2 -2
- package/dist/rpc/transports/ws-server.mjs +124 -60
- package/dist/{serialization-BD_qXGjd.mjs → serialization-DpLXCy13.mjs} +1 -1
- package/dist/{server-wHlpcdZ9.d.mts → server-oju7PTi2.d.mts} +30 -3
- package/dist/{server-BBaBJaUL.mjs → server-wyY3zh67.mjs} +18 -14
- package/dist/{shared-state-u0y123fi.mjs → shared-state-D4PPieA0.mjs} +4 -4
- package/dist/{shared-state-BlBNYziY.mjs → storage-l2ezeend.mjs} +128 -6
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-BkkQ0Txg.d.mts → types-DZEx4ffs.d.mts} +9 -1
- package/dist/utils/crypto-token.d.mts +24 -0
- package/dist/utils/crypto-token.mjs +45 -0
- package/dist/utils/events.d.mts +1 -1
- package/dist/utils/hash.mjs +1 -1
- package/dist/utils/launch-editor.mjs +1 -1
- package/dist/utils/open.mjs +1 -1
- package/dist/utils/scope.d.mts +11 -0
- package/dist/utils/scope.mjs +15 -0
- package/dist/utils/shared-state.d.mts +1 -1
- package/dist/utils/shared-state.mjs +1 -1
- package/dist/utils/streaming-channel.d.mts +1 -1
- package/dist/utils/structured-clone.mjs +1 -1
- package/dist/{ws-client-CVYX9niP.d.mts → ws-client-D0z0pOhn.d.mts} +1 -1
- package/dist/ws-server-BVVMDo2n.d.mts +103 -0
- package/package.json +12 -7
- package/skills/devframe/SKILL.md +123 -23
- package/skills/devframe/templates/counter-devframe.ts +14 -4
- package/skills/devframe/templates/spa-devframe.ts +13 -3
- package/skills/devframe/templates/vite-client.ts +3 -2
- package/dist/context-DaKmhhHY.mjs +0 -164
- package/dist/revoke-CL0LSAN9.mjs +0 -803
- package/dist/utils/human-id.d.mts +0 -8
- package/dist/utils/human-id.mjs +0 -769
- package/dist/ws-server-C1LjmRnp.d.mts +0 -61
- /package/dist/{define-CW9gLnyG.mjs → define-BLWPsH6y.mjs} +0 -0
- /package/dist/{diagnostics-reporter-CBBZwoMv.mjs → diagnostics-reporter-CsIG85Q5.mjs} +0 -0
- /package/dist/{hash-bwOD8RgU.mjs → hash-DFc5WwhO.mjs} +0 -0
- /package/dist/{launch-editor-BbNhtg7b.mjs → launch-editor-CgXBgviI.mjs} +0 -0
- /package/dist/{open-DiQn6zCH.mjs → open-Dusa2Zzd.mjs} +0 -0
- /package/dist/{structured-clone-PdCZwt7F.mjs → structured-clone-CeZOHvkd.mjs} +0 -0
- /package/dist/{structured-clone-jegjz0hM.mjs → structured-clone-XpHLZ8nr.mjs} +0 -0
- /package/dist/{transports-DTFoMUbE.mjs → transports-BmDVn4SF.mjs} +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/utils/crypto-token.ts
|
|
2
|
+
const HEX = "0123456789abcdef";
|
|
3
|
+
/**
|
|
4
|
+
* Generate a high-entropy, URL-safe (hex) random token suitable for use as a
|
|
5
|
+
* bearer credential — e.g. the persistent client auth token or an ephemeral
|
|
6
|
+
* remote-dock token. Defaults to 16 bytes (128 bits) of entropy.
|
|
7
|
+
*/
|
|
8
|
+
function randomToken(byteLength = 16) {
|
|
9
|
+
const bytes = new Uint8Array(byteLength);
|
|
10
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
11
|
+
let out = "";
|
|
12
|
+
for (let i = 0; i < bytes.length; i++) out += HEX[bytes[i] >> 4] + HEX[bytes[i] & 15];
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generate a uniformly-distributed string of decimal digits using rejection
|
|
17
|
+
* sampling to avoid modulo bias. Intended for short, human-typed one-time
|
|
18
|
+
* codes (e.g. a 6-digit authentication code). Leading zeros are preserved.
|
|
19
|
+
*/
|
|
20
|
+
function randomDigits(length) {
|
|
21
|
+
const limit = 250;
|
|
22
|
+
const buf = new Uint8Array(1);
|
|
23
|
+
let out = "";
|
|
24
|
+
while (out.length < length) {
|
|
25
|
+
globalThis.crypto.getRandomValues(buf);
|
|
26
|
+
if (buf[0] < limit) out += String(buf[0] % 10);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Constant-time string equality. Compares every character so the comparison
|
|
32
|
+
* time does not depend on the position of the first mismatch, mitigating
|
|
33
|
+
* timing side-channels when verifying secrets.
|
|
34
|
+
*
|
|
35
|
+
* Length is treated as public (it short-circuits on differing lengths), which
|
|
36
|
+
* is appropriate for fixed-length codes and tokens.
|
|
37
|
+
*/
|
|
38
|
+
function timingSafeEqual(a, b) {
|
|
39
|
+
if (a.length !== b.length) return false;
|
|
40
|
+
let mismatch = 0;
|
|
41
|
+
for (let i = 0; i < a.length; i++) mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
42
|
+
return mismatch === 0;
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { randomDigits, randomToken, timingSafeEqual };
|
package/dist/utils/events.d.mts
CHANGED
package/dist/utils/hash.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as hash } from "../hash-
|
|
1
|
+
import { t as hash } from "../hash-DFc5WwhO.mjs";
|
|
2
2
|
export { hash };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as launchEditor } from "../launch-editor-
|
|
1
|
+
import { t as launchEditor } from "../launch-editor-CgXBgviI.mjs";
|
|
2
2
|
export { launchEditor };
|
package/dist/utils/open.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as open } from "../open-
|
|
1
|
+
import { t as open } from "../open-Dusa2Zzd.mjs";
|
|
2
2
|
export { open };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//#region src/utils/scope.d.ts
|
|
2
|
+
/** Whether a name is already namespaced (contains a `:` separator). */
|
|
3
|
+
declare function isQualifiedName(name: string): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Prefix a bare name with `<namespace>:`. Names that already contain a
|
|
6
|
+
* `:` are returned unchanged, so callers can reference another scope's
|
|
7
|
+
* ids explicitly (e.g. `ctx.rpc.call('other-plugin:fn')`).
|
|
8
|
+
*/
|
|
9
|
+
declare function qualifyName(namespace: string, name: string): string;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { isQualifiedName, qualifyName };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/utils/scope.ts
|
|
2
|
+
/** Whether a name is already namespaced (contains a `:` separator). */
|
|
3
|
+
function isQualifiedName(name) {
|
|
4
|
+
return name.includes(":");
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Prefix a bare name with `<namespace>:`. Names that already contain a
|
|
8
|
+
* `:` are returned unchanged, so callers can reference another scope's
|
|
9
|
+
* ids explicitly (e.g. `ctx.rpc.call('other-plugin:fn')`).
|
|
10
|
+
*/
|
|
11
|
+
function qualifyName(namespace, name) {
|
|
12
|
+
return isQualifiedName(name) ? name : `${namespace}:${name}`;
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { isQualifiedName, qualifyName };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as SharedStatePatch, F as ImmutableObject, I as ImmutableSet, L as SharedState, M as Immutable, N as ImmutableArray, P as ImmutableMap, R as SharedStateEvents, V as createSharedState, z as SharedStateOptions } from "../devframe-BwfavB78.mjs";
|
|
2
2
|
export { Immutable, ImmutableArray, ImmutableMap, ImmutableObject, ImmutableSet, SharedState, SharedStateEvents, SharedStateOptions, SharedStatePatch, createSharedState };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as createSharedState } from "../shared-state-
|
|
1
|
+
import { t as createSharedState } from "../shared-state-D4PPieA0.mjs";
|
|
2
2
|
export { createSharedState };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as
|
|
1
|
+
import { A as createStreamReader, C as BufferedChunk, D as StreamReader, E as StreamErrorPayload, O as StreamSink, T as CreateStreamSinkOptions, j as createStreamSink, k as StreamSinkEvents, w as CreateStreamReaderOptions } from "../devframe-BwfavB78.mjs";
|
|
2
2
|
export { BufferedChunk, CreateStreamReaderOptions, CreateStreamSinkOptions, StreamErrorPayload, StreamReader, StreamSink, StreamSinkEvents, createStreamReader, createStreamSink };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as structuredCloneStringify, n as structuredCloneParse, r as structuredCloneSerialize, t as structuredCloneDeserialize } from "../structured-clone-
|
|
1
|
+
import { i as structuredCloneStringify, n as structuredCloneParse, r as structuredCloneSerialize, t as structuredCloneDeserialize } from "../structured-clone-CeZOHvkd.mjs";
|
|
2
2
|
export { structuredCloneDeserialize, structuredCloneParse, structuredCloneSerialize, structuredCloneStringify };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { v as RpcFunctionDefinitionAny } from "./types-DZEx4ffs.mjs";
|
|
2
|
+
import { BirpcGroup, ChannelOptions } from "birpc";
|
|
3
|
+
import { Peer } from "crossws";
|
|
4
|
+
import { NodeAdapter } from "crossws/adapters/node";
|
|
5
|
+
import { Server } from "node:http";
|
|
6
|
+
import { Server as Server$1, ServerOptions } from "node:https";
|
|
7
|
+
|
|
8
|
+
//#region src/rpc/transports/ws-server.d.ts
|
|
9
|
+
interface DevframeNodeRpcSessionMeta {
|
|
10
|
+
id: number;
|
|
11
|
+
/** The crossws peer backing this session's socket. */
|
|
12
|
+
peer?: Peer;
|
|
13
|
+
clientAuthToken?: string;
|
|
14
|
+
isTrusted?: boolean;
|
|
15
|
+
subscribedStates: Set<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Streams this session has subscribed to via
|
|
18
|
+
* `rpc.streaming.subscribe(channel, id)`. Tracked here for O(1) cleanup
|
|
19
|
+
* on disconnect; the wire format is `${channel}\x1F${id}`.
|
|
20
|
+
*/
|
|
21
|
+
subscribedStreams?: Set<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Inbound streams this session is currently uploading to (via
|
|
24
|
+
* `rpc.streaming.upload(channel, id)`). Tracked for cleanup on
|
|
25
|
+
* disconnect; same wire format as `subscribedStreams`.
|
|
26
|
+
*/
|
|
27
|
+
uploadingStreams?: Set<string>;
|
|
28
|
+
}
|
|
29
|
+
interface WsRpcTransportOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Attach to an existing HTTP(S) server, sharing its port. Combine with
|
|
32
|
+
* `path` to bind the WS endpoint to a single route so it coexists with
|
|
33
|
+
* other upgrade handlers on the same server (e.g. a Vite dev server's HMR
|
|
34
|
+
* socket). The shared server's lifecycle is owned by the caller — closing
|
|
35
|
+
* this transport detaches the upgrade listener without closing the server.
|
|
36
|
+
*/
|
|
37
|
+
server?: Server | Server$1;
|
|
38
|
+
/** Port for a newly-created standalone WS server. */
|
|
39
|
+
port?: number;
|
|
40
|
+
/** Host for a newly-created standalone WS server. Defaults to `localhost`. */
|
|
41
|
+
host?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Restrict the WS endpoint to a single upgrade route (e.g. `/__devframe_ws`). When
|
|
44
|
+
* sharing a `server`, non-matching upgrade requests are left untouched for
|
|
45
|
+
* other listeners to handle, so devframe's socket can sit alongside
|
|
46
|
+
* framework sockets (Vite HMR, etc.).
|
|
47
|
+
*/
|
|
48
|
+
path?: string;
|
|
49
|
+
/**
|
|
50
|
+
* Destroy upgrade requests that don't match `path` instead of leaving them
|
|
51
|
+
* for other listeners. Enable this when devframe owns the shared server
|
|
52
|
+
* outright (nothing else handles its upgrades), so an off-route client is
|
|
53
|
+
* rejected promptly rather than left hanging. Default: `false`
|
|
54
|
+
* (coexist-friendly); servers this transport creates itself always
|
|
55
|
+
* destroy unmatched upgrades.
|
|
56
|
+
*/
|
|
57
|
+
destroyUnmatched?: boolean;
|
|
58
|
+
/** When set, a new https.Server is created and the WS endpoint is attached to it. */
|
|
59
|
+
https?: ServerOptions;
|
|
60
|
+
/**
|
|
61
|
+
* RPC function definitions, used by the per-call wire serializer to
|
|
62
|
+
* dispatch between strict-JSON and structured-clone encoding based
|
|
63
|
+
* on each function's `jsonSerializable` flag.
|
|
64
|
+
*
|
|
65
|
+
* When omitted, all messages fall back to structured-clone — safe but
|
|
66
|
+
* loses dev-time validation for `jsonSerializable: true` declarations.
|
|
67
|
+
*/
|
|
68
|
+
definitions?: ReadonlyMap<string, Pick<RpcFunctionDefinitionAny, 'jsonSerializable'>>;
|
|
69
|
+
onConnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void;
|
|
70
|
+
onDisconnected?: (peer: Peer, meta: DevframeNodeRpcSessionMeta) => void;
|
|
71
|
+
/** Override the default per-call serializer. Most callers should leave this unset. */
|
|
72
|
+
serialize?: ChannelOptions['serialize'];
|
|
73
|
+
/** Override the default per-call deserializer. Most callers should leave this unset. */
|
|
74
|
+
deserialize?: ChannelOptions['deserialize'];
|
|
75
|
+
}
|
|
76
|
+
interface WsRpcTransport {
|
|
77
|
+
/**
|
|
78
|
+
* The crossws node adapter driving the socket — exposes the connected
|
|
79
|
+
* `peers` and pub/sub. See https://crossws.h3.dev.
|
|
80
|
+
*/
|
|
81
|
+
ws: NodeAdapter;
|
|
82
|
+
/** Remove the upgrade listener from a shared `server` (a no-op otherwise). */
|
|
83
|
+
detach: () => void;
|
|
84
|
+
/**
|
|
85
|
+
* Tear the transport down deterministically: detach from a shared server,
|
|
86
|
+
* force-terminate every connected peer, and close any server this
|
|
87
|
+
* transport created itself (`port` / `https` modes).
|
|
88
|
+
*/
|
|
89
|
+
close: () => Promise<void>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Attach a WebSocket transport to an existing RPC group, powered by
|
|
93
|
+
* [crossws](https://crossws.h3.dev). Either attach to an existing HTTP(S)
|
|
94
|
+
* `server` (sharing its port, optionally scoped to a `path`), or let this
|
|
95
|
+
* helper create a standalone server from `port` / `host` / `https`.
|
|
96
|
+
*
|
|
97
|
+
* Returns the crossws node adapter plus `detach` (remove the upgrade
|
|
98
|
+
* listener from a shared `server`) and `close` (full deterministic
|
|
99
|
+
* teardown).
|
|
100
|
+
*/
|
|
101
|
+
declare function attachWsRpcTransport<ClientFunctions extends object, ServerFunctions extends object>(rpcGroup: BirpcGroup<ClientFunctions, ServerFunctions, false>, options?: WsRpcTransportOptions): WsRpcTransport;
|
|
102
|
+
//#endregion
|
|
103
|
+
export { attachWsRpcTransport as i, WsRpcTransport as n, WsRpcTransportOptions as r, DevframeNodeRpcSessionMeta as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devframe",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0-beta.1",
|
|
5
5
|
"description": "Framework for building generic devframes",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -40,13 +40,14 @@
|
|
|
40
40
|
"./rpc/transports/ws-server": "./dist/rpc/transports/ws-server.mjs",
|
|
41
41
|
"./types": "./dist/types/index.mjs",
|
|
42
42
|
"./utils/colors": "./dist/utils/colors.mjs",
|
|
43
|
+
"./utils/crypto-token": "./dist/utils/crypto-token.mjs",
|
|
43
44
|
"./utils/events": "./dist/utils/events.mjs",
|
|
44
45
|
"./utils/hash": "./dist/utils/hash.mjs",
|
|
45
|
-
"./utils/human-id": "./dist/utils/human-id.mjs",
|
|
46
46
|
"./utils/launch-editor": "./dist/utils/launch-editor.mjs",
|
|
47
47
|
"./utils/nanoid": "./dist/utils/nanoid.mjs",
|
|
48
48
|
"./utils/open": "./dist/utils/open.mjs",
|
|
49
49
|
"./utils/promise": "./dist/utils/promise.mjs",
|
|
50
|
+
"./utils/scope": "./dist/utils/scope.mjs",
|
|
50
51
|
"./utils/serve-static": "./dist/utils/serve-static.mjs",
|
|
51
52
|
"./utils/shared-state": "./dist/utils/shared-state.mjs",
|
|
52
53
|
"./utils/streaming-channel": "./dist/utils/streaming-channel.mjs",
|
|
@@ -71,12 +72,14 @@
|
|
|
71
72
|
"@valibot/to-json-schema": "^1.7.0",
|
|
72
73
|
"birpc": "^4.0.0",
|
|
73
74
|
"cac": "^7.0.0",
|
|
75
|
+
"crossws": "^0.4.8",
|
|
76
|
+
"destr": "^2.0.5",
|
|
74
77
|
"h3": "2.0.1-rc.22",
|
|
75
78
|
"mrmime": "^2.0.1",
|
|
76
|
-
"nostics": "^
|
|
79
|
+
"nostics": "^1.1.4",
|
|
77
80
|
"pathe": "^2.0.3",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
81
|
+
"ufo": "^1.6.4",
|
|
82
|
+
"valibot": "^1.4.1"
|
|
80
83
|
},
|
|
81
84
|
"devDependencies": {
|
|
82
85
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -93,7 +96,8 @@
|
|
|
93
96
|
"tinyglobby": "^0.2.16",
|
|
94
97
|
"tsdown": "^0.22.0",
|
|
95
98
|
"ua-parser-modern": "^0.1.1",
|
|
96
|
-
"whenexpr": "^0.1.2"
|
|
99
|
+
"whenexpr": "^0.1.2",
|
|
100
|
+
"ws": "^8.21.0"
|
|
97
101
|
},
|
|
98
102
|
"inlinedDependencies": {
|
|
99
103
|
"bundle-name": "4.1.0",
|
|
@@ -124,6 +128,7 @@
|
|
|
124
128
|
},
|
|
125
129
|
"scripts": {
|
|
126
130
|
"build": "tsdown",
|
|
127
|
-
"watch": "tsdown --watch"
|
|
131
|
+
"watch": "tsdown --watch",
|
|
132
|
+
"typecheck": "tsc --noEmit"
|
|
128
133
|
}
|
|
129
134
|
}
|
package/skills/devframe/SKILL.md
CHANGED
|
@@ -50,11 +50,16 @@ import { defineDevframe, defineRpcFunction } from 'devframe'
|
|
|
50
50
|
export default defineDevframe({
|
|
51
51
|
id: 'my-inspector',
|
|
52
52
|
name: 'My Inspector',
|
|
53
|
+
version: '1.0.0',
|
|
54
|
+
packageName: 'my-inspector',
|
|
55
|
+
homepage: 'https://github.com/me/my-inspector',
|
|
56
|
+
description: 'Inspects things and reports stats.',
|
|
53
57
|
icon: 'ph:magnifying-glass-duotone',
|
|
54
58
|
cli: { distDir: './client/dist' },
|
|
55
59
|
setup(ctx) {
|
|
56
|
-
ctx.
|
|
57
|
-
|
|
60
|
+
const my = ctx.scope('my-inspector') // preferred — auto-namespaces ids
|
|
61
|
+
my.rpc.register(defineRpcFunction({
|
|
62
|
+
name: 'get-stats', // stored as `my-inspector:get-stats`
|
|
58
63
|
type: 'static',
|
|
59
64
|
handler: () => ({ count: 42 }),
|
|
60
65
|
}))
|
|
@@ -62,13 +67,77 @@ export default defineDevframe({
|
|
|
62
67
|
})
|
|
63
68
|
```
|
|
64
69
|
|
|
70
|
+
**Recommended:** keep `version` / `packageName` / `homepage` / `description` in sync with your published package by sourcing them from `package.json` rather than hardcoding. The package's `name` maps to `packageName`; the devframe `name` is a separate display label. Use the JSON import-attribute form so it resolves under both bundlers and Node's native TypeScript execution:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
74
|
+
|
|
75
|
+
export default defineDevframe({
|
|
76
|
+
id: 'my-inspector',
|
|
77
|
+
name: 'My Inspector',
|
|
78
|
+
version: pkg.version,
|
|
79
|
+
packageName: pkg.name,
|
|
80
|
+
homepage: pkg.homepage,
|
|
81
|
+
description: pkg.description,
|
|
82
|
+
// …
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
65
86
|
`setup(ctx)` registers RPC functions, shared state, diagnostics, and any other devframe-level wiring. Host adapters can augment `ctx` with extra surfaces — for example, mounting into Vite DevTools via `createPluginFromDevframe(d)` exposes `docks`, `terminals`, `messages`, and `commands` on the augmented context, and the kit auto-derives an iframe dock entry from `id` / `name` / `icon` / `basePath`. For richer host-side behaviour (custom-render, terminals, palette commands) pass `options.setup` to `createPluginFromDevframe`.
|
|
66
87
|
|
|
67
88
|
See `templates/counter-devframe.ts` for a runnable counter example, `templates/spa-devframe.ts` for an SPA-ready shape, and `templates/vite-client.ts` for the author's client entry.
|
|
68
89
|
|
|
90
|
+
## Scoped context (preferred)
|
|
91
|
+
|
|
92
|
+
`ctx.scope(id)` (server) and `client.scope(id)` (browser) return a namespace-scoped view that auto-prefixes every RPC id, shared-state key, and streaming channel with `id:`, and adds a top-level persisted `settings` store. Prefer it over the raw `ctx.rpc` / client — you name the namespace once and register / call by bare name.
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
// server — setup(ctx)
|
|
96
|
+
const my = ctx.scope('my-inspector')
|
|
97
|
+
my.rpc.register(getStats) // -> my-inspector:get-stats
|
|
98
|
+
await my.rpc.call('get-stats') // invokeLocal, namespaced
|
|
99
|
+
const state = await my.rpc.sharedState('view') // -> my-inspector:view
|
|
100
|
+
await my.settings.project.set('theme', 'dark')
|
|
101
|
+
|
|
102
|
+
// browser — connectDevframe()
|
|
103
|
+
const my = (await connectDevframe()).scope('my-inspector')
|
|
104
|
+
const stats = await my.rpc.call('get-stats')
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Auto-namespacing.** Bare names get `id:` prepended; a name already containing `:` is treated as fully-qualified and passed through (so `my.rpc.call('other-tool:fn')` works). `register` only accepts bare names — passing a namespaced one throws `DF0034`.
|
|
108
|
+
- **Typed bare calls.** Define functions with bare names and augment the registry with `RpcDefinitionsToFunctionsWithNamespace<'my-inspector', typeof serverFunctions>` so the registry keys match the namespaced runtime ids; scoped `call('get-stats')` then stays typed.
|
|
109
|
+
- **`base`.** The scoped context keeps the raw context as `my.base` (and re-exposes `views` / `diagnostics` / `agent` / `host` / `cwd` / `mode` on the server).
|
|
110
|
+
|
|
111
|
+
### Settings
|
|
112
|
+
|
|
113
|
+
`my.settings` is a persisted key-value store at the **top level** of the scoped context (a sibling of `my.rpc`, not under it). Two scopes:
|
|
114
|
+
|
|
115
|
+
- `project` — per-workspace, persisted under the host's `workspace` storage dir.
|
|
116
|
+
- `global` — per-user, persisted under the host's `global` storage dir.
|
|
117
|
+
|
|
118
|
+
Both are file-backed on the server and synced to clients over the shared-state protocol, so a `set` on either side propagates everywhere and survives restarts. All methods are async.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
await my.settings.project.set('theme', 'dark')
|
|
122
|
+
await my.settings.project.get('theme') // 'dark'
|
|
123
|
+
await my.settings.global.all()
|
|
124
|
+
await my.settings.project.delete('theme')
|
|
125
|
+
const off = await my.settings.global.onChange(value => apply(value))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Type a namespace's settings shape by augmenting `DevframeSettingsRegistry`:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
declare module 'devframe' {
|
|
132
|
+
interface DevframeSettingsRegistry {
|
|
133
|
+
'my-inspector': { theme: 'light' | 'dark', recentFiles: string[] }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
69
138
|
## Project layout
|
|
70
139
|
|
|
71
|
-
Once a devframe grows past a handful of RPC functions, split them out — one file per function under `src/rpc/functions/`, with `src/rpc/index.ts` as the barrel. The `functions/` subdirectory leaves room for sibling files like `src/rpc/utils.ts` (helpers, type aliases) as the surface grows. Each function file exports a named const; the barrel collects them into a `const serverFunctions = [...] as const`
|
|
140
|
+
Once a devframe grows past a handful of RPC functions, split them out — one file per function under `src/rpc/functions/`, with `src/rpc/index.ts` as the barrel. The `functions/` subdirectory leaves room for sibling files like `src/rpc/utils.ts` (helpers, type aliases) as the surface grows. Each function file exports a named const with a **bare** name; the barrel collects them into a `const serverFunctions = [...] as const` and feeds the type-safe client registry recipe with the namespace-aware helper `RpcDefinitionsToFunctionsWithNamespace<'my-tool', typeof serverFunctions>` (it prefixes each bare name to match the namespaced runtime id the scope registers).
|
|
72
141
|
|
|
73
142
|
```ts
|
|
74
143
|
// src/rpc/functions/list-files.ts
|
|
@@ -76,7 +145,7 @@ import { defineRpcFunction } from 'devframe'
|
|
|
76
145
|
import { getMyToolContext } from '../../context'
|
|
77
146
|
|
|
78
147
|
export const listFiles = defineRpcFunction({
|
|
79
|
-
name: 'my-tool:list-files
|
|
148
|
+
name: 'list-files', // bare — the scope namespaces it to `my-tool:list-files`
|
|
80
149
|
type: 'query',
|
|
81
150
|
jsonSerializable: true,
|
|
82
151
|
setup: (ctx) => {
|
|
@@ -95,25 +164,34 @@ export const serverFunctions = [getCwd, listFiles] as const
|
|
|
95
164
|
|
|
96
165
|
declare module 'devframe' {
|
|
97
166
|
interface DevframeRpcServerFunctions
|
|
98
|
-
extends import('devframe/rpc').
|
|
167
|
+
extends import('devframe/rpc').RpcDefinitionsToFunctionsWithNamespace<'my-tool', typeof serverFunctions> {}
|
|
99
168
|
}
|
|
100
169
|
```
|
|
101
170
|
|
|
102
171
|
```ts
|
|
103
172
|
// src/devframe.ts
|
|
104
173
|
import { defineDevframe } from 'devframe/types'
|
|
174
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
105
175
|
import { setMyToolContext } from './context'
|
|
106
176
|
import { serverFunctions } from './rpc'
|
|
107
177
|
|
|
108
178
|
export default defineDevframe({
|
|
109
179
|
id: 'my-tool',
|
|
180
|
+
name: 'My Tool',
|
|
181
|
+
version: pkg.version,
|
|
182
|
+
packageName: pkg.name,
|
|
183
|
+
homepage: pkg.homepage,
|
|
184
|
+
description: pkg.description,
|
|
110
185
|
setup(ctx) {
|
|
186
|
+
const my = ctx.scope('my-tool')
|
|
111
187
|
setMyToolContext(ctx, { loaders: createLoaders() })
|
|
112
|
-
serverFunctions.forEach(fn =>
|
|
188
|
+
serverFunctions.forEach(fn => my.rpc.register(fn))
|
|
113
189
|
},
|
|
114
190
|
})
|
|
115
191
|
```
|
|
116
192
|
|
|
193
|
+
Note `setMyToolContext(ctx, …)` keys off the raw `ctx` (the same object the function `setup(ctx)` receives) — store the per-tool context on `ctx`, register through `my.rpc`.
|
|
194
|
+
|
|
117
195
|
### Sharing setup-time state via `src/context.ts`
|
|
118
196
|
|
|
119
197
|
When per-file RPCs need access to runtime values that `setup(ctx)` constructs once — streaming channels, shared state handles, watchers, loaders, caches — expose them through a `WeakMap<DevframeNodeContext, T>` in a sibling `src/context.ts`. This mirrors the framework's own `internalContextMap` in `packages/devframe/src/node/hub-internals/context.ts`. The WeakMap keys off the existing `DevframeNodeContext` so contexts are garbage-collected automatically when the host tears down.
|
|
@@ -153,12 +231,15 @@ Stateless RPCs and tiny demos can keep the inline shorthand inside `setup(ctx)`
|
|
|
153
231
|
'get-modules' // ✗ — may collide with other devframes sharing the host
|
|
154
232
|
```
|
|
155
233
|
|
|
234
|
+
A [scoped context](#scoped-context-preferred) applies this prefix for you — `ctx.scope('my-inspector').rpc.register({ name: 'get-modules' })` registers `my-inspector:get-modules`. Define and call by bare name through the scope; reach for full ids only via `ctx.base` or when targeting another tool. Dock / command IDs are host-level (not part of the scoped `rpc` surface) — prefix those by hand.
|
|
235
|
+
|
|
156
236
|
## DevframeNodeContext at a glance
|
|
157
237
|
|
|
158
238
|
`setup(ctx)` receives the framework-neutral server-side surface. Each host corresponds to a [docs](https://devfra.me/) page:
|
|
159
239
|
|
|
160
240
|
| Host | Purpose |
|
|
161
241
|
|------|---------|
|
|
242
|
+
| `ctx.scope(id)` | **Preferred** namespace-scoped view — auto-prefixed `rpc` + top-level `settings` store |
|
|
162
243
|
| `ctx.rpc` | Register RPC functions, broadcast, shared state, streaming channels |
|
|
163
244
|
| `ctx.views` | Serve static files via `hostStatic(base, distDir)` |
|
|
164
245
|
| `ctx.diagnostics` | Structured diagnostics host (nostics) — register custom error codes |
|
|
@@ -175,7 +256,7 @@ import { defineRpcFunction } from 'devframe'
|
|
|
175
256
|
import * as v from 'valibot'
|
|
176
257
|
|
|
177
258
|
const getModules = defineRpcFunction({
|
|
178
|
-
name: '
|
|
259
|
+
name: 'get-modules', // bare — registered via `ctx.scope('my-inspector').rpc.register`
|
|
179
260
|
type: 'query',
|
|
180
261
|
jsonSerializable: true,
|
|
181
262
|
args: [v.object({ limit: v.number() })],
|
|
@@ -208,12 +289,13 @@ Set `jsonSerializable: true` when your handler returns plain JSON shapes — the
|
|
|
208
289
|
|
|
209
290
|
`agent: {...}` requires `jsonSerializable: true` (registration throws `DF0019` otherwise). MCP tools speak JSON — opting into the agent surface is also opting into JSON-only data.
|
|
210
291
|
|
|
211
|
-
`
|
|
292
|
+
Through the scope, `my.rpc.broadcast({ method, args, optional?, event?, filter? })` pushes to every connected client (method name namespaced), and `my.rpc.call(name, ...args)` invokes a server function locally without going through transport (the scoped form of `ctx.rpc.invokeLocal`, useful for cross-function composition).
|
|
212
293
|
|
|
213
294
|
## Shared state
|
|
214
295
|
|
|
215
296
|
```ts
|
|
216
|
-
const
|
|
297
|
+
const my = ctx.scope('my-inspector')
|
|
298
|
+
const state = await my.rpc.sharedState('state', { // -> my-inspector:state
|
|
217
299
|
initialValue: { count: 0, items: [] as string[] },
|
|
218
300
|
})
|
|
219
301
|
|
|
@@ -232,7 +314,8 @@ state.mutate((draft) => {
|
|
|
232
314
|
For chunk-style data flowing in either direction — LLM deltas, log tails, build progress, file uploads, mic / screen-share frames — use a streaming channel instead of inventing `action + delta/end` events. The same `channel` object handles both directions:
|
|
233
315
|
|
|
234
316
|
```ts
|
|
235
|
-
const
|
|
317
|
+
const my = ctx.scope('my-inspector')
|
|
318
|
+
const channel = my.rpc.streaming.create<string>('tokens', { // -> my-inspector:tokens
|
|
236
319
|
replayWindow: 256, // server keeps last N chunks per stream id
|
|
237
320
|
closedStreamRetention: 30_000, // ms to hold finished streams for late subscribers
|
|
238
321
|
})
|
|
@@ -252,8 +335,8 @@ stream.writable // WritableStream<T> for `pipeTo`-style consumption
|
|
|
252
335
|
// Convenience — start + pipe in one call:
|
|
253
336
|
await channel.pipeFrom(sourceReadable, { id: 'optional' })
|
|
254
337
|
|
|
255
|
-
// Client
|
|
256
|
-
const reader = rpc.streaming.subscribe<string>('
|
|
338
|
+
// Client — my = (await connectDevframe()).scope('my-inspector')
|
|
339
|
+
const reader = my.rpc.streaming.subscribe<string>('tokens', streamId) // -> my-inspector:tokens
|
|
257
340
|
for await (const token of reader) renderToken(token)
|
|
258
341
|
// Or: reader.readable.pipeTo(domWritable)
|
|
259
342
|
reader.cancel() // server `stream.signal` aborts
|
|
@@ -264,9 +347,9 @@ reader.cancel() // server `stream.signal` aborts
|
|
|
264
347
|
The same channel exposes `openInbound()` for the server side of a client→server upload. Pair it with a normal action that returns the id:
|
|
265
348
|
|
|
266
349
|
```ts
|
|
267
|
-
// Server
|
|
268
|
-
|
|
269
|
-
name: 'my-inspector:upload-file
|
|
350
|
+
// Server — my = ctx.scope('my-inspector')
|
|
351
|
+
my.rpc.register(defineRpcFunction({
|
|
352
|
+
name: 'upload-file', // -> my-inspector:upload-file
|
|
270
353
|
type: 'action',
|
|
271
354
|
args: [v.object({ name: v.string() })],
|
|
272
355
|
returns: v.object({ uploadId: v.string() }),
|
|
@@ -279,9 +362,9 @@ ctx.rpc.register(defineRpcFunction({
|
|
|
279
362
|
},
|
|
280
363
|
}))
|
|
281
364
|
|
|
282
|
-
// Client
|
|
283
|
-
const { uploadId } = await rpc.call('
|
|
284
|
-
const upload = rpc.streaming.upload<Uint8Array>('
|
|
365
|
+
// Client — my = (await connectDevframe()).scope('my-inspector')
|
|
366
|
+
const { uploadId } = await my.rpc.call('upload-file', { name: 'foo' })
|
|
367
|
+
const upload = my.rpc.streaming.upload<Uint8Array>('files', uploadId) // -> my-inspector:files
|
|
285
368
|
fileReadable.pipeTo(upload.writable, { signal: upload.signal })
|
|
286
369
|
```
|
|
287
370
|
|
|
@@ -411,10 +494,11 @@ Authors bring their own SPA (any framework or plain HTML). Client entry:
|
|
|
411
494
|
```ts
|
|
412
495
|
import { connectDevframe } from 'devframe/client'
|
|
413
496
|
|
|
414
|
-
const
|
|
415
|
-
// await
|
|
497
|
+
const client = await connectDevframe()
|
|
498
|
+
// await client.ensureTrusted() // WS mode only — blocks until server accepts
|
|
499
|
+
const my = client.scope('my-inspector') // preferred — namespaced calls
|
|
416
500
|
|
|
417
|
-
const data = await rpc.call('
|
|
501
|
+
const data = await my.rpc.call('get-stats', { limit: 10 })
|
|
418
502
|
```
|
|
419
503
|
|
|
420
504
|
`connectDevframe` auto-detects the backend via `/.devframe/.connection.json`:
|
|
@@ -422,7 +506,7 @@ const data = await rpc.call('my-inspector:get-stats', { limit: 10 })
|
|
|
422
506
|
- **websocket** (dev mode) — full read/write, requires auth handshake. Listen for token updates on the `devframe-auth` BroadcastChannel.
|
|
423
507
|
- **static** (build / spa output) — read-only, resolves calls from the baked RPC dump.
|
|
424
508
|
|
|
425
|
-
Use `rpc.sharedState
|
|
509
|
+
Use `my.rpc.sharedState(key)` for observable state, `my.rpc.register(defineRpcFunction(...))` to receive server broadcasts, `my.rpc.callOptional(...)` when a missing handler should resolve to `undefined` instead of throwing, and `my.settings.{project,global}` for persisted per-workspace / per-user settings synced from the server.
|
|
426
510
|
|
|
427
511
|
## Build dumps
|
|
428
512
|
|
|
@@ -468,8 +552,8 @@ Devframe re-exports a curated set of helpers under `devframe/utils/*`. They are
|
|
|
468
552
|
| `launchEditor` from `devframe/utils/launch-editor` | `launch-editor` | Open `file:line:column` in the user's editor (optional `editor` arg) |
|
|
469
553
|
| `hash` from `devframe/utils/hash` | `ohash` | Stable structural hash — cache keys, dedup |
|
|
470
554
|
| `structuredClone{Serialize,Deserialize,Stringify,Parse}` from `devframe/utils/structured-clone` | `structured-clone-es` | JSON-safe round-trip of `Map` / `Set` / `Date` / `BigInt` / cycles |
|
|
471
|
-
| `humanId` from `devframe/utils/human-id` | `human-id` | Human-readable IDs (`bright-orange-tiger`) |
|
|
472
555
|
| `nanoid` from `devframe/utils/nanoid` | (vendored) | URL-safe random IDs |
|
|
556
|
+
| `randomToken` / `randomDigits` / `timingSafeEqual` from `devframe/utils/crypto-token` | (native WebCrypto) | CSPRNG bearer tokens, one-time codes, constant-time compare |
|
|
473
557
|
| `promiseWithResolver` from `devframe/utils/promise` | — | Externally-controlled `Promise` |
|
|
474
558
|
| `createEventEmitter` from `devframe/utils/events` | — | Typed event bus |
|
|
475
559
|
| `createSharedState` from `devframe/utils/shared-state` | (immer internal) | Immutable state container (see `ctx.rpc.sharedState`) |
|
|
@@ -478,6 +562,20 @@ Devframe re-exports a curated set of helpers under `devframe/utils/*`. They are
|
|
|
478
562
|
|
|
479
563
|
For "open file in editor" + "reveal in finder", prefer the prebuilt `openHelpers` RPC recipe — it wires the two utilities into named RPC functions ready to register.
|
|
480
564
|
|
|
565
|
+
## Security (secure by default)
|
|
566
|
+
|
|
567
|
+
RPC handlers run with the full privileges of the host process, so the boundary that matters is who may connect. Keep that boundary tight:
|
|
568
|
+
|
|
569
|
+
- **`auth` defaults to `true`** — dev-mode connections must authenticate before calls are accepted. Devframe ships the node primitives (`exchangeTempAuthCode`, `verifyAuthToken` in `devframe/node/auth`); the host adapter (e.g. Vite DevTools) provides the interactive `devframe:anonymous:auth` + `devframe:auth:exchange` handlers and auth UI.
|
|
570
|
+
- **`auth: false` trusts every reachable connection.** Use it only for single-user `localhost` tools. Never combine it with a non-loopback bind host, a tunnel, or a shared/CI environment. The default bind host is already `localhost`.
|
|
571
|
+
- **Authentication** exchanges a 6-digit one-time code (shown in the developer's terminal) for a node-issued bearer token via `requestTrustWithCode(code)`. The code is single-use, expires in 5 min, compared in constant time, and rotates after repeated failures — show it only in the terminal, never over the network.
|
|
572
|
+
- **Magic-link (optional):** print `buildOtpAuthUrl(origin)` — `<origin>/?devframe_otp=<code>`. `connectDevframe` reads the code, exchanges it, and strips it from the URL. Integrations can opt out (`otpParam: false`) and drive it via the exposed `authenticateWithUrlOtp(rpc)` / `consumeOtpFromUrl()` client utilities. Only the single-use code rides the URL, never the bearer; treat the printed link like the code itself.
|
|
573
|
+
- **Tokens are secrets.** The bearer token rides the WS URL (`?devframe_auth_token=…`) — serve over `wss://`/`https://` beyond loopback. Never log the token or code, never bake them into build output. Revoke via `revokeAuthToken(...)`; clients drop to untrusted on `devframe:auth:revoked`.
|
|
574
|
+
- **Authorize handlers.** Any trusted client can call any registered function — validate inputs, and mark state-changing functions `type: 'destructive'` so MCP/agent clients prompt first.
|
|
575
|
+
- **Origin-lock remote docks** (`originLock`) so a dock token is honored only from its expected origin.
|
|
576
|
+
|
|
577
|
+
See [Security](https://devfra.me/security) for the full reference.
|
|
578
|
+
|
|
481
579
|
## Testing
|
|
482
580
|
|
|
483
581
|
- Unit-test host classes with fake contexts.
|
|
@@ -489,6 +587,7 @@ For "open file in editor" + "reveal in finder", prefer the prebuilt `openHelpers
|
|
|
489
587
|
Devframe-level pages (one-tool, portable surface):
|
|
490
588
|
|
|
491
589
|
- [Devframe Definition](https://devfra.me/devframe-definition) — fields, runtime flags, multi-adapter wiring
|
|
590
|
+
- [Scoped Context](https://devfra.me/scoped-context) — `ctx.scope(id)`, auto-namespacing, the `settings` store
|
|
492
591
|
- [Adapters](https://devfra.me/adapters) — full reference for all deployment adapters
|
|
493
592
|
- [RPC](https://devfra.me/rpc) — types, schema, broadcasts, dumps
|
|
494
593
|
- [Shared State](https://devfra.me/shared-state) — patches, events, client-side mutation
|
|
@@ -497,6 +596,7 @@ Devframe-level pages (one-tool, portable surface):
|
|
|
497
596
|
- [Structured Diagnostics](https://devfra.me/diagnostics) — coded errors via `ctx.diagnostics`, register custom codes
|
|
498
597
|
- [Utilities](https://devfra.me/utilities) — bundled `devframe/utils/*` helpers (colors, hash, launchEditor, structured-clone, …)
|
|
499
598
|
- [Client](https://devfra.me/client) — auth handshake, modes, discovery
|
|
599
|
+
- [Security](https://devfra.me/security) — trust model, authentication, secure-by-default practices
|
|
500
600
|
- [Agent-Native](https://devfra.me/agent-native) — agent field, tools/resources, MCP + Claude Desktop
|
|
501
601
|
|
|
502
602
|
Host-specific extras (when mounting into Vite DevTools — other hosts have their own equivalents):
|
|
@@ -2,21 +2,31 @@
|
|
|
2
2
|
// `id` / `name` / `icon` when this definition is mounted into Vite
|
|
3
3
|
// DevTools via `createPluginFromDevframe(devframe)`.
|
|
4
4
|
import { defineDevframe, defineRpcFunction } from 'devframe'
|
|
5
|
+
// Recommended: source version/packageName/homepage/description from your
|
|
6
|
+
// package.json so the published metadata stays in sync. The import-attribute
|
|
7
|
+
// form resolves under both bundlers and Node's native TypeScript execution.
|
|
8
|
+
import pkg from '../package.json' with { type: 'json' }
|
|
5
9
|
|
|
6
10
|
let counter = 0
|
|
7
11
|
|
|
8
12
|
export default defineDevframe({
|
|
9
13
|
id: 'counter',
|
|
10
14
|
name: 'Counter',
|
|
15
|
+
version: pkg.version,
|
|
16
|
+
packageName: pkg.name,
|
|
17
|
+
homepage: pkg.homepage,
|
|
18
|
+
description: pkg.description,
|
|
11
19
|
icon: 'ph:counter-duotone',
|
|
12
20
|
setup(ctx) {
|
|
13
|
-
|
|
14
|
-
|
|
21
|
+
// Scoped context — auto-namespaces ids with `counter:`.
|
|
22
|
+
const my = ctx.scope('counter')
|
|
23
|
+
my.rpc.register(defineRpcFunction({
|
|
24
|
+
name: 'get', // -> counter:get
|
|
15
25
|
type: 'static',
|
|
16
26
|
handler: () => ({ count: counter }),
|
|
17
27
|
}))
|
|
18
|
-
|
|
19
|
-
name: 'counter:bump
|
|
28
|
+
my.rpc.register(defineRpcFunction({
|
|
29
|
+
name: 'bump', // -> counter:bump
|
|
20
30
|
type: 'action',
|
|
21
31
|
handler: () => ({ count: ++counter }),
|
|
22
32
|
}))
|