fix: vnode clone compatible with frozen intrinsics#5078
Conversation
📊 Tachometer Benchmark ResultsSummaryduration
usedJSHeapSize
Resultscreate10kduration
usedJSHeapSize
filter-listduration
usedJSHeapSize
hydrate1kduration
usedJSHeapSize
many-updatesduration
usedJSHeapSize
replace1kduration
usedJSHeapSize
run-warmup-0
run-warmup-1
run-warmup-2
run-warmup-3
run-warmup-4
run-final
text-updateduration
usedJSHeapSize
tododuration
usedJSHeapSize
update10th1kduration
usedJSHeapSize
|
|
requesting a review from @JoviDeCroock (bc of recent change to vnode constructor logic (strict equality)) |
|
While I think the fix makes sense, there's a site that has been forgotten by the LLM. |
good catch, thanks for the review.
can you help me understand this? sounds like 3rd parties are also cloning nodes, but I don't get the "through option hooks" part. the good news is this change would not break those ecosystem libraries, and users running in HardenedJS environments get a working core to help the ecosystem we could export |
4b73972 to
00658dc
Compare
|
The through options hook part is mostly the Do note that this is a PR for the main branch which is currently v11 (beta) and we also have the v10.x branch which is the mainline atm. |
Object.assign({}, vnode) in renderComponent(), cloneNode in
src/diff/index.js, and detachedClone in compat/src/suspense.js
tries to copy a vnode's `constructor: undefined` own property via
[[Set]]. Under environments where `Object.prototype.constructor`
is a non-writable data property (e.g. raw
`Object.freeze(Object.prototype)` or hardenedjs configured with
`overrideTaming: 'min'`), the assignment trips the override
mistake and fails with:
TypeError: Cannot assign to read only property 'constructor'
Initial render works because createVNode builds vnodes with an
object literal ([[CreateDataProperty]], not Set), but any
setState/forceUpdate-triggered re-render goes through
Object.assign({}, oldVNode) and trips the override mistake.
Note: hardening configs that tame the override mistake by
replacing `Object.prototype.constructor` with an accessor pair
(SES default, Node's `--frozen-intrinsics`) do NOT exhibit this
bug — but the fix is still required for the untamed configs
above. The added Node subprocess test guards against future
changes to that taming.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <noreply@anthropic.com>
00658dc to
a628f41
Compare
Disclaimer: I (a human) identified this issue years ago and have always just worked around it via a local patch. I used an LLM (opus 4.7) to generate the fix. I reviewed the change and wrote most of the PR description.
This PR fixes a compatibility issue when Preact is run in Hardened JavaScript environments such as:
The compatibility issue comes from a javascript quirk known as "the override mistake" related to frozen intrinsics. For this bug, the minimal environment change is just
Object.freeze(Object.prototype).In this environment, assigning the
constructorproperty via assignment syntax (=) orObject.assigncauses an error.Specifically,
Object.assign({}, vnode)inrenderComponent()(andcloneNodeinsrc/diff/index.js) tries to copy a vnode'sconstructor: undefinedown property via[[Set]]. Under environments with a frozenObject.prototypetheObject.prototype.constructoris non-writable, so the assignment fails.This PR introduces a
cloneVNodeutility that ensuresVNodesare cloned safely. It also reorders theconstructorkey for consistent object shape betweenVNodesand their clones.Note: I erred on the side of excessive documentation in comments, let me know if you would like this reduced.