Skip to main content

API

Five entry points. The three plugin subpaths run in Node during development only, and each exports one function under the same name.

subpath exports runs in
nanostores-devtools connectDevtools, trackStores, untrack browser
nanostores-devtools/vite nanostoresDevtools Node, dev only
nanostores-devtools/webpack nanostoresDevtools Node, dev only
nanostores-devtools/rspack nanostoresDevtools Node, dev only
nanostores-devtools/runtime internal, injected by the plugin browser

nanostores-devtools/runtime is internal. The plugin injects an import of it into your modules during development, so it appears in your module graph. Never import it yourself.

connectDevtools(options?)

Opens the bridge and returns a DevtoolsHandle. Never throws, never needs await, and safe to call during server-side rendering. Called a second time while connected, it warns once and hands back the first handle, which is what makes a hot reload safe.

const handle = connectDevtools({ name: "my-app" });

if (!handle.connected) {
  console.info("No Redux DevTools extension on this page.");
}
option default what it does
name "nanostores" the entry in the extension’s dropdown
serializers [] your own rules for converting values, checked before ours
platformSerializers true our own rules for Headers, FormData and other platform classes
trace true capture a stack at each direct write
traceLimit 10 how many stack frames to capture
maxAge 500 how many rows the extension keeps
lifecycleEvents true draw the mount, unmount, register, unregister and hot reload rows
throttle [] hold these stores to one row a second
autoThrottle 10 writes a second above which a store is throttled
maxValueDepth 5 levels drawn below a class instance
maxValueMembers 100 members drawn per shape below a class instance

autoThrottle is the one default that drops rows: a store above 10 writes a second is held to one row a second for the rest of the session, and we warn once, naming the store. Pass your own threshold, or false to keep every row. throttle takes names as the tree writes them, "src/model.ts/$remaining", or a function over them.

A serializer draws a value the panel cannot read on its own, such as a MouseEvent, whose every field sits behind a getter. A rule is { match, convert }, and the first match in the array wins.

The plugin reads four comments next to a store, where a rename cannot lose them: // @nanostores-devtools:ignore keeps every store the statement below it makes out of the devtools, // @nanostores-devtools:throttle and // @nanostores-devtools:no-throttle set its row rate, and // @nanostores-devtools:max-members 25 caps how much of one binding the scan walks.

handle.disconnect() closes the bridge and lets the next connectDevtools() open a fresh one. It is there for a page that tears its app down and builds another one.

What each connectDevtools option costs has the full rules for the options, the serializers and the four comments.

trackStores(group, stores)

Registers stores the plugin cannot reach, under a top-level key you name.

  • group - the home those stores sit under. Name it after a file to land on the node the plugin already uses for that file.
  • stores - an object of name -> store. The key is the name the tree draws.

A second registration for the same group/name replaces the store held there, with no warning. A clash here is almost always a hot reload, and we cannot tell the two apart.

untrack(group)

Removes a group and every store in it. Draws one unregister row.

nanostoresDevtools(options?)

The bundler plugin. Exported from /vite, /webpack and /rspack, and the same function behind all three. It reads script files only: .js, .ts and the rest of that family, and nothing under node_modules.

option default what it does
fileKey the home unchanged rewrites the path shown as a store’s home
adoptFactories true wrap named calls we do not recognise: true or false
storeTypes the packages we ship which kind a package’s export makes
maxDepth 10 steps into a top-level binding the scan walks, 1 or more
projectRoot Vite’s own workspace root what a file outside the Vite root is measured from, Vite only

adoptFactories catches a codebase that wraps store creation: a call whose result is stored under a name is registered whatever function it names, so const theme = persistentAtom("theme", "dark") reaches the tree from a file that never imports "nanostores". storeTypes gives that store its kind, and we ship a map of the packages from the Smart Stores list in the nanostores README. maxDepth counts a property, an index and a Map key alike. projectRoot is a Vite option; webpack and Rspack always climb up from context instead.

What each plugin option costs has what each one buys and what it costs.

Types

type DevtoolsHandle = {
  readonly connected: boolean; // false when no extension was on the page
  disconnect: () => void;
};

type Serializer = {
  match: (value: unknown) => boolean;
  convert: (value: unknown) => unknown;
};

// the shape a `throttle` rule is handed, once per registration and never per write
type ThrottleTarget = {
  readonly home: string;
  readonly name: string; // the name the tree draws, without the file and line a clash adds
  readonly type: "atom" | "map" | "deepMap" | "computed" | "batched" | "unknown";
};

type ThrottleOption = readonly string[] | ((store: ThrottleTarget) => boolean);

// what `/webpack` and `/rspack` take. `/vite` takes the same four plus `projectRoot?: string`
type BundlerPluginOptions = {
  fileKey?: (path: string) => string;
  adoptFactories?: boolean;
  // package name, then export name, then the kind that export makes
  storeTypes?: Readonly<Record<string, Readonly<Record<string, ThrottleTarget["type"]>>>>;
  maxDepth?: number;
};

DevtoolsOptions is exported too, and holds the eleven keys of the table above.