Skip to content

feat(rsc): bind accessed member expression value for use server closure#1172

Open
hi-ogawa wants to merge 35 commits intotest-rsc-add-test-for-use-server-binding-and-shadowingfrom
feat-bind-member-expr
Open

feat(rsc): bind accessed member expression value for use server closure#1172
hi-ogawa wants to merge 35 commits intotest-rsc-add-test-for-use-server-binding-and-shadowingfrom
feat-bind-member-expr

Conversation

@hi-ogawa
Copy link
Copy Markdown
Contributor

@hi-ogawa hi-ogawa commented Apr 5, 2026

hi-ogawa and others added 2 commits April 5, 2026 11:34
…of whole root

Captures like `x.y.z` now bind `{ y: { z: x.y.z } }` and keep the
original root name as the hoisted function parameter, avoiding
serialization of the whole root object over the network.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@hi-ogawa hi-ogawa changed the title feat(rsc): bind only accessed member expression value for use server closure feat(rsc): bind accessed member expression value for use server closure Apr 5, 2026
@hi-ogawa hi-ogawa marked this pull request as ready for review April 6, 2026 04:26
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the RSC server-action hoisting transform so closures that reference member-access chains (e.g. config.api.key) bind only the accessed subtree instead of serializing/binding the full root object. This reduces RSC payload size and avoids runtime serialization failures when the root contains non-serializable values.

Changes:

  • Extend scope analysis to track the “outermost bindable” reference node per identifier (identifier vs. non-computed member-expression chain, with callee trimming).
  • Update hoist binding to synthesize partial object literals per root identifier and bind those (while keeping the hoisted function parameter as the root name).
  • Add/refresh fixtures + snapshots and add design notes; update formatter ignore patterns for new *.snap.js files.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/plugin-rsc/src/transforms/utils.ts Adds a shared DefaultMap helper (moved out of tests).
packages/plugin-rsc/src/transforms/scope.ts Tracks member-expression chains per reference (referenceToNode) and applies callee trimming.
packages/plugin-rsc/src/transforms/scope.test.ts Updates scope serializer to optionally display the tracked reference node; uses shared DefaultMap.
packages/plugin-rsc/src/transforms/hoist.ts Changes bind-var extraction to synthesize partial-object bind expressions for member chains.
packages/plugin-rsc/src/transforms/hoist.test.ts Adds fixture-based snapshot testing for hoist output and updates/clarifies some existing edge-case comments.
packages/plugin-rsc/src/transforms/fixtures/scope/reference-node/member-chain.js New fixture for plain member chain reference-node tracking.
packages/plugin-rsc/src/transforms/fixtures/scope/reference-node/member-chain.js.snap.json Snapshot for plain member chain reference-node tracking.
packages/plugin-rsc/src/transforms/fixtures/scope/reference-node/member-chain-optional.js New fixture for optional-chain boundary behavior.
packages/plugin-rsc/src/transforms/fixtures/scope/reference-node/member-chain-optional.js.snap.json Snapshot for optional-chain boundary behavior.
packages/plugin-rsc/src/transforms/fixtures/scope/reference-node/member-chain-computed.js New fixture for computed-access boundary behavior.
packages/plugin-rsc/src/transforms/fixtures/scope/reference-node/member-chain-computed.js.snap.json Snapshot for computed-access boundary behavior.
packages/plugin-rsc/src/transforms/fixtures/scope/reference-node/member-chain-callee.js New fixture for callee-trimming behavior.
packages/plugin-rsc/src/transforms/fixtures/scope/reference-node/member-chain-callee.js.snap.json Snapshot for callee-trimming behavior.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain.js New hoist fixture for partial-object binding of member chains.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain.js.snap.js Snapshot for member-chain partial-object binding.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-siblings.js New hoist fixture for merging sibling paths under one root.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-siblings.js.snap.js Snapshot for sibling-path merge binding.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-root.js New hoist fixture where direct root access forces binding the whole root.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-root.js.snap.js Snapshot for root-access override binding.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-optional.js New hoist fixture for optional-chain conservative fallback behavior.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-optional.js.snap.js Snapshot for optional-chain fallback behavior.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-dedupe.js New hoist fixture for prefix dedupe behavior.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-dedupe.js.snap.js Snapshot for prefix-dedupe binding result.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-computed.js New hoist fixture for computed-access conservative fallback behavior.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-computed.js.snap.js Snapshot for computed-access fallback behavior.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-callee.js New hoist fixture verifying callee trimming preserves receiver capture.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-callee.js.snap.js Snapshot for callee-trimming hoist output.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-unsupported.js New hoist fixture covering multiple unsupported patterns.
packages/plugin-rsc/src/transforms/fixtures/hoist/member-chain-unsupported.js.snap.js Snapshot for unsupported-pattern binding behavior.
packages/plugin-rsc/docs/notes/2026-04-05-rsc-member-chain-binding-plan.md Design note documenting the chosen approach and rationale.
packages/plugin-rsc/docs/notes/2026-04-05-member-chain-optional-computed-follow-up.md Follow-up note outlining optional/computed access support considerations.
.oxfmtrc.json Expands ignore pattern to cover *.snap.* (including new *.snap.js).
Comments suppressed due to low confidence (1)

