Skip to content

Commit c3370a5

Browse files
committed
Comments, and always generate the manifest
1 parent df13b3b commit c3370a5

File tree

3 files changed

+130
-92
lines changed

3 files changed

+130
-92
lines changed

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

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -659,26 +659,22 @@ impl AppEndpoint {
659659
};
660660
evaluatable_assets.push(evaluatable);
661661

662-
if *this
663-
.app_project
664-
.project()
665-
.next_config()
666-
.enable_server_actions()
667-
.await?
668-
{
669-
let (loader, manifest) = create_server_actions_manifest(
670-
app_entry.rsc_entry,
671-
node_root,
672-
&app_entry.original_name,
673-
NextRuntime::NodeJs,
674-
Vc::upcast(this.app_project.edge_rsc_module_context()),
675-
Vc::upcast(chunking_context),
676-
)
677-
.await?;
678-
server_assets.push(manifest);
679-
if let Some(loader) = loader {
680-
evaluatable_assets.push(loader);
681-
}
662+
let (loader, manifest) = create_server_actions_manifest(
663+
app_entry.rsc_entry,
664+
node_root,
665+
&app_entry.original_name,
666+
NextRuntime::NodeJs,
667+
Vc::upcast(this.app_project.edge_rsc_module_context()),
668+
Vc::upcast(chunking_context),
669+
this.app_project
670+
.project()
671+
.next_config()
672+
.enable_server_actions(),
673+
)
674+
.await?;
675+
server_assets.push(manifest);
676+
if let Some(loader) = loader {
677+
evaluatable_assets.push(loader);
682678
}
683679

684680
let files = chunking_context.evaluated_chunk_group(
@@ -777,26 +773,22 @@ impl AppEndpoint {
777773
let mut evaluatable_assets =
778774
this.app_project.rsc_runtime_entries().await?.clone_value();
779775

780-
if *this
781-
.app_project
782-
.project()
783-
.next_config()
784-
.enable_server_actions()
785-
.await?
786-
{
787-
let (loader, manifest) = create_server_actions_manifest(
788-
app_entry.rsc_entry,
789-
node_root,
790-
&app_entry.original_name,
791-
NextRuntime::NodeJs,
792-
Vc::upcast(this.app_project.rsc_module_context()),
793-
Vc::upcast(this.app_project.project().rsc_chunking_context()),
794-
)
795-
.await?;
796-
server_assets.push(manifest);
797-
if let Some(loader) = loader {
798-
evaluatable_assets.push(loader);
799-
}
776+
let (loader, manifest) = create_server_actions_manifest(
777+
app_entry.rsc_entry,
778+
node_root,
779+
&app_entry.original_name,
780+
NextRuntime::NodeJs,
781+
Vc::upcast(this.app_project.rsc_module_context()),
782+
Vc::upcast(this.app_project.project().rsc_chunking_context()),
783+
this.app_project
784+
.project()
785+
.next_config()
786+
.enable_server_actions(),
787+
)
788+
.await?;
789+
server_assets.push(manifest);
790+
if let Some(loader) = loader {
791+
evaluatable_assets.push(loader);
800792
}
801793

802794
let rsc_chunk = this

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

Lines changed: 97 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,80 @@ use turbopack_binding::{
2828
},
2929
};
3030

31+
/// Scans the RSC entry point's full module graph looking for exported Server
32+
/// Actions (identifiable by a magic comment in the transformed module's
33+
/// output), and constructs a evaluatable "action loader" entry point and
34+
/// manifest describing the found actions.
35+
///
36+
/// If Server Actions are not enabled, this returns an empty manifest and a None
37+
/// loader.
38+
pub(crate) async fn create_server_actions_manifest(
39+
entry: Vc<Box<dyn EcmascriptChunkPlaceable>>,
40+
node_root: Vc<FileSystemPath>,
41+
app_page_name: &str,
42+
runtime: NextRuntime,
43+
asset_context: Vc<Box<dyn AssetContext>>,
44+
chunking_context: Vc<Box<dyn EcmascriptChunkingContext>>,
45+
enable_server_actions: Vc<bool>,
46+
) -> Result<(
47+
Option<Vc<Box<dyn EvaluatableAsset>>>,
48+
Vc<Box<dyn OutputAsset>>,
49+
)> {
50+
// If actions aren't enabled, then there's no need to scan the module graph. We
51+
// still need to generate an empty manifest so that the TS side can merge
52+
// the manifest later on.
53+
if !*enable_server_actions.await? {
54+
let manifest = build_manifest(
55+
node_root,
56+
app_page_name,
57+
runtime,
58+
ModuleActionMap::empty(),
59+
Vc::<String>::empty(),
60+
)
61+
.await?;
62+
return Ok((None, manifest));
63+
}
64+
65+
let actions = get_actions(Vc::upcast(entry));
66+
let loader =
67+
build_server_actions_loader(node_root, app_page_name, actions, asset_context).await?;
68+
let Some(evaluable) = Vc::try_resolve_sidecast::<Box<dyn EvaluatableAsset>>(loader).await?
69+
else {
70+
bail!("loader module must be evaluatable");
71+
};
72+
73+
let loader_id = loader.as_chunk_item(chunking_context).id().to_string();
74+
let manifest = build_manifest(node_root, app_page_name, runtime, actions, loader_id).await?;
75+
Ok((Some(evaluable), manifest))
76+
}
77+
78+
/// Builds the "action loader" entry point, which reexports every found action
79+
/// behind a lazy dynamic import.
80+
///
81+
/// The actions are reexported under a hashed name (comprised of the exporting
82+
/// file's name and the action name). This hash matches the id sent to the
83+
/// client and present inside the paired manifest.
3184
async fn build_server_actions_loader(
3285
node_root: Vc<FileSystemPath>,
33-
original_name: &str,
86+
app_page_name: &str,
3487
actions: Vc<ModuleActionMap>,
3588
asset_context: Vc<Box<dyn AssetContext>>,
3689
) -> Result<Vc<Box<dyn EcmascriptChunkPlaceable>>> {
3790
let actions = actions.await?;
3891

3992
let mut contents = RopeBuilder::from("__turbopack_export_value__({\n");
4093
let mut import_map = IndexMap::with_capacity(actions.len());
94+
95+
// Every module which exports an action (that is accessible starting from our
96+
// app page entry point) will be present. We generate a single loader file
97+
// which lazily imports the respective module's chunk_item id and invokes
98+
// the exported action function.
4199
for (i, (module, actions_map)) in actions.iter().enumerate() {
42-
for (id, name) in &*actions_map.await? {
100+
for (hash_id, name) in &*actions_map.await? {
43101
writedoc!(
44102
contents,
45103
"
46-
\x20 '{id}': (...args) => import('ACTIONS_MODULE{i}')
104+
\x20 '{hash_id}': (...args) => import('ACTIONS_MODULE{i}')
47105
.then(mod => (0, mod['{name}'])(...args)),\n
48106
",
49107
)?;
@@ -52,7 +110,7 @@ async fn build_server_actions_loader(
52110
}
53111
write!(contents, "}});")?;
54112

55-
let output_path = node_root.join(format!("server/app{original_name}/actions.js"));
113+
let output_path = node_root.join(format!("server/app{app_page_name}/actions.js"));
56114
let file = File::from(contents.build());
57115
let source = VirtualSource::new(output_path, AssetContent::file(file.into()));
58116
let module = asset_context.process(
@@ -69,73 +127,49 @@ async fn build_server_actions_loader(
69127
Ok(placeable)
70128
}
71129

72-
pub(crate) async fn create_server_actions_manifest(
73-
entry: Vc<Box<dyn EcmascriptChunkPlaceable>>,
130+
/// Builds a manifest containing every action's hashed id, with an internal
131+
/// module id which exports a function using that hashed name.
132+
async fn build_manifest(
74133
node_root: Vc<FileSystemPath>,
75-
original_name: &str,
134+
app_page_name: &str,
76135
runtime: NextRuntime,
77-
asset_context: Vc<Box<dyn AssetContext>>,
78-
chunking_context: Vc<Box<dyn EcmascriptChunkingContext>>,
79-
) -> Result<(
80-
Option<Vc<Box<dyn EvaluatableAsset>>>,
81-
Vc<Box<dyn OutputAsset>>,
82-
)> {
83-
let actions = get_actions(Vc::upcast(entry));
84-
let actions_value = actions.await?;
85-
86-
let path = node_root.join(format!(
87-
"server/app{original_name}/server-reference-manifest.json",
136+
actions: Vc<ModuleActionMap>,
137+
loader_id: Vc<String>,
138+
) -> Result<Vc<Box<dyn OutputAsset>>> {
139+
let manifest_path = node_root.join(format!(
140+
"server/app{app_page_name}/server-reference-manifest.json",
88141
));
89142
let mut manifest = ServerReferenceManifest {
90143
..Default::default()
91144
};
92145

93-
if actions_value.is_empty() {
94-
let manifest = Vc::upcast(VirtualOutputAsset::new(
95-
path,
96-
AssetContent::file(File::from(serde_json::to_string_pretty(&manifest)?).into()),
97-
));
98-
return Ok((None, manifest));
99-
}
100-
146+
let actions_value = actions.await?;
147+
let loader_id_value = loader_id.await?;
101148
let mapping = match runtime {
102149
NextRuntime::Edge => &mut manifest.edge,
103150
NextRuntime::NodeJs => &mut manifest.node,
104151
};
105152

106-
let loader =
107-
build_server_actions_loader(node_root, original_name, actions, asset_context).await?;
108-
let chunk_item_id = loader
109-
.as_chunk_item(chunking_context)
110-
.id()
111-
.to_string()
112-
.await?;
113-
114153
for value in actions_value.values() {
115154
let value = value.await?;
116155
for hash in value.keys() {
117156
let entry = mapping.entry(hash.clone()).or_default();
118157
entry.workers.insert(
119-
format!("app{original_name}"),
120-
ActionManifestWorkerEntry::String(chunk_item_id.clone_value()),
158+
format!("app{app_page_name}"),
159+
ActionManifestWorkerEntry::String(loader_id_value.clone_value()),
121160
);
122161
}
123162
}
124-
let manifest = Vc::upcast(VirtualOutputAsset::new(
125-
path,
126-
AssetContent::file(File::from(serde_json::to_string_pretty(&manifest)?).into()),
127-
));
128-
129-
let Some(evaluable) = Vc::try_resolve_sidecast::<Box<dyn EvaluatableAsset>>(loader).await?
130-
else {
131-
bail!("loader module must be evaluatable");
132-
};
133163

134-
Ok((Some(evaluable), manifest))
164+
Ok(Vc::upcast(VirtualOutputAsset::new(
165+
manifest_path,
166+
AssetContent::file(File::from(serde_json::to_string_pretty(&manifest)?).into()),
167+
)))
135168
}
136169

137-
/// Finds the first page component in our loader tree, which should be the page
138-
/// we're currently rendering.
170+
/// Traverses the entire module graph starting from [module], looking for magic
171+
/// comment which identifies server actions. Every found server action will be
172+
/// returned along with the module which exports that action.
139173
#[turbo_tasks::function]
140174
async fn get_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<ModuleActionMap>> {
141175
let mut all_actions = IndexMap::new();
@@ -155,6 +189,9 @@ async fn get_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<ModuleActionMap>>
155189
Ok(Vc::cell(all_actions))
156190
}
157191

192+
/// Inspects the comments inside [module] looking for the magic actions comment.
193+
/// If found, we return the mapping of every action's hashed id to the name of
194+
/// the exported action function. If not, we return a None.
158195
#[turbo_tasks::function]
159196
async fn parse_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<OptionActionMap>> {
160197
let Some(ecmascript_asset) =
@@ -173,15 +210,24 @@ async fn parse_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<OptionActionMap
173210
Ok(Vc::cell(actions.map(Vc::cell)))
174211
}
175212

213+
/// A mapping of every module which exports a Server Action, with the hashed id
214+
/// and exported name of each found action.
176215
#[turbo_tasks::value(transparent)]
177216
struct ModuleActionMap(IndexMap<Vc<Box<dyn Module>>, Vc<ActionMap>>);
178217

179-
/// Maps the hashed `(filename, exported_action_name) -> exported_action_name`,
180-
/// so that we can invoke the correct action function when we receive a request
181-
/// with the hash in `Next-Action` header.
218+
#[turbo_tasks::value_impl]
219+
impl ModuleActionMap {
220+
#[turbo_tasks::function]
221+
pub fn empty() -> Vc<Self> {
222+
Vc::cell(IndexMap::new())
223+
}
224+
}
225+
226+
/// Maps the hashed action id to the action's exported function name.
182227
#[turbo_tasks::value(transparent)]
183228
struct ActionMap(IndexMap<String, String>);
184229

230+
/// An Option wrapper around [ActionMap].
185231
#[turbo_tasks::value(transparent)]
186232
struct OptionActionMap(Option<Vc<ActionMap>>);
187233

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use turbopack_binding::turbopack::{
1010

1111
use super::module_rule_match_js_no_url;
1212

13-
/// Returns a rule which applies the Next.js font transform.
13+
/// Returns a rule which applies the Next.js Server Actions transform.
1414
pub fn get_server_actions_transform_rule(is_server: bool) -> ModuleRule {
1515
let transformer =
1616
EcmascriptInputTransform::Plugin(Vc::cell(Box::new(NextServerActions { is_server }) as _));

0 commit comments

Comments
 (0)