@nice-code/devtools
v0.67.0
Published
One-call devtools for nice-code — actions, state, realms, wire traffic and a live topology, from the browser, your server, and your Durable Objects.
Downloads
2,324
Readme
@nice-code/devtools
For standalone windows and deployed-session lifecycle management, use
@nice-code/devtools-cli. The Vite plugin remains the zero-config
path while developing an app locally.
Docs: nicecode.io — guides, integrations, and the full API surface. Working with an AI assistant? Point it at nicecode.io/llms-devtools.txt (the devtools suite) or nicecode.io/llms.txt (the whole stack) — the complete, current docs flattened into plain text.
Devtools for nice-code — actions, state, realms, wire traffic, and a live topology of your frontends and backends — in a standalone window, from one function call per runtime:
| Entry point | For | Call |
| --- | --- | --- |
| @nice-code/devtools | your web app | createNiceDevtools() |
| @nice-code/devtools/server | a long-lived server (Bun, Node, …) | createNiceServerDevtools() |
| @nice-code/devtools/server | a Cloudflare Durable Object | createNiceDurableObjectDevtools() |
| @nice-code/devtools/window | the window page (the Vite plugin serves it for you) | mountDevtoolsWindow() |
| @nice-code/devtools/remote | a deployed staging frontend joining a relay session — the import is the production strip | enableRemoteDevtoolsSession() |
All three producers derive their devtools identity — label, clientId, coordEnvId — from the
RuntimeCoordinate you already hand them. There is nothing to restate.
Full guide: nicecode.io/devtools/window · nicecode.io/devtools/backends
Install
bun add @nice-code/devtools
bun add -d @nice-code/devtools-vite@nice-code/action, @nice-code/realm and @nice-code/state are peer dependencies — the window
renders a panel for each. React is an optional peer: the ./server entry is React-free and runs
unchanged in Node, Bun and workerd.
Setup
Add the Vite plugin. It serves the devtools window page and runs the relay:
import { niceDevtools } from "@nice-code/devtools-vite";
export default defineConfig({ plugins: [react(), niceDevtools()] });Wire the app in one place:
import { createNiceDevtools } from "@nice-code/devtools";
export const devtools = createNiceDevtools({
topologyId: "my-app", // names the devtools topology this app belongs to
runtime: getRuntime, // an ActionRuntime, or a getter for one
domains: [act_root], // the ROOT action domains to observe
stores: { UserStore }, // the stores the State panel lists
});Hand its three seams to the code that produces the data:
connectChannel(runtime, channel, { wireTap: devtools.wireTap }); // per connection
connectRealm(gameRealm, { devtools: devtools.realm("game") }); // per realm
<devtools.LauncherButton /> // once, anywhereThat is all of it. There is no devtools.html to commit, no window entry module, no second
rollupOptions.input.
What it costs
In production, nothing. enabled is false, no core is constructed, wireTap and realm()
return undefined, and the launcher renders null. Handing connectRealm and connectChannel an
absent tap is what makes them skip the observation work rather than measure frames into a sink.
(A staging flavor build opts back in explicitly — enabled: true behind a build flag; see
"Deployed staging" below.)
In development, nothing until a window attaches. The bridge subscribes to no scope while nobody is listening. That matters more than it sounds: a realm's projected view is a structured clone per patch tick, and paying for it with no reader is exactly the cost this design refuses.
Beyond the one call
devtools.internals— the live cores and bridge host, for pausing a core or contributing a scope.useDevtoolsWindowAttached()/useDevtoolsClientIdentity()— React hooks for reflecting the window's presence in your own UI.@nice-code/devtools/window—mountDevtoolsWindow(), if you host the window page yourself (a non-Vite dev server). Point the app at it withwindowUrl.startAppDevtoolsBridge()— the seamcreateNiceDevtoolsis built on.
Observing backends
A long-lived server
It dials the relay itself. One call, the mirror of the frontend's:
import { createNiceServerDevtools } from "@nice-code/devtools/server";
export const devtools = createNiceServerDevtools({
topologyId: DEVTOOLS_TOPOLOGY, // the same id your frontend passes; share the constant
runtime, // label, clientId and coordEnvId all derived from it
realms: { match: engine },
domains: [act_root],
});
serveChannel(runtime, channel, { storage, carriers, wireTap: devtools.wireTap });Pass topologyId: ["web-app", "admin-app"] when one backend serves several apps: it joins both relay
rooms and appears in both devtools windows, while the two apps' frontends stay invisible to each
other.
A Cloudflare Durable Object
A DO cannot dial out — an outbound socket pins it awake — so the window dials in: a plain wire connection to the DO's ordinary WebSocket endpoint (the same one its clients use), where devtools rides the wire mux as its own token-gated protocol. No separate dev route.
import { createNiceDurableObjectDevtools } from "@nice-code/devtools/server";
const devtools = createNiceDurableObjectDevtools(ctx, {
runtime,
stage: env.DEVTOOLS_ENV ?? "development", // required; "production" disables everything
token: env.DEVTOOLS_TOKEN, // unset ⇒ inert. No token, no protocol, no bridge, no cost.
realms: { match: () => this.engine },
persistCounters: true,
});
this.server = serveDurableObject(ctx, channel, { runtime, devtools });Point the window at the DO's endpoint from the dev server, which is the only place that can hold
the token without publishing it — never as a VITE_ var:
niceDevtools({
backends: [{ url: "ws://localhost:8787/match/ws", token: env.DEVTOOLS_DO_TOKEN }],
});See nicecode.io/devtools/backends.
Deployed staging (never production)
The same window reaches staging/dev deployments, triple-gated and fail-closed:
- Dial into a deployed backend — the window's ⧉ backend control takes a
wss://URL + the token from your deploy config (a wrangler secret; no token ⇒ the DO serves no devtools at all). Guide: nicecode.io/devtools/remote-backends. - Stream a deployed frontend — producers dial out to a short-lived, token-authenticated
relay session (
@nice-code/devtools-relay/cloudflareis the deployable relay). The dial code lives behind@nice-code/devtools/remote, so a production bundle that never imports it contains none of it; a staging build opts its bridge in withenabled: trueand streams shape, not values (streamContents: false). Guide: nicecode.io/devtools/remote-sessions.
Production cannot participate by construction: the remote entry is absent from a production bundle,
production configs carry no credentials, and both the relay and every host refuse
env: "production" at runtime.
