diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index fc3d3b6562bca..a21e9f9912981 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -1120,8 +1120,21 @@ impl AppEndpoint { }; let server_action_manifest_loader = if process_client_components { + let reduced_graphs = get_reduced_graphs_for_endpoint( + this.app_project.project(), + *rsc_entry, + Vc::upcast(this.app_project.client_module_context()), + ); + let actions = reduced_graphs.get_server_actions_for_endpoint( + *rsc_entry, + match runtime { + NextRuntime::Edge => Vc::upcast(this.app_project.edge_rsc_module_context()), + NextRuntime::NodeJs => Vc::upcast(this.app_project.rsc_module_context()), + }, + ); + let server_action_manifest = create_server_actions_manifest( - *ResolvedVc::upcast(app_entry.rsc_entry), + actions, this.app_project.project().project_path(), node_root, app_entry.original_name.clone(), diff --git a/crates/next-api/src/module_graph.rs b/crates/next-api/src/module_graph.rs index ef964abfe018c..47f26e6a85299 100644 --- a/crates/next-api/src/module_graph.rs +++ b/crates/next-api/src/module_graph.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, collections::{HashMap, HashSet}, future::Future, hash::Hash, @@ -9,6 +10,7 @@ use anyhow::{Context, Result}; use next_core::{ mode::NextMode, next_client_reference::{find_server_entries, ServerEntries}, + next_manifests::ActionLayer, }; use petgraph::{ graph::{DiGraph, NodeIndex}, @@ -35,6 +37,7 @@ use turbopack_core::{ use crate::{ dynamic_imports::{map_next_dynamic, DynamicImports}, project::Project, + server_actions::{map_server_actions, to_rsc_context, AllActions, AllModuleActions}, }; #[turbo_tasks::value(transparent)] @@ -316,6 +319,12 @@ impl SingleModuleGraph { .context("Couldn't find entry module in graph") } + /// Iterate over all nodes in the graph (potentially in the whole app!). + pub fn iter_nodes(&self) -> impl Iterator + '_ { + self.graph.node_weights() + } + + /// Enumerate over all nodes in the graph (potentially in the whole app!). pub fn enumerate_nodes( &self, ) -> impl Iterator + '_ { @@ -533,6 +542,90 @@ impl NextDynamicGraph { } } +#[turbo_tasks::value] +pub struct ServerActionsGraph { + is_single_page: bool, + graph: ResolvedVc, + /// (Layer, RSC or Browser module) -> list of actions + data: ResolvedVc, +} + +#[turbo_tasks::value_impl] +impl ServerActionsGraph { + #[turbo_tasks::function] + pub async fn new_with_entries( + graph: ResolvedVc, + is_single_page: bool, + ) -> Result> { + let mapped = map_server_actions(*graph); + + // TODO shrink graph here + + Ok(ServerActionsGraph { + is_single_page, + graph, + data: mapped.to_resolved().await?, + } + .cell()) + } + + #[turbo_tasks::function] + pub async fn get_server_actions_for_endpoint( + &self, + entry: ResolvedVc>, + rsc_asset_context: Vc>, + ) -> Result> { + let span = tracing::info_span!("collect server actions for endpoint"); + async move { + let data = &*self.data.await?; + let data = if self.is_single_page { + // The graph contains the page (= `entry`) only, no need to filter. + Cow::Borrowed(data) + } else { + // The graph contains the whole app, traverse and collect all reachable imports. + let graph = &*self.graph.await?; + + let mut result = HashMap::new(); + graph.traverse_from_entry(entry, |node| { + if let Some(node_data) = data.get(&node.module) { + result.insert(node.module, *node_data); + } + })?; + Cow::Owned(result) + }; + + let actions = data + .iter() + .map(|(module, (layer, actions))| async move { + actions + .await? + .iter() + .map(|(hash, name)| async move { + Ok(( + hash.to_string(), + ( + *layer, + name.to_string(), + if *layer == ActionLayer::Rsc { + *module + } else { + to_rsc_context(**module, rsc_asset_context).await? + }, + ), + )) + }) + .try_join() + .await + }) + .try_flat_join() + .await?; + Ok(Vc::cell(actions.into_iter().collect())) + } + .instrument(span) + .await + } +} + /// The consumers of this shouldn't need to care about the exact contents since it's abstracted away /// by the accessor functions, but /// - In dev, contains information about the modules of the current endpoint only @@ -540,6 +633,7 @@ impl NextDynamicGraph { #[turbo_tasks::value] pub struct ReducedGraphs { next_dynamic: Vec>, + server_actions: Vec>, // TODO add other graphs } @@ -578,6 +672,38 @@ impl ReducedGraphs { .instrument(span) .await } + + /// Returns the server actions for the given page. + #[turbo_tasks::function] + pub async fn get_server_actions_for_endpoint( + &self, + entry: Vc>, + rsc_asset_context: Vc>, + ) -> Result> { + let span = tracing::info_span!("collect all server actions for endpoint"); + async move { + if let [graph] = &self.server_actions[..] { + // Just a single graph, no need to merge results + Ok(graph.get_server_actions_for_endpoint(entry, rsc_asset_context)) + } else { + let result = self + .server_actions + .iter() + .map(|graph| async move { + Ok(graph + .get_server_actions_for_endpoint(entry, rsc_asset_context) + .await? + .clone_value()) + }) + .try_flat_join() + .await?; + + Ok(Vc::cell(result.into_iter().collect())) + } + } + .instrument(span) + .await + } } #[turbo_tasks::function] @@ -609,7 +735,7 @@ async fn get_reduced_graphs_for_endpoint_inner( ), }; - let next_dynamic = async move { + let next_dynamic = async { graphs .iter() .map(|graph| { @@ -622,7 +748,23 @@ async fn get_reduced_graphs_for_endpoint_inner( .instrument(tracing::info_span!("generating next/dynamic graphs")) .await?; - Ok(ReducedGraphs { next_dynamic }.cell()) + let server_actions = async { + graphs + .iter() + .map(|graph| { + ServerActionsGraph::new_with_entries(**graph, is_single_page).to_resolved() + }) + .try_join() + .await + } + .instrument(tracing::info_span!("generating server actions graphs")) + .await?; + + Ok(ReducedGraphs { + next_dynamic, + server_actions, + } + .cell()) } /// Generates a [ReducedGraph] for the given project and endpoint containing information that is diff --git a/crates/next-api/src/server_actions.rs b/crates/next-api/src/server_actions.rs index 2168d91b6b438..7b63b183f1305 100644 --- a/crates/next-api/src/server_actions.rs +++ b/crates/next-api/src/server_actions.rs @@ -1,9 +1,10 @@ -use std::{collections::BTreeMap, future::Future, io::Write, iter::once}; +use std::{ + collections::{BTreeMap, HashMap}, + io::Write, +}; use anyhow::{bail, Context, Result}; -use indexmap::map::Entry; use next_core::{ - next_client_reference::EcmascriptClientReferenceModule, next_manifests::{ ActionLayer, ActionManifestModuleId, ActionManifestWorkerEntry, ServerReferenceManifest, }, @@ -20,12 +21,8 @@ use swc_core::{ utils::find_pat_ids, }, }; -use tracing::{Instrument, Level}; use turbo_rcstr::RcStr; -use turbo_tasks::{ - graph::{GraphTraversal, NonDeterministic, VisitControlFlow}, - FxIndexMap, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Value, ValueToString, Vc, -}; +use turbo_tasks::{FxIndexMap, ResolvedVc, TryFlatJoinIterExt, Value, ValueToString, Vc}; use turbo_tasks_fs::{self, rope::RopeBuilder, File, FileSystemPath}; use turbopack_core::{ asset::AssetContent, @@ -34,7 +31,6 @@ use turbopack_core::{ file_source::FileSource, module::Module, output::OutputAsset, - reference::primary_referenced_modules, reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType}, resolve::ModulePart, virtual_output::VirtualOutputAsset, @@ -45,6 +41,8 @@ use turbopack_ecmascript::{ tree_shake::asset::EcmascriptModulePartAsset, EcmascriptParsable, }; +use crate::module_graph::SingleModuleGraph; + #[turbo_tasks::value] pub(crate) struct ServerActionsManifest { pub loader: ResolvedVc>, @@ -60,18 +58,16 @@ pub(crate) struct ServerActionsManifest { /// loader. #[turbo_tasks::function] pub(crate) async fn create_server_actions_manifest( - rsc_entry: Vc>, + actions: Vc, project_path: Vc, node_root: Vc, page_name: RcStr, runtime: NextRuntime, - asset_context: Vc>, + rsc_asset_context: Vc>, chunking_context: Vc>, ) -> Result> { - let actions = find_actions(rsc_entry, asset_context); - let loader = - build_server_actions_loader(project_path, page_name.clone(), actions, asset_context); + build_server_actions_loader(project_path, page_name.clone(), actions, rsc_asset_context); let evaluable = Vc::try_resolve_sidecast::>(loader) .await? .context("loader module must be evaluatable")? @@ -187,121 +183,9 @@ async fn build_manifest( )) } -/// Traverses the entire module graph starting from [Module], looking for magic -/// 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 find_actions( - rsc_entry: ResolvedVc>, - asset_context: Vc>, -) -> Result> { - async move { - let actions = NonDeterministic::new() - .skip_duplicates() - .visit( - once(( - ActionLayer::Rsc, - rsc_entry, - rsc_entry.ident().to_string().await?, - )), - FindActionsVisit {}, - ) - .await - .completed()? - .into_inner() - .into_iter() - .map(parse_actions_filter_map) - .try_flat_join() - .await?; - - // Actions can be imported by both Client and RSC layers, in which case we need - // to use the RSC layer's module. We do that by merging the hashes (which match - // in both layers) and preferring the RSC layer's action. - let mut all_actions: HashToLayerNameModule = FxIndexMap::default(); - for ((layer, module, _), actions_map) in actions.iter() { - let module = if *layer == ActionLayer::Rsc { - *module - } else { - to_rsc_context(**module, asset_context).await? - }; - - for (hash_id, name) in &*actions_map.await? { - match all_actions.entry(hash_id.to_owned()) { - Entry::Occupied(e) => { - if e.get().0 == ActionLayer::ActionBrowser { - *e.into_mut() = (*layer, name.to_string(), module); - } - } - Entry::Vacant(e) => { - e.insert((*layer, name.to_string(), module)); - } - } - } - } - - all_actions.sort_keys(); - Ok(Vc::cell(all_actions)) - } - .instrument(tracing::info_span!("find server actions")) - .await -} - -type FindActionsNode = (ActionLayer, ResolvedVc>, ReadRef); -struct FindActionsVisit {} -impl turbo_tasks::graph::Visit for FindActionsVisit { - type Edge = FindActionsNode; - type EdgesIntoIter = impl Iterator; - type EdgesFuture = impl Future>; - - fn visit(&mut self, edge: Self::Edge) -> VisitControlFlow { - VisitControlFlow::Continue(edge) - } - - fn edges(&mut self, node: &Self::Edge) -> Self::EdgesFuture { - get_referenced_modules(node.clone()) - } - - fn span(&mut self, node: &Self::Edge) -> tracing::Span { - let (_, _, name) = node; - tracing::span!( - Level::INFO, - "find server actions visit", - name = display(name) - ) - } -} - -/// Our graph traversal visitor, which finds the primary modules directly -/// referenced by parent. -async fn get_referenced_modules( - (layer, module, _): FindActionsNode, -) -> Result + Send> { - if let Some(module) = - ResolvedVc::try_downcast_type::(module).await? - { - let module: ReadRef = module.await?; - return Ok(vec![( - ActionLayer::ActionBrowser, - ResolvedVc::upcast(module.client_module), - module.client_module.ident().to_string().await?, - )] - .into_iter()); - } - - let modules = primary_referenced_modules(*module).await?; - - Ok(modules - .into_iter() - .copied() - .map(move |m| async move { Ok((layer, m, m.ident().to_string().await?)) }) - .try_join() - .await? - .into_iter()) -} - /// The ActionBrowser layer's module is in the Client context, and we need to /// bring it into the RSC context. -async fn to_rsc_context( +pub async fn to_rsc_context( module: Vc>, asset_context: Vc>, ) -> Result>> { @@ -350,7 +234,7 @@ async fn parse_actions(module: Vc>) -> Result>(module).await? else { - return Ok(OptionActionMap::none()); + return Ok(Vc::cell(None)); }; if let Some(module) = Vc::try_resolve_downcast_type::(module).await? @@ -359,7 +243,7 @@ async fn parse_actions(module: Vc>) -> Result>) -> Result>) -> Result>) -> bool { .unwrap_or(false) } -/// Converts our cached [parse_actions] call into a data type suitable for -/// collecting into a flat-mapped [FxIndexMap]. -async fn parse_actions_filter_map( - (layer, module, name): FindActionsNode, -) -> Result)>> { - parse_actions(*module).await.map(|option_action_map| { - option_action_map - .clone_value() - .map(|action_map| ((layer, module, name), action_map)) - }) -} - type HashToLayerNameModule = FxIndexMap>)>; /// 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 AllActions(HashToLayerNameModule); +pub struct AllActions(HashToLayerNameModule); #[turbo_tasks::value_impl] impl AllActions { @@ -510,16 +382,41 @@ impl AllActions { /// Maps the hashed action id to the action's exported function name. #[turbo_tasks::value(transparent)] -struct ActionMap(FxIndexMap); +pub struct ActionMap(FxIndexMap); /// An Option wrapper around [ActionMap]. #[turbo_tasks::value(transparent)] struct OptionActionMap(Option>); -#[turbo_tasks::value_impl] -impl OptionActionMap { - #[turbo_tasks::function] - pub fn none() -> Vc { - Vc::cell(None) - } +type LayerAndActions = (ActionLayer, ResolvedVc); +/// A mapping of every module module containing Server Actions, mapping to its layer and actions. +#[turbo_tasks::value(transparent)] +pub struct AllModuleActions(HashMap>, LayerAndActions>); + +#[turbo_tasks::function] +pub async fn map_server_actions(graph: Vc) -> Result> { + let actions = graph + .await? + .iter_nodes() + .map(|node| { + async move { + // TODO: compare module contexts instead? + let layer = match &node.layer { + Some(layer) if &**layer == "app-rsc" || &**layer == "app-edge-rsc" => { + ActionLayer::Rsc + } + Some(layer) if &**layer == "app-client" => ActionLayer::ActionBrowser, + // TODO really ignore SSR? + _ => return Ok(None), + }; + // TODO the old implementation did parse_actions(to_rsc_context(module)) + // is that really necessary? + Ok(parse_actions(*node.module) + .await? + .map(|action_map| (node.module, (layer, action_map)))) + } + }) + .try_flat_join() + .await?; + Ok(Vc::cell(actions.into_iter().collect())) }