DevTools
@whisq/devtools is a runtime hook that attaches a read-only inspection surface to globalThis.__WHISQ_DEVTOOLS__. A browser extension or a console script can read the hook to inspect signals, components, and a logged event stream. Apps opt in by calling register / log functions themselves — there is no auto-instrumentation.
Ships as a separate package, versioned lockstep with @whisq/core:
npm install --save-dev @whisq/devtoolsSignature
Section titled “Signature”import { connectDevTools, disconnectDevTools } from "@whisq/devtools";import type { DevToolsHook, SignalInfo, DevToolsEvent } from "@whisq/devtools";
function connectDevTools(): void;function disconnectDevTools(): void;Quick start
Section titled “Quick start”Wire the hook once, guarded by the dev flag, before mount():
import { mount } from "@whisq/core";import { connectDevTools } from "@whisq/devtools";import { App } from "./App";
if (import.meta.env.DEV) connectDevTools();
mount(App({}), document.getElementById("app")!);Then register the signals and components you want to inspect. No magic — you pick what’s interesting:
import { signal } from "@whisq/core";
export const todos = signal<Todo[]>([]);
if (import.meta.env.DEV) { // Safe: globalThis.__WHISQ_DEVTOOLS__ exists after connectDevTools(). globalThis.__WHISQ_DEVTOOLS__?.registerSignal("todos", todos);}Runtime hook (window.__WHISQ_DEVTOOLS__)
Section titled “Runtime hook (window.__WHISQ_DEVTOOLS__)”After connectDevTools(), the hook lives on the global object under __WHISQ_DEVTOOLS__ and implements the DevToolsHook interface:
interface DevToolsHook { version: string;
// Signals registerSignal(name: string, signal: ReadonlySignal<unknown>): void; unregisterSignal(name: string): void; getSignals(): SignalInfo[];
// Components registerComponent(name: string): void; unregisterComponent(name: string): void; getComponents(): string[];
// Event log logEvent(type: string, data: Record<string, unknown>): void; getEvents(): DevToolsEvent[]; clearEvents(): void;}
interface SignalInfo { name: string; value: unknown;}
interface DevToolsEvent { type: string; data: Record<string, unknown>; timestamp: number;}| Method | Use |
|---|---|
registerSignal(name, signal) | Add a named signal to the hook’s ledger. |
unregisterSignal(name) | Remove a named signal — useful in HMR boundaries or component unmount. |
getSignals() | Snapshot of all registered signals. Reads via signal.peek(), so inspection does not subscribe the reader. |
registerComponent(name) | Mark a component as active. |
unregisterComponent(name) | Remove an active component. |
getComponents() | Snapshot of active component names. |
logEvent(type, data) | Append a timestamped event to the log (type + free-form data). |
getEvents() | Snapshot of the event log (copy; mutating the return doesn’t touch the hook). |
clearEvents() | Empty the event log. |
Note on version: the hook reports its own internal protocol version (currently "0.0.1-alpha.0"), independent of the @whisq/devtools package version. That string is what browser extensions check for compatibility.
disconnectDevTools()
Section titled “disconnectDevTools()”Removes globalThis.__WHISQ_DEVTOOLS__. Useful when an HMR or test run re-initialises the app:
import { connectDevTools, disconnectDevTools } from "@whisq/devtools";
// Register teardown BEFORE connecting — the dispose hook runs on every HMR// cycle, and we want it ready for the first hot update after this module load.if (import.meta.hot) { import.meta.hot.dispose(() => disconnectDevTools());}if (import.meta.env.DEV) connectDevTools();Without teardown, a hot reload can leave a stale hook pointing at disposed signals — getSignals() would then .peek() values that no longer fire subscribers.
When to call connectDevTools()
Section titled “When to call connectDevTools()”- Dev builds only. Guard with
import.meta.env.DEV(or your bundler’s equivalent) so the hook doesn’t ship to production. - Before
mount(). Component setup functions can then callregisterComponent(...)/registerSignal(...)during first render. - Once, at module scope. Calling it again overwrites the hook — idempotent in practice but wasteful.
Browser extension
Section titled “Browser extension”There is no official Whisq browser extension yet. The hook is designed to be forward-compatible with one — the version field and the shape of DevToolsHook are the contract a future extension will consume. Until then, the hook is inspectable from the browser console:
// In the browser console, on a running dev build:__WHISQ_DEVTOOLS__?.getSignals();__WHISQ_DEVTOOLS__?.getComponents();__WHISQ_DEVTOOLS__?.getEvents();Semantics
Section titled “Semantics”- Passive surface.
connectDevTools()only exposes the hook object. It does not monkey-patchsignal(), wrapcomponent(), or intercept effects. What the hook knows is exactly what you’ve told it. - No subscriptions.
getSignals()reads via.peek(), so nothing in the hook tracks dependencies — inspecting from the console won’t re-run effects. - No-op on double-register.
registerSignal(name, …)with an existing name replaces the previous entry viaMap.set— no throw, no warn. Same forregisterComponent(Set.addignores duplicates). getSignals()/getEvents()return copies.getEvents()spreads into a new array, so mutating the return doesn’t touch the hook’s internal log. Registered signals are held by reference, but theSignalInfosnapshot is fresh on every call.
See also
Section titled “See also”/api/imports/#whisqdevtools— sub-path imports for DevTools.- Performance guide → Profiling with DevTools — where the hook fits in a performance-investigation workflow.
signal()—peek()(untracked read) is whatgetSignals()uses to snapshot values without subscribing.
Docs current to v0.1.0-alpha.9 . All releases →