Skip to content

fix: use target receiver for scoped proxy accessors#607

Merged
miguel-heygen merged 2 commits intomainfrom
fix/scoped-proxy-host-getters
May 3, 2026
Merged

fix: use target receiver for scoped proxy accessors#607
miguel-heygen merged 2 commits intomainfrom
fix/scoped-proxy-host-getters

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented May 3, 2026

Problem

wrapScopedCompositionScript wraps composition scripts with scoped document, window, and gsap proxies. The current published latest and alpha packages still pass the proxy as the Reflect.get receiver, so browser host accessors like document.body can throw TypeError: Illegal invocation.

When that happens, the wrapper catches the error and aborts the rest of the composition script. For components that start hidden and reveal themselves through GSAP/timeline setup, that means the timeline is never registered and the render can stay visually empty.

Closes #606.

What this fixes

  • Reads scoped proxy properties with the original target as the Reflect.get receiver.
  • Applies the same target-receiver pattern to scoped proxy setters, including remapped timeline registry writes.
  • Preserves the existing behavior of binding returned methods back to the real target.
  • Covers document, window, remapped timeline registry, GSAP, and GSAP utils accessors/setters in regression tests that throw unless the receiver is the original target.

Root cause

The previous proxy traps called Reflect.get(target, prop, receiver). For accessors, that invokes the getter with this === receiver, and in this wrapper the receiver is the proxy. Browser host getters such as Document.prototype.body validate their receiver and reject the proxy, which makes ordinary composition code like document.body fail before timeline registration can run.

Verification

Local checks

  • Confirmed current npm state with npm view @hyperframes/core version dist-tags versions --json and npm view @hyperframes/producer version dist-tags versions --json: latest is 0.4.42, alpha is 0.5.0-alpha.14.
  • Packed @hyperframes/core and @hyperframes/producer at both latest and alpha; all four packed artifacts still contained the bad Reflect.get(target, prop, receiver) / utilsReceiver wrapper patterns before this fix.
  • bun run --cwd packages/core test -- src/compiler/compositionScoping.test.ts
  • bunx oxlint packages/core/src/compiler/compositionScoping.ts packages/core/src/compiler/compositionScoping.test.ts
  • bunx oxfmt --check packages/core/src/compiler/compositionScoping.ts packages/core/src/compiler/compositionScoping.test.ts
  • bun run --cwd packages/core build
  • bun run --cwd packages/core typecheck
  • bun run --cwd packages/producer typecheck
  • bun run --cwd packages/producer build
  • bun test packages/producer/src/services/htmlCompiler.test.ts
  • Confirmed the rebuilt core runtime and producer bundles no longer contain the old Reflect.get(..., receiver) / Reflect.set(..., receiver) scoped-wrapper patterns.
  • git diff --check
  • Pre-commit also reran lint, format, and typecheck successfully.

Browser verification

Used agent-browser against generated local repro pages:

  • core latest 0.4.42: bodyRead: false, titleOpacity: "0", timelineRegistered: false, errorCount: 1
  • core alpha 0.5.0-alpha.14: bodyRead: false, titleOpacity: "0", timelineRegistered: false, errorCount: 1
  • Patched local wrapper: bodyRead: true, titleOpacity: "1", timelineRegistered: true, errorCount: 0

Notes

  • Browser screenshots and the agent-browser recordings are local-only under tmp/issue-606/browser/, including issue-606-browser-proof.webm and issue-606-after-comment.webm.
  • No generated dist/ artifacts are committed.

Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against #606. Fix is correct and well-scoped.

What I checked

  1. Root cause matches the issue. Reflect.get(target, prop, receiver) invokes accessor getters with this === proxy, and host getters like Document.prototype.body reject the proxy with TypeError: Illegal invocation. The fix passes target so host getters see the right this.

  2. All four host-relevant proxy sites are patched in packages/core/src/compiler/compositionScoping.ts:

    • document proxy (line 141 → target)
    • window proxy (line 179 → target)
    • gsap.utils inner proxy (line 252 → utilsTarget)
    • gsap outer proxy (line 257 → target)

    #606 specifically called these out as the load-bearing ones (the others — timeline-registry proxy on a plain object — were noted as not load-bearing).

  3. Single source path. The runtime IIFE shipped in core/dist/hyperframe.runtime.iife.js is bundled from compositionLoader.ts which imports wrapScopedCompositionScript from this same file. So one fix here propagates to both producer-side wrapping AND the runtime IIFE on next build. No duplicate template to keep in sync.

  4. Regression test is honest. compositionScoping.test.ts:90-173 stubs document/window/gsap/gsap.utils with explicit Illegal invocation getter guards (if (this !== fakeDocument) throw). The test fails on the old code (proxy as this) and passes on the new (target as this). Asserts both successful reads (__bodyTag === "BODY", __href, __gsapVersion, __utilsMarker) AND no console.error — symmetric proof.

  5. Browser verification in PR body. Confirmed via agent-browser against generated repro pages: pre-fix bodyRead: false / titleOpacity: "0" / timelineRegistered: false / errorCount: 1, post-fix all four values flipped. Cross-checked the bug exists in published [email protected] AND [email protected] before shipping.

  6. CI green on all required: Lint, Typecheck, Build, Test, Test: runtime contract, CLI smoke, all perf jobs. Regression-shards still running but the changed surface is narrow.

Minor (non-blocking)

The __hfTimelineRegistryProxy get/set traps still pass receiver. #606's author flagged this as non-load-bearing (the registry is a plain HF object, no host getters), so functionally this is correct. If you want consistency across the whole file, applying the same target/utilsTarget/etc. pattern there is zero-risk and would prevent confusion if anyone ever swaps the registry for a host-y target. Not worth a separate round-trip.

LGTM, this fixes #606 cleanly. No event=APPROVE from me — flagging as a review-comment per the team-base policy on stamping unless directly authorized.

— Rames Jusso

@miguel-heygen miguel-heygen merged commit 6bcf3ce into main May 3, 2026
41 of 54 checks passed
Copy link
Copy Markdown
Collaborator Author

Merge activity

@miguel-heygen miguel-heygen deleted the fix/scoped-proxy-host-getters branch May 3, 2026 17:19
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.

[producer] wrapScopedCompositionScript Proxy passes receiver to Reflect.get → TypeError: Illegal invocation on document.body

2 participants