Repository: vercel/next.js
Pull request: Fix name tracking for closures in server actions transform

When a server function passes a closure to a method on a closed-over value (e.g. an array), we must correctly track the value itself so that it's included in the bound arguments.

For example, in this snippet, `list` must be bound to the server action:

```js
export function Component({ list }) {
  return (
    <form
      action={async () => {
        'use server'
        console.log(list.find((x) => !!x))
      }}
    >
      <button>submit</button>
    </form>
  )
}
```

This use case used to work but was accidentally broken by #73189.

fixes #79608
fixes #77247
closes NAR-8

Changed files:
filename: .changeset/tough-peaches-burn.md
patch: @@ -0,0 +1,5 @@
+---
+"next": patch
+---
+
+Fix name tracking for closures in server actions transform

filename: crates/next-custom-transforms/src/transforms/server_actions.rs
patch: @@ -1011,6 +1011,12 @@ impl<C: Comments> VisitMut for ServerActions<C> {
             self.fn_decl_ident = old_fn_decl_ident;
         }
 
+        let mut child_names = take(&mut self.names);
+
+        if self.should_track_names {
+            self.names = [old_names, child_names.clone()].concat();
+        }
+
         if let Some(directive) = directive {
             if !f.is_async {
                 emit_error(ServerActionsErrorKind::InlineSyncFunction {
@@ -1028,12 +1034,6 @@ impl<C: Comments> VisitMut for ServerActions<C> {
                 return;
             }
 
-            let mut child_names = take(&mut self.names);
-
-            if self.should_track_names {
-                self.names = [old_names, child_names.clone()].concat();
-            }
-
             if let Directive::UseCache { cache_kind } = directive {
                 // Collect all the identifiers defined inside the closure and used
                 // in the cache function. With deduplication.
@@ -1181,6 +1181,12 @@ impl<C: Comments> VisitMut for ServerActions<C> {
             self.in_default_export_decl = old_in_default_export_decl;
         }
 
+        let mut child_names = take(&mut self.names);
+
+        if self.should_track_names {
+            self.names = [old_names, child_names.clone()].concat();
+        }
+
         if let Some(directive) = directive {
             if !a.is_async {
                 emit_error(ServerActionsErrorKind::InlineSyncFunction {
@@ -1199,12 +1205,6 @@ impl<C: Comments> VisitMut for ServerActions<C> {
                 return;
             }
 
-            let mut child_names = take(&mut self.names);
-
-            if self.should_track_names {
-                self.names = [old_names, child_names.clone()].concat();
-            }
-
             // Collect all the identifiers defined inside the closure and used
             // in the action function. With deduplication.
             retain_names_from_declared_idents(

filename: crates/next-custom-transforms/tests/fixture/server-actions/server-graph/61/input.js
patch: @@ -0,0 +1,29 @@
+export function ComponentA({ list, y }) {
+  return (
+    <form
+      action={async () => {
+        'use server'
+        console.log(list.find((x) => x === y))
+      }}
+    >
+      <button>submit</button>
+    </form>
+  )
+}
+
+export function ComponentB({ list, y }) {
+  return (
+    <form
+      action={async () => {
+        'use server'
+        console.log(
+          list.find(function (x) {
+            return x === y
+          })
+        )
+      }}
+    >
+      <button>submit</button>
+    </form>
+  )
+}

filename: crates/next-custom-transforms/tests/fixture/server-actions/server-graph/61/output.js
patch: @@ -0,0 +1,22 @@
+/* __next_internal_action_entry_do_not_use__ {"406a88810ecce4a4e8b59d53b8327d7e98bbf251d7":"$$RSC_SERVER_ACTION_0","4090b5db271335765a4b0eab01f044b381b5ebd5cd":"$$RSC_SERVER_ACTION_1"} */ import { registerServerReference } from "private-next-rsc-server-reference";
+import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption";
+export const $$RSC_SERVER_ACTION_0 = async function action($$ACTION_CLOSURE_BOUND) {
+    var [$$ACTION_ARG_0, $$ACTION_ARG_1] = await decryptActionBoundArgs("406a88810ecce4a4e8b59d53b8327d7e98bbf251d7", $$ACTION_CLOSURE_BOUND);
+    console.log($$ACTION_ARG_0.find((x)=>x === $$ACTION_ARG_1));
+};
+export function ComponentA({ list, y }) {
+    return <form action={registerServerReference($$RSC_SERVER_ACTION_0, "406a88810ecce4a4e8b59d53b8327d7e98bbf251d7", null).bind(null, encryptActionBoundArgs("406a88810ecce4a4e8b59d53b8327d7e98bbf251d7", list, y))}>
+      <button>submit</button>
+    </form>;
+}
+export const $$RSC_SERVER_ACTION_1 = async function action($$ACTION_CLOSURE_BOUND) {
+    var [$$ACTION_ARG_0, $$ACTION_ARG_1] = await decryptActionBoundArgs("4090b5db271335765a4b0eab01f044b381b5ebd5cd", $$ACTION_CLOSURE_BOUND);
+    console.log($$ACTION_ARG_0.find(function(x) {
+        return x === $$ACTION_ARG_1;
+    }));
+};
+export function ComponentB({ list, y }) {
+    return <form action={registerServerReference($$RSC_SERVER_ACTION_1, "4090b5db271335765a4b0eab01f044b381b5ebd5cd", null).bind(null, encryptActionBoundArgs("4090b5db271335765a4b0eab01f044b381b5ebd5cd", list, y))}>
+      <button>submit</button>
+    </form>;
+}