Skip to content

Commit 2c4b500

Browse files
hi-ogawaclaudecodex
authored
feat(rsc): bind accessed member expression value for use server closure (#1172)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Codex <noreply@openai.com>
1 parent 623eadf commit 2c4b500

33 files changed

+1515
-28
lines changed

.oxfmtrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"groups": [["builtin"], ["external"]]
1010
},
1111
"ignorePatterns": [
12-
"*.snap.json",
12+
"*.snap.*",
1313
"typescript-eslint/",
1414
"packages/*/CHANGELOG.md",
1515
"playground-temp/",
Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
# Member-Chain Follow-Up: Optional and Computed Access
2+
3+
## Goal
4+
5+
Track the next step after plain non-computed member-chain binding:
6+
7+
- optional chaining, e.g. `x?.y.z`, `x.y?.z`
8+
- computed access, e.g. `x[y].z`, `x.y[k]`
9+
10+
This note is intentionally a follow-up to the plain member-chain work in
11+
[2026-04-05-rsc-member-chain-binding-plan.md](./2026-04-05-rsc-member-chain-binding-plan.md).
12+
It is not part of the current cleanup / plain-chain implementation.
13+
14+
## Current state
15+
16+
The current implementation is intentionally narrow:
17+
18+
- plain non-computed member chains like `x.y.z` are captured precisely
19+
- unsupported hops stop capture at the last safe prefix
20+
- examples:
21+
- `x?.y.z` -> bind `x`
22+
- `a.b?.c` -> bind `{ b: a.b }`
23+
- `x[y].z` -> bind `x`
24+
25+
This is a reasonable conservative failure mode, but it is not full support.
26+
27+
## Why this needs a separate design
28+
29+
The current `BindPath` shape in [src/transforms/hoist.ts](../../src/transforms/hoist.ts)
30+
is effectively:
31+
32+
```ts
33+
type BindPath = {
34+
key: string
35+
segments: string[]
36+
}
37+
```
38+
39+
That is enough for `x.y.z` because codegen can reconstruct the bind expression
40+
from the root identifier plus dot segments.
41+
42+
It is not enough for:
43+
44+
- `x?.y.z`
45+
- `x.y?.z`
46+
- `x[y].z`
47+
- `x?.[y]`
48+
49+
The missing information is not cosmetic. It changes semantics.
50+
51+
### Optional chaining
52+
53+
Each hop needs to preserve whether access is optional.
54+
55+
Example:
56+
57+
```js
58+
x?.y.z
59+
```
60+
61+
Reconstructing this as `x.y.z` is wrong because the bind-time access becomes
62+
stricter than the original expression.
63+
64+
### Computed access
65+
66+
Each computed hop needs the property expression, not just a string segment.
67+
68+
Example:
69+
70+
```js
71+
x[y].z
72+
```
73+
74+
There is no way to reconstruct this faithfully from `["y", "z"]`, because the
75+
first `y` is an expression, not a property name.
76+
77+
### Computed key expressions also have their own closure semantics
78+
79+
Computed access is not only a codegen problem. The key expression itself may
80+
close over outer variables, or it may be local to the action.
81+
82+
Outer-scope key:
83+
84+
```js
85+
function outer() {
86+
let key = 'x'
87+
let obj = {}
88+
async function action() {
89+
'use server'
90+
return obj[key]
91+
}
92+
}
93+
```
94+
95+
Both `obj` and `key` are outer captures.
96+
97+
Action-local key:
98+
99+
```js
100+
function outer() {
101+
let obj = {}
102+
async function action() {
103+
'use server'
104+
let key = 'x'
105+
return obj[key]
106+
}
107+
}
108+
```
109+
110+
Only `obj` is an outer capture; `key` is local to the action.
111+
112+
So any future `obj[expr]` support must treat the computed key as an ordinary
113+
expression with its own scope resolution, not just as a printable suffix on a
114+
member path.
115+
116+
## Minimum data model change
117+
118+
To support these cases, `BindPath` needs richer per-hop metadata.
119+
120+
Sketch:
121+
122+
```ts
123+
type BindSegment =
124+
| { kind: 'property'; name: string; optional: boolean }
125+
| { kind: 'computed'; expr: Node; optional: boolean }
126+
127+
type BindPath = {
128+
key: string
129+
segments: BindSegment[]
130+
}
131+
```
132+
133+
This is enough to represent:
134+
135+
- `.foo`
136+
- `?.foo`
137+
- `[expr]`
138+
- `?.[expr]`
139+
140+
The exact `key` design is still open. It only needs to support dedupe among
141+
captures that are semantically comparable.
142+
143+
## Required implementation areas
144+
145+
### 1. `scope.ts`: capture shape
146+
147+
In [src/transforms/scope.ts](../../src/transforms/scope.ts),
148+
`getOutermostBindableReference()` currently accumulates only plain
149+
non-computed member chains and stops at unsupported hops.
150+
151+
To support optional/computed access, capture analysis must preserve richer
152+
member-hop metadata instead of reducing everything to `Identifier` or
153+
`MemberExpression` with plain identifier-name segments.
154+
155+
That likely means changing either:
156+
157+
- what `referenceToNode` stores, or
158+
- adding a new structured capture representation derived from the AST
159+
160+
### 2. `hoist.ts`: path extraction
161+
162+
In [src/transforms/hoist.ts](../../src/transforms/hoist.ts),
163+
`memberExpressionToPath()` currently extracts only `string[]` segments.
164+
165+
That helper would need to become a structured extractor that records:
166+
167+
- property vs computed
168+
- optional vs non-optional
169+
- enough information to regenerate the bind expression
170+
171+
### 3. Dedupe semantics
172+
173+
Current prefix dedupe is straightforward for plain dot paths:
174+
175+
- `x.y` covers `x.y.z`
176+
- `x` covers everything below it
177+
178+
With optional/computed access, dedupe needs clearer rules.
179+
180+
Questions:
181+
182+
- does `x.y` cover `x.y?.z`?
183+
- does `x[y]` cover `x[y].z` only when the computed key expression is identical?
184+
- how should keys be normalized for comparison?
185+
186+
The current antichain logic should not be reused blindly.
187+
188+
### 3a. Support boundary for `obj[expr]`
189+
190+
This is still intentionally unresolved.
191+
192+
Possible support levels:
193+
194+
1. Keep current safe-prefix bailout only.
195+
Examples:
196+
- `obj[key]` -> bind `obj`, bind `key` separately if it is an outer capture
197+
- `obj[key].value` -> bind `obj`, bind `key` separately if needed
198+
199+
2. Support exact computed member captures only for simple shapes.
200+
Examples:
201+
- `obj[key]`
202+
- `obj[key].value`
203+
but only when we have a clear representation for both the base object and the
204+
key expression.
205+
206+
3. Support computed access as a first-class bind path.
207+
This would require fully defining:
208+
- path equality
209+
- prefix coverage
210+
- codegen for bind expressions
211+
- partial-object synthesis, if still applicable
212+
213+
At the moment, the note does not assume we will reach (3). It is entirely
214+
reasonable to stop at (1) or (2) if the semantics and implementation cost of
215+
full computed-path support are not compelling.
216+
217+
### 4. Bind-expression codegen
218+
219+
Current codegen only needs:
220+
221+
- `root`
222+
- `segments: string[]`
223+
224+
and synthesizes:
225+
226+
```ts
227+
root + segments.map((segment) => `.${segment}`).join('')
228+
```
229+
230+
That must be replaced with codegen that can emit:
231+
232+
- `.foo`
233+
- `?.foo`
234+
- `[expr]`
235+
- `?.[expr]`
236+
237+
### 5. Partial-object synthesis
238+
239+
This is the hardest part.
240+
241+
For plain member paths, partial-object synthesis is natural:
242+
243+
```js
244+
{
245+
y: {
246+
z: x.y.z
247+
}
248+
}
249+
```
250+
251+
For computed access, synthesis is less obvious:
252+
253+
```js
254+
x[k].z
255+
```
256+
257+
Questions:
258+
259+
- should this become an object with computed keys?
260+
- should computed paths fall back to broader binding even after we support
261+
recognizing them?
262+
- does partial-object binding remain the right representation for these cases?
263+
264+
This is where the design may need to diverge from plain member chains.
265+
266+
### 6. Comparison with Next.js
267+
268+
Relevant prior art is documented in
269+
[scope-manager-research/nextjs.md](./scope-manager-research/nextjs.md).
270+
271+
Important comparison points:
272+
273+
- Next.js already models optional member access in its `NamePart` structure.
274+
- Next.js does not support computed properties in the captured member-path
275+
model.
276+
- Next.js member-path capture is deliberately limited to member chains like
277+
`foo.bar.baz`.
278+
279+
That means:
280+
281+
- optional chaining has direct prior art in Next.js's capture model
282+
- computed access does not; if we support it, we are going beyond the current
283+
Next.js design
284+
285+
This should affect scoping decisions for the follow-up:
286+
287+
- optional support is an extension of an already-established member-path model
288+
- computed support is a materially larger design question, especially once key
289+
expression scope and dedupe semantics are included
290+
291+
## Safe intermediate target
292+
293+
If we want a minimal correctness-first follow-up:
294+
295+
1. keep the current safe-prefix bailout behavior
296+
2. add explicit tests for optional/computed cases
297+
3. only implement richer capture metadata once codegen and dedupe rules are
298+
agreed
299+
300+
That avoids regressing semantics while leaving room for a more precise design.
301+
302+
## Temporary conclusion
303+
304+
Current working direction:
305+
306+
- likely support optional chaining next, to align with Next.js's existing
307+
member-path behavior
308+
- keep computed access as a separate, open design problem for now
309+
310+
Rationale:
311+
312+
- optional chaining already has prior art in Next.js's capture model
313+
- computed access is materially more complex because it mixes:
314+
- key-expression scope resolution
315+
- path equality / dedupe rules
316+
- bind-expression codegen
317+
- unclear partial-object synthesis semantics
318+
319+
So the likely near-term path is:
320+
321+
1. support optional member chains
322+
2. keep current conservative behavior for computed access
323+
3. revisit computed support only if there is a clear use case and a concrete
324+
design that handles key-expression closure semantics correctly
325+
326+
## Suggested first questions before coding
327+
328+
1. Optional chains:
329+
Should the first supported version preserve optional syntax exactly in the
330+
bound expression, or should optional hops continue to bail out?
331+
332+
2. Computed access:
333+
Do we want exact support for `x[y].z`, or only a less coarse bailout than
334+
binding the whole root?
335+
336+
3. Binding shape:
337+
Is partial-object synthesis still the preferred strategy for computed access,
338+
or does this push us toward a different representation?
339+
340+
4. Computed key scope:
341+
If we support `obj[expr]`, what is the intended contract for the key
342+
expression?
343+
Specifically:
344+
- must outer variables used in `expr` always be captured independently?
345+
- do we need a representation that distinguishes outer `key` from
346+
action-local `key` when deciding support and dedupe?
347+
348+
5. Comparison target:
349+
Do we want to stay aligned with Next.js and continue treating computed access
350+
as out of scope, or intentionally support a broader feature set?
351+
352+
## Candidate tests
353+
354+
Add focused hoist fixtures for:
355+
356+
1. `x?.y.z`
357+
2. `x.y?.z`
358+
3. `x?.y?.z`
359+
4. `x[y].z`
360+
5. `x.y[k]`
361+
6. `x[y]?.z`
362+
7. `a.b?.c` as a safe-prefix bailout baseline
363+
8. `a[b].c` as a safe-prefix bailout baseline

0 commit comments

Comments
 (0)