diff --git a/crates/napi/src/next_api/endpoint.rs b/crates/napi/src/next_api/endpoint.rs index 01f7cba26e57f..97d02507fb92e 100644 --- a/crates/napi/src/next_api/endpoint.rs +++ b/crates/napi/src/next_api/endpoint.rs @@ -4,21 +4,25 @@ use anyhow::Result; use futures_util::TryFutureExt; use napi::{JsFunction, bindgen_prelude::External}; use next_api::{ + module_graph_snapshot::{ModuleGraphSnapshot, get_module_graph_snapshot}, operation::OptionEndpoint, paths::ServerPath, route::{ - EndpointOutputPaths, endpoint_client_changed_operation, endpoint_server_changed_operation, - endpoint_write_to_disk_operation, + Endpoint, EndpointOutputPaths, endpoint_client_changed_operation, + endpoint_server_changed_operation, endpoint_write_to_disk_operation, }, }; use tracing::Instrument; -use turbo_tasks::{Completion, Effects, OperationVc, ReadRef, Vc}; -use turbopack_core::{diagnostics::PlainDiagnostic, issue::PlainIssue}; +use turbo_tasks::{ + Completion, Effects, OperationVc, ReadRef, TryFlatJoinIterExt, TryJoinIterExt, Vc, +}; +use turbopack_core::{diagnostics::PlainDiagnostic, error::PrettyPrintError, issue::PlainIssue}; use super::utils::{ DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult, strongly_consistent_catch_collectables, subscribe, }; +use crate::next_api::module_graph::NapiModuleGraphSnapshot; #[napi(object)] #[derive(Default)] @@ -81,6 +85,11 @@ impl From> for NapiWrittenEndpoint { } } +#[napi(object)] +pub struct NapiModuleGraphSnapshots { + pub module_graphs: Vec, +} + // NOTE(alexkirsz) We go through an extra layer of indirection here because of // two factors: // 1. rustc currently has a bug where using a dyn trait as a type argument to @@ -155,6 +164,105 @@ pub async fn endpoint_write_to_disk( }) } +#[turbo_tasks::value(serialization = "none")] +struct ModuleGraphsWithIssues { + module_graphs: Option>, + issues: Arc>>, + diagnostics: Arc>>, + effects: Arc, +} + +#[turbo_tasks::function(operation)] +async fn get_module_graphs_with_issues_operation( + endpoint_op: OperationVc, +) -> Result> { + let module_graphs_op = get_module_graphs_operation(endpoint_op); + let (module_graphs, issues, diagnostics, effects) = + strongly_consistent_catch_collectables(module_graphs_op).await?; + Ok(ModuleGraphsWithIssues { + module_graphs, + issues, + diagnostics, + effects, + } + .cell()) +} + +#[turbo_tasks::value(transparent)] +struct ModuleGraphSnapshots(Vec>); + +#[turbo_tasks::function(operation)] +async fn get_module_graphs_operation( + endpoint_op: OperationVc, +) -> Result> { + let Some(endpoint) = *endpoint_op.connect().await? else { + return Ok(Vc::cell(vec![])); + }; + let graphs = endpoint.module_graphs().await?; + let entries = endpoint.entries().await?; + let entry_modules = entries.iter().flat_map(|e| e.entries()).collect::>(); + let snapshots = graphs + .iter() + .map(async |&graph| { + let module_graph = graph.await?; + let entry_modules = entry_modules + .iter() + .map(async |&m| Ok(module_graph.has_entry(m).await?.then_some(m))) + .try_flat_join() + .await?; + Ok((*graph, entry_modules)) + }) + .try_join() + .await? + .into_iter() + .map(|(graph, entry_modules)| (graph, Vc::cell(entry_modules))) + .collect::>() + .into_iter() + .map(async |(graph, entry_modules)| { + get_module_graph_snapshot(graph, Some(entry_modules)).await + }) + .try_join() + .await?; + Ok(Vc::cell(snapshots)) +} + +#[napi] +pub async fn endpoint_module_graphs( + #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External, +) -> napi::Result> { + let endpoint_op: OperationVc = ***endpoint; + let (module_graphs, issues, diagnostics) = endpoint + .turbopack_ctx() + .turbo_tasks() + .run_once(async move { + let module_graphs_op = get_module_graphs_with_issues_operation(endpoint_op); + let ModuleGraphsWithIssues { + module_graphs, + issues, + diagnostics, + effects: _, + } = &*module_graphs_op.connect().await?; + Ok((module_graphs.clone(), issues.clone(), diagnostics.clone())) + }) + .await + .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; + + Ok(TurbopackResult { + result: NapiModuleGraphSnapshots { + module_graphs: module_graphs + .into_iter() + .flat_map(|m| m.into_iter()) + .map(|m| NapiModuleGraphSnapshot::from(&**m)) + .collect(), + }, + issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(), + diagnostics: diagnostics + .iter() + .map(|d| NapiDiagnostic::from(d)) + .collect(), + }) +} + #[napi(ts_return_type = "{ __napiType: \"RootTask\" }")] pub fn endpoint_server_changed_subscribe( #[napi(ts_arg_type = "{ __napiType: \"Endpoint\" }")] endpoint: External, diff --git a/crates/napi/src/next_api/mod.rs b/crates/napi/src/next_api/mod.rs index 4897b2d3bf9ba..d8a1b0ab8cd26 100644 --- a/crates/napi/src/next_api/mod.rs +++ b/crates/napi/src/next_api/mod.rs @@ -1,4 +1,5 @@ pub mod endpoint; +pub mod module_graph; pub mod project; pub mod turbopack_ctx; pub mod utils; diff --git a/crates/napi/src/next_api/module_graph.rs b/crates/napi/src/next_api/module_graph.rs new file mode 100644 index 0000000000000..606aa8186a10d --- /dev/null +++ b/crates/napi/src/next_api/module_graph.rs @@ -0,0 +1,98 @@ +use next_api::module_graph_snapshot::{ModuleGraphSnapshot, ModuleInfo, ModuleReference}; +use turbo_rcstr::RcStr; +use turbopack_core::chunk::ChunkingType; + +#[napi(object)] +pub struct NapiModuleReference { + /// The index of the referenced/referencing module in the modules list. + pub index: u32, + /// The export used in the module reference. + pub export: String, + /// The type of chunking for the module reference. + pub chunking_type: String, +} + +impl From<&ModuleReference> for NapiModuleReference { + fn from(reference: &ModuleReference) -> Self { + Self { + index: reference.index as u32, + export: reference.export.to_string(), + chunking_type: match &reference.chunking_type { + ChunkingType::Parallel { hoisted: true, .. } => "hoisted".to_string(), + ChunkingType::Parallel { hoisted: false, .. } => "sync".to_string(), + ChunkingType::Async => "async".to_string(), + ChunkingType::Isolated { + merge_tag: None, .. + } => "isolated".to_string(), + ChunkingType::Isolated { + merge_tag: Some(name), + .. + } => format!("isolated {name}"), + ChunkingType::Shared { + merge_tag: None, .. + } => "shared".to_string(), + ChunkingType::Shared { + merge_tag: Some(name), + .. + } => format!("shared {name}"), + ChunkingType::Traced => "traced".to_string(), + }, + } + } +} + +#[napi(object)] +pub struct NapiModuleInfo { + pub ident: RcStr, + pub path: RcStr, + pub depth: u32, + pub size: u32, + pub retained_size: u32, + pub references: Vec, + pub incoming_references: Vec, +} + +impl From<&ModuleInfo> for NapiModuleInfo { + fn from(info: &ModuleInfo) -> Self { + Self { + ident: info.ident.clone(), + path: info.path.clone(), + depth: info.depth, + size: info.size, + retained_size: info.retained_size, + references: info + .references + .iter() + .map(NapiModuleReference::from) + .collect(), + incoming_references: info + .incoming_references + .iter() + .map(NapiModuleReference::from) + .collect(), + } + } +} + +#[napi(object)] +#[derive(Default)] +pub struct NapiModuleGraphSnapshot { + pub modules: Vec, + pub entries: Vec, +} + +impl From<&ModuleGraphSnapshot> for NapiModuleGraphSnapshot { + fn from(snapshot: &ModuleGraphSnapshot) -> Self { + Self { + modules: snapshot.modules.iter().map(NapiModuleInfo::from).collect(), + entries: snapshot + .entries + .iter() + .map(|&i| { + // If you have more that 4294967295 entries, you probably have other problems... + i.try_into().unwrap() + }) + .collect(), + } + } +} diff --git a/crates/napi/src/next_api/project.rs b/crates/napi/src/next_api/project.rs index cd4eecf7a093d..63e4560d60ce0 100644 --- a/crates/napi/src/next_api/project.rs +++ b/crates/napi/src/next_api/project.rs @@ -9,6 +9,7 @@ use napi::{ }; use next_api::{ entrypoints::Entrypoints, + module_graph_snapshot::{ModuleGraphSnapshot, get_module_graph_snapshot}, operation::{ EntrypointsOperation, InstrumentationOperation, MiddlewareOperation, OptionEndpoint, RouteOperation, @@ -63,13 +64,14 @@ use url::Url; use crate::{ next_api::{ endpoint::ExternalEndpoint, + module_graph::NapiModuleGraphSnapshot, turbopack_ctx::{ NapiNextTurbopackCallbacks, NapiNextTurbopackCallbacksJsObject, NextTurboTasks, NextTurbopackContext, create_turbo_tasks, }, utils::{ DetachedVc, NapiDiagnostic, NapiIssue, RootTask, TurbopackResult, get_diagnostics, - get_issues, subscribe, + get_issues, strongly_consistent_catch_collectables, subscribe, }, }, register, @@ -987,6 +989,40 @@ async fn output_assets_operation( Ok(Vc::cell(output_assets.into_iter().collect())) } +#[napi] +pub async fn project_entrypoints( + #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, +) -> napi::Result> { + let container = project.container; + + let (entrypoints, issues, diags) = project + .turbopack_ctx + .turbo_tasks() + .run_once(async move { + let entrypoints_with_issues_op = get_entrypoints_with_issues_operation(container); + + // Read and compile the files + let EntrypointsWithIssues { + entrypoints, + issues, + diagnostics, + effects: _, + } = &*entrypoints_with_issues_op + .read_strongly_consistent() + .await?; + + Ok((entrypoints.clone(), issues.clone(), diagnostics.clone())) + }) + .await + .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; + + Ok(TurbopackResult { + result: NapiEntrypoints::from_entrypoints_op(&entrypoints, &project.turbopack_ctx)?, + issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(), + diagnostics: diags.iter().map(|d| NapiDiagnostic::from(d)).collect(), + }) +} + #[napi(ts_return_type = "{ __napiType: \"RootTask\" }")] pub fn project_entrypoints_subscribe( #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, @@ -1650,3 +1686,70 @@ pub fn project_get_source_map_sync( tokio::runtime::Handle::current().block_on(project_get_source_map(project, file_path)) }) } + +#[napi] +pub async fn project_module_graph( + #[napi(ts_arg_type = "{ __napiType: \"Project\" }")] project: External, +) -> napi::Result> { + let container = project.container; + let (module_graph, issues, diagnostics) = project + .turbopack_ctx + .turbo_tasks() + .run_once(async move { + let module_graph_op = get_module_graph_with_issues_operation(container); + let ModuleGraphWithIssues { + module_graph, + issues, + diagnostics, + effects: _, + } = &*module_graph_op.connect().await?; + Ok((module_graph.clone(), issues.clone(), diagnostics.clone())) + }) + .await + .map_err(|e| napi::Error::from_reason(PrettyPrintError(&e).to_string()))?; + + Ok(TurbopackResult { + result: module_graph.map_or_else(NapiModuleGraphSnapshot::default, |m| { + NapiModuleGraphSnapshot::from(&*m) + }), + issues: issues.iter().map(|i| NapiIssue::from(&**i)).collect(), + diagnostics: diagnostics + .iter() + .map(|d| NapiDiagnostic::from(d)) + .collect(), + }) +} + +#[turbo_tasks::value(serialization = "none")] +struct ModuleGraphWithIssues { + module_graph: Option>, + issues: Arc>>, + diagnostics: Arc>>, + effects: Arc, +} + +#[turbo_tasks::function(operation)] +async fn get_module_graph_with_issues_operation( + project: ResolvedVc, +) -> Result> { + let module_graph_op = get_module_graph_operation(project); + let (module_graph, issues, diagnostics, effects) = + strongly_consistent_catch_collectables(module_graph_op).await?; + Ok(ModuleGraphWithIssues { + module_graph, + issues, + diagnostics, + effects, + } + .cell()) +} + +#[turbo_tasks::function(operation)] +async fn get_module_graph_operation( + project: ResolvedVc, +) -> Result> { + let project = project.project(); + let graph = project.whole_app_module_graphs().await?.full; + let snapshot = get_module_graph_snapshot(*graph, None).resolve().await?; + Ok(snapshot) +} diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index 7f021e40be104..093cb5a83b28e 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -82,8 +82,10 @@ use crate::{ all_paths_in_root, all_server_paths, get_asset_paths_from_root, get_js_paths_from_root, get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, - project::{ModuleGraphs, Project}, - route::{AppPageRoute, Endpoint, EndpointOutput, EndpointOutputPaths, Route, Routes}, + project::{BaseAndFullModuleGraph, Project}, + route::{ + AppPageRoute, Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs, Route, Routes, + }, server_actions::{build_server_actions_loader, create_server_actions_manifest}, webpack_stats::generate_webpack_stats, }; @@ -859,7 +861,7 @@ impl AppProject { rsc_entry: ResolvedVc>, client_shared_entries: Vc, has_layout_segments: bool, - ) -> Result> { + ) -> Result> { if *self.project.per_page_module_graph().await? { let should_trace = self.project.next_mode().await?.is_production(); let client_shared_entries = client_shared_entries @@ -956,7 +958,7 @@ impl AppProject { graphs.push(additional_module_graph); let full = ModuleGraph::from_graphs(graphs); - Ok(ModuleGraphs { + Ok(BaseAndFullModuleGraph { base: base.to_resolved().await?, full: full.to_resolved().await?, } @@ -2071,6 +2073,22 @@ impl Endpoint for AppEndpoint { server_actions_loader, ])])) } + + #[turbo_tasks::function] + async fn module_graphs(self: Vc) -> Result> { + let this = self.await?; + let app_entry = self.app_endpoint_entry().await?; + let module_graphs = this + .app_project + .app_module_graphs( + self, + *app_entry.rsc_entry, + this.app_project.client_runtime_entries(), + matches!(this.ty, AppEndpointType::Page { .. }), + ) + .await?; + Ok(Vc::cell(vec![module_graphs.full])) + } } #[turbo_tasks::value] diff --git a/crates/next-api/src/empty.rs b/crates/next-api/src/empty.rs index 7174aa69a1ca1..cc220d178458e 100644 --- a/crates/next-api/src/empty.rs +++ b/crates/next-api/src/empty.rs @@ -2,7 +2,7 @@ use anyhow::{Result, bail}; use turbo_tasks::{Completion, Vc}; use turbopack_core::module_graph::GraphEntries; -use crate::route::{Endpoint, EndpointOutput}; +use crate::route::{Endpoint, EndpointOutput, ModuleGraphs}; #[turbo_tasks::value] pub struct EmptyEndpoint; @@ -36,4 +36,9 @@ impl Endpoint for EmptyEndpoint { fn entries(self: Vc) -> Vc { GraphEntries::empty() } + + #[turbo_tasks::function] + fn module_graphs(self: Vc) -> Vc { + Vc::cell(vec![]) + } } diff --git a/crates/next-api/src/instrumentation.rs b/crates/next-api/src/instrumentation.rs index e5bcb45112614..106c955a76569 100644 --- a/crates/next-api/src/instrumentation.rs +++ b/crates/next-api/src/instrumentation.rs @@ -33,7 +33,7 @@ use crate::{ all_server_paths, get_js_paths_from_root, get_wasm_paths_from_root, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs}, }; #[turbo_tasks::value] @@ -70,7 +70,7 @@ impl InstrumentationEndpoint { } #[turbo_tasks::function] - async fn core_modules(&self) -> Result> { + async fn entry_module(&self) -> Result>> { let userland_module = self .asset_context .process( @@ -81,6 +81,10 @@ impl InstrumentationEndpoint { .to_resolved() .await?; + if !self.is_edge { + return Ok(Vc::upcast(*userland_module)); + } + let edge_entry_module = wrap_edge_entry( *self.asset_context, self.project.project_path().owned().await?, @@ -90,17 +94,13 @@ impl InstrumentationEndpoint { .to_resolved() .await?; - Ok(InstrumentationCoreModules { - userland_module, - edge_entry_module, - } - .cell()) + Ok(Vc::upcast(*edge_entry_module)) } #[turbo_tasks::function] async fn edge_files(self: Vc) -> Result> { let this = self.await?; - let module = self.core_modules().await?.edge_entry_module; + let module = self.entry_module().to_resolved().await?; let module_graph = this.project.module_graph(*module); @@ -137,7 +137,7 @@ impl InstrumentationEndpoint { let chunking_context = this.project.server_chunking_context(false); - let userland_module = self.core_modules().await?.userland_module; + let userland_module = self.entry_module().to_resolved().await?; let module_graph = this.project.module_graph(*userland_module); let Some(module) = ResolvedVc::try_downcast(userland_module) else { @@ -227,12 +227,6 @@ impl InstrumentationEndpoint { } } -#[turbo_tasks::value] -struct InstrumentationCoreModules { - pub userland_module: ResolvedVc>, - pub edge_entry_module: ResolvedVc>, -} - #[turbo_tasks::value_impl] impl Endpoint for InstrumentationEndpoint { #[turbo_tasks::function] @@ -276,13 +270,15 @@ impl Endpoint for InstrumentationEndpoint { #[turbo_tasks::function] async fn entries(self: Vc) -> Result> { - let core_modules = self.core_modules().await?; - Ok(Vc::cell(vec![ChunkGroupEntry::Entry( - if self.await?.is_edge { - vec![core_modules.edge_entry_module] - } else { - vec![core_modules.userland_module] - }, - )])) + let entry_module = self.entry_module().to_resolved().await?; + Ok(Vc::cell(vec![ChunkGroupEntry::Entry(vec![entry_module])])) + } + + #[turbo_tasks::function] + async fn module_graphs(self: Vc) -> Result> { + let this = self.await?; + let module = self.entry_module(); + let module_graph = this.project.module_graph(module).to_resolved().await?; + Ok(Vc::cell(vec![module_graph])) } } diff --git a/crates/next-api/src/lib.rs b/crates/next-api/src/lib.rs index 17360fdb01d21..4db17a756ba88 100644 --- a/crates/next-api/src/lib.rs +++ b/crates/next-api/src/lib.rs @@ -13,6 +13,7 @@ mod instrumentation; mod loadable_manifest; mod middleware; mod module_graph; +pub mod module_graph_snapshot; mod nft_json; pub mod operation; mod pages; diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index ade51e52f8c17..ab89d6ee047b6 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -38,7 +38,7 @@ use crate::{ get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs}, }; #[turbo_tasks::value] @@ -408,4 +408,15 @@ impl Endpoint for MiddlewareEndpoint { self.entry_module().to_resolved().await?, ])])) } + + #[turbo_tasks::function] + async fn module_graphs(self: Vc) -> Result> { + let this = self.await?; + let module_graph = this + .project + .module_graph(self.entry_module()) + .to_resolved() + .await?; + Ok(Vc::cell(vec![module_graph])) + } } diff --git a/crates/next-api/src/module_graph_snapshot.rs b/crates/next-api/src/module_graph_snapshot.rs new file mode 100644 index 0000000000000..dac1bd29502aa --- /dev/null +++ b/crates/next-api/src/module_graph_snapshot.rs @@ -0,0 +1,193 @@ +use std::{cell::RefCell, cmp::Reverse, collections::hash_map::Entry, mem::take}; + +use anyhow::Result; +use either::Either; +use rustc_hash::{FxHashMap, FxHashSet}; +use serde::{Deserialize, Serialize}; +use turbo_rcstr::RcStr; +use turbo_tasks::{ + NonLocalValue, ResolvedVc, TryJoinIterExt, ValueToString, Vc, trace::TraceRawVcs, +}; +use turbopack_core::{ + asset::Asset, + chunk::ChunkingType, + module::{Module, Modules}, + module_graph::{GraphTraversalAction, ModuleGraph}, + resolve::ExportUsage, +}; + +#[derive(PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, Debug)] +pub struct ModuleReference { + pub index: usize, + pub chunking_type: ChunkingType, + pub export: ExportUsage, +} + +#[derive(PartialEq, Eq, Serialize, Deserialize, TraceRawVcs, NonLocalValue, Debug)] +pub struct ModuleInfo { + pub ident: RcStr, + pub path: RcStr, + pub depth: u32, + pub size: u32, + // TODO this should be per layer + pub retained_size: u32, + pub references: Vec, + pub incoming_references: Vec, +} + +#[turbo_tasks::value] +pub struct ModuleGraphSnapshot { + pub modules: Vec, + pub entries: Vec, +} + +#[turbo_tasks::function] +pub async fn get_module_graph_snapshot( + module_graph: Vc, + entry_modules: Option>, +) -> Result> { + let module_graph = module_graph.await?; + + struct RawModuleInfo { + module: ResolvedVc>, + depth: u32, + retained_modules: RefCell>, + references: Vec, + incoming_references: Vec, + } + + let mut entries = Vec::new(); + let mut modules = Vec::new(); + let mut module_to_index = FxHashMap::default(); + + fn get_or_create_module( + modules: &mut Vec, + module_to_index: &mut FxHashMap>, usize>, + module: ResolvedVc>, + ) -> usize { + match module_to_index.entry(module) { + Entry::Occupied(entry) => *entry.get(), + Entry::Vacant(entry) => { + let index = modules.len(); + modules.push(RawModuleInfo { + module, + depth: u32::MAX, + references: Vec::new(), + incoming_references: Vec::new(), + retained_modules: Default::default(), + }); + entry.insert(index); + index + } + } + } + + let entry_modules = if let Some(entry_modules) = entry_modules { + Either::Left(entry_modules.await?) + } else { + Either::Right(module_graph.entries().await?) + }; + module_graph + .traverse_edges_from_entries_bfs(entry_modules.iter().copied(), |parent_info, node| { + let module = node.module; + let module_index = get_or_create_module(&mut modules, &mut module_to_index, module); + + if let Some((parent_module, ty)) = parent_info { + let parent_index = + get_or_create_module(&mut modules, &mut module_to_index, parent_module.module); + let parent_module = &mut modules[parent_index]; + let parent_depth = parent_module.depth; + debug_assert!(parent_depth < u32::MAX); + parent_module.references.push(ModuleReference { + index: module_index, + chunking_type: ty.chunking_type.clone(), + export: ty.export.clone(), + }); + let module = &mut modules[module_index]; + module.depth = module.depth.min(parent_depth + 1); + module.incoming_references.push(ModuleReference { + index: parent_index, + chunking_type: ty.chunking_type.clone(), + export: ty.export.clone(), + }); + } else { + entries.push(module_index); + let module = &mut modules[module_index]; + module.depth = 0; + } + + Ok(GraphTraversalAction::Continue) + }) + .await?; + + let mut modules_by_depth = FxHashMap::default(); + for (index, info) in modules.iter().enumerate() { + modules_by_depth + .entry(info.depth) + .or_insert_with(Vec::new) + .push(index); + } + let mut modules_by_depth = modules_by_depth.into_iter().collect::>(); + modules_by_depth.sort_by_key(|(depth, _)| Reverse(*depth)); + for (depth, module_indicies) in modules_by_depth { + for module_index in module_indicies { + let module = &modules[module_index]; + for ref_info in &module.incoming_references { + let ref_module = &modules[ref_info.index]; + if ref_module.depth < depth { + let mut retained_modules = ref_module.retained_modules.borrow_mut(); + retained_modules.insert(module_index as u32); + for retained in module.retained_modules.borrow().iter() { + retained_modules.insert(*retained); + } + } + } + } + } + + let mut final_modules = modules + .iter_mut() + .map(async |info| { + Ok(ModuleInfo { + ident: info.module.ident().to_string().owned().await?, + path: info.module.ident().path().to_string().owned().await?, + depth: info.depth, + size: info + .module + .content() + .len() + .owned() + .await + // TODO all modules should report some content and should not crash + .unwrap_or_default() + .unwrap_or_default() + .try_into() + .unwrap_or(u32::MAX), + retained_size: 0, + references: take(&mut info.references), + incoming_references: take(&mut info.incoming_references), + }) + }) + .try_join() + .await?; + + for (index, info) in modules.into_iter().enumerate() { + let retained_size = info + .retained_modules + .into_inner() + .iter() + .map(|&retained_index| { + let retained_info = &final_modules[retained_index as usize]; + retained_info.size + }) + .reduce(|a, b| a.saturating_add(b)) + .unwrap_or_default(); + final_modules[index].retained_size = retained_size + final_modules[index].size; + } + + Ok(ModuleGraphSnapshot { + modules: final_modules, + entries, + } + .cell()) +} diff --git a/crates/next-api/src/pages.rs b/crates/next-api/src/pages.rs index 68d50332f38c7..a74ce698ef70b 100644 --- a/crates/next-api/src/pages.rs +++ b/crates/next-api/src/pages.rs @@ -77,7 +77,7 @@ use crate::{ get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings, }, project::Project, - route::{Endpoint, EndpointOutput, EndpointOutputPaths, Route, Routes}, + route::{Endpoint, EndpointOutput, EndpointOutputPaths, ModuleGraphs, Route, Routes}, webpack_stats::generate_webpack_stats, }; @@ -1727,6 +1727,13 @@ impl Endpoint for PageEndpoint { Ok(Vc::cell(modules)) } + + #[turbo_tasks::function] + async fn module_graphs(self: Vc) -> Result> { + let client_module_graph = self.client_module_graph().to_resolved().await?; + let ssr_module_graph = self.ssr_module_graph().to_resolved().await?; + Ok(Vc::cell(vec![client_module_graph, ssr_module_graph])) + } } #[turbo_tasks::value] diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 7f1f0394ab40c..0b0a8399e4b96 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -973,7 +973,9 @@ impl Project { } #[turbo_tasks::function] - pub async fn whole_app_module_graphs(self: ResolvedVc) -> Result> { + pub async fn whole_app_module_graphs( + self: ResolvedVc, + ) -> Result> { async move { let module_graphs_op = whole_app_module_graph_operation(self); let module_graphs_vc = module_graphs_op.resolve_strongly_consistent().await?; @@ -1813,7 +1815,7 @@ impl Project { #[turbo_tasks::function(operation)] async fn whole_app_module_graph_operation( project: ResolvedVc, -) -> Result> { +) -> Result> { mark_root(); let should_trace = project.next_mode().await?.is_production(); @@ -1831,7 +1833,7 @@ async fn whole_app_module_graph_operation( ); let full = ModuleGraph::from_graphs(vec![base_single_module_graph, additional_module_graph]); - Ok(ModuleGraphs { + Ok(BaseAndFullModuleGraph { base: base.to_resolved().await?, full: full.to_resolved().await?, } @@ -1839,7 +1841,7 @@ async fn whole_app_module_graph_operation( } #[turbo_tasks::value(shared)] -pub struct ModuleGraphs { +pub struct BaseAndFullModuleGraph { pub base: ResolvedVc, pub full: ResolvedVc, } diff --git a/crates/next-api/src/route.rs b/crates/next-api/src/route.rs index f5466441ca679..5fda6da82881d 100644 --- a/crates/next-api/src/route.rs +++ b/crates/next-api/src/route.rs @@ -47,6 +47,9 @@ pub enum Route { Conflict, } +#[turbo_tasks::value(transparent)] +pub struct ModuleGraphs(Vec>); + #[turbo_tasks::value_trait] pub trait Endpoint { #[turbo_tasks::function] @@ -65,6 +68,8 @@ pub trait Endpoint { fn additional_entries(self: Vc, _graph: Vc) -> Vc { GraphEntries::empty() } + #[turbo_tasks::function] + fn module_graphs(self: Vc) -> Vc; } #[turbo_tasks::value(transparent)] diff --git a/packages/next/errors.json b/packages/next/errors.json index 2951e118f075d..e8420b4ca8239 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -752,5 +752,8 @@ "751": "%s cannot not be used outside of a request context.", "752": "Route %s used \"connection\" inside \"use cache\". The \\`connection()\\` function is used to indicate the subsequent code must only run when there is an actual request, but caches must be able to be produced before a request, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache", "753": "Route %s used \"connection\" inside \"use cache: private\". The \\`connection()\\` function is used to indicate the subsequent code must only run when there is an actual navigation request, but caches must be able to be produced before a navigation request, so this function is not allowed in this scope. See more info here: https://nextjs.org/docs/messages/next-request-in-use-cache", - "754": "%s cannot be used outside of a request context." + "754": "%s cannot be used outside of a request context.", + "755": "Route must be a string", + "756": "Route %s not found", + "757": "Unknown styled string type: %s" } diff --git a/packages/next/package.json b/packages/next/package.json index 002bd78debb25..53001a727ed92 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -107,6 +107,7 @@ "styled-jsx": "5.1.6" }, "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.15.1", "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", @@ -121,6 +122,9 @@ "sass": { "optional": true }, + "@modelcontextprotocol/sdk": { + "optional": true + }, "@opentelemetry/api": { "optional": true }, @@ -161,6 +165,7 @@ "@hapi/accept": "5.0.2", "@jest/transform": "29.5.0", "@jest/types": "29.5.0", + "@modelcontextprotocol/sdk": "1.15.1", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", "@next/font": "15.4.2-canary.14", diff --git a/packages/next/src/build/swc/generated-native.d.ts b/packages/next/src/build/swc/generated-native.d.ts index 5785496b60eff..34bf982be9d21 100644 --- a/packages/next/src/build/swc/generated-native.d.ts +++ b/packages/next/src/build/swc/generated-native.d.ts @@ -64,9 +64,15 @@ export interface NapiWrittenEndpoint { serverPaths: Array config: NapiEndpointConfig } +export interface NapiModuleGraphSnapshots { + moduleGraphs: Array +} export declare function endpointWriteToDisk(endpoint: { __napiType: 'Endpoint' }): Promise +export declare function endpointModuleGraphs(endpoint: { + __napiType: 'Endpoint' +}): Promise export declare function endpointServerChangedSubscribe( endpoint: { __napiType: 'Endpoint' }, issues: boolean, @@ -76,6 +82,27 @@ export declare function endpointClientChangedSubscribe( endpoint: { __napiType: 'Endpoint' }, func: (...args: any[]) => any ): { __napiType: 'RootTask' } +export interface NapiModuleReference { + /** The index of the referenced/referencing module in the modules list. */ + index: number + /** The export used in the module reference. */ + export: string + /** The type of chunking for the module reference. */ + chunkingType: string +} +export interface NapiModuleInfo { + ident: RcStr + path: RcStr + depth: number + size: number + retainedSize: number + references: Array + incomingReferences: Array +} +export interface NapiModuleGraphSnapshot { + modules: Array + entries: Array +} export interface NapiEnvVar { name: RcStr value: RcStr @@ -286,6 +313,9 @@ export declare function projectWriteAllEntrypointsToDisk( project: { __napiType: 'Project' }, appDirOnly: boolean ): Promise +export declare function projectEntrypoints(project: { + __napiType: 'Project' +}): Promise export declare function projectEntrypointsSubscribe( project: { __napiType: 'Project' }, func: (...args: any[]) => any @@ -362,6 +392,9 @@ export declare function projectGetSourceMapSync( project: { __napiType: 'Project' }, filePath: RcStr ): string | null +export declare function projectModuleGraph(project: { + __napiType: 'Project' +}): Promise /** * A version of [`NapiNextTurbopackCallbacks`] that can accepted as an argument to a napi function. * diff --git a/packages/next/src/build/swc/index.ts b/packages/next/src/build/swc/index.ts index 0299b04ad697a..e3b4251e1e29a 100644 --- a/packages/next/src/build/swc/index.ts +++ b/packages/next/src/build/swc/index.ts @@ -19,6 +19,8 @@ import { isDeepStrictEqual } from 'util' import { type DefineEnvOptions, getDefineEnv } from '../define-env' import { getReactCompilerLoader } from '../get-babel-loader-config' import type { + NapiModuleGraphSnapshot, + NapiModuleGraphSnapshots, NapiPartialProjectOptions, NapiProjectOptions, NapiSourceDiagnostic, @@ -669,6 +671,14 @@ function bindingToApi( return napiEntrypointsToRawEntrypoints(napiEndpoints) } + async getEntrypoints() { + const napiEndpoints = (await binding.projectEntrypoints( + this._nativeProject + )) as TurbopackResult + + return napiEntrypointsToRawEntrypoints(napiEndpoints) + } + entrypointsSubscribe() { const subscription = subscribe>( false, @@ -742,6 +752,12 @@ function bindingToApi( ) } + moduleGraph(): Promise> { + return binding.projectModuleGraph(this._nativeProject) as Promise< + TurbopackResult + > + } + invalidatePersistentCache(): Promise { return binding.projectInvalidatePersistentCache(this._nativeProject) } @@ -793,6 +809,12 @@ function bindingToApi( await serverSubscription.next() return serverSubscription } + + async moduleGraphs(): Promise> { + return binding.endpointModuleGraphs(this._nativeEndpoint) as Promise< + TurbopackResult + > + } } /** diff --git a/packages/next/src/build/swc/types.ts b/packages/next/src/build/swc/types.ts index 6af182f331a39..8af31514e8471 100644 --- a/packages/next/src/build/swc/types.ts +++ b/packages/next/src/build/swc/types.ts @@ -5,6 +5,8 @@ import type { RefCell, NapiTurboEngineOptions, NapiSourceDiagnostic, + NapiModuleGraphSnapshots, + NapiModuleGraphSnapshot, } from './generated-native' export type { NapiTurboEngineOptions as TurboEngineOptions } @@ -216,6 +218,7 @@ export interface Project { appDirOnly: boolean ): Promise> + getEntrypoints(): Promise> entrypointsSubscribe(): AsyncIterableIterator> hmrEvents(identifier: string): AsyncIterableIterator> @@ -244,6 +247,8 @@ export interface Project { invalidatePersistentCache(): Promise + moduleGraph(): Promise> + shutdown(): Promise onExit(): Promise @@ -295,6 +300,11 @@ export interface Endpoint { serverChanged( includeIssues: boolean ): Promise> + + /** + * Gets a snapshot of the module graphs for the endpoint. + */ + moduleGraphs(): Promise> } interface EndpointConfig { diff --git a/packages/next/src/server/lib/router-utils/mcp.ts b/packages/next/src/server/lib/router-utils/mcp.ts new file mode 100644 index 0000000000000..a79d44dbc26e2 --- /dev/null +++ b/packages/next/src/server/lib/router-utils/mcp.ts @@ -0,0 +1,584 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { z } from 'next/dist/compiled/zod' +import type { NextJsHotReloaderInterface } from '../../dev/hot-reloader-types' +import type { + Endpoint, + Issue, + Route, + StyledString, +} from '../../../build/swc/types' +import type { CallToolResult } from '@modelcontextprotocol/sdk/types' +import type { + NapiModuleGraphSnapshot, + NapiModuleInfo, + NapiModuleReference, +} from '../../../build/swc/generated-native' +import { runInNewContext } from 'node:vm' +import * as Log from '../../../build/output/log' +import { formatImportTraces } from '../../../shared/lib/turbopack/utils' +import { inspect } from 'node:util' + +export { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' + +const QUERY_DESCRIPTION = `A piece of JavaScript code that will be executed. +It can access the module graph and extract information it finds useful. +The \`console.log\` function can be used to log messages, which will also be returned in the response. +No Node.js or browser APIs are available, but JavaScript language features are available. +When the user is interested in used exports of modules, you can use the \`export\` property of ModuleReferences (\`incomingReferences\`). +When the user is interested in the import path of a module, follow one of the \`incomingReferences\` with a smaller \`depth\` value than the current module until you hit the root. +Do not try to make any assumptions or estimations. See the typings to see which data you have available. If you don't have the data, tell the user that this data is not available and list alternatives that are available. +See the following TypeScript typings for reference: + +\`\`\` typescript +interface Module { + /// The identifier of the module, which is a unique string. + /// Example: "[project]/packages/next-app/src/app/folder/page.tsx [app-rsc] (ecmascript, Next.js Server Component)" + /// These layers exist in App Router: + /// * Server Components: [app-rsc], [app-edge-rsc] + /// * API routes: [app-route], [app-edge-route] + /// * Client Components: [app-client] + /// * Server Side Rendering of Client Components: [app-ssr], [app-edge-ssr] + /// These layers exist in Pages Router: + /// * Client-side rendering: [client] + /// * Server-side rendering: [ssr], [edge-ssr] + /// * API routes: [api], [edge-api] + /// And these layers also exist: + /// * Middleware: [middleware], [middleware-edge] + /// * Instrumentation: [instrumentation], [instrumentation-edge] + ident: string, + /// The path of the module. It's not unique as multiple modules can have the same path. + /// Separate between application code and node_modules (npm packages, vendor code). + /// Example: "[project]/pages/folder/index.js", + /// Example: "[project]/node_modules/.pnpm/next@file+..+next.js+packages+next_@babel+core@7.27.4_@opentelemetry+api@1.7.0_@playwright+te_5kenhtwdm6lrgjpao5hc34lkgy/node_modules/next/dist/compiled/fresh/index.js", + /// Example: "[project]/apps/site/node_modules/@opentelemtry/api/build/src/trace/instrumentation.js", + path: string, + /// The distance to the entries of the module graph. Use this to traverse the graph in the right direction. + /// This is useful when trying to find the path from a module to the root of the module graph. + /// Example: 0 for the entrypoint, 1 for the first layer of modules, etc. + depth: number, + /// The size of the source code of the module in bytes. + /// Note that it's not the final size of the generated code, but can be a good indicator of that. + /// It's only the size of this single module, not the size of the whole subgraph behind it (see retainedSize instead). + size: number, + /// The size of the whole subgraph behind this module in bytes. + /// Use this value if the user is interested in sizes of modules (except when they are interested in the size of the module itself). + /// Never try to compute the retained size yourself, but use this value instead. + retainedSize: number, + /// The modules that are referenced by this module. + /// Modules could be referenced by \`import\`, \`require\`, \`new URL\`, etc. + /// Beware cycles in the module graph. You can avoid that by only walking edges with a bigger \`depth\` value than the current module. + references: ModuleReference[], + /// The modules that reference this module. + /// Beware cycles in the module graph. + /// You can use this to walk up the graph up to the root. When doing this only walk edges with a smaller \`depth\` value than the current module. + incomingReferences: ModuleReference[], +} + +interface ModuleReference { + /// The referenced/referencing module. + module: Module + /// The thing that is used from the module. + /// export {name}: The named export that is used. + /// evaluation: Imported for side effects only. + /// all: All exports and the side effects. + export: "evaluation" | "all" | \`export \${string}\` + /// How this reference affects chunking of the module. + /// hoisted | sync: The module is placed in the same chunk group as the referencing module. + /// hoisted: The module is loaded before all "sync" modules. + /// async: The module forms a separate chunk group which is loaded asynchronously. + /// isolated: The module forms a separate chunk group which is loaded as separate entry. When it has a name, all modules imported with this name are placed in the same chunk group. + /// shared: The module forms a separate chunk group which is loaded before the current chunk group. When it has a name, all modules imported with this name are placed in the same chunk group. + /// traced: The module is not bundled, but the graph is still traced and all modules are included unbundled. + chunkingType: "hoisted" | "sync" | "async" | "isolated" | \`isolated \${string}\` | "shared" | \`shared \${string}\` | "traced" +} + +// The following global variables are available in the query: + +/// The entries of the module graph. +/// Note that this only includes the entrypoints of the module graph and not all modules. +/// You need to traverse it recursively to find not only children, but also grandchildren (resp, grandparents). +/// Prefer to use \`modules\` over \`entries\` as it contains all modules, not only the entrypoints. +const entries: Module[] + +/// All modules in the module graph. +/// Note that this array already contains all the modules as flat list. +/// Make sure to iterate over this array and not only consider the first one. +/// Prefer to use \`modules\` over \`entries\` as it contains all modules, not only the entrypoints. +const modules: Module[] + +const console: { + /// Logs a message to the console. + /// The message will be returned in the response. + /// The message can be a string or any other value that can be inspected. + log: (...data: any[]) => void +} +\`\`\` +` + +async function measureAndHandleErrors( + name: string, + fn: () => Promise +): Promise { + const start = performance.now() + let content: CallToolResult['content'] = [] + try { + content = await fn() + } catch (error) { + content.push({ + type: 'text', + text: `Error: ${error instanceof Error ? error.stack : String(error)}`, + }) + content.push({ + type: 'text', + text: 'Fix the error and try again.', + }) + } + const duration = performance.now() - start + const formatDurationText = + duration > 2000 + ? `${Math.round(duration / 100) / 10}s` + : `${Math.round(duration)}ms` + Log.event(`MCP ${name} in ${formatDurationText}`) + return { + content, + } +} + +function invariant(value: never, errorMessage: (value: any) => string): never { + throw new Error(errorMessage(value)) +} + +function styledStringToMarkdown( + styledString: StyledString | undefined +): string { + if (!styledString) { + return '' + } + switch (styledString.type) { + case 'text': + return styledString.value + case 'strong': + return `*${styledString.value}*` + case 'code': + return `\`${styledString.value}\`` + case 'line': + return styledString.value.map(styledStringToMarkdown).join('') + case 'stack': + return styledString.value.map(styledStringToMarkdown).join('\n\n') + default: + invariant(styledString, (s) => `Unknown styled string type: ${s.type}`) + } +} + +function indent(str: string, spaces: number = 2): string { + const indentStr = ' '.repeat(spaces) + return `${indentStr}${str.replace(/\n/g, `\n${indentStr}`)}` +} + +function issueToString(issue: Issue & { route: string }): string { + return [ + `${issue.severity} in ${issue.stage} on ${issue.route}`, + `File Path: ${issue.filePath}`, + issue.source && + `Source: + ${issue.source.source.ident} + ${issue.source.range ? `Range: ${issue.source.range?.start.line}:${issue.source.range?.start.column} - ${issue.source.range?.end.line}:${issue.source.range?.end.column}` : 'Unknown range'} +`, + `Title: ${styledStringToMarkdown(issue.title)}`, + issue.description && + `Description: +${indent(styledStringToMarkdown(issue.description))}`, + issue.detail && + `Details: +${indent(styledStringToMarkdown(issue.detail))}`, + issue.documentationLink && `Documentation: ${issue.documentationLink}`, + issue.importTraces && + issue.importTraces.length > 0 && + formatImportTraces(issue.importTraces), + ] + .filter(Boolean) + .join('\n') +} + +function issuesReference(issues: Issue[]): { type: 'text'; text: string } { + if (issues.length === 0) { + return { + type: 'text', + text: 'Note: There are no issues.', + } + } + + const countBySeverity = new Map() + + for (const issue of issues) { + const count = countBySeverity.get(issue.severity) || 0 + countBySeverity.set(issue.severity, count + 1) + } + + const text = [ + `Note: There are ${issues.length} issues in total, with the following severities: ${Array.from( + countBySeverity.entries() + ) + .map(([severity, count]) => `${count} x ${severity}`) + .join(', ')}.`, + ] + + return { + type: 'text', + text: text.join('\n'), + } +} + +function routeToTitle(route: Route): string { + switch (route.type) { + case 'page': + return 'A page using Pages Router.' + case 'app-page': + return `A page using App Router. Original names: ${route.pages.map((page) => page.originalName).join(', ')}.` + case 'page-api': + return 'An API route using Pages Router.' + case 'app-route': + return `A route using App Router. Original name: ${route.originalName}.` + case 'conflict': + return 'Multiple routes conflict on this path. This is an error in the folder structure.' + default: + invariant(route, (r) => `Unknown route type: ${r.type}`) + } +} + +function routeToEndpoints(route: Route): Endpoint[] { + switch (route.type) { + case 'page': + return [route.htmlEndpoint] + case 'app-page': + return route.pages.map((p) => p.htmlEndpoint) + case 'page-api': + return [route.endpoint] + case 'app-route': + return [route.endpoint] + case 'conflict': + return [] + default: + invariant(route, (r) => `Unknown route type: ${r.type}`) + } +} + +function arrayOrSingle(value: T | T[]): T[] { + return Array.isArray(value) ? value : [value] +} + +interface ModuleReference { + module: Module + export: string + chunkingType: string +} +interface Module { + /// The identifier of the module, which is a unique string. + ident: string + /// The path of the module. It's not unique as multiple modules can have the same path. + path: string + /// The distance to the entries of the module graph. Use this to traverse the graph in the right direction. + depth: number + /// The size of the source code of the module in bytes. + size: number + /// The size of the whole subgraph behind this module in bytes. + retainedSize: number + /// The modules that are referenced by this module. + references: ModuleReference[] + /// The modules that reference this module. + incomingReferences: ModuleReference[] +} + +function createModuleObject(rawModule: NapiModuleInfo): Module { + return { + ident: rawModule.ident, + path: rawModule.path, + depth: rawModule.depth, + size: rawModule.size, + retainedSize: rawModule.retainedSize, + references: [], + incomingReferences: [], + } +} + +function processModuleGraphSnapshot( + moduleGraph: NapiModuleGraphSnapshot, + modules: Module[], + entries: Module[] +) { + const queryModules = moduleGraph.modules.map(createModuleObject) + for (let i = 0; i < queryModules.length; i++) { + const rawModule = moduleGraph.modules[i] + const queryModule = queryModules[i] + + queryModule.references = rawModule.references.map( + (ref: NapiModuleReference) => ({ + module: queryModules[ref.index], + export: ref.export, + chunkingType: ref.chunkingType, + }) + ) + queryModule.incomingReferences = rawModule.incomingReferences.map( + (ref: NapiModuleReference) => ({ + module: queryModules[ref.index], + export: ref.export, + chunkingType: ref.chunkingType, + }) + ) + modules.push(queryModule) + } + for (const entry of moduleGraph.entries) { + const queryModule = queryModules[entry] + entries.push(queryModule) + } +} + +function runQuery( + query: string, + modules: Module[], + entries: Module[] +): CallToolResult['content'] { + const response: CallToolResult['content'] = [] + const proto = { + modules, + entries, + console: { + log: (...data: any[]) => { + response.push({ + type: 'text', + text: data + .map((item) => + typeof item === 'string' ? item : inspect(item, false, 2, false) + ) + .join(' '), + }) + }, + }, + } + const contextObject = Object.create(proto) + contextObject.global = contextObject + contextObject.self = contextObject + contextObject.globalThis = contextObject + runInNewContext(query, contextObject, { + displayErrors: true, + filename: 'query.js', + timeout: 20000, + contextName: 'Query Context', + }) + for (const [key, value] of Object.entries(contextObject)) { + if (typeof value === 'function') continue + if (key === 'global' || key === 'self' || key === 'globalThis') continue + response.push({ + type: 'text', + text: `Global variable \`${key}\` = ${inspect(value, false, 2, false)}`, + }) + } + return response +} + +const ROUTES_DESCRIPTION = + 'The routes from which to query the module graph. Can be a single string or an array of strings.' +export function createMcpServer( + hotReloader: NextJsHotReloaderInterface +): McpServer | undefined { + const turbopack = hotReloader.turbopackProject + if (!turbopack) return undefined + const server = new McpServer({ + name: 'next.js', + version: '1.0.0', + instructions: `This is a running next.js dev server with Turbopack. +You can use the Model Context Protocol to query information about pages and modules and their relations.`, + }) + + server.registerTool( + 'entrypoints', + { + title: 'Entrypoints', + description: + 'Get all entrypoints of a Turbopack project, which are all pages, routes and the middleware.', + }, + async () => + measureAndHandleErrors('entrypoints', async () => { + let entrypoints = await turbopack.getEntrypoints() + + const list = [] + + for (const [key, route] of entrypoints.routes.entries()) { + list.push(`\`${key}\` (${routeToTitle(route)})`) + } + + if (entrypoints.middleware) { + list.push('Middleware') + } + + if (entrypoints.instrumentation) { + list.push('Instrumentation') + } + + const content: CallToolResult['content'] = [ + issuesReference(entrypoints.issues), + { + type: 'text', + text: `These are the routes of the application: + +${list.map((e) => `- ${e}`).join('\n')}`, + }, + ] + return content + }) + ) + + server.registerTool( + 'query-routes-module-graph', + { + title: 'Query module graph of routes', + description: 'Query details about the module graph of routes.', + inputSchema: { + routes: z + .union([z.string(), z.array(z.string())]) + .describe(ROUTES_DESCRIPTION), + query: z.string().describe(QUERY_DESCRIPTION), + }, + }, + async ({ routes, query }) => + measureAndHandleErrors( + `module graph query on ${arrayOrSingle(routes).join(', ')}`, + async () => { + const entrypoints = await turbopack.getEntrypoints() + const endpoints = [] + for (const route of arrayOrSingle(routes)) { + const routeInfo = entrypoints.routes.get(route) + if (!routeInfo) { + throw new Error(`Route ${route} not found`) + } + endpoints.push(...routeToEndpoints(routeInfo)) + } + const issues = [] + const modules: Module[] = [] + const entries: Module[] = [] + + for (const endpoint of endpoints) { + const result = await endpoint.moduleGraphs() + issues.push(...result.issues) + const moduleGraphs = result.moduleGraphs + for (const moduleGraph of moduleGraphs) { + processModuleGraphSnapshot(moduleGraph, modules, entries) + } + } + const content: CallToolResult['content'] = [] + content.push(issuesReference(issues)) + const response = runQuery(query, modules, entries) + content.push(...response) + return content + } + ) + ) + + server.registerTool( + 'query-module-graph', + { + title: 'Query whole app module graph', + description: + 'Query details about the module graph the whole application. This is a expensive operation and should only be used when module graph of the whole application is needed.', + inputSchema: { + query: z.string().describe(QUERY_DESCRIPTION), + }, + }, + async ({ query }) => + measureAndHandleErrors(`whole app module graph query`, async () => { + const moduleGraph = await turbopack.moduleGraph() + const issues = moduleGraph.issues + const modules: Module[] = [] + const entries: Module[] = [] + processModuleGraphSnapshot(moduleGraph, modules, entries) + const content: CallToolResult['content'] = [] + content.push(issuesReference(issues)) + const response = runQuery(query, modules, entries) + content.push(...response) + return content + }) + ) + + server.registerTool( + 'query-issues', + { + title: 'Query issues of routes', + description: + 'Query issues (errors, warnings, lints, etc.) that are reported on routes.', + inputSchema: { + routes: z + .union([z.string(), z.array(z.string())]) + .describe(ROUTES_DESCRIPTION), + page: z + .optional(z.number()) + .describe( + 'Issues are paginated when there are more than 50 issues. The first page is number 0.' + ), + }, + }, + async ({ routes, page }) => + measureAndHandleErrors( + `issues on ${arrayOrSingle(routes).join(', ')}`, + async () => { + const entrypoints = await turbopack.getEntrypoints() + const issues = [] + for (const route of arrayOrSingle(routes)) { + const routeInfo = entrypoints.routes.get(route) + if (!routeInfo) { + throw new Error(`Route ${route} not found`) + } + for (const endpoint of routeToEndpoints(routeInfo)) { + const result = await endpoint.moduleGraphs() + for (const issue of result.issues) { + const issuesWithRoute = issue as Issue & { route: string } + issuesWithRoute.route = route + issues.push(issuesWithRoute) + } + } + } + const severitiesArray = [ + 'bug', + 'fatal', + 'error', + 'warning', + 'hint', + 'note', + 'suggestion', + 'info', + ] + const severities = new Map( + severitiesArray.map((severity, index) => [severity, index]) + ) + issues.sort((a, b) => { + const severityA = severities.get(a.severity) + const severityB = severities.get(b.severity) + if (severityA !== undefined && severityB !== undefined) { + return severityA - severityB + } + return 0 + }) + + const content: CallToolResult['content'] = [] + content.push(issuesReference(issues)) + page = page ?? 0 + const currentPage = issues.slice(page * 50, (page + 1) * 50) + for (const issue of currentPage) { + content.push({ + type: 'text', + text: issueToString(issue), + }) + } + if (issues.length >= (page + 1) * 50) { + content.push({ + type: 'text', + text: `Note: There are more issues available. Use the \`page\` parameter to query the next page.`, + }) + } + + return content + } + ) + ) + + return server +} diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index fb2a0c0301603..33d7d3b2722e1 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -85,6 +85,8 @@ import { getDefineEnv } from '../../../build/define-env' import { TurbopackInternalError } from '../../../shared/lib/turbopack/internal-error' import { normalizePath } from '../../../lib/normalize-path' import { JSON_CONTENT_TYPE_HEADER } from '../../../lib/constants' +import { parseBody } from '../../api-utils/node/parse-body' +import { timingSafeEqual } from 'crypto' export type SetupOpts = { renderServer: LazyRenderServerInstance @@ -969,9 +971,114 @@ async function startWatcher( const devTurbopackMiddlewareManifestPath = `/_next/${CLIENT_STATIC_FILES_PATH}/development/${TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST}` opts.fsChecker.devVirtualFsItems.add(devTurbopackMiddlewareManifestPath) + const mcpPath = `/_next/mcp` + opts.fsChecker.devVirtualFsItems.add(mcpPath) + + let mcpSecret = process.env.NEXT_EXPERIMENTAL_MCP_SECRET + ? Buffer.from(process.env.NEXT_EXPERIMENTAL_MCP_SECRET) + : undefined + + if (mcpSecret) { + try { + // eslint-disable-next-line import/no-extraneous-dependencies + require('@modelcontextprotocol/sdk/package.json') + } catch (error) { + Log.error( + 'To use the MCP server, please install the `@modelcontextprotocol/sdk` package.' + ) + mcpSecret = undefined + } + } + let createMcpServer: typeof import('./mcp').createMcpServer | undefined + let StreamableHTTPServerTransport: + | typeof import('./mcp').StreamableHTTPServerTransport + | undefined + if (mcpSecret) { + ;({ createMcpServer, StreamableHTTPServerTransport } = + require('./mcp') as typeof import('./mcp')) + Log.info( + `Experimental MCP server is available at: /_next/mcp?${mcpSecret.toString()}` + ) + } + async function requestHandler(req: IncomingMessage, res: ServerResponse) { const parsedUrl = url.parse(req.url || '/') + if (parsedUrl.pathname?.includes(mcpPath)) { + function sendMcpInternalError(message: string) { + res.statusCode = 500 + res.setHeader('Content-Type', 'application/json; charset=utf-8') + res.end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + // "Internal error" see https://www.jsonrpc.org/specification + code: -32603, + message, + }, + id: null, + }) + ) + return { finished: true } + } + if (!mcpSecret) { + Log.error('Next.js MCP server is not enabled') + Log.info( + 'To enable it, set the NEXT_EXPERIMENTAL_MCP_SECRET environment variable to a secret value. This will make the MCP server available at /_next/mcp?{NEXT_EXPERIMENTAL_MCP_SECRET}' + ) + return sendMcpInternalError( + 'Missing NEXT_EXPERIMENTAL_MCP_SECRET environment variable' + ) + } + if (!createMcpServer || !StreamableHTTPServerTransport) { + return sendMcpInternalError( + 'Model Context Protocol (MCP) server is not available' + ) + } + if (!parsedUrl.query) { + Log.error('No MCP secret provided in request query') + Log.info( + `Experimental MCP server is available at: /_next/mcp?${mcpSecret.toString()}` + ) + return sendMcpInternalError('No MCP secret provided in request query') + } + let mcpSecretQuery = Buffer.from(parsedUrl.query) + if ( + mcpSecretQuery.length !== mcpSecret.length || + !timingSafeEqual(mcpSecretQuery, mcpSecret) + ) { + Log.error('Invalid MCP secret provided in request query') + Log.info( + `Experimental MCP server is available at: /_next/mcp?${mcpSecret.toString()}` + ) + return sendMcpInternalError( + 'Invalid MCP secret provided in request query' + ) + } + + const server = createMcpServer(hotReloader) + if (server) { + try { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }) + res.on('close', () => { + transport.close() + server.close() + }) + await server.connect(transport) + const parsedBody = await parseBody(req, 1024 * 1024 * 1024) + await transport.handleRequest(req, res, parsedBody) + } catch (error) { + Log.error('Error handling MCP request:', error) + if (!res.headersSent) { + return sendMcpInternalError('Internal server error') + } + } + return { finished: true } + } + } + if (parsedUrl.pathname?.includes(clientPagesManifestPath)) { res.statusCode = 200 res.setHeader('Content-Type', JSON_CONTENT_TYPE_HEADER) diff --git a/packages/next/src/shared/lib/turbopack/utils.ts b/packages/next/src/shared/lib/turbopack/utils.ts index f88fc6ef6711f..22ca4707f06cf 100644 --- a/packages/next/src/shared/lib/turbopack/utils.ts +++ b/packages/next/src/shared/lib/turbopack/utils.ts @@ -185,36 +185,7 @@ export function formatIssue(issue: Issue) { if (importTraces?.length) { // This is the same logic as in turbopack/crates/turbopack-cli-utils/src/issue.rs - // We end up with multiple traces when the file with the error is reachable from multiple - // different entry points (e.g. ssr, client) - message += `Import trace${importTraces.length > 1 ? 's' : ''}:\n` - const everyTraceHasADistinctRootLayer = - new Set(importTraces.map(leafLayerName).filter((l) => l != null)).size === - importTraces.length - for (let i = 0; i < importTraces.length; i++) { - const trace = importTraces[i] - const layer = leafLayerName(trace) - let traceIndent = ' ' - // If this is true, layer must be present - if (everyTraceHasADistinctRootLayer) { - message += ` ${layer}:\n` - } else { - if (importTraces.length > 1) { - // Otherwise use simple 1 based indices to disambiguate - message += ` #${i + 1}` - if (layer) { - message += ` [${layer}]` - } - message += ':\n' - } else if (layer) { - message += ` [${layer}]:\n` - } else { - // If there is a single trace and no layer name just don't indent it. - traceIndent = ' ' - } - } - message += formatIssueTrace(trace, traceIndent, !identicalLayers(trace)) - } + message += formatImportTraces(importTraces) } if (documentationLink) { message += documentationLink + '\n\n' @@ -222,6 +193,40 @@ export function formatIssue(issue: Issue) { return message } +export function formatImportTraces(importTraces: PlainTraceItem[][]) { + // We end up with multiple traces when the file with the error is reachable from multiple + // different entry points (e.g. ssr, client) + let message = `Import trace${importTraces.length > 1 ? 's' : ''}:\n` + const everyTraceHasADistinctRootLayer = + new Set(importTraces.map(leafLayerName).filter((l) => l != null)).size === + importTraces.length + for (let i = 0; i < importTraces.length; i++) { + const trace = importTraces[i] + const layer = leafLayerName(trace) + let traceIndent = ' ' + // If this is true, layer must be present + if (everyTraceHasADistinctRootLayer) { + message += ` ${layer}:\n` + } else { + if (importTraces.length > 1) { + // Otherwise use simple 1 based indices to disambiguate + message += ` #${i + 1}` + if (layer) { + message += ` [${layer}]` + } + message += ':\n' + } else if (layer) { + message += ` [${layer}]:\n` + } else { + // If there is a single trace and no layer name just don't indent it. + traceIndent = ' ' + } + } + message += formatIssueTrace(trace, traceIndent, !identicalLayers(trace)) + } + return message +} + /** Returns the first present layer name in the trace */ function leafLayerName(items: PlainTraceItem[]): string | undefined { for (const item of items) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eaf5dd7e35d5..34a740b130a99 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1039,6 +1039,9 @@ importers: '@jest/types': specifier: 29.5.0 version: 29.5.0 + '@modelcontextprotocol/sdk': + specifier: 1.15.1 + version: 1.15.1 '@mswjs/interceptors': specifier: 0.23.0 version: 0.23.0 @@ -4424,6 +4427,10 @@ packages: '@types/react': 19.1.8 react: 19.2.0-canary-7513996f-20250722 + '@modelcontextprotocol/sdk@1.15.1': + resolution: {integrity: sha512-W/XlN9c528yYn+9MQkVjxiTPgPxoxt+oczfjHBDsJx0+59+O7B75Zhsp0B16Xbwbz8ANISDajh6+V7nIcPMc5w==} + engines: {node: '>=18'} + '@module-federation/error-codes@0.15.0': resolution: {integrity: sha512-CFJSF+XKwTcy0PFZ2l/fSUpR4z247+Uwzp1sXVkdIfJ/ATsnqf0Q01f51qqSEA6MYdQi6FKos9FIcu3dCpQNdg==} @@ -6188,6 +6195,10 @@ packages: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} @@ -6791,6 +6802,10 @@ packages: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + bonjour-service@1.3.0: resolution: {integrity: sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==} @@ -6942,6 +6957,10 @@ packages: resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + call-bind@1.0.2: resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} @@ -6949,6 +6968,10 @@ packages: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + caller-callsite@2.0.0: resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} engines: {node: '>=4'} @@ -7446,6 +7469,10 @@ packages: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + content-type@1.0.4: resolution: {integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==} engines: {node: '>= 0.6'} @@ -7498,6 +7525,10 @@ packages: cookie-signature@1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + cookie@0.4.0: resolution: {integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==} engines: {node: '>= 0.6'} @@ -8506,6 +8537,10 @@ packages: downsample-lttb@0.0.1: resolution: {integrity: sha512-Olebo5gyh44OAXTd2BKdcbN5VaZOIKFzoeo9JUFwxDlGt6Sd8fUo6SKaLcafy8aP2UrsKmWDpsscsFEghMjeZA==} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + duplexer3@0.1.4: resolution: {integrity: sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==} @@ -8653,6 +8688,10 @@ packages: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} @@ -8671,6 +8710,10 @@ packages: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + es-set-tostringtag@2.0.3: resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} engines: {node: '>= 0.4'} @@ -9057,6 +9100,14 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.3: + resolution: {integrity: sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA==} + engines: {node: '>=20.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} @@ -9110,6 +9161,12 @@ packages: resolution: {integrity: sha512-WVi2V4iHKw/vHEyye00Q9CSZz7KHDbJkJyteUI8kTih9jiyMl3bIk7wLYFcY9D1Blnadlyb5w5NBuNjQBow99g==} engines: {node: ^16.10.0 || ^18.12.0 || >=20.0.0} + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + express@4.17.0: resolution: {integrity: sha512-1Z7/t3Z5ZnBG252gKUPyItc4xdeaA0X934ca2ewckAsVsw9EG71i++ZHZPYnus8g/s5Bty8IMpSVEuRkmwwPRQ==} engines: {node: '>= 0.10.0'} @@ -9118,6 +9175,10 @@ packages: resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + ext@1.6.0: resolution: {integrity: sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg==} @@ -9253,6 +9314,10 @@ packages: resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + find-cache-dir@2.1.0: resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} engines: {node: '>=6'} @@ -9402,6 +9467,10 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} @@ -9504,6 +9573,10 @@ packages: resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} engines: {node: '>= 0.4'} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} @@ -9523,6 +9596,10 @@ packages: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + get-stdin@4.0.1: resolution: {integrity: sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==} engines: {node: '>=0.10.0'} @@ -9705,6 +9782,10 @@ packages: gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + got@7.1.0: resolution: {integrity: sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==} engines: {node: '>=4'} @@ -9804,6 +9885,10 @@ packages: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + has-to-string-tag-x@1.4.1: resolution: {integrity: sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==} @@ -10564,6 +10649,9 @@ packages: is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -11605,6 +11693,10 @@ packages: resolution: {integrity: sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q==} engines: {node: '>=0.10.0'} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + maximatch@0.1.0: resolution: {integrity: sha512-9ORVtDUFk4u/NFfo0vG/ND/z7UQCVZBL539YW0+U1I7H1BkZwizcPx5foFv7LCPcBnm2U6RjFnQOsIvN4/Vm2A==} engines: {node: '>=0.10.0'} @@ -11710,6 +11802,10 @@ packages: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + memfs@3.5.3: resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} engines: {node: '>= 4.0.0'} @@ -11743,6 +11839,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -11978,6 +12078,10 @@ packages: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.18: resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==} engines: {node: '>= 0.6'} @@ -11986,6 +12090,10 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + mime@1.6.0: resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} engines: {node: '>=4'} @@ -12240,6 +12348,10 @@ packages: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + neo-async@2.6.1: resolution: {integrity: sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==} @@ -12503,6 +12615,10 @@ packages: resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==} engines: {node: '>= 0.4'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + object-is@1.0.2: resolution: {integrity: sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ==} engines: {node: '>= 0.4'} @@ -12941,6 +13057,10 @@ packages: path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@1.1.0: resolution: {integrity: sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==} engines: {node: '>=0.10.0'} @@ -13046,6 +13166,10 @@ packages: resolution: {integrity: sha512-ugJ4Imy92u55zeznaN/5d7iqOBIZjZ7q10/T+dcd0IuFtbLlsGDvAUabFu1cafER+G9f0T1WtTqvzm4KAdcDgQ==} engines: {node: '>=4.0.0', npm: '>=1.2.10'} + pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} + engines: {node: '>=16.20.0'} + pkg-dir@3.0.0: resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} engines: {node: '>=6'} @@ -13993,6 +14117,10 @@ packages: resolution: {integrity: sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==} engines: {node: '>=0.6'} + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + qs@6.5.2: resolution: {integrity: sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==} engines: {node: '>=0.6'} @@ -14067,6 +14195,10 @@ packages: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} + raw-body@3.0.0: + resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} + engines: {node: '>= 0.8'} + rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true @@ -14667,6 +14799,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-applescript@7.0.0: resolution: {integrity: sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==} engines: {node: '>=18'} @@ -14853,6 +14989,10 @@ packages: resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} @@ -14880,6 +15020,10 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} @@ -14946,10 +15090,26 @@ packages: engines: {node: '>=4'} hasBin: true + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + side-channel@1.0.6: resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} engines: {node: '>= 0.4'} + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -15989,6 +16149,10 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + type@1.2.0: resolution: {integrity: sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==} @@ -16976,6 +17140,11 @@ packages: yoga-wasm-web@0.3.3: resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + zod-validation-error@3.4.0: resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==} engines: {node: '>=18.0.0'} @@ -20118,6 +20287,23 @@ snapshots: '@types/react': 19.1.8 react: 19.2.0-canary-7513996f-20250722 + '@modelcontextprotocol/sdk@1.15.1': + dependencies: + ajv: 6.12.6 + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.3 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.0 + raw-body: 3.0.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@module-federation/error-codes@0.15.0': {} '@module-federation/runtime-core@0.15.0': @@ -22359,6 +22545,11 @@ snapshots: mime-types: 2.1.35 negotiator: 0.6.3 + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + acorn-globals@7.0.1: dependencies: acorn: 8.14.0 @@ -23030,6 +23221,20 @@ snapshots: transitivePeerDependencies: - supports-color + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.0 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.0 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + bonjour-service@1.3.0: dependencies: fast-deep-equal: 3.1.3 @@ -23224,6 +23429,11 @@ snapshots: package-hash: 4.0.0 write-file-atomic: 3.0.3 + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + call-bind@1.0.2: dependencies: function-bind: 1.1.2 @@ -23237,6 +23447,11 @@ snapshots: get-intrinsic: 1.2.4 set-function-length: 1.2.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + caller-callsite@2.0.0: dependencies: callsites: 2.0.0 @@ -23781,6 +23996,10 @@ snapshots: dependencies: safe-buffer: 5.2.1 + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + content-type@1.0.4: {} content-type@1.0.5: {} @@ -23857,6 +24076,8 @@ snapshots: cookie-signature@1.0.6: {} + cookie-signature@1.2.2: {} + cookie@0.4.0: {} cookie@0.4.1: {} @@ -24985,6 +25206,12 @@ snapshots: downsample-lttb@0.0.1: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + duplexer3@0.1.4: {} duplexer@0.1.1: {} @@ -25187,6 +25414,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + es-define-property@1.0.1: {} + es-errors@1.3.0: {} es-get-iterator@1.1.3: @@ -25224,6 +25453,10 @@ snapshots: dependencies: es-errors: 1.3.0 + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + es-set-tostringtag@2.0.3: dependencies: get-intrinsic: 1.2.4 @@ -25973,6 +26206,12 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.3: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.3 + evp_bytestokey@1.0.3: dependencies: md5.js: 1.3.5 @@ -26073,6 +26312,10 @@ snapshots: jest-mock: 30.0.0-alpha.6 jest-util: 30.0.0-alpha.6 + express-rate-limit@7.5.1(express@5.1.0): + dependencies: + express: 5.1.0 + express@4.17.0: dependencies: accepts: 1.3.7 @@ -26144,6 +26387,38 @@ snapshots: transitivePeerDependencies: - supports-color + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.2.2 + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.1 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + ext@1.6.0: dependencies: type: 2.5.0 @@ -26311,6 +26586,17 @@ snapshots: transitivePeerDependencies: - supports-color + finalhandler@2.1.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + find-cache-dir@2.1.0: dependencies: commondir: 1.0.1 @@ -26492,6 +26778,8 @@ snapshots: fresh@0.5.2: {} + fresh@2.0.0: {} + from@0.1.7: {} fromentries@1.3.2: {} @@ -26606,6 +26894,19 @@ snapshots: has-symbols: 1.0.3 hasown: 2.0.2 + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} get-package-type@0.1.0: {} @@ -26622,6 +26923,11 @@ snapshots: get-port@5.1.1: {} + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + get-stdin@4.0.1: {} get-stream@3.0.0: {} @@ -26874,6 +27180,8 @@ snapshots: dependencies: get-intrinsic: 1.2.4 + gopd@1.2.0: {} + got@7.1.0: dependencies: '@types/keyv': 3.1.1 @@ -26978,6 +27286,8 @@ snapshots: has-symbols@1.0.3: {} + has-symbols@1.1.0: {} + has-to-string-tag-x@1.4.1: dependencies: has-symbol-support-x: 1.4.2 @@ -27794,6 +28104,8 @@ snapshots: is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.7 @@ -29202,6 +29514,8 @@ snapshots: markdown-extensions@1.1.1: {} + math-intrinsics@1.1.0: {} + maximatch@0.1.0: dependencies: array-differ: 1.0.0 @@ -29469,6 +29783,8 @@ snapshots: media-typer@0.3.0: {} + media-typer@1.1.0: {} + memfs@3.5.3: dependencies: fs-monkey: 1.0.6 @@ -29531,6 +29847,8 @@ snapshots: merge-descriptors@1.0.3: {} + merge-descriptors@2.0.0: {} + merge-stream@2.0.0: {} merge2@1.4.1: {} @@ -30102,6 +30420,8 @@ snapshots: mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.18: dependencies: mime-db: 1.33.0 @@ -30110,6 +30430,10 @@ snapshots: dependencies: mime-db: 1.52.0 + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + mime@1.6.0: {} mime@2.5.2: {} @@ -30332,6 +30656,8 @@ snapshots: negotiator@0.6.3: {} + negotiator@1.0.0: {} + neo-async@2.6.1: {} neo-async@2.6.2: {} @@ -30662,6 +30988,8 @@ snapshots: object-inspect@1.13.2: {} + object-inspect@1.13.4: {} + object-is@1.0.2: {} object-is@1.1.6: @@ -31174,6 +31502,8 @@ snapshots: path-to-regexp@6.3.0: {} + path-to-regexp@8.2.0: {} + path-type@1.1.0: dependencies: graceful-fs: 4.2.11 @@ -31260,6 +31590,8 @@ snapshots: postcss: 7.0.32 reduce-css-calc: 2.1.7 + pkce-challenge@5.0.0: {} + pkg-dir@3.0.0: dependencies: find-up: 3.0.0 @@ -32256,6 +32588,10 @@ snapshots: dependencies: side-channel: 1.0.6 + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + qs@6.5.2: {} qs@6.7.0: {} @@ -32320,6 +32656,13 @@ snapshots: iconv-lite: 0.4.24 unpipe: 1.0.0 + raw-body@3.0.0: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.6.3 + unpipe: 1.0.0 + rc@1.2.8: dependencies: deep-extend: 0.6.0 @@ -33153,6 +33496,16 @@ snapshots: fsevents: 2.3.3 optional: true + router@2.2.0: + dependencies: + debug: 4.4.0 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.2.0 + transitivePeerDependencies: + - supports-color + run-applescript@7.0.0: {} run-async@2.4.1: {} @@ -33361,6 +33714,22 @@ snapshots: transitivePeerDependencies: - supports-color + send@1.2.0: + dependencies: + debug: 4.4.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + sentence-case@3.0.4: dependencies: no-case: 3.0.4 @@ -33419,6 +33788,15 @@ snapshots: transitivePeerDependencies: - supports-color + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + server-only@0.0.1: {} set-blocking@2.0.0: {} @@ -33508,6 +33886,26 @@ snapshots: interpret: 1.4.0 rechoir: 0.6.2 + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + side-channel@1.0.6: dependencies: call-bind: 1.0.7 @@ -33515,6 +33913,14 @@ snapshots: get-intrinsic: 1.2.4 object-inspect: 1.13.2 + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: optional: true @@ -34638,6 +35044,12 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + type@1.2.0: {} type@2.5.0: {} @@ -35891,6 +36303,10 @@ snapshots: yoga-wasm-web@0.3.3: {} + zod-to-json-schema@3.24.6(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-validation-error@3.4.0(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/turbopack/crates/turbopack-core/src/module_graph/mod.rs b/turbopack/crates/turbopack-core/src/module_graph/mod.rs index 2d5bcc610a991..c844ef54957fa 100644 --- a/turbopack/crates/turbopack-core/src/module_graph/mod.rs +++ b/turbopack/crates/turbopack-core/src/module_graph/mod.rs @@ -1221,6 +1221,23 @@ impl ModuleGraph { Ok(idx) } + pub async fn entries(&self) -> Result>>> { + Ok(self + .get_graphs() + .await? + .iter() + .flat_map(|g| g.entries.iter()) + .flat_map(|e| e.entries()) + .collect()) + } + + pub async fn has_entry(&self, entry: ResolvedVc>) -> Result { + let graphs = self.get_graphs().await?; + Ok(graphs + .iter() + .any(|graph| graph.modules.contains_key(&entry))) + } + /// Traverses all reachable edges exactly once and calls the visitor with the edge source and /// target. ///