packages/plugin-rsc/src/transforms/scope.test.ts:10

  • Typo in comment: “snaphsot” → “snapshot”.
// - use single markdown as snaphsot? (cf. review-scope-fixtures.ts)

describe('fixtures', () => {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +287 to +292
function serialize(node: TrieNode, segments: string[]): string {
if (node.size === 0) {
return root + segments.map((segment) => `.${segment}`).join('')
}
const entries = [...node.entries()]
.map(([k, child]) => `${k}: ${serialize(child, [...segments, k])}`)
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

serialize emits object-literal keys as bare identifiers (e.g. { __proto__: ... }). If a captured segment is __proto__, this changes the resulting object's prototype (prototype pollution / semantic change). Consider emitting a computed key for __proto__ (e.g. ['__proto__']) or otherwise guarding special keys when generating the partial object literal.

Suggested change
function serialize(node: TrieNode, segments: string[]): string {
if (node.size === 0) {
return root + segments.map((segment) => `.${segment}`).join('')
}
const entries = [...node.entries()]
.map(([k, child]) => `${k}: ${serialize(child, [...segments, k])}`)
function serializeObjectKey(segment: string): string {
return segment === '__proto__' ? `["${segment}"]` : segment
}
function serialize(node: TrieNode, segments: string[]): string {
if (node.size === 0) {
return root + segments.map((segment) => `.${segment}`).join('')
}
const entries = [...node.entries()]
.map(
([k, child]) =>
`${serializeObjectKey(k)}: ${serialize(child, [...segments, k])}`,
)

Copilot uses AI. Check for mistakes.
return node.name
}
// <unknown>/computed/optional shouldn't show up
// since they aren't colelcted as reference node yet.
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

Typo in comment: “colelcted” → “collected”.

Suggested change
// since they aren't colelcted as reference node yet.
// since they aren't collected as reference node yet.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +26
const snaphsot = result.output.hasChanged()
? result.output.toString()
: '/* NO CHANGE */'
await expect(snaphsot).toMatchFileSnapshot(file + '.snap.js')
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

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

Typo in variable name: snaphsotsnapshot (and corresponding uses). Keeping consistent spelling makes fixture failures easier to scan.

Suggested change
const snaphsot = result.output.hasChanged()
? result.output.toString()
: '/* NO CHANGE */'
await expect(snaphsot).toMatchFileSnapshot(file + '.snap.js')
const snapshot = result.output.hasChanged()
? result.output.toString()
: '/* NO CHANGE */'
await expect(snapshot).toMatchFileSnapshot(file + '.snap.js')

Copilot uses AI. Check for mistakes.
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.

2 participants