Skip to content

Don't clone root-less child nodes in container constructor#2097

Merged
ai merged 1 commit into
postcss:mainfrom
MahinAnowar:fix/adopt-rootless-nodes
Jun 28, 2026
Merged

Don't clone root-less child nodes in container constructor#2097
ai merged 1 commit into
postcss:mainfrom
MahinAnowar:fix/adopt-rootless-nodes

Conversation

@MahinAnowar

Copy link
Copy Markdown
Contributor

Closes #1987.

Problem

Passing a freshly created (parent-less) node as a child in a container constructor clones it, so the caller's original reference never gets a parent and later operations on it throw:

const decl = postcss.decl({ prop: 'foo', value: 'bar' })
postcss.atRule({ name: 'foo', nodes: [decl] })
decl.before(postcss.comment({ text: 'hello' }))
// TypeError: Cannot read properties of undefined (reading 'insertBefore')

The constructor unconditionally clones any real Node child:

if (typeof node.clone === 'function') {
  this.append(node.clone())
} else {
  this.append(node)
}

That clone was added in 9d19bce only to keep the source tree intact when moving nodes between two roots — but it also fires for brand-new, parent-less nodes that aren't part of any tree, where there's nothing to protect.

Fix

As you suggested on the issue, only clone when the node already belongs to another tree (node.parent is set); otherwise adopt the instance directly so append() sets its parent:

if (typeof node.clone === 'function' && node.parent) {
  this.append(node.clone())
} else {
  this.append(node)
}

Nodes that already have a parent (e.g. new Root({ nodes: root1.nodes })) are still cloned, so the move-between-trees behavior from 9d19bce is unchanged.

Tests

Adds a regression test (adopts root-less nodes in constructor instead of cloning them) that builds a container from a parent-less node and asserts the original instance is adopted (decl.parent is the new container, container.first === decl) and that operating on the original reference no longer throws. It fails on the current code and passes with this change. The two existing tests from 9d19bce (updates parent in overrides.nodes in constructor, allows to clone nodes) still pass — the full unit suite (653 tests) is green and ESLint is clean.

The container constructor cloned every real Node passed in `nodes`, so a
freshly created (parent-less) node was adopted as a copy and the caller's
original reference never had its `parent` set — later operating on it threw
`Cannot read properties of undefined`. The clone was added only to keep the
source tree intact when moving nodes between roots, so it's skipped when the
node has no parent. Nodes that already belong to another tree are still
cloned, preserving the existing behavior. Closes postcss#1987.
@ai
ai merged commit d4feed6 into postcss:main Jun 28, 2026
9 of 10 checks passed
@ai

ai commented Jun 28, 2026

Copy link
Copy Markdown
Member

Thanks!

@ai

ai commented Jun 28, 2026

Copy link
Copy Markdown
Member

Released in 8.5.16.

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.

Child nodes are cloned rather than updated when creating a container node

2 participants