feat(rsc): bind accessed member expression value for use server closure#1172
feat(rsc): bind accessed member expression value for use server closure#1172hi-ogawa wants to merge 35 commits intotest-rsc-add-test-for-use-server-binding-and-shadowingfrom
use server closure#1172Conversation
…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>
use server closureuse server closure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
Co-authored-by: Codex <noreply@openai.com>
There was a problem hiding this comment.
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.jsfiles.
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.
| 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])}`) |
There was a problem hiding this comment.
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.
| 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])}`, | |
| ) |
| return node.name | ||
| } | ||
| // <unknown>/computed/optional shouldn't show up | ||
| // since they aren't colelcted as reference node yet. |
There was a problem hiding this comment.
Typo in comment: “colelcted” → “collected”.
| // since they aren't colelcted as reference node yet. | |
| // since they aren't collected as reference node yet. |
| const snaphsot = result.output.hasChanged() | ||
| ? result.output.toString() | ||
| : '/* NO CHANGE */' | ||
| await expect(snaphsot).toMatchFileSnapshot(file + '.snap.js') |
There was a problem hiding this comment.
Typo in variable name: snaphsot → snapshot (and corresponding uses). Keeping consistent spelling makes fixture failures easier to scan.
| 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') |
Description
use servervariable binding #1170TODO