Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

turbopack: Implement Server Actions from Client Components #57391

Merged
merged 13 commits into from
Oct 25, 2023
15 changes: 7 additions & 8 deletions packages/next-swc/crates/next-api/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use next_core::{
get_edge_resolve_options_context,
mode::NextMode,
next_app::{
app_client_references_chunks::get_app_server_reference_modules,
get_app_client_references_chunks, get_app_client_shared_chunks, get_app_page_entry,
get_app_route_entry, metadata::route::get_app_metadata_route_entry, AppEntry, AppPage,
},
Expand Down Expand Up @@ -823,7 +824,8 @@ impl AppEndpoint {
evaluatable_assets.push(evaluatable);

let (loader, manifest) = create_server_actions_manifest(
app_entry.rsc_entry,
Vc::upcast(app_entry.rsc_entry),
get_app_server_reference_modules(client_reference_types),
node_root,
&app_entry.pathname,
&app_entry.original_name,
Expand All @@ -833,9 +835,7 @@ impl AppEndpoint {
)
.await?;
server_assets.push(manifest);
if let Some(loader) = loader {
evaluatable_assets.push(loader);
}
evaluatable_assets.push(loader);

let files = chunking_context.evaluated_chunk_group(
app_entry.rsc_entry.ident(),
Expand Down Expand Up @@ -973,7 +973,8 @@ impl AppEndpoint {
this.app_project.rsc_runtime_entries().await?.clone_value();

let (loader, manifest) = create_server_actions_manifest(
app_entry.rsc_entry,
Vc::upcast(app_entry.rsc_entry),
get_app_server_reference_modules(client_reference_types),
node_root,
&app_entry.pathname,
&app_entry.original_name,
Expand All @@ -983,9 +984,7 @@ impl AppEndpoint {
)
.await?;
server_assets.push(manifest);
if let Some(loader) = loader {
evaluatable_assets.push(loader);
}
evaluatable_assets.push(loader);

let rsc_chunk = this
.app_project
Expand Down
83 changes: 56 additions & 27 deletions packages/next-swc/crates/next-api/src/server_actions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::io::Write;
use std::{io::Write, iter::once};

use anyhow::{bail, Result};
use indexmap::IndexMap;
Expand All @@ -16,13 +16,13 @@ use turbopack_binding::{
turbo::tasks_fs::{rope::RopeBuilder, File, FileSystemPath},
turbopack::{
core::{
asset::AssetContent,
asset::{Asset, AssetContent},
chunk::{ChunkItemExt, ChunkableModule, EvaluatableAsset},
context::AssetContext,
module::Module,
output::OutputAsset,
reference::primary_referenced_modules,
reference_type::ReferenceType,
reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType},
virtual_output::VirtualOutputAsset,
virtual_source::VirtualSource,
},
Expand All @@ -42,18 +42,16 @@ use turbopack_binding::{
/// If Server Actions are not enabled, this returns an empty manifest and a None
/// loader.
pub(crate) async fn create_server_actions_manifest(
entry: Vc<Box<dyn EcmascriptChunkPlaceable>>,
rsc_entry: Vc<Box<dyn Module>>,
server_reference_modules: Vc<Vec<Vc<Box<dyn Module>>>>,
node_root: Vc<FileSystemPath>,
pathname: &str,
page_name: &str,
runtime: NextRuntime,
asset_context: Vc<Box<dyn AssetContext>>,
chunking_context: Vc<Box<dyn EcmascriptChunkingContext>>,
) -> Result<(
Option<Vc<Box<dyn EvaluatableAsset>>>,
Vc<Box<dyn OutputAsset>>,
)> {
let actions = get_actions(Vc::upcast(entry));
) -> Result<(Vc<Box<dyn EvaluatableAsset>>, Vc<Box<dyn OutputAsset>>)> {
let actions = get_actions(rsc_entry, server_reference_modules, asset_context);
let loader = build_server_actions_loader(node_root, page_name, actions, asset_context).await?;
let Some(evaluable) = Vc::try_resolve_sidecast::<Box<dyn EvaluatableAsset>>(loader).await?
else {
Expand All @@ -66,7 +64,7 @@ pub(crate) async fn create_server_actions_manifest(
.to_string();
let manifest =
build_manifest(node_root, pathname, page_name, runtime, actions, loader_id).await?;
Ok((Some(evaluable), manifest))
Ok((evaluable, manifest))
}

/// Builds the "action loader" entry point, which reexports every found action
Expand Down Expand Up @@ -101,7 +99,7 @@ async fn build_server_actions_loader(
",
)?;
}
import_map.insert(module_name, *module);
import_map.insert(module_name, module.1);
}
write!(contents, "}});")?;

Expand Down Expand Up @@ -147,17 +145,15 @@ async fn build_manifest(
NextRuntime::NodeJs => &mut manifest.node,
};

for value in actions_value.values() {
let value = value.await?;
for hash in value.keys() {
for ((layer, _), action_map) in actions_value {
let action_map = action_map.await?;
for hash in action_map.keys() {
let entry = mapping.entry(hash.clone()).or_default();
entry.workers.insert(
format!("app{page_name}"),
ActionManifestWorkerEntry::String(loader_id_value.clone_value()),
);
entry
.layer
.insert(format!("app{page_name}"), ActionLayer::Rsc);
entry.layer.insert(format!("app{page_name}"), *layer);
}
}

Expand All @@ -171,10 +167,22 @@ async fn build_manifest(
/// comment which identifies server actions. Every found server action will be
/// returned along with the module which exports that action.
#[turbo_tasks::function]
async fn get_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<ModuleActionMap>> {
async fn get_actions(
rsc_entry: Vc<Box<dyn Module>>,
server_reference_modules: Vc<Vec<Vc<Box<dyn Module>>>>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rename that?

asset_context: Vc<Box<dyn AssetContext>>,
) -> Result<Vc<ModuleActionMap>> {
let mut all_actions = NonDeterministic::new()
.skip_duplicates()
.visit([module], get_referenced_modules)
.visit(
once((ActionLayer::Rsc, rsc_entry)).chain(
server_reference_modules
.await?
.iter()
.map(|m| (ActionLayer::ActionBrowser, *m)),
),
get_referenced_modules,
)
.await
.completed()?
.into_inner()
Expand All @@ -183,6 +191,25 @@ async fn get_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<ModuleActionMap>>
.try_flat_join()
.await?
.into_iter()
.map(|((layer, module), actions)| {
let module = if layer == ActionLayer::Rsc {
module
} else {
// The ActionBrowser layer's module is in the Client context, and we need to
// bring it into the RSC context.
let source = VirtualSource::new(
module.ident().path().join("action.js".to_string()),
module.content(),
);
asset_context.process(
Vc::upcast(source),
Value::new(ReferenceType::EcmaScriptModules(
EcmaScriptModulesReferenceSubType::Undefined,
)),
)
};
((layer, module), actions)
})
.collect::<IndexMap<_, _>>();

all_actions.sort_keys();
Expand All @@ -192,11 +219,11 @@ async fn get_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<ModuleActionMap>>
/// Our graph traversal visitor, which finds the primary modules directly
/// referenced by [parent].
async fn get_referenced_modules(
parent: Vc<Box<dyn Module>>,
) -> Result<impl Iterator<Item = Vc<Box<dyn Module>>> + Send> {
primary_referenced_modules(parent)
(layer, module): (ActionLayer, Vc<Box<dyn Module>>),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't that just 2 arguments

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is invoked by NonDeterministic::visit's fn impl, which only accepts a single arg.

) -> Result<impl Iterator<Item = (ActionLayer, Vc<Box<dyn Module>>)> + Send> {
primary_referenced_modules(module)
.await
.map(|modules| modules.clone_value().into_iter())
.map(|modules| modules.clone_value().into_iter().map(move |m| (layer, m)))
}

/// Inspects the comments inside [module] looking for the magic actions comment.
Expand Down Expand Up @@ -231,19 +258,21 @@ async fn parse_actions(module: Vc<Box<dyn Module>>) -> Result<Vc<OptionActionMap
/// Converts our cached [parsed_actions] call into a data type suitable for
/// collecting into a flat-mapped [IndexMap].
async fn parse_actions_filter_map(
module: Vc<Box<dyn Module>>,
) -> Result<Option<(Vc<Box<dyn Module>>, Vc<ActionMap>)>> {
(layer, module): (ActionLayer, Vc<Box<dyn Module>>),
) -> Result<Option<((ActionLayer, Vc<Box<dyn Module>>), Vc<ActionMap>)>> {
parse_actions(module).await.map(|option_action_map| {
option_action_map
.clone_value()
.map(|action_map| (module, action_map))
.map(|action_map| ((layer, module), action_map))
})
}

type LayerModuleActionMap = IndexMap<(ActionLayer, Vc<Box<dyn Module>>), Vc<ActionMap>>;

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

#[turbo_tasks::value_impl]
impl ModuleActionMap {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ use turbopack_binding::{
},
};

const ECMASCRIPT_CLIENT_TRANSITION_NAME: &str = "next-ecmascript-client-reference";

#[turbo_tasks::value]
pub struct AppEntries {
/// All app entries.
Expand Down Expand Up @@ -140,8 +142,6 @@ pub async fn get_app_entries(

transitions.insert("next-ssr".to_string(), Vc::upcast(ssr_transition));

const ECMASCRIPT_CLIENT_TRANSITION_NAME: &str = "next-ecmascript-client-reference";

transitions.insert(
ECMASCRIPT_CLIENT_TRANSITION_NAME.to_string(),
Vc::upcast(NextEcmascriptClientReferenceTransition::new(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use anyhow::Result;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use turbo_tasks::{debug::ValueDebugFormat, trace::TraceRawVcs, TryJoinIterExt, Vc};
use turbo_tasks::{
debug::ValueDebugFormat, trace::TraceRawVcs, TryFlatJoinIterExt, TryJoinIterExt, Vc,
};
use turbopack_binding::turbopack::{
core::{chunk::ChunkingContextExt, output::OutputAssets},
core::{chunk::ChunkingContextExt, module::Module, output::OutputAssets},
ecmascript::chunk::EcmascriptChunkingContext,
};

Expand Down Expand Up @@ -70,3 +72,27 @@ pub async fn get_app_client_references_chunks(

Ok(Vc::cell(app_client_references_chunks))
}

/// Crawls all modules emitted in the client transition, returning a list of all
/// client JS modules.
#[turbo_tasks::function]
pub async fn get_app_server_reference_modules(
app_client_reference_types: Vc<ClientReferenceTypes>,
) -> Result<Vc<Vec<Vc<Box<dyn Module>>>>> {
Ok(Vc::cell(
app_client_reference_types
.await?
.iter()
.map(|client_reference_ty| async move {
Ok(match client_reference_ty {
ClientReferenceType::EcmascriptClientReference(ecmascript_client_reference) => {
let ecmascript_client_reference_ref = ecmascript_client_reference.await?;
Some(Vc::upcast(ecmascript_client_reference_ref.client_module))
}
_ => None,
})
})
.try_flat_join()
.await?,
))
}
55 changes: 31 additions & 24 deletions packages/next-swc/crates/next-core/src/next_import_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,27 +298,6 @@ pub async fn get_next_server_import_map(
ServerContextType::AppSSR { .. }
| ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. } => {
import_map.insert_exact_alias(
"private-next-rsc-action-proxy",
request_to_import_mapping(
project_path,
"next/dist/build/webpack/loaders/next-flight-loader/action-proxy",
),
);
import_map.insert_exact_alias(
"private-next-rsc-action-client-wrapper",
request_to_import_mapping(
project_path,
"next/dist/build/webpack/loaders/next-flight-loader/action-client-wrapper",
),
);
import_map.insert_exact_alias(
"private-next-rsc-action-validate",
request_to_import_mapping(
project_path,
"next/dist/build/webpack/loaders/next-flight-loader/action-validate",
),
);
import_map.insert_exact_alias(
"next/head",
request_to_import_mapping(project_path, "next/dist/client/components/noop-head"),
Expand Down Expand Up @@ -579,9 +558,7 @@ async fn insert_next_server_special_aliases(

// see https://github.com/vercel/next.js/blob/8013ef7372fc545d49dbd060461224ceb563b454/packages/next/src/build/webpack-config.ts#L1449-L1531
match ty {
ServerContextType::Pages { .. }
| ServerContextType::PagesData { .. }
| ServerContextType::AppSSR { .. } => {
ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } => {
insert_exact_alias_map(
import_map,
project_path,
Expand All @@ -596,6 +573,7 @@ async fn insert_next_server_special_aliases(
// TODO: should include `ServerContextType::PagesApi` routes, but that type doesn't exist.
ServerContextType::AppRSC { .. }
| ServerContextType::AppRoute { .. }
| ServerContextType::AppSSR { .. }
| ServerContextType::Middleware => {
insert_exact_alias_map(
import_map,
Expand Down Expand Up @@ -828,6 +806,35 @@ async fn insert_next_shared_aliases(
request_to_import_mapping(project_path, "next/dist/compiled/setimmediate"),
);

import_map.insert_exact_alias(
"private-next-rsc-action-proxy",
request_to_import_mapping(
project_path,
"next/dist/build/webpack/loaders/next-flight-loader/action-proxy",
),
);
import_map.insert_exact_alias(
"private-next-rsc-action-client-wrapper",
request_to_import_mapping(
project_path,
"next/dist/build/webpack/loaders/next-flight-loader/action-client-wrapper",
),
);
import_map.insert_exact_alias(
"private-next-rsc-action-validate",
request_to_import_mapping(
project_path,
"next/dist/build/webpack/loaders/next-flight-loader/action-validate",
),
);
import_map.insert_exact_alias(
"private-next-rsc-action-encryption",
request_to_import_mapping(
project_path,
"next/dist/server/app-render/action-encryption",
),
);

insert_turbopack_dev_alias(import_map);
insert_package_alias(
import_map,
Expand Down
Loading