@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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +576 -2
- package/dist/dev/agent.d.ts +239 -0
- package/dist/dev/agent.js +626 -0
- package/dist/dev/agent.js.map +1 -0
- package/dist/dev/backend.d.ts +53 -0
- package/dist/dev/backend.js +53 -0
- package/dist/dev/backend.js.map +1 -0
- package/dist/dev/hook.d.ts +83 -0
- package/dist/dev/hook.js +136 -0
- package/dist/dev/hook.js.map +1 -0
- package/dist/dev/index.d.ts +17 -0
- package/dist/dev/index.js +17 -0
- package/dist/dev/inspection.d.ts +226 -0
- package/dist/dev/inspection.js +263 -0
- package/dist/dev/inspection.js.map +1 -0
- package/dist/dev/inspector.d.ts +331 -0
- package/dist/dev/inspector.js +429 -0
- package/dist/dev/inspector.js.map +1 -0
- package/dist/dev/model.d.ts +418 -0
- package/dist/dev/model.js +559 -0
- package/dist/dev/model.js.map +1 -0
- package/dist/dev/mount.d.ts +207 -0
- package/dist/dev/mount.js +256 -0
- package/dist/dev/mount.js.map +1 -0
- package/dist/dev/overlay.d.ts +102 -0
- package/dist/dev/overlay.js +176 -0
- package/dist/dev/overlay.js.map +1 -0
- package/dist/dev/preview.d.ts +74 -0
- package/dist/dev/preview.js +122 -0
- package/dist/dev/preview.js.map +1 -0
- package/dist/dev/protocol.d.ts +288 -0
- package/dist/dev/protocol.js +109 -0
- package/dist/dev/protocol.js.map +1 -0
- package/dist/dev/ring.d.ts +50 -0
- package/dist/dev/ring.js +94 -0
- package/dist/dev/ring.js.map +1 -0
- package/dist/dev/transport.d.ts +347 -0
- package/dist/dev/transport.js +394 -0
- package/dist/dev/transport.js.map +1 -0
- package/dist/dev/ui/details.js +294 -0
- package/dist/dev/ui/details.js.map +1 -0
- package/dist/dev/ui/inspection-state.d.ts +43 -0
- package/dist/dev/ui/inspection-state.js +62 -0
- package/dist/dev/ui/inspection-state.js.map +1 -0
- package/dist/dev/ui/issues.js +85 -0
- package/dist/dev/ui/issues.js.map +1 -0
- package/dist/dev/ui/panel.d.ts +55 -0
- package/dist/dev/ui/panel.js +160 -0
- package/dist/dev/ui/panel.js.map +1 -0
- package/dist/dev/ui/rows.d.ts +44 -0
- package/dist/dev/ui/rows.js +132 -0
- package/dist/dev/ui/rows.js.map +1 -0
- package/dist/dev/ui/state-tab.js +88 -0
- package/dist/dev/ui/state-tab.js.map +1 -0
- package/dist/dev/ui/state.d.ts +192 -0
- package/dist/dev/ui/state.js +147 -0
- package/dist/dev/ui/state.js.map +1 -0
- package/dist/dev/ui/styles.d.ts +31 -0
- package/dist/dev/ui/styles.js +113 -0
- package/dist/dev/ui/styles.js.map +1 -0
- package/dist/dev/ui/timeline.js +135 -0
- package/dist/dev/ui/timeline.js.map +1 -0
- package/dist/dev/ui/tree.js +186 -0
- package/dist/dev/ui/tree.js.map +1 -0
- package/dist/prod/agent.js +2 -0
- package/dist/prod/agent.js.map +1 -0
- package/dist/prod/backend.js +2 -0
- package/dist/prod/backend.js.map +1 -0
- package/dist/prod/hook.js +2 -0
- package/dist/prod/hook.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/inspection.js +2 -0
- package/dist/prod/inspection.js.map +1 -0
- package/dist/prod/inspector.js +2 -0
- package/dist/prod/inspector.js.map +1 -0
- package/dist/prod/model.js +2 -0
- package/dist/prod/model.js.map +1 -0
- package/dist/prod/mount.js +2 -0
- package/dist/prod/mount.js.map +1 -0
- package/dist/prod/overlay.js +11 -0
- package/dist/prod/overlay.js.map +1 -0
- package/dist/prod/preview.js +2 -0
- package/dist/prod/preview.js.map +1 -0
- package/dist/prod/protocol.js +2 -0
- package/dist/prod/protocol.js.map +1 -0
- package/dist/prod/ring.js +2 -0
- package/dist/prod/ring.js.map +1 -0
- package/dist/prod/transport.js +2 -0
- package/dist/prod/transport.js.map +1 -0
- package/dist/prod/ui/details.js +2 -0
- package/dist/prod/ui/details.js.map +1 -0
- package/dist/prod/ui/inspection-state.js +2 -0
- package/dist/prod/ui/inspection-state.js.map +1 -0
- package/dist/prod/ui/issues.js +2 -0
- package/dist/prod/ui/issues.js.map +1 -0
- package/dist/prod/ui/panel.js +2 -0
- package/dist/prod/ui/panel.js.map +1 -0
- package/dist/prod/ui/rows.js +2 -0
- package/dist/prod/ui/rows.js.map +1 -0
- package/dist/prod/ui/state-tab.js +2 -0
- package/dist/prod/ui/state-tab.js.map +1 -0
- package/dist/prod/ui/state.js +2 -0
- package/dist/prod/ui/state.js.map +1 -0
- package/dist/prod/ui/styles.js +82 -0
- package/dist/prod/ui/styles.js.map +1 -0
- package/dist/prod/ui/timeline.js +2 -0
- package/dist/prod/ui/timeline.js.map +1 -0
- package/dist/prod/ui/tree.js +2 -0
- package/dist/prod/ui/tree.js.map +1 -0
- 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
|
-
|
|
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
|
-
|
|
3
|
+
# @shadow-js/devtools
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@shadow-js/devtools)
|
|
6
|
+
[](https://github.com/shadowjs-dev/devtools/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/shadowjs-dev/devtools/blob/main/LICENSE)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](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.
|