Skip to content

Commit feca3ce

Browse files
jridgewelltimneutkenshuozhi
authored
Turbopack: Implement Server Actions (#53890)
### What? This implements Server Actions inside the new Turbopack next-api bundles. ### How? Server Actions requires: 1. A `.next/server/server-reference-manifest.json` manifest describing what loader module to import to invoke a server action 2. A "loader" entry point that then imports server actions from our internal chunk items 3. Importing the bundled `react-experimental` module instead of regular `react` 4. A little 🪄 pixie dust 5. A small change in the magic comment generated in modules that export server actions I had to change the magic `__next_internal_action_entry_do_not_use__` comment generated by the server actions transformer. When I traverse the module graph to find all exported actions _after chunking_ has been performed, we no longer have access to the original file name needed to generate the server action's id hash. Adding the filename to comment allows me to recover this without overcomplicating our output pipeline. Closes WEB-1279 Depends on vercel/turborepo#5705 Co-authored-by: Tim Neutkens <[email protected]> Co-authored-by: Jiachi Liu <[email protected]>
1 parent ad42b61 commit feca3ce

File tree

33 files changed

+805
-180
lines changed

33 files changed

+805
-180
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ opt-level = 3
2727
next-api = { path = "packages/next-swc/crates/next-api", default-features = false }
2828
next-build = { path = "packages/next-swc/crates/next-build", default-features = false }
2929
next-core = { path = "packages/next-swc/crates/next-core", default-features = false }
30+
next-swc = { path = "packages/next-swc/crates/core" }
3031
next-transform-font = { path = "packages/next-swc/crates/next-transform-font" }
3132
next-transform-dynamic = { path = "packages/next-swc/crates/next-transform-dynamic" }
3233
next-transform-strip-page-exports = { path = "packages/next-swc/crates/next-transform-strip-page-exports" }

packages/next-swc/crates/core/src/server_actions.rs

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::convert::{TryFrom, TryInto};
1+
use std::{
2+
collections::HashMap,
3+
convert::{TryFrom, TryInto},
4+
};
25

36
use hex::encode as hex_encode;
47
use serde::Deserialize;
@@ -25,6 +28,9 @@ pub struct Config {
2528
pub enabled: bool,
2629
}
2730

31+
/// A mapping of hashed action id to the action's exported function name.
32+
pub type ActionsMap = HashMap<String, String>;
33+
2834
pub fn server_actions<C: Comments>(
2935
file_name: &FileName,
3036
config: Config,
@@ -33,7 +39,7 @@ pub fn server_actions<C: Comments>(
3339
as_folder(ServerActions {
3440
config,
3541
comments,
36-
file_name: file_name.clone(),
42+
file_name: file_name.to_string(),
3743
start_pos: BytePos(0),
3844
in_action_file: false,
3945
in_export_decl: false,
@@ -55,10 +61,40 @@ pub fn server_actions<C: Comments>(
5561
})
5662
}
5763

64+
/// Parses the Server Actions comment for all exported action function names.
65+
///
66+
/// Action names are stored in a leading BlockComment prefixed by
67+
/// `__next_internal_action_entry_do_not_use__`.
68+
pub fn parse_server_actions<C: Comments>(program: &Program, comments: C) -> Option<ActionsMap> {
69+
let byte_pos = match program {
70+
Program::Module(m) => m.span.lo,
71+
Program::Script(s) => s.span.lo,
72+
};
73+
comments.get_leading(byte_pos).and_then(|comments| {
74+
comments.iter().find_map(|c| {
75+
c.text
76+
.split_once("__next_internal_action_entry_do_not_use__")
77+
.and_then(|(_, actions)| match serde_json::from_str(actions) {
78+
Ok(v) => Some(v),
79+
Err(_) => None,
80+
})
81+
})
82+
})
83+
}
84+
85+
/// Serializes the Server Actions into a magic comment prefixed by
86+
/// `__next_internal_action_entry_do_not_use__`.
87+
fn generate_server_actions_comment(actions: ActionsMap) -> String {
88+
format!(
89+
" __next_internal_action_entry_do_not_use__ {} ",
90+
serde_json::to_string(&actions).unwrap()
91+
)
92+
}
93+
5894
struct ServerActions<C: Comments> {
5995
#[allow(unused)]
6096
config: Config,
61-
file_name: FileName,
97+
file_name: String,
6298
comments: C,
6399

64100
start_pos: BytePos,
@@ -216,7 +252,7 @@ impl<C: Comments> ServerActions<C> {
216252
.cloned()
217253
.map(|id| Some(id.as_arg()))
218254
.collect(),
219-
self.file_name.to_string(),
255+
&self.file_name,
220256
export_name.to_string(),
221257
Some(action_ident.clone()),
222258
);
@@ -317,7 +353,7 @@ impl<C: Comments> ServerActions<C> {
317353
.cloned()
318354
.map(|id| Some(id.as_arg()))
319355
.collect(),
320-
self.file_name.to_string(),
356+
&self.file_name,
321357
export_name.to_string(),
322358
Some(action_ident.clone()),
323359
);
@@ -923,8 +959,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
923959
let ident = Ident::new(id.0.clone(), DUMMY_SP.with_ctxt(id.1));
924960

925961
if !self.config.is_server {
926-
let action_id =
927-
generate_action_id(self.file_name.to_string(), export_name.to_string());
962+
let action_id = generate_action_id(&self.file_name, export_name);
928963

929964
if export_name == "default" {
930965
let export_expr = ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(
@@ -973,7 +1008,7 @@ impl<C: Comments> VisitMut for ServerActions<C> {
9731008
&mut self.annotations,
9741009
ident.clone(),
9751010
Vec::new(),
976-
self.file_name.to_string(),
1011+
&self.file_name,
9771012
export_name.to_string(),
9781013
None,
9791014
);
@@ -1037,26 +1072,22 @@ impl<C: Comments> VisitMut for ServerActions<C> {
10371072
}
10381073

10391074
if self.has_action {
1075+
let actions = if self.in_action_file {
1076+
self.exported_idents.iter().map(|e| e.1.clone()).collect()
1077+
} else {
1078+
self.export_actions.clone()
1079+
};
1080+
let actions = actions
1081+
.into_iter()
1082+
.map(|name| (generate_action_id(&self.file_name, &name), name))
1083+
.collect::<ActionsMap>();
10401084
// Prepend a special comment to the top of the file.
10411085
self.comments.add_leading(
10421086
self.start_pos,
10431087
Comment {
10441088
span: DUMMY_SP,
10451089
kind: CommentKind::Block,
1046-
// Append a list of exported actions.
1047-
text: format!(
1048-
" __next_internal_action_entry_do_not_use__ {} ",
1049-
if self.in_action_file {
1050-
self.exported_idents
1051-
.iter()
1052-
.map(|e| e.1.to_string())
1053-
.collect::<Vec<_>>()
1054-
.join(",")
1055-
} else {
1056-
self.export_actions.join(",")
1057-
}
1058-
)
1059-
.into(),
1090+
text: generate_server_actions_comment(actions).into(),
10601091
},
10611092
);
10621093

@@ -1162,7 +1193,7 @@ fn collect_pat_idents(pat: &Pat, closure_idents: &mut Vec<Id>) {
11621193
}
11631194
}
11641195

1165-
fn generate_action_id(file_name: String, export_name: String) -> String {
1196+
fn generate_action_id(file_name: &str, export_name: &str) -> String {
11661197
// Attach a checksum to the action using sha1:
11671198
// $$id = sha1('file_name' + ':' + 'export_name');
11681199
let mut hasher = Sha1::new();
@@ -1178,7 +1209,7 @@ fn annotate_ident_as_action(
11781209
annotations: &mut Vec<Stmt>,
11791210
ident: Ident,
11801211
bound: Vec<Option<ExprOrSpread>>,
1181-
file_name: String,
1212+
file_name: &str,
11821213
export_name: String,
11831214
maybe_orig_action_ident: Option<Ident>,
11841215
) {
@@ -1188,7 +1219,7 @@ fn annotate_ident_as_action(
11881219
// $$id
11891220
ExprOrSpread {
11901221
spread: None,
1191-
expr: Box::new(generate_action_id(file_name, export_name).into()),
1222+
expr: Box::new(generate_action_id(file_name, &export_name).into()),
11921223
},
11931224
// myAction.$$bound = [arg1, arg2, arg3];
11941225
// or myAction.$$bound = null; if there are no bound values.

packages/next-swc/crates/napi/Cargo.toml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,13 @@ default = ["rustls-tls"]
1313
# when build (i.e napi --build --features plugin), same for the wasm as well.
1414
# this is due to some of transitive dependencies have features cannot be enabled at the same time
1515
# (i.e wasmer/default vs wasmer/js-default) while cargo merges all the features at once.
16-
plugin = ["turbopack-binding/__swc_core_binding_napi_plugin", "turbopack-binding/__swc_core_binding_napi_plugin_filesystem_cache", "turbopack-binding/__swc_core_binding_napi_plugin_shared_runtime", "next-swc/plugin", "next-core/plugin"]
16+
plugin = [
17+
"turbopack-binding/__swc_core_binding_napi_plugin",
18+
"turbopack-binding/__swc_core_binding_napi_plugin_filesystem_cache",
19+
"turbopack-binding/__swc_core_binding_napi_plugin_shared_runtime",
20+
"next-swc/plugin",
21+
"next-core/plugin",
22+
]
1723
sentry_native_tls = ["sentry", "sentry/native-tls", "native-tls"]
1824
sentry_rustls = ["sentry", "sentry/rustls", "rustls-tls"]
1925

@@ -24,9 +30,7 @@ image-avif = ["next-core/image-avif"]
2430
# Enable all the available image codec support.
2531
# Currently this is identical to `image-webp`, as we are not able to build
2632
# other codecs easily yet.
27-
image-extended = [
28-
"image-webp",
29-
]
33+
image-extended = ["image-webp"]
3034

3135
# Enable dhat profiling allocator for heap profiling.
3236
__internal_dhat-heap = ["dhat"]
@@ -47,7 +51,7 @@ napi = { version = "2", default-features = false, features = [
4751
"error_anyhow",
4852
] }
4953
napi-derive = "2"
50-
next-swc = { version = "0.0.0", path = "../core" }
54+
next-swc = { workspace = true }
5155
next-api = { workspace = true }
5256
next-build = { workspace = true }
5357
next-core = { workspace = true }
@@ -73,9 +77,7 @@ turbopack-binding = { workspace = true, features = [
7377
] }
7478

7579
[target.'cfg(not(all(target_os = "linux", target_env = "musl", target_arch = "aarch64")))'.dependencies]
76-
turbopack-binding = { workspace = true, features = [
77-
"__turbo_tasks_malloc"
78-
] }
80+
turbopack-binding = { workspace = true, features = ["__turbo_tasks_malloc"] }
7981

8082
# There are few build targets we can't use native-tls which default features rely on,
8183
# allow to specify alternative (rustls) instead via features.
@@ -94,6 +96,4 @@ serde = "1"
9496
serde_json = "1"
9597
# It is not a mistake this dependency is specified in dep / build-dep both.
9698
shadow-rs = { workspace = true }
97-
turbopack-binding = { workspace = true, features = [
98-
"__turbo_tasks_build"
99-
]}
99+
turbopack-binding = { workspace = true, features = ["__turbo_tasks_build"] }

packages/next-swc/crates/next-api/Cargo.toml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@ bench = false
1111

1212
[features]
1313
default = ["custom_allocator"]
14-
custom_allocator = ["turbopack-binding/__turbo_tasks_malloc", "turbopack-binding/__turbo_tasks_malloc_custom_allocator"]
14+
custom_allocator = [
15+
"turbopack-binding/__turbo_tasks_malloc",
16+
"turbopack-binding/__turbo_tasks_malloc_custom_allocator",
17+
]
1518

1619
[dependencies]
1720
anyhow = { workspace = true, features = ["backtrace"] }
1821
futures = { workspace = true }
22+
next-swc = { workspace = true }
1923
indexmap = { workspace = true }
24+
indoc = { workspace = true }
2025
next-core = { workspace = true }
2126
once_cell = { workspace = true }
2227
serde = { workspace = true }
@@ -35,14 +40,12 @@ turbopack-binding = { workspace = true, features = [
3540
"__turbopack_cli_utils",
3641
"__turbopack_node",
3742
"__turbopack_dev_server",
38-
]}
43+
] }
3944
turbo-tasks = { workspace = true }
4045
tracing = { workspace = true }
4146
tracing-subscriber = { workspace = true, features = ["env-filter", "json"] }
4247

4348
[build-dependencies]
4449
# It is not a mistake this dependency is specified in dep / build-dep both.
4550
shadow-rs = { workspace = true }
46-
turbopack-binding = { workspace = true, features = [
47-
"__turbo_tasks_build"
48-
]}
51+
turbopack-binding = { workspace = true, features = ["__turbo_tasks_build"] }

packages/next-swc/crates/next-api/src/app.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ use turbopack_binding::{
5454
use crate::{
5555
project::Project,
5656
route::{Endpoint, Route, Routes, WrittenEndpoint},
57+
server_actions::create_server_actions_manifest,
5758
server_paths::all_server_paths,
5859
};
5960

@@ -685,6 +686,26 @@ impl AppEndpoint {
685686
bail!("Entry module must be evaluatable");
686687
};
687688
evaluatable_assets.push(evaluatable);
689+
690+
let (loader, manifest) = create_server_actions_manifest(
691+
app_entry.rsc_entry,
692+
node_root,
693+
&app_entry.pathname,
694+
&app_entry.original_name,
695+
NextRuntime::Edge,
696+
Vc::upcast(this.app_project.edge_rsc_module_context()),
697+
Vc::upcast(chunking_context),
698+
this.app_project
699+
.project()
700+
.next_config()
701+
.enable_server_actions(),
702+
)
703+
.await?;
704+
server_assets.push(manifest);
705+
if let Some(loader) = loader {
706+
evaluatable_assets.push(loader);
707+
}
708+
688709
let files = chunking_context.evaluated_chunk_group(
689710
app_entry
690711
.rsc_entry
@@ -783,6 +804,28 @@ impl AppEndpoint {
783804
}
784805
}
785806
NextRuntime::NodeJs => {
807+
let mut evaluatable_assets =
808+
this.app_project.rsc_runtime_entries().await?.clone_value();
809+
810+
let (loader, manifest) = create_server_actions_manifest(
811+
app_entry.rsc_entry,
812+
node_root,
813+
&app_entry.pathname,
814+
&app_entry.original_name,
815+
NextRuntime::NodeJs,
816+
Vc::upcast(this.app_project.rsc_module_context()),
817+
Vc::upcast(this.app_project.project().rsc_chunking_context()),
818+
this.app_project
819+
.project()
820+
.next_config()
821+
.enable_server_actions(),
822+
)
823+
.await?;
824+
server_assets.push(manifest);
825+
if let Some(loader) = loader {
826+
evaluatable_assets.push(loader);
827+
}
828+
786829
let rsc_chunk = this
787830
.app_project
788831
.project()
@@ -793,7 +836,7 @@ impl AppEndpoint {
793836
original_name = app_entry.original_name
794837
)),
795838
app_entry.rsc_entry,
796-
this.app_project.rsc_runtime_entries(),
839+
Vc::cell(evaluatable_assets),
797840
);
798841
server_assets.push(rsc_chunk);
799842

@@ -816,8 +859,10 @@ impl AppEndpoint {
816859
client_assets: Vc::cell(client_assets),
817860
}
818861
}
819-
};
820-
Ok(endpoint_output.cell())
862+
}
863+
.cell();
864+
865+
Ok(endpoint_output)
821866
}
822867
}
823868

packages/next-swc/crates/next-api/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod middleware;
88
mod pages;
99
pub mod project;
1010
pub mod route;
11+
mod server_actions;
1112
pub mod server_paths;
1213
mod versioned_content_map;
1314

packages/next-swc/crates/next-api/src/project.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ impl Project {
476476
self.env(),
477477
self.server_addr(),
478478
this.define_env.nodejs(),
479+
self.next_config(),
479480
))
480481
}
481482

0 commit comments

Comments
 (0)