Skip to content

feat: ownership tree panel - #19

Open
lxsmnsyc wants to merge 4 commits into
mainfrom
ownership-tree
Open

feat: ownership tree panel#19
lxsmnsyc wants to merge 4 commits into
mainfrom
ownership-tree

Conversation

@lxsmnsyc

Copy link
Copy Markdown
Member

Adds a panel to the dev toolbar that shows the app as a tree of owners, and a demo app whose component tree changes while you use it.

The panel

Two modes:

  • Component mode lists components only and folds the scopes between them into the component above, so a component shows the signals, memos and effects created inside it.
  • Owner mode shows every owner, including roots, memos and effects. In the fixture that is 4 rows versus 15.

Selecting a row lists its prop names, the signals it holds with their values, the scopes folded into it, its children, and its ancestry. Ancestry frames run nearest first like a stack trace, and clicking a frame selects that owner so you can walk back up the tree. Rows flash when an owner is created, and the tree can be searched by component, scope or signal, which keeps the ancestors of a match visible so the row stays reachable.

How it finds components

The dev build of solid-js wraps every component in devComponent, which stamps the root it creates with _component = { fn, props, name }. That gives real component names and the live props object with no compiler support.

Source locations come from the hot reload transform, which records file:line:column on the wrapper it creates. @solidjs/vite-plugin runs that transform in development, so locations appear without extra setup, and clicking one calls Vite's /__open-in-editor. Components compiled without the transform simply have no location. Names also drop the [solid-refresh] tag the wrapper adds.

Prop values are getters, so the panel lists prop names and never reads them. Reading would run app code and could throw from inside a devtool.

Details worth review

  • The toolbar wraps the app, so excluding the toolbar's own owner would exclude the app with it. AppScope marks the scope holding props.children as app code, the nearest marker wins during the walk, and the marker itself never becomes a row.
  • Each snapshot carries a fingerprint. An unchanged fingerprint is not written to state, which stops the panel's own renders from feeding it another update.
  • The runtime keeps the newest child first, so the walk reverses children into creation order.
  • The panel installs the dev hooks only while it is open, and reads the tree through them, so it stays empty in a production build of the runtime.

The demo

examples/explorer is a file explorer, run with pnpm demo. FolderNode renders itself for every nested folder, so opening one mounts a component per child and the tree takes the shape of the folder. SelectionProvider owns the selection signals every row reads out of context, and PreviewPane sits behind a toggle so hiding it disposes an owner and its scopes.

Testing

  • 21 unit tests over the pure tree builder, covering folding, signal attribution, exclusion and the include marker, disposed owners, shared subtrees, name cleanup, locations and fingerprint stability.
  • An end to end test covering both modes, the detail pane, the ancestry frames and search.
  • pnpm build, pnpm typecheck and pnpm format:check are clean.
  • The demo was driven by hand: expanding a folder took the tree from 26 to 32 owners, hiding the preview dropped <PreviewPane>, and the page hydrates with no console warnings.

Note on the other branch

reactivity-graph (#18) carries its own copy of the dev hook install, the toolbar exclusion with the AppScope marker, and a value preview formatter. This branch reimplements them because it starts from main. Whichever lands second should drop its copy in favour of a shared registry.

🤖 Generated with Claude Code

lxsmnsyc and others added 4 commits September 11, 2026 21:45
The toolbar gains a panel that shows the app as a tree of owners.

- Component mode lists components only and folds the scopes between them into the component above, so a component shows the signals, memos and effects created inside it.
- Owner mode shows every owner, including roots, memos and effects.
- Selecting a row lists its prop names, the signals it holds with their values, the scopes folded into it and its children.
- Rows flash when an owner is created, and the tree can be searched by component, scope or signal.

Components are found through `_component`, which the dev build of solid-js
writes on the root it creates for every component, so rows carry real component
names and prop names. Prop values are getters, so the panel lists the names and
never reads them.

The panel reads the tree through the development hooks in solid-js, so it stays
empty in a production build of the runtime, and it installs those hooks only
while it is open. The toolbar marks its own scope so its components never show
up, and marks the scope that owns the app so the wrapped app still does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
examples/explorer is an app whose component tree changes while you use it.
Run it with `pnpm demo`.

- `FolderNode` renders itself for every nested folder, so opening a folder mounts a component per child and the ownership tree takes the shape of the folder.
- `SelectionProvider` owns the selection signals that every row reads out of context.
- `PreviewPane` sits behind a toggle, so hiding it disposes an owner and the scopes it holds.
- Several components create memos, which component mode folds into the component that owns them.

Component names now drop the `[solid-refresh]` prefix that the hot reload
transform adds to its wrapper, so rows read as the component the app declared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ownership panel shows a component's source location under its name, and
clicking it asks the dev server to open the file.

- The location comes from the hot reload transform, which records `file:line:column` on the wrapper it creates. `@solidjs/vite-plugin` runs that transform in development, so no extra setup is needed. Components compiled without it simply have no location.
- Clicking calls Vite's `/__open-in-editor` endpoint.
- Names now drop the `[solid-refresh]` tag everywhere, not just on components, so the memo the wrapper creates reads as the component it wraps.
- Long names in the signal and scope lists are clipped instead of overlapping their value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The detail pane gains an ancestry section below the children.

- Frames run nearest first, numbered like a stack, with the selected owner as frame zero.
- Each frame shows its kind, name and source location, and clicking one selects that owner, so you can walk back up the tree.
- The breadcrumb line above the props is gone, because the ancestry says the same thing with more detail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​solidjs/​vite-plugin@​3.0.0-next.359610010094100

View full report

@lxsmnsyc

lxsmnsyc commented Sep 11, 2026

Copy link
Copy Markdown
Member Author
Screenshot 2026-09-11 at 10 07 58 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant