Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
127 commits
Select commit Hold shift + click to select a range
d18b8e4
test(rsc): add test for `use server` binding and shadowing
hi-ogawa Apr 4, 2026
6afa067
test: more
hi-ogawa Apr 4, 2026
7b186d3
chore: task note
hi-ogawa Apr 4, 2026
9f92bbf
chore: more plan
hi-ogawa Apr 4, 2026
100c63c
wip: buildScopeTree
hi-ogawa Apr 4, 2026
c11c39d
wip: intergrate buildScopeTree
hi-ogawa Apr 4, 2026
a580b42
chore: still planning
hi-ogawa Apr 4, 2026
8182c60
wip: still bad
hi-ogawa Apr 4, 2026
6d08484
chore: fix types
hi-ogawa Apr 4, 2026
7ff6f86
chore: still planning
hi-ogawa Apr 4, 2026
c144d07
wip: better take
hi-ogawa Apr 4, 2026
8a009eb
wip: still planning
hi-ogawa Apr 4, 2026
cd3805e
chore: null -> undefined
hi-ogawa Apr 4, 2026
089195e
refactor: align extractIdentifiers
hi-ogawa Apr 4, 2026
1b14260
chore: nit braces
hi-ogawa Apr 4, 2026
c3a30cb
wip: still smells though
hi-ogawa Apr 4, 2026
64e4a7f
wip
hi-ogawa Apr 4, 2026
7c03a52
test: var/function hoisting
hi-ogawa Apr 4, 2026
379630a
refactor: nit
hi-ogawa Apr 4, 2026
206f93a
refactor: reduce slop
hi-ogawa Apr 4, 2026
e98aa06
refactor: rename
hi-ogawa Apr 4, 2026
37b77b3
refactor: nit
hi-ogawa Apr 4, 2026
47bf4e1
refactor: nit slop
hi-ogawa Apr 4, 2026
1b914eb
chore: nit
hi-ogawa Apr 4, 2026
e5a23ec
chore: nit
hi-ogawa Apr 4, 2026
a161866
fix: more scope nodes
hi-ogawa Apr 4, 2026
6462a63
fix(plugin-rsc): fix isBindingIdentifier for computed destructuring k…
hi-ogawa Apr 4, 2026
90027fa
fix: fix function hoisting in strict mode
hi-ogawa Apr 4, 2026
a1db3c6
chore: still planning
hi-ogawa Apr 4, 2026
a7ebf93
refactor: single ast walk + loop
hi-ogawa Apr 4, 2026
9e77cc3
refactor: nit slop
hi-ogawa Apr 4, 2026
9b15444
fix: track import too
hi-ogawa Apr 4, 2026
4ee5e3a
chore: todo
hi-ogawa Apr 4, 2026
9963908
refactor: replace periscopic
hi-ogawa Apr 4, 2026
45ebc39
refactor: move code
hi-ogawa Apr 4, 2026
003578f
refactor: rename
hi-ogawa Apr 4, 2026
f54dfc3
refactor: move code
hi-ogawa Apr 4, 2026
941f3d9
chore: rename
hi-ogawa Apr 4, 2026
d08504b
test: add scope test
hi-ogawa Apr 4, 2026
44c967f
test: use glob fixtures
hi-ogawa Apr 4, 2026
3bccb3d
test: use .snap.json
hi-ogawa Apr 4, 2026
716cef5
test: tweak scope tree format
hi-ogawa Apr 4, 2026
deb1077
refactor: WeakMap -> Map
hi-ogawa Apr 4, 2026
b361668
refactor: less slop
hi-ogawa Apr 4, 2026
114837a
refactor: nit
hi-ogawa Apr 4, 2026
018eeb7
test: nit slop
hi-ogawa Apr 4, 2026
cb00f2a
refactor: nit slop
hi-ogawa Apr 4, 2026
1ce7784
chore: done TODO
hi-ogawa Apr 4, 2026
b69dd9a
docs(plugin-rsc): clarify extractIdentifiers intent
hi-ogawa Apr 4, 2026
07a17ed
chore: nit slop comment
hi-ogawa Apr 4, 2026
8d93c97
chore: todo slop
hi-ogawa Apr 4, 2026
21dbd43
feat(plugin-rsc): add scope fixture review helper
hi-ogawa Apr 4, 2026
220b3f1
refactor(plugin-rsc): rewrite scope review helper in ts
hi-ogawa Apr 4, 2026
40807f5
test: tweak SerializedScope
hi-ogawa Apr 4, 2026
9a38574
chore: todo
hi-ogawa Apr 4, 2026
6ea923a
wip: codex fixed something
hi-ogawa Apr 4, 2026
e6d7f71
Revert "wip: codex fixed something"
hi-ogawa Apr 4, 2026
2aa5b1b
fix: port isReferenceIdentifier from vite ssr transform
hi-ogawa Apr 4, 2026
81264c7
chore: todo
hi-ogawa Apr 4, 2026
d1a79ce
fix: fix rest arguments as reference
hi-ogawa Apr 4, 2026
f071db6
test: ensure new line in snapshot
hi-ogawa Apr 4, 2026
f0eef77
fix: fix import specifier as reference
hi-ogawa Apr 4, 2026
ec94642
chore: todo
hi-ogawa Apr 4, 2026
f9ea930
test(plugin-rsc): extend scope assignment coverage
hi-ogawa Apr 4, 2026
e97a20e
chore: this is slop
hi-ogawa Apr 4, 2026
f064b1d
Revert "chore: this is slop"
hi-ogawa Apr 4, 2026
e29f15e
test: import typescript-eslint fixtures
hi-ogawa Apr 4, 2026
73fe300
test: fix import
hi-ogawa Apr 4, 2026
a600f6b
test(plugin-rsc): import typescript-eslint scope fixtures
hi-ogawa Apr 4, 2026
c6cbf90
fix(plugin-rsc): handle named class expression self-binding
hi-ogawa Apr 4, 2026
16cd5c0
chore: readme
hi-ogawa Apr 4, 2026
a9f21cf
chore: ignore fixtures format
hi-ogawa Apr 4, 2026
2ecf607
test(plugin-rsc): add local class self-reference fixtures
hi-ogawa Apr 4, 2026
f72f3d4
refactor: nit
hi-ogawa Apr 4, 2026
f6f1035
chore: more comment for human
hi-ogawa Apr 5, 2026
a6bcca0
refactor: de-slop
hi-ogawa Apr 5, 2026
fb56a35
refactor: de-slop
hi-ogawa Apr 5, 2026
a23b1f6
chore: move code for human
hi-ogawa Apr 5, 2026
0309e2b
chore: more comment
hi-ogawa Apr 5, 2026
1160c58
chore: more comment
hi-ogawa Apr 5, 2026
f45172a
fix(rsc): refine scope reference classification comments
hi-ogawa Apr 5, 2026
ab43378
docs(plugin-rsc): scope manager prior art research
hi-ogawa Apr 5, 2026
7e8d5e5
test(rsc): cover param default var hoisting gap
hi-ogawa Apr 5, 2026
1bd9638
docs(plugin-rsc): replace local paths with GitHub URLs in research notes
hi-ogawa Apr 5, 2026
5252fb1
fix(plugin-rsc): remove hard-coded fixture path
hi-ogawa Apr 5, 2026
3aaba5c
docs(rsc): refresh scope analysis notes
hi-ogawa Apr 5, 2026
1f9cc70
chore: reviewed slop
hi-ogawa Apr 5, 2026
a8a11d2
docs(plugin-rsc): fill in comparison table in scope manager research …
hi-ogawa Apr 5, 2026
3892031
chore: todo comment
hi-ogawa Apr 5, 2026
8e68493
feat(plugin-rsc): bind member-chain paths as partial objects instead …
hi-ogawa Apr 5, 2026
f85e547
test(plugin-rsc): add fixture-based tests for hoist transform
hi-ogawa Apr 5, 2026
eb8b4f3
test: add targeted RSC member-chain hoist fixtures
hi-ogawa Apr 5, 2026
8998f65
refactor: de-slop
hi-ogawa Apr 5, 2026
467c84a
chore: todo slop
hi-ogawa Apr 5, 2026
376bfd6
refactor: de-slop
hi-ogawa Apr 5, 2026
47e1cc0
refactor: remove source slicing from RSC bind var analysis
hi-ogawa Apr 5, 2026
aa5d118
refactor: nit
hi-ogawa Apr 5, 2026
0376890
chore: todo for computed and optional access
hi-ogawa Apr 5, 2026
e9851ff
fix: stop unsupported member binding at safe prefix
hi-ogawa Apr 5, 2026
623102d
docs: add RSC member-chain task notes
hi-ogawa Apr 5, 2026
ca081d4
docs: clarify optional and computed member-chain follow-up
hi-ogawa Apr 5, 2026
3307d06
refactor: nit
hi-ogawa Apr 5, 2026
97f2700
test(plugin-rsc): add optional reference node snapshots
hi-ogawa Apr 6, 2026
d99a90d
refactor: de-slop
hi-ogawa Apr 6, 2026
0a2a7d7
test: tweak serializeReferenceNode
hi-ogawa Apr 6, 2026
2fb1a27
test: nit
hi-ogawa Apr 6, 2026
de579d9
test: more
hi-ogawa Apr 6, 2026
af07ac2
refactor: deslop
hi-ogawa Apr 6, 2026
4f3c0da
refactor: deslop
hi-ogawa Apr 6, 2026
be555fd
test: more
hi-ogawa Apr 6, 2026
557b59a
refactor: de-slop
hi-ogawa Apr 6, 2026
2c289a0
chore: move DefaultMap
hi-ogawa Apr 6, 2026
44bac65
refactor(plugin-rsc): simplify bind var grouping
hi-ogawa Apr 6, 2026
45fa0c5
refactor: nit
hi-ogawa Apr 6, 2026
37e704b
chore: still de-slopping
hi-ogawa Apr 6, 2026
cc7b257
refactor: de-slop
hi-ogawa Apr 6, 2026
7119748
refactor: de-slop
hi-ogawa Apr 6, 2026
9591c6b
refactor: de-slop
hi-ogawa Apr 6, 2026
2a46b06
refactor(plugin-rsc): tighten prefix path dedupe
hi-ogawa Apr 6, 2026
b6db313
chore: comment
hi-ogawa Apr 6, 2026
fb4fade
refactor: de-slop
hi-ogawa Apr 6, 2026
ad8ef95
test: update
hi-ogawa Apr 6, 2026
e25a88f
chore: cleanup
hi-ogawa Apr 6, 2026
64385cc
fix(plugin-rsc): escape __proto__ in bind objects
hi-ogawa Apr 6, 2026
48c4fbb
refactor: nit
hi-ogawa Apr 6, 2026
a03a4a4
chore: typo
hi-ogawa Apr 6, 2026
056a435
Merge branch 'main' into feat-bind-member-expr
hi-ogawa Apr 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .oxfmtrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"groups": [["builtin"], ["external"]]
},
"ignorePatterns": [
"*.snap.json",
"*.snap.*",
"typescript-eslint/",
"packages/*/CHANGELOG.md",
"playground-temp/",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,363 @@
# Member-Chain Follow-Up: Optional and Computed Access

## Goal

Track the next step after plain non-computed member-chain binding:

- optional chaining, e.g. `x?.y.z`, `x.y?.z`
- computed access, e.g. `x[y].z`, `x.y[k]`

This note is intentionally a follow-up to the plain member-chain work in
[2026-04-05-rsc-member-chain-binding-plan.md](./2026-04-05-rsc-member-chain-binding-plan.md).
It is not part of the current cleanup / plain-chain implementation.

## Current state

The current implementation is intentionally narrow:

- plain non-computed member chains like `x.y.z` are captured precisely
- unsupported hops stop capture at the last safe prefix
- examples:
- `x?.y.z` -> bind `x`
- `a.b?.c` -> bind `{ b: a.b }`
- `x[y].z` -> bind `x`

This is a reasonable conservative failure mode, but it is not full support.

## Why this needs a separate design

The current `BindPath` shape in [src/transforms/hoist.ts](../../src/transforms/hoist.ts)
is effectively:

```ts
type BindPath = {
key: string
segments: string[]
}
```

That is enough for `x.y.z` because codegen can reconstruct the bind expression
from the root identifier plus dot segments.

It is not enough for:

- `x?.y.z`
- `x.y?.z`
- `x[y].z`
- `x?.[y]`

The missing information is not cosmetic. It changes semantics.

### Optional chaining

Each hop needs to preserve whether access is optional.

Example:

```js
x?.y.z
```

Reconstructing this as `x.y.z` is wrong because the bind-time access becomes
stricter than the original expression.

### Computed access

Each computed hop needs the property expression, not just a string segment.

Example:

```js
x[y].z
```

There is no way to reconstruct this faithfully from `["y", "z"]`, because the
first `y` is an expression, not a property name.

### Computed key expressions also have their own closure semantics

Computed access is not only a codegen problem. The key expression itself may
close over outer variables, or it may be local to the action.

Outer-scope key:

```js
function outer() {
let key = 'x'
let obj = {}
async function action() {
'use server'
return obj[key]
}
}
```

Both `obj` and `key` are outer captures.

Action-local key:

```js
function outer() {
let obj = {}
async function action() {
'use server'
let key = 'x'
return obj[key]
}
}
```

Only `obj` is an outer capture; `key` is local to the action.

So any future `obj[expr]` support must treat the computed key as an ordinary
expression with its own scope resolution, not just as a printable suffix on a
member path.

## Minimum data model change

To support these cases, `BindPath` needs richer per-hop metadata.

Sketch:

```ts
type BindSegment =
| { kind: 'property'; name: string; optional: boolean }
| { kind: 'computed'; expr: Node; optional: boolean }

type BindPath = {
key: string
segments: BindSegment[]
}
```

This is enough to represent:

- `.foo`
- `?.foo`
- `[expr]`
- `?.[expr]`

The exact `key` design is still open. It only needs to support dedupe among
captures that are semantically comparable.

## Required implementation areas

### 1. `scope.ts`: capture shape

In [src/transforms/scope.ts](../../src/transforms/scope.ts),
`getOutermostBindableReference()` currently accumulates only plain
non-computed member chains and stops at unsupported hops.

To support optional/computed access, capture analysis must preserve richer
member-hop metadata instead of reducing everything to `Identifier` or
`MemberExpression` with plain identifier-name segments.

That likely means changing either:

- what `referenceToNode` stores, or
- adding a new structured capture representation derived from the AST

### 2. `hoist.ts`: path extraction

In [src/transforms/hoist.ts](../../src/transforms/hoist.ts),
`memberExpressionToPath()` currently extracts only `string[]` segments.

That helper would need to become a structured extractor that records:

- property vs computed
- optional vs non-optional
- enough information to regenerate the bind expression

### 3. Dedupe semantics

Current prefix dedupe is straightforward for plain dot paths:

- `x.y` covers `x.y.z`
- `x` covers everything below it

With optional/computed access, dedupe needs clearer rules.

Questions:

- does `x.y` cover `x.y?.z`?
- does `x[y]` cover `x[y].z` only when the computed key expression is identical?
- how should keys be normalized for comparison?

The current antichain logic should not be reused blindly.

### 3a. Support boundary for `obj[expr]`

This is still intentionally unresolved.

Possible support levels:

1. Keep current safe-prefix bailout only.
Examples:
- `obj[key]` -> bind `obj`, bind `key` separately if it is an outer capture
- `obj[key].value` -> bind `obj`, bind `key` separately if needed

2. Support exact computed member captures only for simple shapes.
Examples:
- `obj[key]`
- `obj[key].value`
but only when we have a clear representation for both the base object and the
key expression.

3. Support computed access as a first-class bind path.
This would require fully defining:
- path equality
- prefix coverage
- codegen for bind expressions
- partial-object synthesis, if still applicable

At the moment, the note does not assume we will reach (3). It is entirely
reasonable to stop at (1) or (2) if the semantics and implementation cost of
full computed-path support are not compelling.

### 4. Bind-expression codegen

Current codegen only needs:

- `root`
- `segments: string[]`

and synthesizes:

```ts
root + segments.map((segment) => `.${segment}`).join('')
```

That must be replaced with codegen that can emit:

- `.foo`
- `?.foo`
- `[expr]`
- `?.[expr]`

### 5. Partial-object synthesis

This is the hardest part.

For plain member paths, partial-object synthesis is natural:

```js
{
y: {
z: x.y.z
}
}
```

For computed access, synthesis is less obvious:

```js
x[k].z
```

Questions:

- should this become an object with computed keys?
- should computed paths fall back to broader binding even after we support
recognizing them?
- does partial-object binding remain the right representation for these cases?

This is where the design may need to diverge from plain member chains.

### 6. Comparison with Next.js

Relevant prior art is documented in
[scope-manager-research/nextjs.md](./scope-manager-research/nextjs.md).

Important comparison points:

- Next.js already models optional member access in its `NamePart` structure.
- Next.js does not support computed properties in the captured member-path
model.
- Next.js member-path capture is deliberately limited to member chains like
`foo.bar.baz`.

That means:

- optional chaining has direct prior art in Next.js's capture model
- computed access does not; if we support it, we are going beyond the current
Next.js design

This should affect scoping decisions for the follow-up:

- optional support is an extension of an already-established member-path model
- computed support is a materially larger design question, especially once key
expression scope and dedupe semantics are included

## Safe intermediate target

If we want a minimal correctness-first follow-up:

1. keep the current safe-prefix bailout behavior
2. add explicit tests for optional/computed cases
3. only implement richer capture metadata once codegen and dedupe rules are
agreed

That avoids regressing semantics while leaving room for a more precise design.

## Temporary conclusion

Current working direction:

- likely support optional chaining next, to align with Next.js's existing
member-path behavior
- keep computed access as a separate, open design problem for now

Rationale:

- optional chaining already has prior art in Next.js's capture model
- computed access is materially more complex because it mixes:
- key-expression scope resolution
- path equality / dedupe rules
- bind-expression codegen
- unclear partial-object synthesis semantics

So the likely near-term path is:

1. support optional member chains
2. keep current conservative behavior for computed access
3. revisit computed support only if there is a clear use case and a concrete
design that handles key-expression closure semantics correctly

## Suggested first questions before coding

1. Optional chains:
Should the first supported version preserve optional syntax exactly in the
bound expression, or should optional hops continue to bail out?

2. Computed access:
Do we want exact support for `x[y].z`, or only a less coarse bailout than
binding the whole root?

3. Binding shape:
Is partial-object synthesis still the preferred strategy for computed access,
or does this push us toward a different representation?

4. Computed key scope:
If we support `obj[expr]`, what is the intended contract for the key
expression?
Specifically:
- must outer variables used in `expr` always be captured independently?
- do we need a representation that distinguishes outer `key` from
action-local `key` when deciding support and dedupe?

5. Comparison target:
Do we want to stay aligned with Next.js and continue treating computed access
as out of scope, or intentionally support a broader feature set?

## Candidate tests

Add focused hoist fixtures for:

1. `x?.y.z`
2. `x.y?.z`
3. `x?.y?.z`
4. `x[y].z`
5. `x.y[k]`
6. `x[y]?.z`
7. `a.b?.c` as a safe-prefix bailout baseline
8. `a[b].c` as a safe-prefix bailout baseline
Loading
Loading