diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index df2876bea5f48..9e96b75116c1b 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -8,7 +8,6 @@ use next_core::{ }, get_edge_resolve_options_context, get_next_package, next_app::{ - app_client_references_chunks::get_app_server_reference_modules, get_app_client_references_chunks, get_app_client_shared_chunk_group, get_app_page_entry, get_app_route_entry, include_modules_module::IncludeModulesModule, metadata::route::get_app_metadata_route_entry, AppEntry, AppPage, @@ -909,7 +908,7 @@ impl AppEndpoint { }; let ( - app_server_reference_modules, + server_action_manifest_loader, client_dynamic_imports, client_references, client_references_chunks, @@ -970,7 +969,10 @@ impl AppEndpoint { .values() { let result = collect_next_dynamic_imports( - refs.clone(), + refs.iter() + .map(|r| async move { Ok(Vc::upcast(*r.await?.ssr_module)) }) + .try_join() + .await?, Vc::upcast(this.app_project.client_module_context()), visited_modules, ) @@ -1122,10 +1124,26 @@ impl AppEndpoint { } } + let server_action_manifest = create_server_actions_manifest( + *ResolvedVc::upcast(app_entry.rsc_entry), + client_references_cell, + this.app_project.project().project_path(), + node_root, + app_entry.original_name.clone(), + runtime, + match runtime { + NextRuntime::Edge => Vc::upcast(this.app_project.edge_rsc_module_context()), + NextRuntime::NodeJs => Vc::upcast(this.app_project.rsc_module_context()), + }, + this.app_project + .project() + .runtime_chunking_context(process_client_assets, runtime), + ) + .await?; + server_assets.insert(server_action_manifest.manifest); + ( - Some(get_app_server_reference_modules( - client_references_cell.types(), - )), + Some(server_action_manifest.loader), Some(client_dynamic_imports), Some(client_references_cell), Some(client_references_chunks), @@ -1134,30 +1152,6 @@ impl AppEndpoint { (None, None, None, None) }; - let server_action_manifest_loader = - if let Some(app_server_reference_modules) = app_server_reference_modules { - let server_action_manifest = create_server_actions_manifest( - *ResolvedVc::upcast(app_entry.rsc_entry), - app_server_reference_modules, - this.app_project.project().project_path(), - node_root, - app_entry.original_name.clone(), - runtime, - match runtime { - NextRuntime::Edge => Vc::upcast(this.app_project.edge_rsc_module_context()), - NextRuntime::NodeJs => Vc::upcast(this.app_project.rsc_module_context()), - }, - this.app_project - .project() - .runtime_chunking_context(process_client_assets, runtime), - ) - .await?; - server_assets.insert(server_action_manifest.manifest); - Some(server_action_manifest.loader) - } else { - None - }; - let (app_entry_chunks, app_entry_chunks_availability) = &*self .app_entry_chunks( client_references, @@ -1636,15 +1630,32 @@ impl Endpoint for AppEndpoint { .project() .emit_all_output_assets(Vc::cell(output_assets)); - let node_root = this.app_project.project().node_root(); - let server_paths = all_server_paths(output_assets, node_root) + let (server_paths, client_paths) = if this + .app_project + .project() + .next_mode() .await? - .clone_value(); + .is_development() + { + let node_root = this.app_project.project().node_root(); + let server_paths = all_server_paths(output_assets, node_root) + .await? + .clone_value(); - let client_relative_root = this.app_project.project().client_relative_path(); - let client_paths = all_paths_in_root(output_assets, client_relative_root) - .await? - .clone_value(); + let client_relative_root = this.app_project.project().client_relative_path(); + let client_paths = async { + anyhow::Ok( + all_paths_in_root(output_assets, client_relative_root) + .await? + .clone_value(), + ) + } + .instrument(tracing::info_span!("client_paths")) + .await?; + (server_paths, client_paths) + } else { + (vec![], vec![]) + }; let written_endpoint = match *output { AppEndpointOutput::NodeJs { rsc_chunk, .. } => WrittenEndpoint::NodeJs { diff --git a/crates/next-api/src/instrumentation.rs b/crates/next-api/src/instrumentation.rs index e01514c3631bc..ef76071d2ea76 100644 --- a/crates/next-api/src/instrumentation.rs +++ b/crates/next-api/src/instrumentation.rs @@ -252,10 +252,14 @@ impl Endpoint for InstrumentationEndpoint { let _ = output_assets.resolve().await?; let _ = this.project.emit_all_output_assets(Vc::cell(output_assets)); - let node_root = this.project.node_root(); - let server_paths = all_server_paths(output_assets, node_root) - .await? - .clone_value(); + let server_paths = if this.project.next_mode().await?.is_development() { + let node_root = this.project.node_root(); + all_server_paths(output_assets, node_root) + .await? + .clone_value() + } else { + vec![] + }; Ok(WrittenEndpoint::Edge { server_paths, diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index fee7bc715f682..8671a9941b545 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -277,16 +277,27 @@ impl Endpoint for MiddlewareEndpoint { let _ = output_assets.resolve().await?; let _ = this.project.emit_all_output_assets(Vc::cell(output_assets)); - let node_root = this.project.node_root(); - let server_paths = all_server_paths(output_assets, node_root) - .await? - .clone_value(); - - // Middleware could in theory have a client path (e.g. `new URL`). - let client_relative_root = this.project.client_relative_path(); - let client_paths = all_paths_in_root(output_assets, client_relative_root) - .await? - .clone_value(); + let (server_paths, client_paths) = if this.project.next_mode().await?.is_development() { + let node_root = this.project.node_root(); + let server_paths = all_server_paths(output_assets, node_root) + .await? + .clone_value(); + + // Middleware could in theory have a client path (e.g. `new URL`). + let client_relative_root = this.project.client_relative_path(); + let client_paths = async { + anyhow::Ok( + all_paths_in_root(output_assets, client_relative_root) + .await? + .clone_value(), + ) + } + .instrument(tracing::info_span!("client_paths")) + .await?; + (server_paths, client_paths) + } else { + (vec![], vec![]) + }; Ok(WrittenEndpoint::Edge { server_paths, diff --git a/crates/next-api/src/pages.rs b/crates/next-api/src/pages.rs index 76b61d07a680e..2b48816577a64 100644 --- a/crates/next-api/src/pages.rs +++ b/crates/next-api/src/pages.rs @@ -1273,14 +1273,32 @@ impl Endpoint for PageEndpoint { .emit_all_output_assets(Vc::cell(output_assets)); let node_root = this.pages_project.project().node_root(); - let server_paths = all_server_paths(output_assets, node_root) - .await? - .clone_value(); - let client_relative_root = this.pages_project.project().client_relative_path(); - let client_paths = all_paths_in_root(output_assets, client_relative_root) + let (server_paths, client_paths) = if this + .pages_project + .project() + .next_mode() .await? - .clone_value(); + .is_development() + { + let server_paths = all_server_paths(output_assets, node_root) + .await? + .clone_value(); + + let client_relative_root = this.pages_project.project().client_relative_path(); + let client_paths = async { + anyhow::Ok( + all_paths_in_root(output_assets, client_relative_root) + .await? + .clone_value(), + ) + } + .instrument(tracing::info_span!("client_paths")) + .await?; + (server_paths, client_paths) + } else { + (vec![], vec![]) + }; let node_root = &node_root.await?; let written_endpoint = match *output { diff --git a/crates/next-api/src/paths.rs b/crates/next-api/src/paths.rs index 547df5838055d..694b6c01ef98c 100644 --- a/crates/next-api/src/paths.rs +++ b/crates/next-api/src/paths.rs @@ -1,6 +1,7 @@ use anyhow::Result; use next_core::{all_assets_from_entries, next_manifests::AssetBinding}; use serde::{Deserialize, Serialize}; +use tracing::Instrument; use turbo_rcstr::RcStr; use turbo_tasks::{trace::TraceRawVcs, ResolvedVc, TryFlatJoinIterExt, Vc}; use turbo_tasks_fs::FileSystemPath; @@ -29,30 +30,35 @@ pub async fn all_server_paths( assets: Vc, node_root: Vc, ) -> Result> { - let all_assets = all_assets_from_entries(assets).await?; - let node_root = &node_root.await?; - Ok(Vc::cell( - all_assets - .iter() - .map(|&asset| async move { - Ok( - if let Some(path) = node_root.get_path_to(&*asset.ident().path().await?) { - let content_hash = match *asset.content().await? { - AssetContent::File(file) => *file.hash().await?, - AssetContent::Redirect { .. } => 0, - }; - Some(ServerPath { - path: path.to_string(), - content_hash, - }) - } else { - None - }, - ) - }) - .try_flat_join() - .await?, - )) + let span = tracing::info_span!("all_server_paths"); + async move { + let all_assets = all_assets_from_entries(assets).await?; + let node_root = &node_root.await?; + Ok(Vc::cell( + all_assets + .iter() + .map(|&asset| async move { + Ok( + if let Some(path) = node_root.get_path_to(&*asset.ident().path().await?) { + let content_hash = match *asset.content().await? { + AssetContent::File(file) => *file.hash().await?, + AssetContent::Redirect { .. } => 0, + }; + Some(ServerPath { + path: path.to_string(), + content_hash, + }) + } else { + None + }, + ) + }) + .try_flat_join() + .await?, + )) + } + .instrument(span) + .await } /// Return a list of relative paths to `root` for all output assets references diff --git a/crates/next-api/src/server_actions.rs b/crates/next-api/src/server_actions.rs index ddec197beee2c..a7bbfa6e64bbb 100644 --- a/crates/next-api/src/server_actions.rs +++ b/crates/next-api/src/server_actions.rs @@ -1,8 +1,15 @@ -use std::{collections::BTreeMap, io::Write, iter::once}; +use std::{ + collections::{BTreeMap, HashSet}, + future::Future, + io::Write, +}; use anyhow::{bail, Context, Result}; use indexmap::map::Entry; use next_core::{ + next_client_reference::{ + find_server_entries, ClientReferenceGraphResult, ClientReferenceType, ServerEntries, + }, next_manifests::{ ActionLayer, ActionManifestModuleId, ActionManifestWorkerEntry, ServerReferenceManifest, }, @@ -16,11 +23,11 @@ use swc_core::{ utils::find_pat_ids, }, }; -use tracing::Instrument; +use tracing::{Instrument, Level}; use turbo_rcstr::RcStr; use turbo_tasks::{ - graph::{GraphTraversal, NonDeterministic}, - FxIndexMap, ResolvedVc, TryFlatJoinIterExt, Value, ValueToString, Vc, + graph::{GraphTraversal, NonDeterministic, VisitControlFlow, VisitedNodes}, + FxIndexMap, ReadRef, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Value, ValueToString, Vc, }; use turbo_tasks_fs::{self, rope::RopeBuilder, File, FileSystemPath}; use turbopack_core::{ @@ -28,7 +35,7 @@ use turbopack_core::{ chunk::{ChunkItem, ChunkItemExt, ChunkableModule, ChunkingContext, EvaluatableAsset}, context::AssetContext, file_source::FileSource, - module::{Module, Modules}, + module::Module, output::OutputAsset, reference::primary_referenced_modules, reference_type::{EcmaScriptModulesReferenceSubType, ReferenceType}, @@ -57,7 +64,7 @@ pub(crate) struct ServerActionsManifest { #[turbo_tasks::function] pub(crate) async fn create_server_actions_manifest( rsc_entry: Vc>, - server_reference_modules: Vc, + client_references: Vc, project_path: Vc, node_root: Vc, page_name: RcStr, @@ -65,7 +72,80 @@ pub(crate) async fn create_server_actions_manifest( asset_context: Vc>, chunking_context: Vc>, ) -> Result> { - let actions = get_actions(rsc_entry, server_reference_modules, asset_context); + let client_references_by_server_component = &client_references + .await? + .client_references_by_server_component; + + let actions = { + let ServerEntries { + server_component_entries, + server_utils, + } = &*find_server_entries(rsc_entry).await?; + + let mut actions = find_actions( + server_utils.clone(), + client_references_by_server_component + .get(&None) + .map_or(&[] as &[_], |vec| vec.as_slice()) + .iter() + .map(|r| async move { Ok(Vc::upcast(*r.await?.ssr_module)) }) + .try_join() + .await?, + asset_context, + VisitedFindActionsNodes::empty(), + ) + .await? + .clone_value(); + + for module in server_component_entries { + let refs = client_references_by_server_component + .get(&Some(*module)) + .map_or(&[] as &[_], |vec| vec.as_slice()) + .iter() + .map(|r| async move { Ok(Vc::upcast(*r.await?.client_module)) }) + .try_join() + .await?; + let current_actions = find_actions( + vec![Vc::upcast(*module)], + refs, + asset_context, + actions.visited_nodes, + ) + .await?; + + actions.extend(¤t_actions); + } + + actions.extend( + &*find_actions( + vec![rsc_entry], + // API routes don't have any server_component_entries, handle that here + client_references + .types() + .await? + .iter() + .map(|client_reference_ty| async move { + Ok(match client_reference_ty { + ClientReferenceType::EcmascriptClientReference { + module: ecmascript_client_reference, + .. + } => Some(*ResolvedVc::upcast( + ecmascript_client_reference.await?.client_module, + )), + _ => None, + }) + }) + .try_flat_join() + .await?, + asset_context, + actions.visited_nodes, + ) + .await?, + ); + + Vc::cell(actions.actions) + }; + let loader = build_server_actions_loader(project_path, page_name.clone(), actions, asset_context); let evaluable = Vc::try_resolve_sidecast::>(loader) @@ -181,30 +261,81 @@ async fn build_manifest( )) } +type FindActionsNode = (ActionLayer, ResolvedVc>, ReadRef); + +#[turbo_tasks::value(shared)] +pub struct VisitedFindActionsNodes(HashSet); + +#[turbo_tasks::value_impl] +impl VisitedFindActionsNodes { + #[turbo_tasks::function] + pub fn empty() -> Vc { + VisitedFindActionsNodes(Default::default()).cell() + } +} + +#[turbo_tasks::value(shared)] +#[derive(Clone)] +struct FindActionsResult { + pub actions: HashToLayerNameModule, + pub visited_nodes: Vc, +} + +impl FindActionsResult { + /// Merges multiple return values of client_reference_graph together. + pub fn extend(&mut self, other: &Self) { + self.actions + .extend(other.actions.iter().map(|(k, v)| (k.clone(), v.clone()))); + self.visited_nodes = other.visited_nodes; + } +} + /// 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 get_actions( - rsc_entry: ResolvedVc>, - server_reference_modules: Vc, +async fn find_actions( + entries: Vec>>, + server_reference_modules: Vec>>, asset_context: Vc>, -) -> Result> { + visited_nodes: Vc, +) -> Result> { + let entries_names = entries + .iter() + .map(|m| m.ident().to_string()) + .try_join() + .await?; + let server_reference_modules_names = server_reference_modules + .iter() + .map(|m| m.ident().to_string()) + .try_join() + .await?; async move { - let actions = NonDeterministic::new() - .skip_duplicates() + let (actions, visited_nodes) = NonDeterministic::new() + .skip_duplicates_with_visited_nodes(VisitedNodes(visited_nodes.await?.0.clone())) .visit( - once((ActionLayer::Rsc, rsc_entry)).chain( - server_reference_modules - .await? - .iter() - .map(|m| (ActionLayer::ActionBrowser, *m)), - ), - get_referenced_modules, + entries + .into_iter() + .map(|m| async move { Ok((ActionLayer::Rsc, m, m.ident().to_string().await?)) }) + .try_join() + .await? + .into_iter() + .chain( + server_reference_modules + .iter() + .map(|m| async move { + Ok((ActionLayer::ActionBrowser, *m, m.ident().to_string().await?)) + }) + .try_join() + .await?, + ), + FindActionsVisit {}, ) .await .completed()? - .into_inner() + .into_inner_with_visited(); + + let actions = actions .into_iter() .map(parse_actions_filter_map) .try_flat_join() @@ -214,7 +345,7 @@ async fn get_actions( // 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() { + for ((layer, module, _), actions_map) in actions.iter() { let module = if *layer == ActionLayer::Rsc { *module } else { @@ -236,12 +367,59 @@ async fn get_actions( } all_actions.sort_keys(); - Ok(Vc::cell(all_actions)) + Ok(FindActionsResult { + actions: all_actions, + visited_nodes: VisitedFindActionsNodes(visited_nodes.0).cell(), + } + .cell()) } - .instrument(tracing::info_span!("find server actions")) + .instrument(tracing::info_span!( + "find server actions", + entries = debug(entries_names), + references = debug(server_reference_modules_names) + )) .await } +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> { + let modules = primary_referenced_modules(*module).await?; + + Ok(modules + .into_iter() + .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( @@ -262,16 +440,6 @@ async fn to_rsc_context( Ok(module) } -/// Our graph traversal visitor, which finds the primary modules directly -/// referenced by parent. -async fn get_referenced_modules( - (layer, module): (ActionLayer, ResolvedVc>), -) -> Result>)> + Send> { - primary_referenced_modules(*module) - .await - .map(|modules| modules.into_iter().map(move |&m| (layer, m))) -} - /// Parses the Server Actions comment for all exported action function names. /// /// Action names are stored in a leading BlockComment prefixed by @@ -417,12 +585,12 @@ fn all_export_names(program: &Program) -> Vec { /// 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): (ActionLayer, ResolvedVc>), -) -> Result>), Vc)>> { + (layer, module, name): FindActionsNode, +) -> Result)>> { parse_actions(*module).await.map(|option_action_map| { option_action_map .clone_value() - .map(|action_map| ((layer, module), action_map)) + .map(|action_map| ((layer, module, name), action_map)) }) } diff --git a/crates/next-core/src/next_app/app_client_references_chunks.rs b/crates/next-core/src/next_app/app_client_references_chunks.rs index 489231ac754b5..c8484a2a5a6d7 100644 --- a/crates/next-core/src/next_app/app_client_references_chunks.rs +++ b/crates/next-core/src/next_app/app_client_references_chunks.rs @@ -6,7 +6,7 @@ use turbo_tasks::{ }; use turbopack_core::{ chunk::{availability_info::AvailabilityInfo, ChunkingContext, ChunkingContextExt}, - module::{Module, Modules}, + module::Module, output::OutputAssets, }; @@ -14,7 +14,6 @@ use super::include_modules_module::IncludeModulesModule; use crate::{ next_client_reference::{ visit_client_reference::ClientReferenceGraphResult, ClientReferenceType, - ClientReferenceTypes, }, next_server_component::server_component_module::NextServerComponentModule, }; @@ -316,35 +315,3 @@ pub async fn get_app_client_references_chunks( .instrument(tracing::info_span!("process client references")) .await } - -/// 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, -) -> Result> { - Ok(Vc::cell( - app_client_reference_types - .await? - .iter() - .map(|client_reference_ty| async move { - Ok(match client_reference_ty { - ClientReferenceType::EcmascriptClientReference { - module: ecmascript_client_reference, - .. - } => { - let ecmascript_client_reference_ref = ecmascript_client_reference.await?; - Some(ResolvedVc::upcast( - ecmascript_client_reference_ref - .client_module - .to_resolved() - .await?, - )) - } - _ => None, - }) - }) - .try_flat_join() - .await?, - )) -} diff --git a/crates/next-core/src/next_client_reference/visit_client_reference.rs b/crates/next-core/src/next_client_reference/visit_client_reference.rs index 3626cf364d020..cd87d05774d45 100644 --- a/crates/next-core/src/next_client_reference/visit_client_reference.rs +++ b/crates/next-core/src/next_client_reference/visit_client_reference.rs @@ -53,10 +53,8 @@ pub enum ClientReferenceType { #[derive(Clone, Debug)] pub struct ClientReferenceGraphResult { pub client_references: Vec, - /// Only the [`ClientReferenceType::EcmascriptClientReference`]s are listed in this map. - #[allow(clippy::type_complexity)] pub client_references_by_server_component: - FxIndexMap>, Vec>>>, + FxIndexMap>, Vec>>, pub server_component_entries: Vec>, pub server_utils: Vec>>, pub visited_nodes: Vc, @@ -187,9 +185,7 @@ pub async fn client_reference_graph( client_references_by_server_component .entry(client_reference.server_component) .or_insert_with(Vec::new) - .push(*ResolvedVc::upcast::>( - entry.await?.ssr_module, - )); + .push(entry); } } VisitClientReferenceNodeType::ServerUtilEntry(server_util, _) => {