@shadow-js/devtools 0.0.0-stage → 1.0.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +21 -0
  3. package/README.md +576 -2
  4. package/dist/dev/agent.d.ts +239 -0
  5. package/dist/dev/agent.js +626 -0
  6. package/dist/dev/agent.js.map +1 -0
  7. package/dist/dev/backend.d.ts +53 -0
  8. package/dist/dev/backend.js +53 -0
  9. package/dist/dev/backend.js.map +1 -0
  10. package/dist/dev/hook.d.ts +83 -0
  11. package/dist/dev/hook.js +136 -0
  12. package/dist/dev/hook.js.map +1 -0
  13. package/dist/dev/index.d.ts +17 -0
  14. package/dist/dev/index.js +17 -0
  15. package/dist/dev/inspection.d.ts +226 -0
  16. package/dist/dev/inspection.js +263 -0
  17. package/dist/dev/inspection.js.map +1 -0
  18. package/dist/dev/inspector.d.ts +331 -0
  19. package/dist/dev/inspector.js +429 -0
  20. package/dist/dev/inspector.js.map +1 -0
  21. package/dist/dev/model.d.ts +418 -0
  22. package/dist/dev/model.js +559 -0
  23. package/dist/dev/model.js.map +1 -0
  24. package/dist/dev/mount.d.ts +207 -0
  25. package/dist/dev/mount.js +256 -0
  26. package/dist/dev/mount.js.map +1 -0
  27. package/dist/dev/overlay.d.ts +102 -0
  28. package/dist/dev/overlay.js +176 -0
  29. package/dist/dev/overlay.js.map +1 -0
  30. package/dist/dev/preview.d.ts +74 -0
  31. package/dist/dev/preview.js +122 -0
  32. package/dist/dev/preview.js.map +1 -0
  33. package/dist/dev/protocol.d.ts +288 -0
  34. package/dist/dev/protocol.js +109 -0
  35. package/dist/dev/protocol.js.map +1 -0
  36. package/dist/dev/ring.d.ts +50 -0
  37. package/dist/dev/ring.js +94 -0
  38. package/dist/dev/ring.js.map +1 -0
  39. package/dist/dev/transport.d.ts +347 -0
  40. package/dist/dev/transport.js +394 -0
  41. package/dist/dev/transport.js.map +1 -0
  42. package/dist/dev/ui/details.js +294 -0
  43. package/dist/dev/ui/details.js.map +1 -0
  44. package/dist/dev/ui/inspection-state.d.ts +43 -0
  45. package/dist/dev/ui/inspection-state.js +62 -0
  46. package/dist/dev/ui/inspection-state.js.map +1 -0
  47. package/dist/dev/ui/issues.js +85 -0
  48. package/dist/dev/ui/issues.js.map +1 -0
  49. package/dist/dev/ui/panel.d.ts +55 -0
  50. package/dist/dev/ui/panel.js +160 -0
  51. package/dist/dev/ui/panel.js.map +1 -0
  52. package/dist/dev/ui/rows.d.ts +44 -0
  53. package/dist/dev/ui/rows.js +132 -0
  54. package/dist/dev/ui/rows.js.map +1 -0
  55. package/dist/dev/ui/state-tab.js +88 -0
  56. package/dist/dev/ui/state-tab.js.map +1 -0
  57. package/dist/dev/ui/state.d.ts +192 -0
  58. package/dist/dev/ui/state.js +147 -0
  59. package/dist/dev/ui/state.js.map +1 -0
  60. package/dist/dev/ui/styles.d.ts +31 -0
  61. package/dist/dev/ui/styles.js +113 -0
  62. package/dist/dev/ui/styles.js.map +1 -0
  63. package/dist/dev/ui/timeline.js +135 -0
  64. package/dist/dev/ui/timeline.js.map +1 -0
  65. package/dist/dev/ui/tree.js +186 -0
  66. package/dist/dev/ui/tree.js.map +1 -0
  67. package/dist/prod/agent.js +2 -0
  68. package/dist/prod/agent.js.map +1 -0
  69. package/dist/prod/backend.js +2 -0
  70. package/dist/prod/backend.js.map +1 -0
  71. package/dist/prod/hook.js +2 -0
  72. package/dist/prod/hook.js.map +1 -0
  73. package/dist/prod/index.js +1 -0
  74. package/dist/prod/inspection.js +2 -0
  75. package/dist/prod/inspection.js.map +1 -0
  76. package/dist/prod/inspector.js +2 -0
  77. package/dist/prod/inspector.js.map +1 -0
  78. package/dist/prod/model.js +2 -0
  79. package/dist/prod/model.js.map +1 -0
  80. package/dist/prod/mount.js +2 -0
  81. package/dist/prod/mount.js.map +1 -0
  82. package/dist/prod/overlay.js +11 -0
  83. package/dist/prod/overlay.js.map +1 -0
  84. package/dist/prod/preview.js +2 -0
  85. package/dist/prod/preview.js.map +1 -0
  86. package/dist/prod/protocol.js +2 -0
  87. package/dist/prod/protocol.js.map +1 -0
  88. package/dist/prod/ring.js +2 -0
  89. package/dist/prod/ring.js.map +1 -0
  90. package/dist/prod/transport.js +2 -0
  91. package/dist/prod/transport.js.map +1 -0
  92. package/dist/prod/ui/details.js +2 -0
  93. package/dist/prod/ui/details.js.map +1 -0
  94. package/dist/prod/ui/inspection-state.js +2 -0
  95. package/dist/prod/ui/inspection-state.js.map +1 -0
  96. package/dist/prod/ui/issues.js +2 -0
  97. package/dist/prod/ui/issues.js.map +1 -0
  98. package/dist/prod/ui/panel.js +2 -0
  99. package/dist/prod/ui/panel.js.map +1 -0
  100. package/dist/prod/ui/rows.js +2 -0
  101. package/dist/prod/ui/rows.js.map +1 -0
  102. package/dist/prod/ui/state-tab.js +2 -0
  103. package/dist/prod/ui/state-tab.js.map +1 -0
  104. package/dist/prod/ui/state.js +2 -0
  105. package/dist/prod/ui/state.js.map +1 -0
  106. package/dist/prod/ui/styles.js +82 -0
  107. package/dist/prod/ui/styles.js.map +1 -0
  108. package/dist/prod/ui/timeline.js +2 -0
  109. package/dist/prod/ui/timeline.js.map +1 -0
  110. package/dist/prod/ui/tree.js +2 -0
  111. package/dist/prod/ui/tree.js.map +1 -0
  112. package/package.json +69 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 1.0.1
