Skip to content

fix: capture lynx.getDevtool() once to avoid errors on page close#521

Closed
upupming wants to merge 16 commits into
preactjs:mainfrom
lynx-family:fix/capture-devtool-reference
Closed

fix: capture lynx.getDevtool() once to avoid errors on page close#521
upupming wants to merge 16 commits into
preactjs:mainfrom
lynx-family:fix/capture-devtool-reference

Conversation

@upupming

Copy link
Copy Markdown
Contributor

Problem

lynx.getDevtool().dispatchEvent(...) throws when the page is being closed, because at that point lynx.getDevtool() returns undefined and accessing .dispatchEvent on undefined fails.

This affects both the postMessage path (App → Devtools) and the addEventListener registration.

Fix

Capture the devtool instance once, right after the existing typeof lynx.getDevtool !== "function" check, and reuse that reference for dispatchEvent / addEventListener instead of calling lynx.getDevtool() again at runtime.

// @ts-ignore
const devtool = lynx.getDevtool();

Since the reference is captured during setup (when it is guaranteed valid), later calls no longer depend on lynx.getDevtool() still returning a valid object.

Verification

  • oxfmt --check / oxlint: no new errors (only pre-existing console warnings)
  • tsc --noEmit: no errors for this file

upupming and others added 16 commits September 18, 2025 14:17
…er (#2)

The `root-order-page` listener was the only call site in
`createAdapter` still reading the bare global `document` instead of
going through the `window = preactDevtoolsCtx` alias used by every
other DOM access in this file.

In browser shells `globalThis.document` and `preactDevtoolsCtx.document`
point at the same object so this worked by accident. In the ReactLynx
host `globalThis.document` is undefined and `react-lynx/setup.ts` shims
`preactDevtoolsCtx.document` with the background root snapshot, so any
`refresh` from the panel crashed with:

  TypeError: cannot read property 'body' of undefined
    at sendToDevtools
    at applyFilters (renderer.ts:275)
    at refresh (renderer.ts:215)

Route the access through `window.document.body` so the ReactLynx shim
is honoured. Add a regression test under `src/adapter/adapter/` that
exercises the `root-order-page` handler with `globalThis.document`
deliberately removed to lock in the fix.
Reference internal-preact's release workflow:
- Trigger publish on push to main (keep manual workflow_dispatch)
- Insert a UTC timestamp before the commit sha in the published
  version so prerelease ordering stays monotonic across commits
# Conflicts:
#	.gitignore
#	package-lock.json
#	package.json
#	src/adapter/10/bindings.ts
#	src/adapter/adapter/port.ts
#	src/adapter/protocol/events.ts
#	test-e2e/tests/profiler/flamegraph/profiler-static-subtree.test.ts
#	tools/build-plugins/esbuild-plugins.mjs
#	tools/test-setup.js
#	tsconfig.json
…t to 0.121.0 (#4)

* fix: broken hooks detection (preactjs#507)

Note that hooks are pretty much supported everywhere so this had no production impact.

* fix: incorrect props string truncation (preactjs#508)

* chore: only fetch required preact versions for tests (preactjs#509)

* chore: harden e2e tests (preactjs#510)

Lots of flakyness was caused by trying to one-shot assertions by grabbing an element and asserting on it instead of polling. Other issues were basically race conditions between elements of which there were plenty.

All of this is now much more reliable thanks to new assertions in playwright.

* fix: potential memory leak due to growing string table (preactjs#511)

The string table was unbounded, so add a LRU cache to limit growth. Also make encoding of strings faster while we're at it.

* fix: custom filter not removed + use popover (preactjs#512)

* chore: move unit tests to vitest (preactjs#513)

* chore: switch to oxlint + oxfmt (preactjs#514)

* chore: remove husky + lint-staged (preactjs#515)

* chore: update dependencies (preactjs#516)

Also fix text signal issue with newer signals versions

* chore: fix inline build failing (preactjs#517)

* chore: update dependencies (preactjs#518)

* fix: make hook inspection attachment more robust (preactjs#519)

Using a `setTimeout` was always a bit meh.

---------

Co-authored-by: Marvin Hagemeister <hello@marvinh.dev>
…tes roots

`_root` decided whether the user-passed container is a real DOM node
or a virtual islands container by checking `"Node" in globalThis &&
parent instanceof preactDevtoolsCtx.Node`. The original code used
`parent instanceof Node`; the ReactLynx fork swapped the right hand
side to `preactDevtoolsCtx.Node` (a shim of `BackgroundSnapshotInstance`)
but kept `"Node" in globalThis` as the guard.

In the ReactLynx Background VM `globalThis.Node` does not exist, so
the AND short-circuits and `treeParent` falls through to
`parent.parentNode`. The application root (`__root`) has no parent,
so `userRootToContainer` stored `null`. `_commit` then skipped
`roots.set(vnode, dom)` because of the `if (dom)` guard, leaving
`getRootMappings()` permanently empty.

User-visible effect: panel-side `refresh` (`adapter.ts:195` ->
`renderer.applyFilters`) iterated zero roots and emitted no
`operation_v2`, so a reconnecting panel could not recover the tree
without a full `Page.reload`. The browser content-script even
contains a comment explaining this is the path it relies on:
`shells/shared/content-script.ts:30-36`.

Use the same shim on both sides of the guard. Behavior is unchanged
on browser hosts, where `preactDevtoolsCtx.Node === window.Node`,
and unchanged in test fixtures that do not set
`preactDevtoolsCtx.Node`.

Add `src/adapter/10/options.test.tsx` that simulates the ReactLynx VM
(no `globalThis.Node`, parentless container, `preactDevtoolsCtx.Node`
set to a host-specific class) and asserts the renderer's `roots` map
gets the container after a `_commit`. Verified the test fails on
`main` (`expected 0 to equal 1`) and passes with the fix.
…omium)

The Lynx DevTool host embeds Chromium 108 (Electron 22), which predates the
HTML Popover API (Chromium 114). preactjs#512 moved the element/profiler filter
dropdowns to `popover="auto"` + `popovertarget`, so on that host they render
visible by default and cannot be dismissed.

Revert just the popover mechanism (restore OutsideClick + `filterVisible`
show/hide); keep preactjs#512's id-based custom-filter handling.
fix: avoid native Popover API for filter dropdowns (broken on old Chromium)
When the devtools panel opened with no tree (it connected after the page
rendered, or reopened without its state), it reloaded the whole page
(`Page.reload`), losing app state.

Instead emit a `refresh` so the in-app hook re-walks its existing roots and
re-sends the tree, with no reload. The Lynx bridge has no content-script-style
buffer, so the panel also missed the initial `attach` and never learned
`supportsHooks`; re-send `attach` for each renderer on `refresh` (idempotent)
so hooks inspection works too. If there are genuinely no roots, an empty tree
is correct — reloading is left to the user.
…f-reload

fix: recover the tree via refresh instead of reloading the page
…dev (#8)

Setup was gated on `__DEV__`, so it never ran in production. With
`@lynx-js/react-alias-rsbuild-plugin` stripping `@lynx-js/preact-devtools` in
production unless `REACT_DEVTOOL` is set, the import itself is the opt-in: if
this module runs at all, devtools is wanted. Drop the `__DEV__` guard so a
user-imported devtools also works in production (paired with the lynx-stack
`REACT_DEVTOOL` toolchain changes). The test environment is still skipped.
Bring the demo's Lynx dependencies up to current:

- @lynx-js/rspeedy 0.10.6 -> 0.15.0
- @lynx-js/react-rsbuild-plugin 0.10.11 -> 0.17.0
- @lynx-js/react 0.112.2 -> 0.121.2
- @lynx-js/qrcode-rsbuild-plugin 0.4.0 -> 0.5.0
- @lynx-js/types 3.3.0 -> 3.7.0
- @lynx-js/react 0.121.0 -> 0.121.2
- @lynx-js/types ^3.3.0 -> ^4.0.0
- @lynx-js/testing-environment 0.1.5 -> 0.3.0

Adapt the lynx test setup to `@lynx-js/testing-environment` >=0.2, which takes
the host `window` via the constructor instead of reading `global.jsdom`.
type-check, vitest, and vitest:lynx all pass.
lynx.getDevtool() can return undefined later in the lifecycle (e.g.
when the page is being closed), so calling .dispatchEvent /
.addEventListener on the result throws. Capture the devtool instance
once after the type check and reuse it.
@upupming

Copy link
Copy Markdown
Contributor Author

Opened against the wrong repository by mistake; reopening on lynx-family/preact-devtools.

@upupming upupming closed this Jul 14, 2026
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.

2 participants