@@ -10,6 +10,59 @@ import { walk } from 'estree-walker'
1010import MagicString from 'magic-string'
1111import { 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+ */
1366export 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