use std::borrow::Cow; use anyhow::{Ok, Result}; use either::Either; use futures::join; use next_core::{ next_client_reference::{ ClientReference, ClientReferenceGraphResult, ClientReferenceType, ServerEntries, find_server_entries, }, next_dynamic::NextDynamicEntryModule, next_manifests::ActionLayer, next_server_utility::server_utility_module::NextServerUtilityModule, }; use rustc_hash::FxHashMap; use tracing::Instrument; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ CollectiblesSource, FxIndexMap, FxIndexSet, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, Vc, }; use turbo_tasks_fs::FileSystemPath; use turbopack_core::{ context::AssetContext, issue::{Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString}, module::Module, module_graph::{GraphTraversalAction, ModuleGraph, SingleModuleGraphWithBindingUsage}, }; use turbopack_css::{CssModuleAsset, ModuleCssAsset}; use crate::{ client_references::{ClientManifestEntryType, ClientReferenceData, map_client_references}, dynamic_imports::{DynamicImportEntries, DynamicImportEntriesMapType, map_next_dynamic}, server_actions::{AllActions, AllModuleActions, map_server_actions, to_rsc_context}, }; #[turbo_tasks::value] pub struct NextDynamicGraph { graph: SingleModuleGraphWithBindingUsage, is_single_page: bool, /// list of NextDynamicEntryModules data: ResolvedVc, } #[turbo_tasks::value] pub struct NextDynamicGraphs(Vec>); #[turbo_tasks::value_impl] impl NextDynamicGraphs { #[turbo_tasks::function(operation)] async fn new_operation( graphs: ResolvedVc, is_single_page: bool, ) -> Result> { let graphs_ref = &graphs.await?; let next_dynamic = async { graphs_ref .iter_graphs() .map(|graph| { NextDynamicGraph::new_with_entries(graph, is_single_page).to_resolved() }) .try_join() .await } .instrument(tracing::info_span!("generating next/dynamic graphs")) .await?; Ok(Self(next_dynamic).cell()) } #[turbo_tasks::function] pub async fn new(graphs: ResolvedVc, is_single_page: bool) -> Result> { // TODO get rid of this function once everything inside of // `get_global_information_for_endpoint_inner` calls `take_collectibles()` when needed let result_op = Self::new_operation(graphs, is_single_page); let result_vc = if !is_single_page { let result_vc = result_op.resolve_strongly_consistent().await?; result_op.drop_collectibles::>(); *result_vc } else { result_op.connect() }; Ok(result_vc) } /// Returns the next/dynamic-ally imported (client) modules (from RSC and SSR modules) for the /// given endpoint. #[turbo_tasks::function] pub async fn get_next_dynamic_imports_for_endpoint( &self, entry: Vc>, ) -> Result> { let span = tracing::info_span!("collect all next/dynamic imports for endpoint"); async move { if let [graph] = &self.0[..] { // Just a single graph, no need to merge results Ok(graph.get_next_dynamic_imports_for_endpoint(entry)) } else { let result = self .0 .iter() .map(|graph| async move { Ok(graph .get_next_dynamic_imports_for_endpoint(entry) .await? .into_iter() .map(|(k, v)| (*k, *v)) // TODO remove this collect and return an iterator instead .collect::>()) }) .try_flat_join() .await?; Ok(Vc::cell(result.into_iter().collect())) } } .instrument(span) .await } } #[turbo_tasks::value(transparent)] pub struct DynamicImportEntriesWithImporter( pub Vec<( ResolvedVc, Option, )>, ); #[turbo_tasks::value_impl] impl NextDynamicGraph { #[turbo_tasks::function] pub async fn new_with_entries( graph: SingleModuleGraphWithBindingUsage, is_single_page: bool, ) -> Result> { let mapped = map_next_dynamic(*graph.graph); Ok(NextDynamicGraph { is_single_page, graph, data: mapped.to_resolved().await?, } .cell()) } #[turbo_tasks::function] pub async fn get_next_dynamic_imports_for_endpoint( &self, entry: ResolvedVc>, ) -> Result> { let span = tracing::info_span!("collect next/dynamic imports for endpoint"); async move { let data = &*self.data.await?; let graph = self.graph.read().await?; #[derive(Clone, PartialEq, Eq)] enum VisitState { Entry, InClientReference(ClientReferenceType), } let entries = if !self.is_single_page { if !graph.graphs.first().unwrap().has_entry_module(entry) { // the graph doesn't contain the entry, e.g. for the additional module graph return Ok(Vc::cell(vec![])); } Either::Left(std::iter::once(entry)) } else { Either::Right(graph.graphs.first().unwrap().entry_modules()) }; let mut result = vec![]; // module -> the client reference entry (if any) let mut state_map = FxHashMap::default(); graph.traverse_edges_dfs( entries, &mut (), |parent_info, node, _| { let module = node; let Some((parent_node, _)) = parent_info else { state_map.insert(module, VisitState::Entry); return Ok(GraphTraversalAction::Continue); }; let parent_module = parent_node; let module_type = data.get(&module); let parent_state = state_map.get(&parent_module).unwrap().clone(); let parent_client_reference = if let Some(DynamicImportEntriesMapType::ClientReference(module)) = module_type { Some(ClientReferenceType::EcmascriptClientReference(*module)) } else if let VisitState::InClientReference(ty) = parent_state { Some(ty) } else { None }; Ok(match module_type { Some(DynamicImportEntriesMapType::DynamicEntry(dynamic_entry)) => { result.push((*dynamic_entry, parent_client_reference)); state_map.insert(module, parent_state); GraphTraversalAction::Skip } Some(DynamicImportEntriesMapType::ClientReference(client_reference)) => { state_map.insert( module, VisitState::InClientReference( ClientReferenceType::EcmascriptClientReference( *client_reference, ), ), ); GraphTraversalAction::Continue } None => { state_map.insert(module, parent_state); GraphTraversalAction::Continue } }) }, |_, _, _| Ok(()), )?; Ok(Vc::cell(result)) } .instrument(span) .await } } #[turbo_tasks::value] pub struct ServerActionsGraph { graph: SingleModuleGraphWithBindingUsage, is_single_page: bool, /// (Layer, RSC or Browser module) -> list of actions data: ResolvedVc, } #[turbo_tasks::value] pub struct ServerActionsGraphs(Vec>); #[turbo_tasks::value_impl] impl ServerActionsGraphs { #[turbo_tasks::function(operation)] async fn new_operation( graphs: ResolvedVc, is_single_page: bool, ) -> Result> { let graphs_ref = &graphs.await?; let server_actions = async { graphs_ref .iter_graphs() .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(Self(server_actions).cell()) } #[turbo_tasks::function] pub async fn new(graphs: ResolvedVc, is_single_page: bool) -> Result> { // TODO get rid of this function once everything inside of // `get_global_information_for_endpoint_inner` calls `take_collectibles()` when needed let result_op = Self::new_operation(graphs, is_single_page); let result_vc = if !is_single_page { let result_vc = result_op.resolve_strongly_consistent().await?; result_op.drop_collectibles::>(); *result_vc } else { result_op.connect() }; Ok(result_vc) } /// 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.0[..] { // Just a single graph, no need to merge results Ok(graph.get_server_actions_for_endpoint(entry, rsc_asset_context)) } else { let result = self .0 .iter() .map(|graph| async move { graph .get_server_actions_for_endpoint(entry, rsc_asset_context) .owned() .await }) .try_flat_join() .await?; Ok(Vc::cell(result)) } } .instrument(span) .await } } #[turbo_tasks::value_impl] impl ServerActionsGraph { #[turbo_tasks::function] pub async fn new_with_entries( graph: SingleModuleGraphWithBindingUsage, is_single_page: bool, ) -> Result> { let mapped = map_server_actions(*graph.graph); 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.read().await?; if !graph.graphs.first().unwrap().has_entry_module(entry) { // the graph doesn't contain the entry, e.g. for the additional module graph return Ok(Vc::cell(Default::default())); } let mut result = FxIndexMap::default(); graph.traverse_nodes_dfs( vec![entry], &mut result, |node, result| { if let Some(node_data) = data.get(&node) { result.insert(node, *node_data); } Ok(GraphTraversalAction::Continue) }, |_, _| Ok(()), )?; Cow::Owned(result) }; let actions = data .iter() .map(|(module, (layer, actions))| async move { let actions = actions.await?; actions .actions .iter() .map(async |(hash, name)| { Ok(( hash.to_string(), ( *layer, name.to_string(), if *layer == ActionLayer::Rsc { *module } else { to_rsc_context( **module, &actions.entry_path, &actions.entry_query, rsc_asset_context, ) .await? }, ), )) }) .try_join() .await }) .try_flat_join() .await?; Ok(Vc::cell(actions)) } .instrument(span) .await } } #[turbo_tasks::value] pub struct ClientReferencesGraph { is_single_page: bool, graph: SingleModuleGraphWithBindingUsage, /// List of client references (modules that entries into the client graph) data: ResolvedVc, } #[turbo_tasks::value] pub struct ClientReferencesGraphs(Vec>); #[turbo_tasks::value_impl] impl ClientReferencesGraphs { #[turbo_tasks::function(operation)] async fn new_operation( graphs: ResolvedVc, is_single_page: bool, ) -> Result> { let graphs_ref = &graphs.await?; let client_references = async { graphs_ref .iter_graphs() .map(|graph| { ClientReferencesGraph::new_with_entries(graph, is_single_page).to_resolved() }) .try_join() .await } .instrument(tracing::info_span!("generating client references graphs")) .await?; Ok(Self(client_references).cell()) } #[turbo_tasks::function] pub async fn new(graphs: ResolvedVc, is_single_page: bool) -> Result> { // TODO get rid of this function once everything inside of // `get_global_information_for_endpoint_inner` calls `take_collectibles()` when needed let result_op = Self::new_operation(graphs, is_single_page); let result_vc = if !is_single_page { let result_vc = result_op.resolve_strongly_consistent().await?; result_op.drop_collectibles::>(); *result_vc } else { result_op.connect() }; Ok(result_vc) } /// Returns the client references for the given page. #[turbo_tasks::function] pub async fn get_client_references_for_endpoint( &self, entry: Vc>, has_layout_segments: bool, include_traced: bool, include_binding_usage: bool, ) -> Result> { let span = tracing::info_span!("collect all client references for endpoint"); async move { let result = if let [graph] = &self.0[..] { // Just a single graph, no need to merge results This also naturally aggregates // server components and server utilities in the correct order graph.get_client_references_for_endpoint(entry) } else { let results = self .0 .iter() .map(|graph| graph.get_client_references_for_endpoint(entry)) .try_join(); // Do this separately for now, because the aggregation of multiple graph traversals // messes up the order of the server_component_entries. let server_entries = async { if has_layout_segments { let server_entries = find_server_entries(entry, include_traced, include_binding_usage) .await?; Ok(Some(server_entries)) } else { Ok(None) } }; // Wait for both in parallel since `find_server_entries` tends to be slower than the // graph traversals let (results, server_entries) = join!(results, server_entries); let mut result = ClientReferenceGraphResult { client_references: results? .iter() .flat_map(|r| r.client_references.iter().copied()) .collect(), ..Default::default() }; if let Some(ServerEntries { server_utils, server_component_entries, }) = server_entries?.as_deref() { result.server_utils = server_utils.clone(); result.server_component_entries = server_component_entries.clone(); } result.cell() }; Ok(result) } .instrument(span) .await } } #[turbo_tasks::value_impl] impl ClientReferencesGraph { #[turbo_tasks::function] pub async fn new_with_entries( graph: SingleModuleGraphWithBindingUsage, is_single_page: bool, ) -> Result> { let mapped = map_client_references(*graph.graph); Ok(Self { is_single_page, graph, data: mapped.to_resolved().await?, } .cell()) } #[turbo_tasks::function] async fn get_client_references_for_endpoint( &self, entry: ResolvedVc>, ) -> Result> { let span = tracing::info_span!("collect client references for endpoint"); async move { let data = &*self.data.await?; let graph = self.graph.read().await?; let entries = if !self.is_single_page { if !graph.graphs.first().unwrap().has_entry_module(entry) { // the graph doesn't contain the entry, e.g. for the additional module graph return Ok(ClientReferenceGraphResult::default().cell()); } Either::Left(std::iter::once(entry)) } else { Either::Right(graph.graphs.first().unwrap().entry_modules()) }; // Because we care about 'evaluation order' we need to collect client references in the // post_order callbacks which is the same as evaluation order let mut client_references = Vec::new(); let mut server_utils = FxIndexSet::default(); let mut server_components = FxIndexSet::default(); // Perform a DFS traversal to find all server components included by this page. graph.traverse_nodes_dfs( entries, &mut (), |node, _| { let module_type = data.get(&node); Ok(match module_type { Some( ClientManifestEntryType::EcmascriptClientReference { .. } | ClientManifestEntryType::CssClientReference { .. } | ClientManifestEntryType::ServerComponent { .. }, ) => GraphTraversalAction::Skip, None => GraphTraversalAction::Continue, }) }, |node, _| { if let Some(server_util_module) = ResolvedVc::try_downcast_type::(node) { // Server utility used by the template, not a server component server_utils.insert(server_util_module); return Ok(()); } let module_type = data.get(&node); let ty = match module_type { Some(ClientManifestEntryType::EcmascriptClientReference { module, ssr_module: _, }) => ClientReferenceType::EcmascriptClientReference(*module), Some(ClientManifestEntryType::CssClientReference(module)) => { ClientReferenceType::CssClientReference(*module) } Some(ClientManifestEntryType::ServerComponent(sc)) => { server_components.insert(*sc); return Ok(()); } None => { return Ok(()); } }; // Client reference used by the template, not a server component client_references.push(ClientReference { server_component: None, ty, }); Ok(()) }, )?; // Traverse each server component separately. Because not all server components are // necessarily rendered at the same time (not-found, or parallel routes), we need to // determine the order of client references individually for each server component. for sc in server_components.iter().copied() { graph.traverse_nodes_dfs( std::iter::once(ResolvedVc::upcast(sc)), &mut (), |node, _| { let module = node; let module_type = data.get(&module); Ok(match module_type { Some( ClientManifestEntryType::EcmascriptClientReference { .. } | ClientManifestEntryType::CssClientReference { .. }, ) => GraphTraversalAction::Skip, _ => GraphTraversalAction::Continue, }) }, |node, _| { let module = node; if let Some(server_util_module) = ResolvedVc::try_downcast_type::(module) { server_utils.insert(server_util_module); } let Some(module_type) = data.get(&module) else { return Ok(()); }; let ty = match module_type { ClientManifestEntryType::EcmascriptClientReference { module, ssr_module: _, } => ClientReferenceType::EcmascriptClientReference(*module), ClientManifestEntryType::CssClientReference(module) => { ClientReferenceType::CssClientReference(*module) } ClientManifestEntryType::ServerComponent(_) => { return Ok(()); } }; client_references.push(ClientReference { server_component: Some(sc), ty, }); Ok(()) }, )?; } Ok(ClientReferenceGraphResult { client_references: client_references.into_iter().collect(), // The order of server_utils does not matter server_utils: server_utils.into_iter().collect(), server_component_entries: server_components.into_iter().collect(), } .cell()) } .instrument(span) .await } } #[turbo_tasks::value(shared)] struct CssGlobalImportIssue { pub parent_module: ResolvedVc>, pub module: ResolvedVc>, } impl CssGlobalImportIssue { fn new( parent_module: ResolvedVc>, module: ResolvedVc>, ) -> Self { Self { parent_module, module, } } } #[turbo_tasks::value_impl] impl Issue for CssGlobalImportIssue { #[turbo_tasks::function] async fn title(&self) -> Vc { StyledString::Stack(vec![ StyledString::Text(rcstr!("Failed to compile")), StyledString::Text(rcstr!( "Global CSS cannot be imported from files other than your Custom . Due to \ the Global nature of stylesheets, and to avoid conflicts, Please move all \ first-party global CSS imports to pages/_app.js. Or convert the import to \ Component-Level CSS (CSS Modules)." )), StyledString::Text(rcstr!( "Read more: https://nextjs.org/docs/messages/css-global" )), ]) .cell() } #[turbo_tasks::function] async fn description(&self) -> Result> { let parent_path = self.parent_module.ident().path().owned().await?; let module_path = self.module.ident().path().owned().await?; let relative_import_location = parent_path.parent(); let import_path = match relative_import_location.get_relative_path_to(&module_path) { Some(path) => path, None => module_path.path.clone(), }; let cleaned_import_path = if import_path.ends_with(".scss.css") || import_path.ends_with(".sass.css") { RcStr::from(import_path.trim_end_matches(".css")) } else { import_path }; Ok(Vc::cell(Some( StyledString::Stack(vec![ StyledString::Text(format!("Location: {}", parent_path.path).into()), StyledString::Text(format!("Import path: {cleaned_import_path}",).into()), ]) .resolved_cell(), ))) } fn severity(&self) -> IssueSeverity { IssueSeverity::Error } #[turbo_tasks::function] fn file_path(&self) -> Vc { self.parent_module.ident().path() } #[turbo_tasks::function] fn stage(&self) -> Vc { IssueStage::ProcessModule.cell() } // TODO(PACK-4879): compute the source information by following the module references } type FxModuleNameMap = FxIndexMap>, RcStr>; #[turbo_tasks::value(transparent)] struct ModuleNameMap(#[bincode(with = "turbo_bincode::indexmap")] pub FxModuleNameMap); #[tracing::instrument(level = "info", name = "validate pages css imports", skip_all)] #[turbo_tasks::function] async fn validate_pages_css_imports_individual( graph: SingleModuleGraphWithBindingUsage, is_single_page: bool, entry: Vc>, app_module: ResolvedVc>, ) -> Result<()> { let graph = graph.read().await?; let entry = entry.to_resolved().await?; let entries = if !is_single_page { if !graph.graphs.first().unwrap().has_entry_module(entry) { // the graph doesn't contain the entry, e.g. for the additional module graph return Ok(()); } Either::Left(std::iter::once(entry)) } else { Either::Right(graph.graphs.first().unwrap().entry_modules()) }; let mut candidates = vec![]; graph.traverse_edges_dfs( entries, &mut (), |parent_info, node, _| { let module = node; // If we're at a root node, there is nothing importing this module and we can skip // any further validations. let Some((parent_node, _)) = parent_info else { return Ok(GraphTraversalAction::Continue); }; let parent_module = parent_node; // Importing CSS from _app.js is always allowed. if parent_module == app_module { return Ok(GraphTraversalAction::Continue); } // If the module being imported isn't a global css module, there is nothing to // validate. let module_is_global_css = ResolvedVc::try_downcast_type::(module).is_some(); if !module_is_global_css { return Ok(GraphTraversalAction::Continue); } let parent_is_css_module = ResolvedVc::try_downcast_type::(parent_module).is_some() || ResolvedVc::try_downcast_type::(parent_module).is_some(); // We also always allow .module css/scss/sass files to import global css files as // well. if parent_is_css_module { return Ok(GraphTraversalAction::Continue); } // If all of the above invariants have been checked, we look to see if the parent // module is the same as the app module. If it isn't we know it // isn't a valid place to import global css. if parent_module != app_module { candidates.push(CssGlobalImportIssue::new(parent_module, module)) } Ok(GraphTraversalAction::Continue) }, |_, _, _| Ok(()), )?; candidates .into_iter() .map(async |issue| { // We allow imports of global CSS files which are inside of `node_modules`. Ok( if !issue.module.ident().path().await?.is_in_node_modules() { Some(issue) } else { None }, ) }) .try_flat_join() .await? .into_iter() .for_each(|issue| { issue.resolved_cell().emit(); }); Ok(()) } /// Validates that the global CSS/SCSS/SASS imports are only valid imports with the following /// rules: /// * The import is made from a `node_modules` package /// * The import is made from a `.module.css` file /// * The import is made from the `pages/_app.js`, or equivalent file. #[turbo_tasks::function] pub async fn validate_pages_css_imports( graph: Vc, is_single_page: bool, entry: Vc>, app_module: Vc>, ) -> Result<()> { let graphs = &graph.await?; graphs .iter_graphs() .map(|graph| { validate_pages_css_imports_individual(graph, is_single_page, entry, app_module) .as_side_effect() }) .try_join() .await?; Ok(()) }