Skip to content

Commit 7a16ffe

Browse files
hi-ogawaOpenCode
andauthored
chore(rsc): explain inline hoist transform with comments (#1331)
Co-authored-by: Hiroshi Ogawa <[email protected]> Co-authored-by: OpenCode <[email protected]>
1 parent b14ed96 commit 7a16ffe

1 file changed

Lines changed: 78 additions & 3 deletions

File tree

  • packages/plugin-rsc/src/transforms

‎packages/plugin-rsc/src/transforms/hoist.ts‎

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,59 @@ import { walk } from 'estree-walker'
1010
import MagicString from 'magic-string'
1111
import { buildScopeTree, type ScopeTree } from './scope'
1212

13+
/**
14+
* Turns an inline directive function into a module-level registered function.
15+
* Conceptually:
16+
*
17+
* ```js
18+
* function Component() {
19+
* const x = 1
20+
* async function action(y) {
21+
* "use server"
22+
* return x + y
23+
* }
24+
* }
25+
* ```
26+
*
27+
* becomes:
28+
*
29+
* ```js
30+
* function Component() {
31+
* const x = 1
32+
* const action = __RUNTIME__($$hoist_0_action).bind(null, x)
33+
* }
34+
* export async function $$hoist_0_action(x, y) {
35+
* "use server"
36+
* return x + y
37+
* }
38+
* ```
39+
*
40+
* The generated export is `$$hoist_0_action`, so `names` contains
41+
* `['$$hoist_0_action']` for this example.
42+
*
43+
* Here, `__RUNTIME__(...)` represents the registration expression returned by the
44+
* `runtime` callback. When `encode` and `decode` are provided, the closure
45+
* captures instead travel as one encoded bound argument:
46+
*
47+
* ```js
48+
* function Component() {
49+
* const x = 1
50+
* const action = __RUNTIME__($$hoist_0_action).bind(
51+
* null,
52+
* __ENCODE__([x]),
53+
* )
54+
* }
55+
*
56+
* export async function $$hoist_0_action($$hoist_encoded, y) {
57+
* const [x] = __DECODE__($$hoist_encoded)
58+
* "use server"
59+
* return x + y
60+
* }
61+
* ```
62+
*
63+
* In this second sketch, `__ENCODE__(...)` and `__DECODE__(...)` likewise
64+
* represent the expressions returned by those code-generation callbacks.
65+
*/
1366
export function transformHoistInlineDirective(
1467
input: string,
1568
ast: Program,
@@ -27,13 +80,15 @@ export function transformHoistInlineDirective(
2780
rejectNonAsyncFunction?: boolean
2881
encode?: (value: string) => string
2982
decode?: (value: string) => string
83+
/** Keep generated hoisted declarations module-local instead of exporting them. */
3084
noExport?: boolean
3185
},
3286
): {
3387
output: MagicString
3488
names: string[]
3589
} {
36-
// ensure ending space so we can move node at the end without breaking magic-string
90+
// MagicString needs an existing boundary at the move destination. The newline
91+
// also keeps the first appended declaration separate from the original source.
3792
if (!input.endsWith('\n')) {
3893
input += '\n'
3994
}
@@ -43,6 +98,8 @@ export function transformHoistInlineDirective(
4398
? exactRegex(options.directive)
4499
: options.directive
45100

101+
// Build the complete scope tree once so each hoisted function can distinguish
102+
// closure captures from module bindings and globals, which remain in scope.
46103
const scopeTree = buildScopeTree(ast)
47104
const names: string[] = []
48105

@@ -54,6 +111,8 @@ export function transformHoistInlineDirective(
54111
node.type === 'ArrowFunctionExpression') &&
55112
node.body.type === 'BlockStatement'
56113
) {
114+
// Only transform functions whose block contains the requested
115+
// directive. Other function shapes cannot contain directive prologues.
57116
const match = matchDirective(node.body.body, directive)?.match
58117
if (!match) return
59118
if (!node.async && rejectNonAsyncFunction) {
@@ -65,6 +124,9 @@ export function transformHoistInlineDirective(
65124
)
66125
}
67126

127+
// Capture the source-level name so the hoisted function can preserve it
128+
// with Object.defineProperty below. Anonymous functions get a stable
129+
// fallback for registration and diagnostics.
68130
const declName = node.type === 'FunctionDeclaration' && node.id.name
69131
const originalName =
70132
declName ||
@@ -73,12 +135,17 @@ export function transformHoistInlineDirective(
73135
parent.id.name) ||
74136
'anonymous_server_function'
75137

138+
// Convert closure captures into leading parameters of the hoisted
139+
// function. At the original call site, registration below binds the
140+
// corresponding values in the same order.
76141
const bindVars = getBindVars(node, scopeTree)
77142
let newParams = [
78143
...bindVars.map((b) => b.root),
79144
...node.params.map((n) => input.slice(n.start, n.end)),
80145
].join(', ')
81146
if (bindVars.length > 0 && options.decode) {
147+
// Encoded captures travel as one bound argument, then are restored to
148+
// the individual parameter names before the original body executes.
82149
newParams = [
83150
'$$hoist_encoded',
84151
...node.params.map((n) => input.slice(n.start, n.end)),
@@ -91,7 +158,8 @@ export function transformHoistInlineDirective(
91158
)
92159
}
93160

94-
// append a new `FunctionDeclaration` at the end
161+
// Rewrite and hoist the original function range into its module-level form.
162+
// These edits must happen before `.move()` (hoist) so they travel with the range.
95163
const newName =
96164
`$$hoist_${names.length}` + (originalName ? `_${originalName}` : '')
97165
names.push(newName)
@@ -110,7 +178,9 @@ export function transformHoistInlineDirective(
110178
)
111179
output.move(node.start, node.end, input.length)
112180

113-
// replace original declartion with action register + bind
181+
// Replace the original function with the runtime expression for its
182+
// hoisted declaration. Bind closure captures to the prepended capture
183+
// parameters (or the single encoded parameter).
114184
let newCode = `/* #__PURE__ */ ${runtime(newName, newName, {
115185
directiveMatch: match,
116186
})}`
@@ -121,6 +191,8 @@ export function transformHoistInlineDirective(
121191
newCode = `${newCode}.bind(null, ${bindArgs})`
122192
}
123193
if (declName) {
194+
// A function declaration becomes a const declaration. For a default
195+
// export, retain the export as a separate statement after that const.
124196
newCode = `const ${declName} = ${newCode};`
125197
if (parent?.type === 'ExportDefaultDeclaration') {
126198
output.remove(parent.start, node.start)
@@ -132,6 +204,9 @@ export function transformHoistInlineDirective(
132204
},
133205
})
134206

207+
// Expose the generated hoisted declaration names. These are the new
208+
// exports by default (unless noExport is set), so callers can also track them
209+
// as runtime references.
135210
return {
136211
output,
137212
names,

0 commit comments

Comments
 (0)