4
+
5
+ ### Changed
6
+
7
+ - Released at 1.0.1 with the other packages, which share one version. No change to this package.
8
+
9
+ ## 1.0.0
10
+
11
+ ### Added
12
+
13
+ - Development instrumentation and a browser extension for inspecting ShadowJS applications.
14
+ - A bridge and panel for reactive state, components and runtime diagnostics.
15
+ - A Firefox build of the browser extension (Firefox 128 or newer), generated from the same sources as the Chromium
16
+ build by a typed manifest generator with a permission audit. The build writes `dist/extension/chromium` and
17
+ `dist/extension/firefox`; the Chromium manifest is unchanged.
18
+ - A Timeline tab in the extension panel: records signal writes, computation runs with durations and flushes into a
19
+ bounded ring buffer (10 000 entries by default), draws one lane per computation with zoom and pan, explains which
20
+ write caused which runs, and exports and imports a versioned, validated JSON recording. It listens to nothing until
21
+ recording starts. Keyboard operable, reduced-motion safe and right-to-left safe.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ShadowJS contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,577 @@
1
- # Temporary Holding Version
1
+ <p align="center"><img src="https://raw.githubusercontent.com/shadowjs-dev/devtools/main/.github/banner.png" alt="ShadowJS" width="100%"></p>
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # @shadow-js/devtools
4
+
5
+ [![npm](https://img.shields.io/npm/v/@shadow-js/devtools.svg)](https://www.npmjs.com/package/@shadow-js/devtools)
6
+ [![CI](https://github.com/shadowjs-dev/devtools/actions/workflows/ci.yml/badge.svg)](https://github.com/shadowjs-dev/devtools/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/license-MIT-yellow.svg)](https://github.com/shadowjs-dev/devtools/blob/main/LICENSE)
8
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22.12-brightgreen.svg)](https://nodejs.org/)
9
+ [![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue.svg)](https://www.typescriptlang.org/)
10
+
11
+ Developer tools for the ShadowJS reactive graph. A floating panel, built with ShadowJS, shows the
12
+ components, signals, memos, effects, stores and resources of a running app, the flushes that update
13
+ them, the state values (which you can edit) and the runtime errors. The same panel can also run in
14
+ another page, such as a browser extension or another tab, through a transport.
15
+
16
+ > **Development only.** Never ship this package to production. See [Development only](#development-only).
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install -D @shadow-js/devtools
22
+ ```
23
+
24
+ Install it as a development dependency. The package has no runtime dependencies. It declares
25
+ `@shadow-js/core` and `@shadow-js/web` as peer dependencies, which your app already uses.
26
+
27
+ ShadowJS 1.0.0 is not on npm yet. Until it is published, install the packages from this repository's workspace.
28
+
29
+ ## Quick start
30
+
31
+ ```tsx
32
+ /// <reference types="vite/client" />
33
+ import { useSignal } from "@shadow-js/core";
34
+ import { mountDevtools } from "@shadow-js/devtools";
35
+ import { render } from "@shadow-js/web";
36
+
37
+ function Counter() {
38
+ const [count, setCount] = useSignal(0);
39
+ return <button on:click={() => setCount(count() + 1)}>Count: {count()}</button>;
40
+ }
41
+
42
+ // Dev only. Mount the panel before the app renders, so no event is missed.
43
+ if (import.meta.env.DEV) mountDevtools({ open: true });
44
+
45
+ render(() => <Counter />, document.body);
46
+ ```
47
+
48
+ Run the app with `vite dev`. The panel opens in the top-left corner of the page. Press `Alt+Shift+D`
49
+ to toggle it. The `vite/client` reference types `import.meta.env`; apps made with `create-shadow-app` already have it
50
+ in `src/vite-env.d.ts`.
51
+
52
+ ## Enable it in development
53
+
54
+ `mountDevtools(options?)` creates the panel and returns a handle. Call it before the application
55
+ renders, because events reported before the hook exists are lost. It installs the hook itself.
56
+ Without `open: true`, only a small launcher button labeled "S" shows; click it or press
57
+ `Alt+Shift+D` to open the panel.
58
+
59
+ | Option | Default | What it does |
60
+ | --- | --- | --- |
61
+ | `open` | `false` | Start with the panel open. |
62
+ | `toggleKey` | `"Alt+Shift+D"` | Keyboard shortcut that toggles the panel. `false` turns the shortcut off. |
63
+ | `tab` | `"tree"` | First view: `"tree"`, `"timeline"`, `"state"` or `"issues"`. |
64
+ | `position` | `{ x: 16, y: 16 }` | Starting position in pixels from the top-left corner. |
65
+ | `flash` | `true` | Flash components on the page when they update. The header button switches it. |
66
+ | `launcher` | `true` | Show the launcher button. |
67
+ | `serve` | none | Also serve the agent over a transport, for a [remote panel](#remote-panel). |
68
+
69
+ `hook`, `agent`, `container` and `document` are for tests and unusual setups.
70
+
71
+ The handle returned by `mountDevtools()` has `open()`, `close()`, `toggle()`, `moveTo(x, y)`,
72
+ `setTab(tab)`, `select(id)`, `flush()` and `dispose()`, plus the read-only `isOpen`, `host`, `root`,
73
+ `agent` and `backend`. `dispose()` removes the panel, the overlay and every listener.
74
+
75
+ ## The panel
76
+
77
+ The panel is draggable by its header. The header also pauses and clears the recording. It has four
78
+ views:
79
+
80
+ - **Components**: the owner tree (components, memos, effects, signals, resources and stores), with
81
+ re-runs rolled up per component. A filter box narrows the tree. The arrow keys move the selection
82
+ (Up and Down), collapse and expand (Left and Right), and Home and End jump to the first and last
83
+ row. The detail pane shows what the node reads, what reads it, and why it last ran. Hovering a row
84
+ highlights the component on the page.
85
+ - **Timeline**: the newest flushes as flame lanes (a memo pulled by an effect sits under it), the
86
+ render and user queue sizes of each flush, and the components that re-ran most.
87
+ - **State**: every signal, store and resource with its current value. Select a signal or a store to
88
+ see its value as JSON, change the text and press **Apply**. The write goes through the engine like
89
+ any other write. Resources are listed but cannot be edited.
90
+ - **Issues**: runtime errors with their diagnostic code, and hydration mismatches.
91
+
92
+ ## API
93
+
94
+ | Export | What it is |
95
+ | --- | --- |
96
+ | `mountDevtools(options?)` | The floating panel in a shadow root, plus the in-page overlay. Dev only. |
97
+ | `installHook(options?)`, `getHook(target?)`, `uninstallHook(target?)` | The global hook `globalThis.__SHADOWJS_DEVTOOLS_HOOK__` that the runtime reports to. |
98
+ | `createAgent(options?)` | Receives hook events, keeps bounded buffers and answers commands (read and edit values). |
99
+ | `InspectorModel`, `snapshotOf`, `ownerTree`, `componentStats`, `explainRun`, `flameTimeline`, `nodeLabel`, `foldEvents` | The headless inspector: pure functions over the recorded events. |
100
+ | `serveAgent()`, `connectRemote()` and the transports | The panel over `postMessage`, `MessagePort` or `BroadcastChannel`. |
101
+ | `Panel`, `createPanelState()` | The panel component and its state, for a custom panel page. |
102
+ | `EVENT`, `PROTOCOL_VERSION`, `HOOK_KEY` | The protocol constants (see below). |
103
+ | `registerInspection(name, source)` | Shows an app's router, query cache or other state in the extension (see [Inspection sources](#inspection-sources)). Dev only. |
104
+ | `routerInspection(router)`, `queryInspection(client)` | Inspection sources for a router (`@shadow-js/router`) and a query client (`@shadow-js/query`), read through structural types. |
105
+ | `InspectionSource`, `InspectionRow`, `readInspections()`, `parseInspectResult()`, `sanitizeRows()` | The inspection contract: the rows a source shows, bounded and plain. |
106
+ | `createInspectionState(backend)` | The state of the router and query views of a panel. |
107
+ | `MAX_INSPECTIONS`, `MAX_INSPECTION_ROWS` | The limits of the inspection registry: 16 sources, 500 rows each. |
108
+
109
+ ### The hook
110
+
111
+ `installHook()` installs the hook once and returns it. It returns the hook that is already installed
112
+ when its `version` matches `PROTOCOL_VERSION`. It returns `undefined`, and installs nothing, when a
113
+ hook of another version is present or in production builds. Its options are:
114
+
115
+ - `target`: the object to install on (default `globalThis`).
116
+ - `buffer`: how many events to keep for a sink that attaches later (default 5000). The buffer is
117
+ dropped when the first sink attaches.
118
+
119
+ `getHook()` returns the installed hook when its version matches, and `uninstallHook()` removes it
120
+ from the global. Sinks stay attached to the old object. The hook's `attach(sink)` adds a sink,
121
+ replays the buffered events to it and returns a function that detaches it. `faults` counts sinks
122
+ that threw, and `lastFault` holds the last error; the application never sees either.
123
+
124
+ ```ts
125
+ import { createAgent, getHook, installHook, uninstallHook } from "@shadow-js/devtools";
126
+
127
+ // The hook is undefined in production builds, so the agent is only created in development.
128
+ const hook = installHook({ buffer: 5000 });
129
+ if (hook !== undefined) {
130
+ const agent = createAgent();
131
+ const detach = agent.attach(hook);
132
+ console.log(agent.stats.recorded);
133
+ detach();
134
+ }
135
+
136
+ console.log(getHook() === hook); // true while the hook is installed
137
+ uninstallHook();
138
+ ```
139
+
140
+ `createAgent()` does not make signals editable. Pass `writeSignal` from `@shadow-js/core/extend` to
141
+ allow edits; `mountDevtools()` does this for you.
142
+
143
+ ## Remote panel
144
+
145
+ A remote panel runs in another page and talks to the agent over a transport. The inspected page
146
+ serves the agent:
147
+
148
+ ```ts
149
+ /// <reference types="vite/client" />
150
+ import { createBroadcastChannelTransport, mountDevtools } from "@shadow-js/devtools";
151
+
152
+ if (import.meta.env.DEV) {
153
+ mountDevtools({ serve: createBroadcastChannelTransport("shadowjs-devtools") });
154
+ }
155
+ ```
156
+
157
+ The panel page connects to it and renders the same `Panel` against the remote backend:
158
+
159
+ ```tsx
160
+ /// <reference types="vite/client" />
161
+ import {
162
+ connectRemote,
163
+ createBroadcastChannelTransport,
164
+ createPanelState,
165
+ Panel,
166
+ } from "@shadow-js/devtools";
167
+ import { render } from "@shadow-js/web";
168
+
169
+ // The backend keeps a copy of the model, filled by the events the agent sends.
170
+ const backend = connectRemote(createBroadcastChannelTransport("shadowjs-devtools"));
171
+ await backend.ready;
172
+
173
+ render(() => {
174
+ const state = createPanelState(backend);
175
+ // onMove is a no-op here, so this panel stays at 16, 16 and does not persist its position.
176
+ return <Panel state={state} open={true} x={16} y={16} onMove={() => {}} onClose={() => {}} />;
177
+ }, document.body);
178
+ ```
179
+
180
+ The transports:
181
+
182
+ - `createBroadcastChannelTransport(name)` uses a `BroadcastChannel`, so it reaches the other pages of
183
+ the same origin.
184
+ - `createWindowTransport({ target, origin })` uses `postMessage` between a page and a panel in
185
+ another window or frame. It refuses `"*"` as the origin and accepts messages only from `target`
186
+ and `origin`.
187
+ - `createPortTransport(port)` wraps a `MessagePort`.
188
+ - `createMemoryTransportPair()` returns two connected in-memory transports, for tests.
189
+
190
+ `serveAgent(agent, transport, options?)` serves an agent and returns a function that stops serving.
191
+ `connectRemote(transport, options?)` returns a `Backend`; its `ready` promise resolves when the first
192
+ complete state arrives. Every message is plain, structured-clonable data and carries `channel`, `v`
193
+ and `from`.
194
+
195
+ ## Inspection sources
196
+
197
+ The panel shows the components, signals, stores and effects of the graph by itself. An app's router and
198
+ query cache are not in the graph, so the app describes them with an inspection source:
199
+
200
+ ```ts
201
+ import { queryInspection, registerInspection, routerInspection } from "@shadow-js/devtools";
202
+
203
+ if (import.meta.env.DEV) {
204
+ registerInspection("router", routerInspection(router));
205
+ registerInspection("query", queryInspection(queryClient));
206
+ }
207
+ ```
208
+
209
+ An `InspectionSource` has a `title` and a `rows()` function. The panel asks for the rows when it shows
210
+ the source, so each view is the state at the time of the request, not a copy from registration.
211
+ `routerInspection` lists the location, whether a navigation is pending, the last error and the matched
212
+ routes. `queryInspection` lists one row per query: its key, fetch status, observers, age and whether it
213
+ was invalidated.
214
+
215
+ The adapters are structural: `routerInspection` takes anything with the `state` accessors of a router, and
216
+ `queryInspection` takes anything whose `queryCache` has `find()` (query snapshots) and `subscribe(listener)`. A
217
+ `QueryClient` from `@shadow-js/query` and a `Router` from `@shadow-js/router` fit those shapes, so devtools does not
218
+ depend on either package.
219
+
220
+ Live updates: a source may have a `subscribe(notify)` function that calls `notify` when its rows may have changed.
221
+ `queryInspection` uses the query cache's `subscribe`, and the extension's page side forwards each change to the
222
+ panel as an inspection ping, so the Queries view updates as the cache changes. The query cache offers `subscribe`
223
+ through its `inspection` feature: create the client with `features: [inspection]` (from `@shadow-js/query/inspection`).
224
+ Without it the source still lists the queries, and the panel reads them when it opens the view.
225
+
226
+ Limits, so a broken source cannot flood the panel: at most 16 sources (`MAX_INSPECTIONS`; a 17th new name is
227
+ refused, and re-registering a name replaces its source), at most 500 rows per source (`MAX_INSPECTION_ROWS`;
228
+ the last row says how many were cut), and every field is text of at most 200 characters. A source that
229
+ throws is shown with its error. `registerInspection` returns `false` when nothing was registered (no hook,
230
+ an empty name, or the limit); it registers nothing in production builds. The hook keeps a registered source
231
+ until the page unloads or another source is registered under the same name, so a source that reads a
232
+ router or a client keeps that object reachable: register sources for the lifetime of the app, not per
233
+ screen.
234
+
235
+ ## Browser extension
236
+
237
+ The extension is a Manifest V3 panel for Chromium (Chrome and Edge are built and tested) and for Firefox 128 or
238
+ newer (built from the same sources; see [Firefox](#firefox) for what was and was not run). It adds a **ShadowJS**
239
+ tab to the DevTools window of a page on `localhost`, `127.0.0.1` or `[::1]`. The panel is a ShadowJS application
240
+ (`extension/src/panel/`) with these views:
241
+
242
+ | View | Shows |
243
+ | --- | --- |
244
+ | Components | the component tree, with search, arrow-key navigation and the selected node's details |
245
+ | Signals | every signal with its current value and write count, updated as the page writes it |
246
+ | Stores | every store as JSON. Apply writes each changed top-level key with a `store-set` command |
247
+ | Effects | effects and memos with their runs, updates and durations |
248
+ | Timeline | a recording of reactive activity over time: signal writes and computation runs on lanes (see [Timeline](#timeline)) |
249
+ | Resources | resources with their last reported state |
250
+ | Router | the source the app registered with `registerInspection("router", routerInspection(router))` |
251
+ | Queries | the source the app registered with `registerInspection("query", queryInspection(client))`, pushed when the cache changes |
252
+ | Issues | runtime errors and hydration mismatches |
253
+
254
+ The toolbar has a search box (it filters the current view), **Pause** and **Resume** (recording in the page; the
255
+ label changes only after the page confirms the command), a light and dark theme (dark by default) and the
256
+ connection status. The tabs work with the arrow keys, Home and End, and motion stops when the operating system asks
257
+ for reduced motion.
258
+
259
+ ### Build it
260
+
261
+ ```sh
262
+ npm run build:extension -w @shadow-js/devtools
263
+ ```
264
+
265
+ The build writes two folders from the same bundles, `dist/extension/chromium` and
266
+ `dist/extension/firefox`. They hold the same eight files; only `manifest.json` differs.
267
+ `npm run build -w @shadow-js/devtools` builds the library and then the extension. The folders are not part of the
268
+ published npm package.
269
+
270
+ ### Load it in Chrome or Edge
271
+
272
+ 1. Open `chrome://extensions` (or `edge://extensions`) and turn on **Developer mode**.
273
+ 2. Choose **Load unpacked** and select `dist/extension/chromium`.
274
+ 3. Run your app in development on `localhost`, open DevTools and choose the **ShadowJS** tab.
275
+
276
+ Without DevTools, open the panel page of the extension: `chrome-extension://<id>/panel.html`, where the id is on
277
+ the extension's card. The panel asks the background which pages on localhost it can inspect. When there is one
278
+ it attaches to it; when there are several it lists them and you choose one. `panel.html?tabId=<id>` attaches to
279
+ the tab with that id directly.
280
+
281
+ ### Firefox
282
+
283
+ Install from source as a temporary add-on (Firefox removes it when it quits):
284
+
285
+ 1. Build the folders as above.
286
+ 2. Open `about:debugging#/runtime/this-firefox` and choose **Load Temporary Add-on...**.
287
+ 3. Select `dist/extension/firefox/manifest.json`.
288
+ 4. Firefox lists the localhost access the add-on asks for. Allow it (Firefox 127 and newer are meant to ask at
289
+ install; if the panel stays empty, check the add-on's permissions in `about:addons`).
290
+ 5. Run your app in development on `localhost`, open the Firefox developer tools and choose **ShadowJS**.
291
+
292
+ The Firefox manifest is generated by the same code as the Chromium one (`extension/src/manifest.ts`). It differs
293
+ where Firefox requires it: `background.scripts` (an event page) instead of `background.service_worker`;
294
+ `browser_specific_settings.gecko` with the add-on id `devtools@shadowjs.net`, `strict_min_version` 128.0 (the first
295
+ Firefox that runs a content script in the page's `MAIN` world) and the data collection answer `none`; and the same
296
+ three loopback patterns as `host_permissions`. It has no `minimum_chrome_version`. One small adapter
297
+ (`extension/src/extension-api.ts`) reads the `browser` namespace when it exists and `chrome` otherwise, so the
298
+ scripts are the same for both browsers.
299
+
300
+ > **Status.** The Firefox build is verified by the manifest tests and by loading the same scripts in Chromium. It
301
+ > has not been run in Firefox: no Firefox was available where it was built. Before you rely on it, load it once
302
+ > and check that the panel connects. Two behaviors are expected from the documentation and unverified: whether
303
+ > Firefox keeps the event page alive while the panel's port is open (if it does not, the panel reloads and
304
+ > attaches again, as it does when Chromium stops the service worker), and whether it accepts the empty panel icon
305
+ > path.
306
+
307
+ ### Timeline
308
+
309
+ The **Timeline** tab records what the reactive graph does over time. Press **Start recording**, use the app, and
310
+ press **Stop recording**. Each computation (memo or effect) that ran gets a lane; a bar is one run, as wide as its
311
+ duration, and a fainter bar means the run produced the same value. The top lane holds the signal writes. Flushes of
312
+ the scheduler are drawn as bands behind the lanes.
313
+
314
+ - **Selection explains.** Select a run to see the writes that caused it (a write names the computations subscribed
315
+ to it) and the runs of the memos it read; select a write to see the runs it set off, up to the end of the flush
316
+ that ran them. The causes are what the recording shows: a write older than the buffer is not listed.
317
+ - **Zoom and pan.** Buttons (Zoom in, Zoom out, Earlier, Later, Show everything), the wheel with Ctrl or Cmd, Shift
318
+ and the wheel, or a drag. **Follow live** keeps a zoomed window on the newest entries while recording.
319
+ - **Bounded memory.** Entries live in a ring buffer (default 10 000, from 1 to 200 000 in **Buffer size**). When it is
320
+ full the oldest entries are overwritten and counted ("N older overwritten"). Each entry keeps at most 64 ids per
321
+ list, at most 50 000 labels are kept, and a lane draws at most 300 bars (closer bars merge), at most 200 lanes exist
322
+ (the busiest computations) and 40 are shown until you ask for more.
323
+ - **Zero cost when idle.** Nothing is attached to the event stream until you press Start recording: no listener, no
324
+ buffer, no request to the page. The page's hook and agent record the same events whether the tab is recording or not
325
+ (`DVT-TL-ZO.*` counts them).
326
+ - **Export and import.** **Export JSON** saves the recording (`shadowjs-timeline.json`, `format` `shadowjs-timeline`,
327
+ `version` 1). **Import JSON** reads one back and shows it in place of the live recording; Start recording or Clear
328
+ returns to the live view. The file is validated as untrusted input: at most 32 MiB, a plain object with exactly the
329
+ schema's fields, at most 200 000 entries, integer ids, numbers in range, non-decreasing sequence numbers, labels of
330
+ at most 200 characters, and no unknown keys (so `__proto__` and `constructor` are refused). A file that fails comes back
331
+ as a message and changes nothing.
332
+ - **Accessible.** Every control is a labeled button, field or checkbox. The plot takes the keyboard: arrows select (left
333
+ and right along a lane, up and down between lanes), `+` and `-` zoom, Shift with an arrow pans, `0` shows everything,
334
+ Escape clears the selection. The selection is announced in a polite status line, and the entries in view are listed in
335
+ a table with a Select button each, so nothing needs the drawing. Nothing animates, and the layout uses logical CSS
336
+ properties only, so it mirrors in a right-to-left browser (the time axis always runs from the earliest to the latest
337
+ entry).
338
+
339
+ ### Store submission
340
+
341
+ `npm run zip:extension -w @shadow-js/devtools` builds both folders and writes
342
+ `extension/store/shadowjs-devtools-chromium-<version>.zip` and
343
+ `extension/store/shadowjs-devtools-firefox-<version>.zip`. Each zip holds the eight built files of
344
+ its folder (checked with `unzip -t` and by the test `[DVT-EXT-ZP.1]`); they are written locally and nothing is
345
+ uploaded. The package does
346
+ not submit the extension to any store. A person does that, after reading the store's policy for development
347
+ tools.
348
+
349
+ ### How it works
350
+
351
+ - `page-main.js` runs in the page's own world at `document_start`. It installs the devtools hook before the
352
+ application's code runs, attaches an agent and serves it over `window.postMessage`. When an inspection source
353
+ registered with `registerInspection` reports a change, it posts one inspection ping per burst of changes.
354
+ - `content.js` runs in the isolated world. It relays the agent's messages to the background and the panel's
355
+ requests back to the page. It checks both directions: the source and origin of a message, its envelope and
356
+ its shape. It also limits what one page can send: a burst of 2000 messages, refilled at 1000 a second, and no
357
+ message over 1 000 000 characters of JSON. Messages it drops are counted, and the panel is not told.
358
+ - `background.js` is the service worker (Chromium) or the event page (Firefox). It pairs each tab's page port with the panel port of that tab, and
359
+ answers a panel's request for the list of pages.
360
+ - `panel-main.js` is the panel page. It attaches to a tab, then renders the panel over a port transport. When the
361
+ browser stops the service worker, the page reloads and attaches again, so it resynchronizes from the page's full
362
+ state; the page's bridge reconnects on its own.
363
+
364
+ ### Limits
365
+
366
+ - The Firefox build has not been run in Firefox (see [Firefox](#firefox)).
367
+ - Signals cannot be edited from the extension, because writing one needs the app's own copy of the runtime.
368
+ Stores can be edited.
369
+ - The hover highlight of a component is not drawn on the page (the overlay draws it), and the flame graph
370
+ of the last flushes is in the overlay only. The extension has the recording [Timeline](#timeline) instead.
371
+ - One panel per tab. The extension is not submitted to a store by this repository.
372
+
373
+ ### Tests
374
+
375
+ The unit tests cover the manifests (`DVT-FX-*`), the timeline recorder, file schema, model and session
376
+ (`DVT-TL-*`), the bridge (spoofed origin and source, malformed and oversized messages, the flood limit,
377
+ the inspection ping), the relay (pairing, the page list, pings), the page side (the subscriptions of the
378
+ inspection sources), the panel's state (`panel/state.ts`, a pure reducer and the store-draft rules), the manifest
379
+ and the zip writer. DOM tests mount the Timeline tab and the panel (`timeline-view.dom.test.ts`). A real-browser run loads the built
380
+ folder into full Chromium, opens a fixture page that uses the dev runtime, and drives the standalone panel: signals,
381
+ effects, stores, resources, router, queries (pushed), pause, search, the tab keys and the theme; another records a click,
382
+ selects the run, exports, imports and refuses a hostile file.
383
+
384
+ ## Security
385
+
386
+ Read this before you use the extension.
387
+
388
+ - **Development only.** The extension is a development tool and is not a production artifact. Its manifest
389
+ matches only the three loopback hosts, so it does nothing on a deployed site. The devtools code is not in
390
+ any production bundle (`DVT-15.*` and `DVT-EXT-*` tests). On a production build of an app the runtime reports
391
+ nothing to the hook, so the panel stays empty.
392
+ - **Permissions.** The Chromium manifest asks for no permissions and no host permissions. The Firefox manifest asks for
393
+ no permissions either; it lists the same three loopback patterns as `host_permissions`, which Firefox shows at install.
394
+ `auditManifest` checks both manifests and the build fails on a violation. Each thing the extension uses needs none:
395
+ the two content scripts match the loopback hosts in the top frame only (no `<all_urls>`); the devtools page
396
+ uses `chrome.devtools`, which the browser grants to the DevTools window; and the background and the pages use
397
+ `runtime.connect` and `runtime.onConnect`, which are not permissions. `tabs`, `scripting`, `storage` and
398
+ `activeTab` are not requested. The tab ids that the page list returns come from the browser's port senders, not
399
+ from the `tabs` API. `DVT-EXT-MF.1` fails if a permission is added.
400
+ - **Content Security Policy.** The extension pages allow scripts from the extension only
401
+ (`script-src 'self'; object-src 'none'`). The page hook is a `MAIN`-world content script, not an inline
402
+ `<script>` element and not a web-accessible resource, so a strict page CSP does not block it and pages cannot
403
+ load the extension's files.
404
+ - **No eval, no remote code.** No bundle contains `eval`, `new Function`, a string timer or `document.write`:
405
+ the build scans its output and fails when one is found. The bundles load nothing from the network.
406
+ - **Origin and source checks.** The page side accepts a message only when its source is the page's own window
407
+ and its origin is the page's origin. The background accepts a port only from the extension's own pages and
408
+ scripts, and only with a tab for a content script (or an `attach` with an integer tab id for the panel).
409
+ - **Message validation and limits.** Every message is checked for its envelope (`channel`, `from`, `v`, `type`)
410
+ and the fields its type needs before it is forwarded. An events batch holds at most 2 000 events. A message is
411
+ dropped when it is larger than 1 000 000 characters of JSON, or when its page has sent more than its rate (a
412
+ burst of 2 000, refilled at 1 000 a second). The inspection ping is accepted only in its exact four-field shape.
413
+ The panel checks the answer to an `inspect` request for shape and bounds (`parseInspectResult`) before it shows
414
+ it. Anything else is dropped.
415
+ - **What the page can do.** Window messages are visible to every script of the page, so a script of the inspected
416
+ page can post messages that look like the agent's. The panel treats what the page sends as data: it shows it,
417
+ and it sends the page only the commands the user makes in the panel. A page script can already run any code in
418
+ the page, so this adds no capability, but the panel can show data that did not come from the hook. Do not trust a
419
+ value shown in the panel for a security decision.
420
+ - **What leaves the machine.** Nothing. The extension makes no network request, stores nothing (no `storage`
421
+ permission, no `localStorage`), and sends no telemetry. The events and previews (signal values up to 80
422
+ characters, store state) travel between the page, the extension's background and the panel, all on this
423
+ machine.
424
+ - **Do not use it with real data.** The state of a development build can contain secrets, personal data and
425
+ session tokens. Use the extension only on a development server with development data.
426
+
427
+ ## The dev-hook protocol (version 1)
428
+
429
+ The runtime calls `hook.report(kind, a, b, c, d, e)` in development builds only. The arguments are
430
+ positional and are the runtime's own objects, so reporting allocates nothing. The hook forwards every
431
+ kind to its sinks. The agent ignores kinds it does not know, so a newer runtime degrades instead of
432
+ breaking. A sink that throws is counted in `hook.faults` and never reaches the application.
433
+
434
+ | kind | a | b | c | d | e |
435
+ | --- | --- | --- | --- | --- | --- |
436
+ | `OWNER_CREATED` | owner | parent owner or `null` | `"root"` or `"owner"` | name or `undefined` | |
437
+ | `OWNER_DISPOSED` | owner | | | | |
438
+ | `SIGNAL_CREATED` | signal node | owner or `null` | name or `undefined` | initial value (shown as a preview) | |
439
+ | `SIGNAL_WRITTEN` | signal node | next value | writing computation or `null` | observers array or `null` | |
440
+ | `COMPUTATION_CREATED` | computation | owner or `null` | `"memo"` or `"effect"` | name or `undefined` | render flag |
441
+ | `COMPUTATION_RUN` | computation | start time (ms) | duration (ms) | value changed | sources array |
442
+ | `COMPUTATION_DISPOSED` | computation | | | | |
443
+ | `COMPONENT_MOUNTED` | owner of the component | name | rendered nodes (Node, Node[] or `null`) | | |
444
+ | `COMPONENT_UNMOUNTED` | owner of the component | | | | |
445
+ | `RESOURCE_STATE` | resource handle (object) | state: `"unresolved"`, `"pending"`, `"ready"`, `"refreshing"` or `"errored"` for `useResource`; the query status or `"fetching"` for the query client | name or `undefined` | owner or `null` | |
446
+ | `ERROR` | source owner or `null` | the thrown value | diagnostic code or `undefined` | | |
447
+ | `HYDRATION_MISMATCH` | message | expected (node or text) | actual (node or text) | | |
448
+ | `FLUSH_START` | render queue size | user queue size | | | |
449
+ | `FLUSH_END` | computations run in the flush | | | | |
450
+ | `STORE_CREATED` | store handle (`name`, `snapshot()`, `set(path, value)`) | owner or `null` | | | |
451
+ | `TRANSITION_START` | | | | | |
452
+ | `TRANSITION_END` | transitions resolved | | | | |
453
+
454
+ The kind numbers are the values of `EVENT`: `OWNER_CREATED` is 1 and `TRANSITION_END` is 17. The
455
+ runtime's own call sites are listed in [Runtime integration](#runtime-integration).
456
+
457
+ Kinds 16 and 17 are reported by `transition()` (`TRANSITION_START`, with no arguments, after the
458
+ transition callback has run) and by the transition lane (`TRANSITION_END`, with the number of
459
+ transitions it resolved, when the lane is idle). The agent does not use them yet: it counts them as
460
+ ignored, like any kind it does not know.
461
+
462
+ `hook.version` must equal `PROTOCOL_VERSION` (1). Adding an event kind does not change the version;
463
+ changing the meaning of an existing argument does.
464
+
465
+ ## Development only
466
+
467
+ This package is for development. Never ship it to production (the [Security](#security) section covers the
468
+ browser extension too):
469
+
470
+ - It exposes application state: component names, signal and store values, resource states and
471
+ errors.
472
+ - It can write to signals and stores through the State tab.
473
+ - Its `BroadcastChannel` transport is readable by every script of the same origin, and its
474
+ `postMessage` transport sends the state to the window you name.
475
+
476
+ Import it only in development code, and guard the call with `if (import.meta.env.DEV)`. The
477
+ `development` export condition resolves the full package only under `vite dev`. The production
478
+ build resolves the no-op build instead. That is a safety net, not a reason to leave the call in
479
+ your bundle.
480
+
481
+ ## Production behavior
482
+
483
+ The package has two builds:
484
+
485
+ - `dist/dev` has the tools. The `development` export condition resolves to it.
486
+ - `dist/prod` is built with `__DEV__` set to `false`. The `default` export condition resolves to it.
487
+ In that build, `installHook()` returns `undefined`, `mountDevtools()` returns a handle of no-ops,
488
+ and nothing touches the DOM or `globalThis`. Without a `document` (on the server), `mountDevtools()`
489
+ also returns the no-op handle.
490
+
491
+ Vite resolves the `development` condition in `vite dev` and the `default` condition in `vite build`
492
+ (its `development|production` condition resolves to `production` in a build). So `vite build` uses
493
+ the no-op build.
494
+
495
+ The package declares `"sideEffects": false`. An app that only calls `mountDevtools()` bundles to under
496
+ 1000 characters with none of the tools' code left (the package's `production.dom.test.ts`, DVT-15.1).
497
+
498
+ ## Cost
499
+
500
+ Recording does constant work per event. `test/perf.test.ts` (DVT-08) records 100 000 events and
501
+ checks that every buffer stays at its bound. It also checks that the second batch of 50 000 events
502
+ costs less than four times the first.
503
+
504
+ The default bounds are:
505
+
506
+ - model: 20 000 runs, 10 000 signal writes, 1 000 flushes, 200 errors, 4 000 recently disposed
507
+ nodes (the graveyard), and 100 000 live nodes;
508
+ - agent: 10 000 raw events in the events log and 10 000 events held for subscribers;
509
+ - hook: 5 000 buffered events before the first sink attaches;
510
+ - serving: at most 2 000 events per message.
511
+
512
+ The agent holds application objects only through `WeakRef` and `WeakMap`. A signal, a store or a
513
+ resource that nothing references is released, and the finalizer removes its node from the model (DVT-16,
514
+ DVT-RTGC). Stores and resources have no dispose event of their own, so this is how they leave the model
515
+ when nothing owns them or their owner never disposes. A store's handle is anchored to the store's root
516
+ object by the runtime, so the handle lives exactly as long as the store the app can still reach. Owners and
517
+ computations are not retired by collection: an owner can be unreachable while its signals are in use, and
518
+ a computation can own signals. Their records stay until the runtime disposes them or `maxNodes` is reached.
519
+ The hook's replay buffer holds the first events (up to `buffer`) until the first sink attaches; that is
520
+ the one place the hook keeps application objects before any agent is attached.
521
+
522
+ ## Runtime integration
523
+
524
+ The runtime does not import this package. The runtime's side of the protocol is
525
+ `packages/core/src/internal/devtools.ts`. Its `report()` returns at once unless `__DEV__` is true,
526
+ looks up the global hook, and calls `hook.report` only when `hook.version === 1`. Core exports it as
527
+ `devtoolsReport`, together with `DEVTOOLS_EVENT` (the kind numbers), from `@shadow-js/core/extend`.
528
+ `@shadow-js/web` imports both from there (`packages/web/src/component.ts` and
529
+ `packages/web/src/internal/claim.ts`). Each call site is wrapped in `if (__DEV__)`, so production
530
+ bundles drop it.
531
+
532
+ | Event | Where it is reported | Arguments |
533
+ | --- | --- | --- |
534
+ | `OWNER_CREATED` | `Owner` constructor (`internal/owner.ts`), only when `new.target === Owner`; computations report their own | `(owner, parent, "root" or "owner")` |
535
+ | `OWNER_DISPOSED`, `COMPUTATION_DISPOSED` | `Owner.disposeSelf()` after `disposed` is set, through `reportDisposed()`; `Computation` overrides it | `(node)` |
536
+ | `SIGNAL_CREATED` | `Signal` constructor (`internal/graph.ts`) | `(signal, owner, name, value)` |
537
+ | `SIGNAL_WRITTEN` | `writeSignal` (`extend.ts`), after the equality check and before `notify` | `(signal, next, running computation, observers)` |
538
+ | `COMPUTATION_CREATED` | `Computation` constructor (`internal/graph.ts`) | `(computation, owner, "memo" or "effect", name, render flag)` |
539
+ | `COMPUTATION_RUN` | end of `recompute` (`internal/graph.ts`), timed with `performance.now()` around the user function | `(computation, start, duration, changed, sources)` |
540
+ | `FLUSH_START`, `FLUSH_END` | `flush()` (`internal/scheduler.ts`) when it starts the loop, and in its `finally` | `(render queue size, user queue size)`; `(computations run)` |
541
+ | `ERROR` | `handleError()` (`internal/owner.ts`) | `(owner, error, diagnostic code)` |
542
+ | `COMPONENT_MOUNTED`, `COMPONENT_UNMOUNTED` | `createComponent` in `@shadow-js/web`, after the component returns; a cleanup on the component's owner | `(owner, name, result)`; `(owner)` |
543
+ | `RESOURCE_STATE` | `setState` in `useResource` (`resource.ts`), and the query client (`packages/query/src/internal/core.ts`) | `useResource`: `(resource, state, options.name, owner)`. The query client: `(entry, status or "fetching", entry hash, owner)`. The third argument is the name the code gave, and the agent shows it as the name. |
544
+ | `STORE_CREATED` | `useStore` (`store/use-store.ts`) | `({ name, snapshot, set }, owner)` |
545
+ | `HYDRATION_MISMATCH` | `claimElement` in `@shadow-js/web`, when no server element has the key | `(message, key, undefined)` |
546
+ | `TRANSITION_START`, `TRANSITION_END` | `internal/concurrency.ts`: after a transition callback runs; when the transition lane is idle | `()`; `(transitions resolved)` |
547
+
548
+ Each component runs under its own owner in every build, not only in development. That is what lets
549
+ devtools attribute an update to a component.
550
+
551
+ ## Links
552
+
553
+ - [`@shadow-js/core`](https://shadowjs.net/reference/core/): the reactive engine and the `@shadow-js/core/extend` entry
554
+ point that exports `devtoolsReport`.
555
+ - [`@shadow-js/web`](https://shadowjs.net/reference/web/): the renderer that `mountDevtools()` uses for its panel.
556
+
557
+ ## Dependencies
558
+
559
+ - Runtime: none.
560
+ - Peers: `@shadow-js/core` and `@shadow-js/web` `^1.0.0`: the runtime the inspector reads and hooks into.
561
+
562
+ ## Types
563
+
564
+ - Names from the root: `Backend`, `PageHooks`, `InstalledHook`, `InstallHookOptions`, `InspectorModel`, `InspectionSource`.
565
+
566
+ ## Support
567
+
568
+ - Node.js `>=22.12.0`. ESM only: use `import`; `require` is not supported.
569
+ - Browsers: current evergreen releases.
570
+ - Versioning: every `@shadow-js/*` package releases together at one version. Upgrade them as one set.
571
+ - Documentation: [ShadowJS documentation](https://shadowjs.net).
572
+
573
+ ## Using, changing and contributing
574
+
575
+ - **Use the package** (a person or a coding agent that builds with it): this README, and [SKILL.md](SKILL.md), the short page an agent reads first.
576
+ - **Change the package** (fix a bug, add a feature): [AGENTS.md](AGENTS.md) says how the repository is laid out, how to implement and test a change, and how a pull request is reviewed. It is written for people and coding agents alike.
577
+ - **Take part**: [CONTRIBUTING.md](CONTRIBUTING.md) is the short guide; [SECURITY.md](SECURITY.md) is for vulnerabilities.