Skip to content

Commit 04ce445

Browse files
authored
Fix Server Actions closure idents tracking (#66601)
This PR fixes the same case mention in #66464. Instead of collecting all values eagerly, here we merge fields (on any level of depth) of the same value and skip methods. For example: ```ts foo.bar foo.bar.baz qux.fn() ``` Previously we're (wrongly) collecting `[foo.bar, foo.bar.baz, qux.fn]`, and now it will be just `[foo.bar, qux]`. Merging of fields is critical for collecting methods correctly because in theory we can't tell if an object member is a method or not: ```ts data.push.call(data, 1) // or inside a function that does the same: doPush(data.push, data) ``` If we don't merge fields we'll collect `[data.push, data]` which still fails.
1 parent 9aa197c commit 04ce445

File tree

4 files changed

+109
-33
lines changed

4 files changed

+109
-33
lines changed

packages/next-swc/crates/next-custom-transforms/src/transforms/server_actions.rs

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub fn server_actions<C: Comments>(
4848
in_action_file: false,
4949
in_export_decl: false,
5050
in_default_export_decl: false,
51+
in_callee: false,
5152
has_action: false,
5253

5354
reference_index: 0,
@@ -89,6 +90,7 @@ struct ServerActions<C: Comments> {
8990
in_action_file: bool,
9091
in_export_decl: bool,
9192
in_default_export_decl: bool,
93+
in_callee: bool,
9294
has_action: bool,
9395

9496
reference_index: u32,
@@ -709,9 +711,24 @@ impl<C: Comments> VisitMut for ServerActions<C> {
709711
n.visit_mut_children_with(self);
710712
}
711713

714+
fn visit_mut_callee(&mut self, n: &mut Callee) {
715+
let old_in_callee = self.in_callee;
716+
self.in_callee = true;
717+
n.visit_mut_children_with(self);
718+
self.in_callee = old_in_callee;
719+
}
720+
712721
fn visit_mut_expr(&mut self, n: &mut Expr) {
713722
if !self.in_module_level && self.should_track_names {
714-
if let Ok(name) = Name::try_from(&*n) {
723+
if let Ok(mut name) = Name::try_from(&*n) {
724+
if self.in_callee {
725+
// This is a callee i.e. `foo.bar()`,
726+
// we need to track the actual value instead of the method name.
727+
if !name.1.is_empty() {
728+
name.1.pop();
729+
}
730+
}
731+
715732
self.names.push(name);
716733
self.should_track_names = false;
717734
n.visit_mut_children_with(self);
@@ -1168,19 +1185,46 @@ impl<C: Comments> VisitMut for ServerActions<C> {
11681185
}
11691186

11701187
fn retain_names_from_declared_idents(child_names: &mut Vec<Name>, current_declared_idents: &[Id]) {
1171-
// Collect all the identifiers defined inside the closure and used
1172-
// in the action function. With deduplication.
1173-
let mut added_names = Vec::new();
1174-
child_names.retain(|name| {
1175-
if added_names.contains(name) {
1176-
false
1177-
} else if current_declared_idents.contains(&name.0) {
1178-
added_names.push(name.clone());
1179-
true
1180-
} else {
1181-
false
1188+
// Collect the names to retain in a separate vector
1189+
let mut retained_names = Vec::new();
1190+
1191+
for name in child_names.iter() {
1192+
let mut should_retain = true;
1193+
1194+
// Merge child_names. For example if both `foo.bar` and `foo.bar.baz` are used,
1195+
// we only need to keep `foo.bar` as it covers the other.
1196+
1197+
// Currently this is O(n^2) and we can potentially improve this to O(n log n)
1198+
// by sorting or using a hashset.
1199+
for another_name in child_names.iter() {
1200+
if name != another_name
1201+
&& name.0 == another_name.0
1202+
&& name.1.len() >= another_name.1.len()
1203+
{
1204+
let mut is_prefix = true;
1205+
for i in 0..another_name.1.len() {
1206+
if name.1[i] != another_name.1[i] {
1207+
is_prefix = false;
1208+
break;
1209+
}
1210+
}
1211+
if is_prefix {
1212+
should_retain = false;
1213+
break;
1214+
}
1215+
}
11821216
}
1183-
});
1217+
1218+
if should_retain
1219+
&& current_declared_idents.contains(&name.0)
1220+
&& !retained_names.contains(name)
1221+
{
1222+
retained_names.push(name.clone());
1223+
}
1224+
}
1225+
1226+
// Replace the original child_names with the retained names
1227+
*child_names = retained_names;
11841228
}
11851229

11861230
fn gen_ident(cnt: &mut u32) -> JsWord {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export function Component() {
2+
const data = [1, 2, 3]
3+
const foo = [2, 3, 4]
4+
const baz = { value: { current: 1 } }
5+
6+
async function action() {
7+
'use server'
8+
9+
console.log(data.at(1), baz.value, baz.value.current)
10+
console.log(foo.push.call(foo, 5))
11+
}
12+
13+
return <form action={action} />
14+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/* __next_internal_action_entry_do_not_use__ {"6d53ce510b2e36499b8f56038817b9bad86cabb4":"$$ACTION_0"} */ import { registerServerReference } from "private-next-rsc-server-reference";
2+
import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption";
3+
export function Component() {
4+
const data = [
5+
1,
6+
2,
7+
3
8+
];
9+
const foo = [
10+
2,
11+
3,
12+
4
13+
];
14+
const baz = {
15+
value: {
16+
current: 1
17+
}
18+
};
19+
var action = registerServerReference("6d53ce510b2e36499b8f56038817b9bad86cabb4", $$ACTION_0).bind(null, encryptActionBoundArgs("6d53ce510b2e36499b8f56038817b9bad86cabb4", [
20+
data,
21+
baz.value,
22+
foo
23+
]));
24+
return <form action={action}/>;
25+
}
26+
export async function $$ACTION_0($$ACTION_CLOSURE_BOUND) {
27+
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2] = await decryptActionBoundArgs("6d53ce510b2e36499b8f56038817b9bad86cabb4", $$ACTION_CLOSURE_BOUND);
28+
console.log($$ACTION_ARG_0.at(1), $$ACTION_ARG_1, $$ACTION_ARG_1.current);
29+
console.log($$ACTION_ARG_2.push.call($$ACTION_ARG_2, 5));
30+
}

packages/next-swc/crates/next-custom-transforms/tests/fixture/server-actions/server/7/output.js

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,61 +3,49 @@ import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc
33
import deleteFromDb from 'db';
44
export function Item1(product, foo, bar) {
55
const a = registerServerReference("6d53ce510b2e36499b8f56038817b9bad86cabb4", $$ACTION_0).bind(null, encryptActionBoundArgs("6d53ce510b2e36499b8f56038817b9bad86cabb4", [
6-
product.id,
7-
product?.foo,
8-
product.bar.baz,
96
product,
107
foo,
118
bar
129
]));
1310
return <Button action={a}>Delete</Button>;
1411
}
1512
export async function $$ACTION_0($$ACTION_CLOSURE_BOUND) {
16-
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2, $$ACTION_ARG_3, $$ACTION_ARG_4, $$ACTION_ARG_5] = await decryptActionBoundArgs("6d53ce510b2e36499b8f56038817b9bad86cabb4", $$ACTION_CLOSURE_BOUND);
17-
await deleteFromDb($$ACTION_ARG_3.id, $$ACTION_ARG_3?.foo, $$ACTION_ARG_3.bar.baz, $$ACTION_ARG_3[$$ACTION_ARG_4, $$ACTION_ARG_5]);
13+
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2] = await decryptActionBoundArgs("6d53ce510b2e36499b8f56038817b9bad86cabb4", $$ACTION_CLOSURE_BOUND);
14+
await deleteFromDb($$ACTION_ARG_0.id, $$ACTION_ARG_0?.foo, $$ACTION_ARG_0.bar.baz, $$ACTION_ARG_0[$$ACTION_ARG_1, $$ACTION_ARG_2]);
1815
}
1916
export function Item2(product, foo, bar) {
2017
var deleteItem2 = registerServerReference("188d5d945750dc32e2c842b93c75a65763d4a922", $$ACTION_1).bind(null, encryptActionBoundArgs("188d5d945750dc32e2c842b93c75a65763d4a922", [
21-
product.id,
22-
product?.foo,
23-
product.bar.baz,
2418
product,
2519
foo,
2620
bar
2721
]));
2822
return <Button action={deleteItem2}>Delete</Button>;
2923
}
3024
export async function $$ACTION_1($$ACTION_CLOSURE_BOUND) {
31-
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2, $$ACTION_ARG_3, $$ACTION_ARG_4, $$ACTION_ARG_5] = await decryptActionBoundArgs("188d5d945750dc32e2c842b93c75a65763d4a922", $$ACTION_CLOSURE_BOUND);
32-
await deleteFromDb($$ACTION_ARG_3.id, $$ACTION_ARG_3?.foo, $$ACTION_ARG_3.bar.baz, $$ACTION_ARG_3[$$ACTION_ARG_4, $$ACTION_ARG_5]);
25+
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2] = await decryptActionBoundArgs("188d5d945750dc32e2c842b93c75a65763d4a922", $$ACTION_CLOSURE_BOUND);
26+
await deleteFromDb($$ACTION_ARG_0.id, $$ACTION_ARG_0?.foo, $$ACTION_ARG_0.bar.baz, $$ACTION_ARG_0[$$ACTION_ARG_1, $$ACTION_ARG_2]);
3327
}
3428
export function Item3(product, foo, bar) {
3529
const deleteItem3 = registerServerReference("56a859f462d35a297c46a1bbd1e6a9058c104ab8", $$ACTION_3).bind(null, encryptActionBoundArgs("56a859f462d35a297c46a1bbd1e6a9058c104ab8", [
36-
product.id,
37-
product?.foo,
38-
product.bar.baz,
3930
product,
4031
foo,
4132
bar
4233
]));
4334
return <Button action={deleteItem3}>Delete</Button>;
4435
}
4536
export async function $$ACTION_3($$ACTION_CLOSURE_BOUND) {
46-
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2, $$ACTION_ARG_3, $$ACTION_ARG_4, $$ACTION_ARG_5] = await decryptActionBoundArgs("56a859f462d35a297c46a1bbd1e6a9058c104ab8", $$ACTION_CLOSURE_BOUND);
47-
await deleteFromDb($$ACTION_ARG_3.id, $$ACTION_ARG_3?.foo, $$ACTION_ARG_3.bar.baz, $$ACTION_ARG_3[$$ACTION_ARG_4, $$ACTION_ARG_5]);
37+
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2] = await decryptActionBoundArgs("56a859f462d35a297c46a1bbd1e6a9058c104ab8", $$ACTION_CLOSURE_BOUND);
38+
await deleteFromDb($$ACTION_ARG_0.id, $$ACTION_ARG_0?.foo, $$ACTION_ARG_0.bar.baz, $$ACTION_ARG_0[$$ACTION_ARG_1, $$ACTION_ARG_2]);
4839
}
4940
export function Item4(product, foo, bar) {
5041
const deleteItem4 = registerServerReference("9c0dd1f7c2b3f41d32e10f5c437de3d67ad32c6c", $$ACTION_4).bind(null, encryptActionBoundArgs("9c0dd1f7c2b3f41d32e10f5c437de3d67ad32c6c", [
51-
product.id,
52-
product?.foo,
53-
product.bar.baz,
5442
product,
5543
foo,
5644
bar
5745
]));
5846
return <Button action={deleteItem4}>Delete</Button>;
5947
}
6048
export async function $$ACTION_4($$ACTION_CLOSURE_BOUND) {
61-
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2, $$ACTION_ARG_3, $$ACTION_ARG_4, $$ACTION_ARG_5] = await decryptActionBoundArgs("9c0dd1f7c2b3f41d32e10f5c437de3d67ad32c6c", $$ACTION_CLOSURE_BOUND);
62-
await deleteFromDb($$ACTION_ARG_3.id, $$ACTION_ARG_3?.foo, $$ACTION_ARG_3.bar.baz, $$ACTION_ARG_3[$$ACTION_ARG_4, $$ACTION_ARG_5]);
49+
var [$$ACTION_ARG_0, $$ACTION_ARG_1, $$ACTION_ARG_2] = await decryptActionBoundArgs("9c0dd1f7c2b3f41d32e10f5c437de3d67ad32c6c", $$ACTION_CLOSURE_BOUND);
50+
await deleteFromDb($$ACTION_ARG_0.id, $$ACTION_ARG_0?.foo, $$ACTION_ARG_0.bar.baz, $$ACTION_ARG_0[$$ACTION_ARG_1, $$ACTION_ARG_2]);
6351
}

0 commit comments

Comments
 (0)