Skip to content

Fix insertBefore leaving the previous sibling pointing past the inserted node#622

Open
airhorns wants to merge 1 commit into
Shopify:mainfrom
airhorns:polyfill-insert-before-sibling-chain
Open

Fix insertBefore leaving the previous sibling pointing past the inserted node#622
airhorns wants to merge 1 commit into
Shopify:mainfrom
airhorns:polyfill-insert-before-sibling-chain

Conversation

@airhorns

@airhorns airhorns commented Jul 16, 2026

Copy link
Copy Markdown

The bug

ParentNode.insertInto(child, before) relinks the new child's NEXT/PREV and the reference node's PREV, but only repoints the previous sibling's NEXT when the reference node is the first child:

if (before) {
  child[NEXT] = before;
  child[PREV] = before[PREV];
  if (before[PREV] === null) this[CHILD] = child;  // only insert-at-front is handled
  before[PREV] = child;                            // missing: else child[PREV][NEXT] = child
}

Insert before any later child and the forward sibling chain skips the new node.

That chain is exactly what querySelector / querySelectorAll recurse through (within[CHILD], then [CHILD] / [NEXT]), and what firstChild + nextSibling traversal walks. So an affected node becomes unreachable from every selector query and sibling walk — while childNodes (a spliced array), parentNode, previousSibling, isConnected, and the host mirror (built from the insertChild(parent, child, index) hook) all still report it as present.

The node renders correctly on the host and is invisible to the remote environment's own lookups, with no error raised. replaceChild (removeChild + insertInto(new, next)), prepend of more than one node, and ChildNode.after on a non-last child all route through the same branch and were affected too.

The divergence, on main, from a hand-rolled host mirror fed by the real insertChild hook:

sandbox chain host mirror querySelector('s-modal')
before [s-page, style] [s-page, s-modal, style] null
after [s-page, s-modal, style] [s-page, s-modal, style] s-modal

Why it matters

This is easy to hit whenever a framework renders ahead of existing content. A remote environment that appends a <style> element to the body and then lets Preact render into it gets every root inserted before that style element — mid-list. Code as ordinary as this then silently does nothing:

document.querySelector('#my-modal').showModal();

It also looks non-deterministic, because it tracks whether a <style> element happens to be present: with no style block the roots insert at the front, before[PREV] === null, and that is the one case the buggy branch got right.

The fix

Hoist the reference node's previous sibling and link it forward to the new child, keeping the insert-at-front case:

const prev = before[PREV];
child[NEXT] = before;
child[PREV] = prev;
if (prev === null) this[CHILD] = child;
else prev[NEXT] = child;
before[PREV] = child;

…ted node

ParentNode.insertBefore relinked the new child's NEXT/PREV and the reference
node's PREV, but only repointed the previous sibling's NEXT when the reference
node was the first child. Inserting before any later child therefore left the
forward sibling chain skipping the new node.

That chain is what querySelector, querySelectorAll, and firstChild/nextSibling
traversal walk, so an affected node was unreachable from every selector query and
sibling walk, while childNodes, parentNode, previousSibling, and the host mirror
all still reported it as present. The node rendered correctly on the host and was
invisible to the remote environment's own lookups, with no error raised.
replaceChild, prepend of more than one node, and ChildNode.after all route
through the same branch and were affected too.

Hoist the reference node's previous sibling and link it forward to the new child,
keeping the insert-at-front case that the old branch already handled.

Co-Authored-By: Claude <noreply@anthropic.com>
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