Guides

DevTools for Packages

registerDevtools for packages (describe, inspect, sources, commands, panels, events, reveal), operations and write records; the core side of a package's own DevTools panel.

A package (a component library, a data layer, a store) can show its own state in DevTools without depending on a DevTools UI: it registers with the core, and the UI reads what it provides. This page covers the core side: what registerDevtools takes, what its handle does, and how a package says which piece of work changed what. The object the UI reads is on The DevTools Contract page. What the @motifx-community/devtools panel draws from panel metadata, its widgets, routes and limits are described in that package's docs/extensions.md file, not here.

registerDevtools for packages

A package opens its own state from the @motifx/core/devtools subpath. describe and inspect provide data; with sources, commands and panels the UI reads when it wants to, sends commands to the page and knows what to draw. In the example, the folder chart from the Wrapping Third-Party Libraries page keeps the charts it builds by component and gives their data to DevTools:

import { registerDevtools } from "@motifx/core/devtools";
import type { ComponentBase } from "@motifx/core";
import type { Chart } from "chart.js/auto";

const charts = new Map<object, Chart>();

const devtools = registerDevtools("folder-chart", {
  describe: () => ({ charts: charts.size }),
  inspect: (component) => charts.get(component)?.data.datasets[0].data,
  sources: {
    charts: { read: () => [...charts.values()].map(summaryOf), key: "id", events: { update: "refresh" } },
    chart: { read: (params: { id: string }) => detailOf(params.id) },
  },
  commands: {
    redraw: (args: { id: string }) => chartById(args.id)?.update(),
  },
  panels: [{
    id: "charts", title: "Folder charts", icon: "chart", order: 40,
    capabilities: { read: true, subscribe: true, write: true },
    layout: { /* what the UI draws; its own schema */ },
  }],
});

function track(canvas: ComponentBase, chart: Chart) {
  charts.set(canvas, chart);
  canvas.motif.setDisposable(() => charts.delete(canvas));
}

function updated(canvas: ComponentBase, counts: number[]) {
  devtools.emit("update", { path: devtools.pathOf(canvas), counts });
  devtools.changed("charts");
}
import { registerDevtools } from "@motifx/core/devtools";

const charts = new Map();

const devtools = registerDevtools("folder-chart", {
  describe: () => ({ charts: charts.size }),
  inspect: (component) => charts.get(component)?.data.datasets[0].data,
  sources: {
    charts: { read: () => [...charts.values()].map(summaryOf), key: "id", events: { update: "refresh" } },
    chart: { read: (params) => detailOf(params.id) },
  },
  commands: {
    redraw: (args) => chartById(args.id)?.update(),
  },
  panels: [{
    id: "charts", title: "Folder charts", icon: "chart", order: 40,
    capabilities: { read: true, subscribe: true, write: true },
    layout: { /* what the UI draws; its own schema */ },
  }],
});

function track(canvas, chart) {
  charts.set(canvas, chart);
  canvas.motif.setDisposable(() => charts.delete(canvas));
}

function updated(canvas, counts) {
  devtools.emit("update", { path: devtools.pathOf(canvas), counts });
  devtools.changed("charts");
}

Everything the core does not read (the shape of a panel apart from id and capabilities, the meaning of key, events and max, the content of a reveal target) is a contract between the UI and the package; the core carries it as it is.

Operations and write records

@motifx/core/devtools also exports what a package needs to say which piece of work changed what: operations and write records. They are for packages that keep history, draw a timeline or explain a change; application code has no need for them.

An operation is the work running at the moment: a click handler, a navigation, a package's action. It is a single value the core holds while a function is running, just as it holds the running effect. It is not tied to the application or the component tree, it is not passed down, and components do not see it. Writes, flush triggers and operation events carry its id.

import { runOperation, startOperation, resumeOperation, endOperation, currentOperation } from "@motifx/core/devtools";

runOperation({ kind: "action", name: "cart/add", data: { id: 7 } }, () => {
    cart.items.push(item);                    // written inside this operation
});

await runOperation({ kind: "action", name: "orders/load" }, async () => {
    const operation = currentOperation();
    const rows = await api.orders();
    resumeOperation(operation, () => { state.orders = rows; });
});

const operation = startOperation({ kind: "action", name: "chat/send" });
resumeOperation(operation, () => { state.sending = true; });
socket.onAnswer(() => resumeOperation(operation, () => {
    state.sending = false;
    endOperation(operation);
}));

onWrite(fn) reports every reactive write while DevTools is on, and returns a function that stops it. Every record has a target (the raw object, array, Map, Set or signal) and, if it happened inside an operation, an operationId:

kind When Fields
add a new key on an object, an index past the end of an array, a new key in a Map or a new value in a Set key, value
update a changed value: an object key, an array index, a growing length, a Map value, signal.value (key: 'value') key, previous, value
delete delete obj.key, map.delete(k), set.delete(v) key, previous
clear map.clear(), set.clear() removed: the map's [key, value] pairs, the set's values
splice push, pop, shift, unshift, splice, sort, reverse, a shrinking length method, index, removed, added
mutate signal.mutate(fn); the core cannot see what fn changed –

Next step

For a summary of every exported symbol, go on to API Reference.