Skip to content

Recognize nodes from other realms via realm-safe type checks - #156

Open
lizarusi wants to merge 2 commits into
bigskysoftware:mainfrom
lizarusi:fix-cross-realm-newcontent
Open

Recognize nodes from other realms via realm-safe type checks#156
lizarusi wants to merge 2 commits into
bigskysoftware:mainfrom
lizarusi:fix-cross-realm-newcontent

Conversation

@lizarusi

@lizarusi lizarusi commented Aug 13, 2026

Copy link
Copy Markdown

Hi!

While using idiomorph at Walnut, we hit a bug that we've been carrying as a local patch — contributing the fix upstream.

The bug

Passing a node created in another JS realm (e.g. an iframe's contentDocument) as newContent throws:

TypeError: newContent is not iterable
    at normalizeParent (src/idiomorph.js)
    at Object.morph (src/idiomorph.js)

Repro:

const target = document.querySelector("#target");
const iframe = document.createElement("iframe");
document.body.append(iframe);
const foreign = iframe.contentDocument.createElement("button");
foreign.textContent = "Bar";
Idiomorph.morph(target, foreign); // 💥 TypeError: newContent is not iterable

Every realm has its own constructors, so cross-realm nodes fail instanceof Node in normalizeParent and fall through to the array/HTMLCollection branch, which blows up on the spread.

The crash is only the visible part. instanceof stays false for these nodes even after they are adopted into this document, so once past normalization they also silently fail every other realm-sensitive instanceof check: template content handling, id-based matching of new children (newChild instanceof Element), and syncInputValue — meaning input/option/textarea values would silently not sync from cross-realm content. The second test demonstrates that.

We hit this in production at Walnut, where we morph DOM captured from customer apps across document boundaries — we've been carrying a fix as a local patch since 0.7.2.

The fix

Replace realm-sensitive instanceof checks with tiny duck-typing helpers (nodeType / localName) — the same approach morphdom uses. The helpers carry JSDoc type predicates, so TypeScript narrows exactly like instanceof did and npm run typecheck stays green. The document.activeElement checks in the focus-preservation code keep instanceof, since the active element always belongs to this document.

First commit adds the failing tests, second commit makes them pass. There is a dedicated test per converted check — entry normalization, id-based element preservation (both the dummy-parent and SlicedParentNode paths), template content morphing, input/option/textarea value syncing, head handling, and Document normalization — and each test was verified to fail if its single check alone is reverted to instanceof. npm run typecheck, npm run format:check, and the full suite pass, with coverage at 100%.

Relates to #103 — whichever direction the newContent type-narrowing goes, cross-realm nodes currently crash rather than being handled or rejected cleanly, so this seems worth fixing today.

… behaviors

One test per realm-sensitive check: entry normalization (crash),
id-based element preservation (detached and SlicedParentNode paths),
template content morphing, input/option/textarea value syncing,
head handling, and Document normalization.
Nodes created in another JS realm (e.g. an iframe's document) fail
`instanceof` checks, even after being adopted into this document,
because each realm has its own constructors. This made morph() throw
"TypeError: newContent is not iterable" when given a cross-realm node,
and silently skip template handling, id-based matching,
input/option/textarea value syncing, head handling, and Document
normalization for cross-realm nodes.

Replace realm-sensitive `instanceof` checks with helpers that duck-type
via `nodeType` and `localName` (the approach morphdom uses). The
`document.activeElement` checks keep `instanceof`, since the active
element always belongs to this document.

Each converted check has a dedicated test that fails if that single
check is reverted to `instanceof`.
@lizarusi
lizarusi force-pushed the fix-cross-realm-newcontent branch from c9da9c7 to a52cd2f Compare August 13, 2026 13:10
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