diff --git a/.changeset/silent-houses-lay.md b/.changeset/silent-houses-lay.md new file mode 100644 index 00000000000000..41d24de307ca3b --- /dev/null +++ b/.changeset/silent-houses-lay.md @@ -0,0 +1,5 @@ +--- +'@next/swc': patch +--- + +Added an experimental option for using the system CA store for fetching Google Fonts in Turbopack diff --git a/Cargo.lock b/Cargo.lock index 6b4d54706d0696..28d5eaf6bf1a6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2869,6 +2869,7 @@ dependencies = [ "hyper 1.6.0", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -3655,9 +3656,9 @@ dependencies = [ [[package]] name = "libmimalloc-sys" -version = "0.1.38" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7bb23d733dfcc8af652a78b7bf232f0e967710d044732185e561e47c0336b6" +checksum = "bf88cd67e9de251c1781dbe2f641a1a3ad66eaae831b8a2c38fbdc5ddae16d4d" dependencies = [ "cc", "libc", @@ -4034,9 +4035,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.42" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9186d86b79b52f4a77af65604b51225e8db1d6ee7e3f41aec1e40829c71a176" +checksum = "b1791cbe101e95af5764f06f20f6760521f7158f69dbf9d6baf941ee1bf6bc40" dependencies = [ "libmimalloc-sys", ] @@ -5945,9 +5946,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.20" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64 0.22.1", "bytes", @@ -5966,6 +5967,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", + "rustls-native-certs", "rustls-pki-types", "serde", "serde_json", @@ -6194,6 +6196,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "rustls-pki-types" version = "1.10.1" @@ -9313,7 +9337,7 @@ dependencies = [ "anyhow", "mockito", "quick_cache", - "reqwest 0.12.20", + "reqwest 0.12.22", "serde", "tokio", "turbo-rcstr", diff --git a/Cargo.toml b/Cargo.toml index beb1e1c57e32f6..f0ba997758b035 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -400,7 +400,7 @@ rand = "0.9.0" rayon = "1.10.0" regex = "1.10.6" regress = "0.10.3" -reqwest = { version = "0.12.20", default-features = false } +reqwest = { version = "0.12.22", default-features = false } ringmap = "0.1.3" roaring = "0.10.10" rstest = "0.16.0" diff --git a/crates/napi/src/next_api/endpoint.rs b/crates/napi/src/next_api/endpoint.rs index 01f7cba26e57fb..97d02507fb92e6 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 4897b2d3bf9ba2..d8a1b0ab8cd266 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 00000000000000..606aa8186a10d9 --- /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 cd4eecf7a093d7..63e4560d60ce05 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 7f021e40be1044..093cb5a83b28ea 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 7174aa69a1ca19..cc220d178458e8 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 e5bcb45112614c..106c955a76569b 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 17360fdb01d215..4db17a756ba885 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 ade51e52f8c17f..ab89d6ee047b67 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 00000000000000..dac1bd29502aa8 --- /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 68d50332f38c7a..a74ce698ef70b4 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 7f1f0394ab40c7..0b0a8399e4b961 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 f5466441ca6795..5fda6da82881da 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/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index 627443e51c6924..3f8d1f853c7fa8 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -3,12 +3,13 @@ use rustc_hash::FxHashSet; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value as JsonValue; use turbo_esregex::EsRegex; -use turbo_rcstr::RcStr; +use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ FxIndexMap, NonLocalValue, OperationValue, ResolvedVc, TaskInput, Vc, debug::ValueDebugFormat, trace::TraceRawVcs, }; -use turbo_tasks_env::EnvMap; +use turbo_tasks_env::{EnvMap, ProcessEnv}; +use turbo_tasks_fetch::ReqwestClientConfig; use turbo_tasks_fs::FileSystemPath; use turbopack::module_options::{ ConditionItem, ConditionPath, LoaderRuleItem, OptionWebpackRules, @@ -801,6 +802,7 @@ pub struct ExperimentalConfig { turbopack_source_maps: Option, turbopack_tree_shaking: Option, turbopack_scope_hoisting: Option, + turbopack_use_system_tls_certs: Option, // Whether to enable the global-not-found convention global_not_found: Option, /// Defaults to false in development mode, true in production mode. @@ -1721,6 +1723,32 @@ impl NextConfig { pub fn output_file_tracing_excludes(&self) -> Vc { Vc::cell(self.output_file_tracing_excludes.clone()) } + + #[turbo_tasks::function] + pub async fn reqwest_client_config( + &self, + env: Vc>, + ) -> Result> { + // Support both an env var and the experimental flag to provide more flexibility to + // developers on locked down systems, depending on if they want to configure this on a + // per-system or per-project basis. + let use_system_tls_certs = env + .read(rcstr!("NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS")) + .await? + .as_ref() + .and_then(|env_value| { + // treat empty value same as an unset value + (!env_value.is_empty()).then(|| env_value == "1" || env_value == "true") + }) + .or(self.experimental.turbopack_use_system_tls_certs) + .unwrap_or(false); + Ok(ReqwestClientConfig { + proxy: None, + tls_built_in_webpki_certs: !use_system_tls_certs, + tls_built_in_native_certs: use_system_tls_certs, + } + .cell()) + } } /// A subset of ts/jsconfig that next.js implicitly diff --git a/crates/next-core/src/next_font/google/mod.rs b/crates/next-core/src/next_font/google/mod.rs index 85ac1e7b42e029..cf59798e14526f 100644 --- a/crates/next-core/src/next_font/google/mod.rs +++ b/crates/next-core/src/next_font/google/mod.rs @@ -182,6 +182,7 @@ pub struct NextFontGoogleCssModuleReplacer { project_path: FileSystemPath, execution_context: ResolvedVc, next_mode: ResolvedVc, + reqwest_client_config: ResolvedVc, } #[turbo_tasks::value_impl] @@ -191,11 +192,13 @@ impl NextFontGoogleCssModuleReplacer { project_path: FileSystemPath, execution_context: ResolvedVc, next_mode: ResolvedVc, + reqwest_client_config: ResolvedVc, ) -> Vc { Self::cell(NextFontGoogleCssModuleReplacer { project_path, execution_context, next_mode, + reqwest_client_config, }) } @@ -228,7 +231,14 @@ impl NextFontGoogleCssModuleReplacer { let stylesheet_str = mocked_responses_path .as_ref() .map_or_else( - || fetch_real_stylesheet(stylesheet_url.clone(), css_virtual_path.clone()).boxed(), + || { + fetch_real_stylesheet( + stylesheet_url.clone(), + css_virtual_path.clone(), + *self.reqwest_client_config, + ) + .boxed() + }, |p| get_mock_stylesheet(stylesheet_url.clone(), p, *self.execution_context).boxed(), ) .await?; @@ -365,13 +375,20 @@ struct NextFontGoogleFontFileOptions { #[turbo_tasks::value(shared)] pub struct NextFontGoogleFontFileReplacer { project_path: FileSystemPath, + reqwest_client_config: ResolvedVc, } #[turbo_tasks::value_impl] impl NextFontGoogleFontFileReplacer { #[turbo_tasks::function] - pub fn new(project_path: FileSystemPath) -> Vc { - Self::cell(NextFontGoogleFontFileReplacer { project_path }) + pub fn new( + project_path: FileSystemPath, + reqwest_client_config: ResolvedVc, + ) -> Vc { + Self::cell(NextFontGoogleFontFileReplacer { + project_path, + reqwest_client_config, + }) } } @@ -426,7 +443,12 @@ impl ImportMappingReplacement for NextFontGoogleFontFileReplacer { // doesn't seem ideal to download the font into a string, but probably doesn't // really matter either. - let Some(font) = fetch_from_google_fonts(url.into(), font_virtual_path.clone()).await? + let Some(font) = fetch_from_google_fonts( + url.into(), + font_virtual_path.clone(), + *self.reqwest_client_config, + ) + .await? else { return Ok(ImportMapResult::Result(ResolveResult::unresolvable()).cell()); }; @@ -650,8 +672,10 @@ fn font_file_options_from_query_map(query: &RcStr) -> Result, ) -> Result>> { - let body = fetch_from_google_fonts(stylesheet_url, css_virtual_path).await?; + let body = + fetch_from_google_fonts(stylesheet_url, css_virtual_path, reqwest_client_config).await?; Ok(body.map(|body| body.to_string())) } @@ -659,11 +683,12 @@ async fn fetch_real_stylesheet( async fn fetch_from_google_fonts( url: RcStr, virtual_path: FileSystemPath, + reqwest_client_config: Vc, ) -> Result>> { let result = fetch( url, Some(rcstr!(USER_AGENT_FOR_GOOGLE_FONTS)), - ReqwestClientConfig { proxy: None }.cell(), + reqwest_client_config, ) .await?; diff --git a/crates/next-core/src/next_import_map.rs b/crates/next-core/src/next_import_map.rs index 6685ba4f6d39e3..83e9a623a6ceab 100644 --- a/crates/next-core/src/next_import_map.rs +++ b/crates/next-core/src/next_import_map.rs @@ -1022,6 +1022,7 @@ async fn insert_next_shared_aliases( next_font_google_replacer_mapping, ); + let reqwest_client_config = next_config.reqwest_client_config(execution_context.env()); import_map.insert_alias( AliasPattern::exact("@vercel/turbopack-next/internal/font/google/cssmodule.module.css"), ImportMapping::Dynamic(ResolvedVc::upcast( @@ -1029,6 +1030,7 @@ async fn insert_next_shared_aliases( project_path.clone(), execution_context, next_mode, + reqwest_client_config, ) .to_resolved() .await?, @@ -1039,7 +1041,7 @@ async fn insert_next_shared_aliases( import_map.insert_alias( AliasPattern::exact(GOOGLE_FONTS_INTERNAL_PREFIX), ImportMapping::Dynamic(ResolvedVc::upcast( - NextFontGoogleFontFileReplacer::new(project_path.clone()) + NextFontGoogleFontFileReplacer::new(project_path.clone(), reqwest_client_config) .to_resolved() .await?, )) diff --git a/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx b/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx index 0c288f6da044e0..c74b012b1585b0 100644 --- a/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx +++ b/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx @@ -34,7 +34,7 @@ module.exports = { } ``` -Rewrites are applied to client-side routing, a `` will have the rewrite applied in the above example. +Rewrites are applied to client-side routing. In the example above, navigating to `` will serve content from `/` while keeping the URL as `/about`. `rewrites` is an async function that expects to return either an array or an object of arrays (see below) holding objects with `source` and `destination` properties: diff --git a/lerna.json b/lerna.json index 5aeaa6ecaa66b6..c80f060354ff70 100644 --- a/lerna.json +++ b/lerna.json @@ -16,5 +16,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "15.4.2-canary.14" + "version": "15.4.2-canary.15" } diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 8db54a69c1a2fd..cfab3190c8b3c4 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index af7cc65e78b651..143d1b316136f9 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "ESLint configuration used by Next.js.", "main": "index.js", "license": "MIT", @@ -10,7 +10,7 @@ }, "homepage": "https://nextjs.org/docs/app/api-reference/config/eslint", "dependencies": { - "@next/eslint-plugin-next": "15.4.2-canary.14", + "@next/eslint-plugin-next": "15.4.2-canary.15", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index e9103bcd195a87..0bb57fdeb72b03 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,7 +1,7 @@ { "name": "@next/eslint-plugin-internal", "private": true, - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "ESLint plugin for working on Next.js.", "exports": { ".": "./src/eslint-plugin-internal.js" diff --git a/packages/eslint-plugin-next/package.json b/packages/eslint-plugin-next/package.json index 408708f691ece7..87cdab5209eab4 100644 --- a/packages/eslint-plugin-next/package.json +++ b/packages/eslint-plugin-next/package.json @@ -1,6 +1,6 @@ { "name": "@next/eslint-plugin-next", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "ESLint plugin for Next.js.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/font/package.json b/packages/font/package.json index ef3b97c51598f2..fe8c202074a301 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/font" diff --git a/packages/next-bundle-analyzer/package.json b/packages/next-bundle-analyzer/package.json index e44d19d0d36a96..12088f1ec46a38 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index e4eab880cb02fb..d4350875fd5c5e 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 4d5801bf58cc2e..08b9e0a754dbed 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index 5fdf95c80e075a..a76f4b5890438e 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 4c3c4c52279f37..b41af01f97c782 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-plugin-storybook" diff --git a/packages/next-polyfill-module/package.json b/packages/next-polyfill-module/package.json index 84db44405c6a81..4636d2e4e71ce4 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "A standard library polyfill for ES Modules supporting browsers (Edge 16+, Firefox 60+, Chrome 61+, Safari 10.1+)", "main": "dist/polyfill-module.js", "license": "MIT", diff --git a/packages/next-polyfill-nomodule/package.json b/packages/next-polyfill-nomodule/package.json index 31ede27d7e88aa..3ecf45b1f2f575 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index 893d77a9ed7c56..e13631ab0ac543 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index b2f4add9e6b38e..7b39bfd37a21ad 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "private": true, "files": [ "native/" diff --git a/packages/next/errors.json b/packages/next/errors.json index 2951e118f075d9..e8420b4ca8239a 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 002bd78debb251..8a99a6f8011ac7 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -100,13 +100,14 @@ ] }, "dependencies": { - "@next/env": "15.4.2-canary.14", + "@next/env": "15.4.2-canary.15", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "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,13 +165,14 @@ "@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", - "@next/polyfill-module": "15.4.2-canary.14", - "@next/polyfill-nomodule": "15.4.2-canary.14", - "@next/react-refresh-utils": "15.4.2-canary.14", - "@next/swc": "15.4.2-canary.14", + "@next/font": "15.4.2-canary.15", + "@next/polyfill-module": "15.4.2-canary.15", + "@next/polyfill-nomodule": "15.4.2-canary.15", + "@next/react-refresh-utils": "15.4.2-canary.15", + "@next/swc": "15.4.2-canary.15", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.51.1", "@rspack/core": "1.4.5", diff --git a/packages/next/src/build/swc/generated-native.d.ts b/packages/next/src/build/swc/generated-native.d.ts index 5785496b60effb..34bf982be9d21a 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 0299b04ad697af..e3b4251e1e29a7 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 6af182f331a393..8af31514e84712 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/build/templates/app-page.ts b/packages/next/src/build/templates/app-page.ts index 70568be634f437..bc14749d10ab36 100644 --- a/packages/next/src/build/templates/app-page.ts +++ b/packages/next/src/build/templates/app-page.ts @@ -1240,7 +1240,7 @@ export async function handler( } } catch (err) { // if we aren't wrapped by base-server handle here - if (!activeSpan) { + if (!activeSpan && !(err instanceof NoFallbackError)) { await routeModule.onRequestError( req, err, diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index a3ccf4b88f0b93..229d897844f5ae 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -449,7 +449,7 @@ export async function handler( } } catch (err) { // if we aren't wrapped by base-server handle here - if (!activeSpan) { + if (!activeSpan && !(err instanceof NoFallbackError)) { await routeModule.onRequestError(req, err, { routerKind: 'App Router', routePath: normalizedSrcPage, diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index e971530f4ff41f..e37b2487980e6e 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -462,6 +462,26 @@ export const configSchema: zod.ZodType = z.lazy(() => turbopackTreeShaking: z.boolean().optional(), turbopackRemoveUnusedExports: z.boolean().optional(), turbopackScopeHoisting: z.boolean().optional(), + /** + * Use the system-provided CA roots instead of bundled CA roots for external HTTPS requests + * made by Turbopack. Currently this is only used for fetching data from Google Fonts. + * + * This may be useful in cases where you or an employer are MITMing traffic. + * + * This option is experimental because: + * - This may cause small performance problems, as it uses [`rustls-native-certs`]( + * https://github.com/rustls/rustls-native-certs). + * - In the future, this may become the default, and this option may be eliminated, once + * is resolved. + * + * Users who need to configure this behavior system-wide can override the project + * configuration using the `NEXT_TURBOPACK_EXPERIMENTAL_USE_SYSTEM_TLS_CERTS=1` environment + * variable. + * + * This option is ignored on Windows on ARM, where the native TLS implementation is always + * used. + */ + turbopackUseSystemTlsCerts: z.boolean().optional(), optimizePackageImports: z.array(z.string()).optional(), optimizeServerReact: z.boolean().optional(), clientTraceMetadata: z.array(z.string()).optional(), diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index fdba36790f712d..e4ad95a12ce646 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -894,7 +894,12 @@ export default class DevServer extends Server { fallbackMode: fallback, } - if (res.value?.fallbackMode !== undefined) { + if ( + res.value?.fallbackMode !== undefined && + // This matches the hasGenerateStaticParams logic + // we do during build + (!isAppPath || (staticPaths && staticPaths.length > 0)) + ) { // we write the static paths to partial manifest for // fallback handling inside of entry handler's const rawExistingManifest = await fs.promises.readFile( 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 00000000000000..a79d44dbc26e23 --- /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/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index c9c9ed1064bb37..7b3f5c05ced75b 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -33,7 +33,6 @@ import { addRequestMeta } from '../../request-meta' import { compileNonPath, matchHas, - parseDestination, prepareDestination, } from '../../../shared/lib/router/utils/prepare-destination' import type { TLSSocket } from 'tls' @@ -761,16 +760,6 @@ export function getResolveRoutes( // so we'll just use the params from the route matcher } - // We extract the search params of the destination so we can set it on - // the response headers. We don't want to use the following - // `parsedDestination` as the query object is mutated. - const { search: destinationSearch, pathname: destinationPathname } = - parseDestination({ - destination: route.destination, - params: rewriteParams, - query: parsedUrl.query, - }) - const { parsedDestination } = prepareDestination({ appendParamsToQuery: true, destination: route.destination, @@ -790,14 +779,17 @@ export function getResolveRoutes( if (req.headers[RSC_HEADER.toLowerCase()] === '1') { // We set the rewritten path and query headers on the response now // that we know that the it's not an external rewrite. - if (parsedUrl.pathname !== destinationPathname) { - res.setHeader(NEXT_REWRITTEN_PATH_HEADER, destinationPathname) + if (parsedUrl.pathname !== parsedDestination.pathname) { + res.setHeader( + NEXT_REWRITTEN_PATH_HEADER, + parsedDestination.pathname + ) } - if (destinationSearch) { + if (parsedUrl.search !== parsedDestination.search) { res.setHeader( NEXT_REWRITTEN_QUERY_HEADER, // remove the leading ? from the search - destinationSearch.slice(1) + parsedDestination.search.slice(1) ) } } 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 fb2a0c03016031..33d7d3b2722e10 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/server/route-modules/pages/pages-handler.ts b/packages/next/src/server/route-modules/pages/pages-handler.ts index e888bfc9984c7d..83af0ec60ef018 100644 --- a/packages/next/src/server/route-modules/pages/pages-handler.ts +++ b/packages/next/src/server/route-modules/pages/pages-handler.ts @@ -745,20 +745,22 @@ export const getHandler = ({ ) } } catch (err) { - await routeModule.onRequestError( - req, - err, - { - routerKind: 'Pages Router', - routePath: srcPage, - routeType: 'render', - revalidateReason: getRevalidateReason({ - isRevalidate: hasStaticProps, - isOnDemandRevalidate, - }), - }, - routerServerContext - ) + if (!(err instanceof NoFallbackError)) { + await routeModule.onRequestError( + req, + err, + { + routerKind: 'Pages Router', + routePath: srcPage, + routeType: 'render', + revalidateReason: getRevalidateReason({ + isRevalidate: hasStaticProps, + isOnDemandRevalidate, + }), + }, + routerServerContext + ) + } // rethrow so that we can handle serving error page throw err diff --git a/packages/next/src/shared/lib/router/utils/prepare-destination.ts b/packages/next/src/shared/lib/router/utils/prepare-destination.ts index 25f3d26ad31f2f..9cc4320a291a04 100644 --- a/packages/next/src/shared/lib/router/utils/prepare-destination.ts +++ b/packages/next/src/shared/lib/router/utils/prepare-destination.ts @@ -193,12 +193,18 @@ export function parseDestination(args: { hash = unescapeSegments(hash) } + let search = parsed.search + if (search) { + search = unescapeSegments(search) + } + return { ...parsed, pathname, hostname, href, hash, + search, } } @@ -210,7 +216,11 @@ export function prepareDestination(args: { }) { const parsedDestination = parseDestination(args) - const { hostname: destHostname, query: destQuery } = parsedDestination + const { + hostname: destHostname, + query: destQuery, + search: destSearch, + } = parsedDestination // The following code assumes that the pathname here includes the hash if it's // present. @@ -311,7 +321,9 @@ export function prepareDestination(args: { } parsedDestination.pathname = pathname parsedDestination.hash = `${hash ? '#' : ''}${hash || ''}` - delete (parsedDestination as any).search + parsedDestination.search = destSearch + ? compileNonPath(destSearch, args.params) + : '' } catch (err: any) { if (err.message.match(/Expected .*? to not repeat, but got an array/)) { throw new Error( diff --git a/packages/next/src/shared/lib/turbopack/utils.ts b/packages/next/src/shared/lib/turbopack/utils.ts index f88fc6ef6711f5..22ca4707f06cfb 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/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 7a7fea01b1a85f..71f42b95eedae0 100644 --- a/packages/react-refresh-utils/package.json +++ b/packages/react-refresh-utils/package.json @@ -1,6 +1,6 @@ { "name": "@next/react-refresh-utils", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "description": "An experimental package providing utilities for React Refresh.", "repository": { "url": "vercel/next.js", diff --git a/packages/third-parties/package.json b/packages/third-parties/package.json index d107f7e440b1dd..579e252147a8d1 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "15.4.2-canary.14", + "version": "15.4.2-canary.15", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -26,7 +26,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "15.4.2-canary.14", + "next": "15.4.2-canary.15", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "5.8.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8eaf5dd7e35d5c..8f4b7178314f7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -854,7 +854,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../eslint-plugin-next '@rushstack/eslint-patch': specifier: ^1.10.3 @@ -924,7 +924,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -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 @@ -1046,19 +1049,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../font '@next/polyfill-module': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../react-refresh-utils '@next/swc': - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1758,7 +1761,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 15.4.2-canary.14 + specifier: 15.4.2-canary.15 version: link:../next outdent: specifier: 0.8.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/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 8e81f823064cfd..3f595697eb07ab 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -40,6 +40,18 @@ describe('app-dir static/dynamic handling', () => { } }) + if (!process.env.__NEXT_EXPERIMENTAL_PPR) { + it('should respond correctly for dynamic route with dynamicParams false in layout', async () => { + const res = await next.fetch('/partial-params-false/en/another') + expect(res.status).toBe(200) + }) + + it('should respond correctly for partially dynamic route with dynamicParams false in layout', async () => { + const res = await next.fetch('/partial-params-false/en/static') + expect(res.status).toBe(200) + }) + } + it('should use auto no cache when no fetch config', async () => { const res = await next.fetch('/no-config-fetch') expect(res.status).toBe(200) @@ -877,6 +889,10 @@ describe('app-dir static/dynamic handling', () => { "partial-gen-params-no-additional-slug/fr/first.rsc", "partial-gen-params-no-additional-slug/fr/second.html", "partial-gen-params-no-additional-slug/fr/second.rsc", + "partial-params-false/en/static.html", + "partial-params-false/en/static.rsc", + "partial-params-false/fr/static.html", + "partial-params-false/fr/static.rsc", "prerendered-not-found/first.html", "prerendered-not-found/first.rsc", "prerendered-not-found/second.html", @@ -1822,6 +1838,54 @@ describe('app-dir static/dynamic handling', () => { "initialRevalidateSeconds": false, "srcRoute": "/partial-gen-params-no-additional-slug/[lang]/[slug]", }, + "/partial-params-false/en/static": { + "allowHeader": [ + "host", + "x-matched-path", + "x-prerender-revalidate", + "x-prerender-revalidate-if-generated", + "x-next-revalidated-tags", + "x-next-revalidate-tag-token", + ], + "dataRoute": "/partial-params-false/en/static.rsc", + "experimentalBypassFor": [ + { + "key": "Next-Action", + "type": "header", + }, + { + "key": "content-type", + "type": "header", + "value": "multipart/form-data;.*", + }, + ], + "initialRevalidateSeconds": false, + "srcRoute": "/partial-params-false/[locale]/static", + }, + "/partial-params-false/fr/static": { + "allowHeader": [ + "host", + "x-matched-path", + "x-prerender-revalidate", + "x-prerender-revalidate-if-generated", + "x-next-revalidated-tags", + "x-next-revalidate-tag-token", + ], + "dataRoute": "/partial-params-false/fr/static.rsc", + "experimentalBypassFor": [ + { + "key": "Next-Action", + "type": "header", + }, + { + "key": "content-type", + "type": "header", + "value": "multipart/form-data;.*", + }, + ], + "initialRevalidateSeconds": false, + "srcRoute": "/partial-params-false/[locale]/static", + }, "/prerendered-not-found/first": { "allowHeader": [ "host", @@ -2580,6 +2644,31 @@ describe('app-dir static/dynamic handling', () => { "fallback": false, "routeRegex": "^\\/partial\\-gen\\-params\\-no\\-additional\\-slug\\/([^\\/]+?)\\/([^\\/]+?)(?:\\/)?$", }, + "/partial-params-false/[locale]/static": { + "allowHeader": [ + "host", + "x-matched-path", + "x-prerender-revalidate", + "x-prerender-revalidate-if-generated", + "x-next-revalidated-tags", + "x-next-revalidate-tag-token", + ], + "dataRoute": "/partial-params-false/[locale]/static.rsc", + "dataRouteRegex": "^\\/partial\\-params\\-false\\/([^\\/]+?)\\/static\\.rsc$", + "experimentalBypassFor": [ + { + "key": "Next-Action", + "type": "header", + }, + { + "key": "content-type", + "type": "header", + "value": "multipart/form-data;.*", + }, + ], + "fallback": false, + "routeRegex": "^\\/partial\\-params\\-false\\/([^\\/]+?)\\/static(?:\\/)?$", + }, "/prerendered-not-found/[slug]": { "allowHeader": [ "host", diff --git a/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/[slug]/page.js b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/[slug]/page.js new file mode 100644 index 00000000000000..7ab100431b35e4 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/[slug]/page.js @@ -0,0 +1,10 @@ +export default async function Page({ params }) { + const { locale, slug } = await params + + return ( + <> +

/[locale]/[slug]/page

+

params: {JSON.stringify({ locale, slug })}

+ + ) +} diff --git a/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/layout.js b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/layout.js new file mode 100644 index 00000000000000..e9b0920c6fa565 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/layout.js @@ -0,0 +1,16 @@ +export const dynamicParams = false + +export function generateStaticParams() { + return [ + { + locale: 'en', + }, + { + locale: 'fr', + }, + ] +} + +export default function Layout({ children }) { + return <>{children} +} diff --git a/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/static/page.js b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/static/page.js new file mode 100644 index 00000000000000..7cd2718329aee7 --- /dev/null +++ b/test/e2e/app-dir/app-static/app/partial-params-false/[locale]/static/page.js @@ -0,0 +1,9 @@ +export default async function Page({ params }) { + const { locale } = await params + return ( + <> +

/[locale]/static

+

locale: {locale}

+ + ) +} diff --git a/test/e2e/app-dir/rewrite-headers/next.config.js b/test/e2e/app-dir/rewrite-headers/next.config.js index f994c79c6f8a1f..26712a03853189 100644 --- a/test/e2e/app-dir/rewrite-headers/next.config.js +++ b/test/e2e/app-dir/rewrite-headers/next.config.js @@ -18,6 +18,10 @@ module.exports = { source: '/hello/(.*)/google', destination: 'https://www.google.$1/', }, + { + source: '/rewrites-to-query/:name', + destination: '/other?key=:name', + }, ] }, } diff --git a/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts b/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts index 3384d52ece8a0e..9f4247ab1e870c 100644 --- a/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts +++ b/test/e2e/app-dir/rewrite-headers/rewrite-headers.test.ts @@ -368,6 +368,37 @@ const cases: { 'x-nextjs-rewritten-query': 'key=value', }, }, + { + name: 'next.config.js rewrites with path-matched query HTML', + pathname: '/rewrites-to-query/andrew', + expected: { + 'x-nextjs-rewritten-path': null, + 'x-nextjs-rewritten-query': null, + }, + }, + { + name: 'next.config.js rewrites with path-matched query RSC', + pathname: '/rewrites-to-query/andrew', + headers: { + RSC: '1', + }, + expected: { + 'x-nextjs-rewritten-path': '/other', + 'x-nextjs-rewritten-query': 'key=andrew', + }, + }, + { + name: 'next.config.js rewrites with path-matched query Prefetch RSC', + pathname: '/rewrites-to-query/andrew', + headers: { + RSC: '1', + 'Next-Router-Prefetch': '1', + }, + expected: { + 'x-nextjs-rewritten-path': '/other', + 'x-nextjs-rewritten-query': 'key=andrew', + }, + }, ] describe('rewrite-headers', () => { diff --git a/turbopack/crates/turbo-tasks-fetch/Cargo.toml b/turbopack/crates/turbo-tasks-fetch/Cargo.toml index 5aec449b4fda86..2113a046d64c06 100644 --- a/turbopack/crates/turbo-tasks-fetch/Cargo.toml +++ b/turbopack/crates/turbo-tasks-fetch/Cargo.toml @@ -20,8 +20,9 @@ workspace = true [target.'cfg(all(target_os = "windows", target_arch = "aarch64"))'.dependencies] reqwest = { workspace = true, features = ["native-tls"] } +# If you modify this cfg, make sure you update `ReqwestClientConfig::try_build`. [target.'cfg(not(any(all(target_os = "windows", target_arch = "aarch64"), target_arch="wasm32")))'.dependencies] -reqwest = { workspace = true, features = ["rustls-tls"] } +reqwest = { workspace = true, features = ["rustls-tls-webpki-roots", "rustls-tls-native-roots"] } [dependencies] anyhow = { workspace = true } diff --git a/turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs b/turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs index 2a695dc640f9de..4c9e278bd2b83b 100644 --- a/turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs +++ b/turbopack/crates/turbo-tasks-fetch/src/reqwest_client_cache.rs @@ -26,6 +26,29 @@ pub enum ProxyConfig { #[derive(Hash)] pub struct ReqwestClientConfig { pub proxy: Option, + /// Whether to load embedded webpki root certs with rustls. Default is true. + /// + /// Ignored for: + /// - Windows on ARM, which uses `native-tls` instead of `rustls-tls`. + /// - Ignored for WASM targets, which use the runtime's TLS implementation. + pub tls_built_in_webpki_certs: bool, + /// Whether to load native root certs using the `rustls-native-certs` crate. This may make + /// reqwest client initialization slower, so it's not used by default. + /// + /// Ignored for: + /// - Windows on ARM, which uses `native-tls` instead of `rustls-tls`. + /// - Ignored for WASM targets, which use the runtime's TLS implementation. + pub tls_built_in_native_certs: bool, +} + +impl Default for ReqwestClientConfig { + fn default() -> Self { + Self { + proxy: None, + tls_built_in_webpki_certs: true, + tls_built_in_native_certs: false, + } + } } impl ReqwestClientConfig { @@ -40,6 +63,17 @@ impl ReqwestClientConfig { } None => {} }; + + // make sure this cfg matches the one in `Cargo.toml`! + #[cfg(not(any( + all(target_os = "windows", target_arch = "aarch64"), + target_arch = "wasm32" + )))] + let client_builder = client_builder + .tls_built_in_root_certs(false) + .tls_built_in_webpki_certs(self.tls_built_in_webpki_certs) + .tls_built_in_native_certs(self.tls_built_in_native_certs); + client_builder.build() } } diff --git a/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs b/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs index d32431550cd0da..31fd898a14aa4b 100644 --- a/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs +++ b/turbopack/crates/turbo-tasks-fetch/tests/fetch.rs @@ -6,7 +6,7 @@ use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::Vc; use turbo_tasks_fetch::{ __test_only_reqwest_client_cache_clear, __test_only_reqwest_client_cache_len, FetchErrorKind, - ProxyConfig, ReqwestClientConfig, fetch, + ReqwestClientConfig, fetch, }; use turbo_tasks_fs::{DiskFileSystem, FileSystem, FileSystemPath}; use turbo_tasks_testing::{Registration, register, run}; @@ -29,7 +29,7 @@ async fn basic_get() { .create_async() .await; - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response = &*fetch( RcStr::from(format!("{}/foo.woff", server.url())), /* user_agent */ None, @@ -63,7 +63,7 @@ async fn sends_user_agent() { eprintln!("{}", server.url()); - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response = &*fetch( RcStr::from(format!("{}/foo.woff", server.url())), Some(rcstr!("mock-user-agent")), @@ -98,7 +98,7 @@ async fn invalidation_does_not_invalidate() { .await; let url = RcStr::from(format!("{}/foo.woff", server.url())); - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response = &*fetch(url.clone(), /* user_agent */ None, config_vc) .await? .unwrap() @@ -136,7 +136,7 @@ async fn errors_on_failed_connection() { // `ECONNREFUSED`. // Other values (e.g. domain name, reserved IP address block) may result in long timeouts. let url = rcstr!("http://127.0.0.1:0/foo.woff"); - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response_vc = fetch(url.clone(), None, config_vc); let err_vc = &*response_vc.await?.unwrap_err(); let err = err_vc.await?; @@ -171,7 +171,7 @@ async fn errors_on_404() { .await; let url = RcStr::from(server.url()); - let config_vc = ReqwestClientConfig { proxy: None }.cell(); + let config_vc = ReqwestClientConfig::default().cell(); let response_vc = fetch(url.clone(), None, config_vc); let err_vc = &*response_vc.await?.unwrap_err(); let err = err_vc.await?; @@ -225,26 +225,39 @@ async fn client_cache() { __test_only_reqwest_client_cache_clear(); assert_eq!(__test_only_reqwest_client_cache_len(), 0); - simple_fetch("/foo", ReqwestClientConfig { proxy: None }) - .await - .unwrap(); + simple_fetch( + "/foo", + ReqwestClientConfig { + tls_built_in_native_certs: false, + ..Default::default() + }, + ) + .await + .unwrap(); assert_eq!(__test_only_reqwest_client_cache_len(), 1); // the client is reused if the config is the same (by equality) - simple_fetch("/bar", ReqwestClientConfig { proxy: None }) - .await - .unwrap(); + simple_fetch( + "/bar", + ReqwestClientConfig { + tls_built_in_native_certs: false, + ..Default::default() + }, + ) + .await + .unwrap(); assert_eq!(__test_only_reqwest_client_cache_len(), 1); // the client is recreated if the config is different simple_fetch( "/bar", ReqwestClientConfig { - proxy: Some(ProxyConfig::Http(RcStr::from("http://127.0.0.1:0/"))), + tls_built_in_native_certs: true, + ..Default::default() }, ) .await - .unwrap_err(); + .unwrap(); assert_eq!(__test_only_reqwest_client_cache_len(), 2); Ok(()) diff --git a/turbopack/crates/turbo-tasks-fs/src/lib.rs b/turbopack/crates/turbo-tasks-fs/src/lib.rs index 83d718a52b1b2d..a9238567bf8862 100644 --- a/turbopack/crates/turbo-tasks-fs/src/lib.rs +++ b/turbopack/crates/turbo-tasks-fs/src/lib.rs @@ -309,7 +309,7 @@ impl DiskFileSystemInner { } fn invalidate(&self) { - let _span = tracing::info_span!("invalidate filesystem", path = &*self.root).entered(); + let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered(); let span = tracing::Span::current(); let handle = tokio::runtime::Handle::current(); let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap()); @@ -332,7 +332,7 @@ impl DiskFileSystemInner { &self, reason: impl Fn(String) -> R + Sync, ) { - let _span = tracing::info_span!("invalidate filesystem", path = &*self.root).entered(); + let _span = tracing::info_span!("invalidate filesystem", name = &*self.root).entered(); let span = tracing::Span::current(); let handle = tokio::runtime::Handle::current(); let invalidator_map = take(&mut *self.invalidator_map.lock().unwrap()); @@ -388,7 +388,7 @@ impl DiskFileSystemInner { // create the directory for the filesystem on disk, if it doesn't exist retry_blocking(root_path.clone(), move |path| { let _tracing = - tracing::info_span!("create root directory", path = display(path.display())) + tracing::info_span!("create root directory", name = display(path.display())) .entered(); std::fs::create_dir_all(path) @@ -413,7 +413,7 @@ impl DiskFileSystemInner { .concurrency_limited(&self.semaphore) .instrument(tracing::info_span!( "create directory", - path = display(directory.display()) + name = display(directory.display()) )) .await?; ApplyEffectsContext::with(|fs_context: &mut DiskFileSystemApplyContext| { @@ -558,7 +558,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&self.inner.semaphore) .instrument(tracing::info_span!( "read file", - path = display(full_path.display()) + name = display(full_path.display()) )) .await { @@ -583,7 +583,7 @@ impl FileSystem for DiskFileSystem { // node-file-trace let read_dir = match retry_blocking(full_path.clone(), |path| { let _span = - tracing::info_span!("read directory", path = display(path.display())).entered(); + tracing::info_span!("read directory", name = display(path.display())).entered(); std::fs::read_dir(path) }) .concurrency_limited(&self.inner.semaphore) @@ -640,7 +640,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&self.inner.semaphore) .instrument(tracing::info_span!( "read symlink", - path = display(full_path.display()) + name = display(full_path.display()) )) .await { @@ -745,7 +745,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&inner.semaphore) .instrument(tracing::info_span!( "read file before write", - path = display(full_path.display()) + name = display(full_path.display()) )) .await?; if compare == FileComparison::Equal { @@ -812,7 +812,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&inner.semaphore) .instrument(tracing::info_span!( "write file", - path = display(full_path.display()) + name = display(full_path.display()) )) .await .with_context(|| format!("failed to write to {}", full_path.display()))?; @@ -824,7 +824,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&inner.semaphore) .instrument(tracing::info_span!( "remove file", - path = display(full_path.display()) + name = display(full_path.display()) )) .await .or_else(|err| { @@ -873,7 +873,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&inner.semaphore) .instrument(tracing::info_span!( "read symlink before write", - path = display(full_path.display()) + name = display(full_path.display()) )) .await { @@ -923,7 +923,7 @@ impl FileSystem for DiskFileSystem { retry_blocking(target_path, move |target_path| { let _span = tracing::info_span!( "write symlink", - path = display(target_path.display()) + name = display(target_path.display()) ) .entered(); // we use the sync std method here because `symlink` is fast @@ -980,7 +980,7 @@ impl FileSystem for DiskFileSystem { .concurrency_limited(&self.inner.semaphore) .instrument(tracing::info_span!( "read metadata", - path = display(full_path.display()) + name = display(full_path.display()) )) .await .with_context(|| format!("reading metadata for {}", full_path.display()))?; diff --git a/turbopack/crates/turbo-tasks-malloc/Cargo.toml b/turbopack/crates/turbo-tasks-malloc/Cargo.toml index b3331d12867983..1743f2329a0018 100644 --- a/turbopack/crates/turbo-tasks-malloc/Cargo.toml +++ b/turbopack/crates/turbo-tasks-malloc/Cargo.toml @@ -10,10 +10,10 @@ autobenches = false bench = false [target.'cfg(not(any(target_os = "linux", target_family = "wasm", target_env = "musl")))'.dependencies] -mimalloc = { version = "0.1.42", features = [], optional = true } +mimalloc = { version = "0.1.47", features = [], optional = true } [target.'cfg(all(target_os = "linux", not(any(target_family = "wasm", target_env = "musl"))))'.dependencies] -mimalloc = { version = "0.1.42", features = [ +mimalloc = { version = "0.1.47", features = [ "local_dynamic_tls", ], optional = true } diff --git a/turbopack/crates/turbopack-browser/src/chunking_context.rs b/turbopack/crates/turbopack-browser/src/chunking_context.rs index eeeb27aebb195d..0b70e283cd3e09 100644 --- a/turbopack/crates/turbopack-browser/src/chunking_context.rs +++ b/turbopack/crates/turbopack-browser/src/chunking_context.rs @@ -571,7 +571,7 @@ impl ChunkingContext for BrowserChunkingContext { module_graph: Vc, availability_info: AvailabilityInfo, ) -> Result> { - let span = tracing::info_span!("chunking", ident = ident.to_string().await?.to_string()); + let span = tracing::info_span!("chunking", name = ident.to_string().await?.to_string()); async move { let this = self.await?; let modules = chunk_group.entries(); diff --git a/turbopack/crates/turbopack-core/src/module_graph/mod.rs b/turbopack/crates/turbopack-core/src/module_graph/mod.rs index 2d5bcc610a9916..c844ef54957fa3 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. /// diff --git a/turbopack/crates/turbopack-core/src/resolve/mod.rs b/turbopack/crates/turbopack-core/src/resolve/mod.rs index 00b0b9805cfaaa..ad3a1947df191e 100644 --- a/turbopack/crates/turbopack-core/src/resolve/mod.rs +++ b/turbopack/crates/turbopack-core/src/resolve/mod.rs @@ -1573,7 +1573,7 @@ pub async fn resolve_inline( tracing::info_span!( "resolving", lookup_path = lookup_path, - request = request, + name = request, reference_type = display(&reference_type), ) }; @@ -1778,7 +1778,7 @@ async fn resolve_internal_inline( tracing::info_span!( "internal resolving", lookup_path = lookup_path, - request = request + name = request ) }; async move { diff --git a/turbopack/crates/turbopack-ecmascript/src/lib.rs b/turbopack/crates/turbopack-ecmascript/src/lib.rs index 33f6c48a2d95a3..498efdee64eadd 100644 --- a/turbopack/crates/turbopack-ecmascript/src/lib.rs +++ b/turbopack/crates/turbopack-ecmascript/src/lib.rs @@ -810,7 +810,7 @@ impl EcmascriptChunkItem for ModuleChunkItem { ) -> Result> { let span = tracing::info_span!( "code generation", - module = self.asset_ident().to_string().await?.to_string() + name = self.asset_ident().to_string().await?.to_string() ); async { let this = self.await?; diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index 2383f46054a0f6..d24e50a94dfdaa 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -495,7 +495,7 @@ pub(crate) async fn analyse_ecmascript_module( ) -> Result> { let span = { let module = module.ident().to_string().await?.to_string(); - tracing::info_span!("analyse ecmascript module", module = module) + tracing::info_span!("analyse ecmascript module", name = module) }; let result = analyse_ecmascript_module_internal(module, part) .instrument(span) diff --git a/turbopack/crates/turbopack-nodejs/src/chunking_context.rs b/turbopack/crates/turbopack-nodejs/src/chunking_context.rs index 57c992875d25bc..535fd67cd6fbfe 100644 --- a/turbopack/crates/turbopack-nodejs/src/chunking_context.rs +++ b/turbopack/crates/turbopack-nodejs/src/chunking_context.rs @@ -384,7 +384,7 @@ impl ChunkingContext for NodeJsChunkingContext { module_graph: Vc, availability_info: AvailabilityInfo, ) -> Result> { - let span = tracing::info_span!("chunking", module = ident.to_string().await?.to_string()); + let span = tracing::info_span!("chunking", name = ident.to_string().await?.to_string()); async move { let modules = chunk_group.entries(); let MakeChunkGroupResult { diff --git a/turbopack/crates/turbopack-trace-server/src/bottom_up.rs b/turbopack/crates/turbopack-trace-server/src/bottom_up.rs index 76397fc0ea558c..c29ef2f5328d9a 100644 --- a/turbopack/crates/turbopack-trace-server/src/bottom_up.rs +++ b/turbopack/crates/turbopack-trace-server/src/bottom_up.rs @@ -5,12 +5,13 @@ use hashbrown::HashMap; use crate::{ span::{SpanBottomUp, SpanIndex}, span_ref::SpanRef, + string_tuple_ref::StringTupleRef, }; pub struct SpanBottomUpBuilder { // These values won't change after creation: pub self_spans: Vec, - pub children: HashMap, + pub children: HashMap<(String, String), SpanBottomUpBuilder>, pub example_span: SpanIndex, } @@ -42,7 +43,7 @@ pub fn build_bottom_up_graph<'a>( .ok() .and_then(|s| s.parse().ok()) .unwrap_or(usize::MAX); - let mut roots: HashMap = HashMap::default(); + let mut roots: HashMap<(String, String), SpanBottomUpBuilder> = HashMap::default(); // unfortunately there is a rustc bug that fails the typechecking here // when using Either. This error appears @@ -52,30 +53,40 @@ pub fn build_bottom_up_graph<'a>( let mut current_iterators: Vec>>> = vec![Box::new(spans.flat_map(|span| span.children()))]; - let mut current_path: Vec<(&'_ str, SpanIndex)> = vec![]; + let mut current_path: Vec<((&'_ str, &'_ str), SpanIndex)> = vec![]; while let Some(mut iter) = current_iterators.pop() { if let Some(child) = iter.next() { current_iterators.push(iter); - let name = child.group_name(); + let (category, name) = child.group_name(); let (_, mut bottom_up) = roots .raw_entry_mut() - .from_key(name) - .or_insert_with(|| (name.to_string(), SpanBottomUpBuilder::new(child.index()))); + .from_key(&StringTupleRef(category, name)) + .or_insert_with(|| { + ( + (category.to_string(), name.to_string()), + SpanBottomUpBuilder::new(child.index()), + ) + }); bottom_up.self_spans.push(child.index()); let mut prev = None; - for &(name, example_span) in current_path.iter().rev().take(max_depth) { - if prev == Some(name) { + for &((category, title), example_span) in current_path.iter().rev().take(max_depth) { + if prev == Some((category, title)) { continue; } let (_, child_bottom_up) = bottom_up .children .raw_entry_mut() - .from_key(name) - .or_insert_with(|| (name.to_string(), SpanBottomUpBuilder::new(example_span))); + .from_key(&StringTupleRef(category, title)) + .or_insert_with(|| { + ( + (category.to_string(), title.to_string()), + SpanBottomUpBuilder::new(example_span), + ) + }); child_bottom_up.self_spans.push(child.index()); bottom_up = child_bottom_up; - prev = Some(name); + prev = Some((category, title)); } current_path.push((child.group_name(), child.index())); diff --git a/turbopack/crates/turbopack-trace-server/src/lib.rs b/turbopack/crates/turbopack-trace-server/src/lib.rs index ef164659a8a6e2..0a086985455215 100644 --- a/turbopack/crates/turbopack-trace-server/src/lib.rs +++ b/turbopack/crates/turbopack-trace-server/src/lib.rs @@ -17,6 +17,7 @@ mod span_graph_ref; mod span_ref; mod store; mod store_container; +mod string_tuple_ref; mod timestamp; mod u64_empty_string; mod u64_string; diff --git a/turbopack/crates/turbopack-trace-server/src/main.rs b/turbopack/crates/turbopack-trace-server/src/main.rs index c3f9ddaa965491..7800b96a8d6ad9 100644 --- a/turbopack/crates/turbopack-trace-server/src/main.rs +++ b/turbopack/crates/turbopack-trace-server/src/main.rs @@ -18,6 +18,7 @@ mod span_graph_ref; mod span_ref; mod store; mod store_container; +mod string_tuple_ref; mod timestamp; mod u64_empty_string; mod u64_string; diff --git a/turbopack/crates/turbopack-trace-server/src/span.rs b/turbopack/crates/turbopack-trace-server/src/span.rs index 0cca4825fbb426..1fb66072851e1d 100644 --- a/turbopack/crates/turbopack-trace-server/src/span.rs +++ b/turbopack/crates/turbopack-trace-server/src/span.rs @@ -71,7 +71,7 @@ pub struct SpanExtra { pub struct SpanNames { // These values are computed when accessed (and maybe deleted during writing): pub nice_name: OnceLock<(String, String)>, - pub group_name: OnceLock, + pub group_name: OnceLock<(String, String)>, } impl Span { diff --git a/turbopack/crates/turbopack-trace-server/src/span_bottom_up_ref.rs b/turbopack/crates/turbopack-trace-server/src/span_bottom_up_ref.rs index f152d22d44eb0e..02f36e3e96efbf 100644 --- a/turbopack/crates/turbopack-trace-server/src/span_bottom_up_ref.rs +++ b/turbopack/crates/turbopack-trace-server/src/span_bottom_up_ref.rs @@ -6,9 +6,9 @@ use std::{ use crate::{ FxIndexMap, - span::{SpanBottomUp, SpanGraphEvent, SpanIndex}, + span::{SpanBottomUp, SpanGraphEvent}, span_graph_ref::{SpanGraphEventRef, SpanGraphRef, event_map_to_list}, - span_ref::SpanRef, + span_ref::{GroupNameToDirectAndRecusiveSpans, SpanRef}, store::{SpanId, Store}, timestamp::Timestamp, }; @@ -54,7 +54,7 @@ impl<'a> SpanBottomUpRef<'a> { self.bottom_up.self_spans.len() } - pub fn group_name(&self) -> &'a str { + pub fn group_name(&self) -> (&'a str, &'a str) { self.first_span().group_name() } @@ -62,7 +62,7 @@ impl<'a> SpanBottomUpRef<'a> { if self.count() == 1 { self.example_span().nice_name() } else { - ("", self.example_span().group_name()) + self.example_span().group_name() } } @@ -85,8 +85,7 @@ impl<'a> SpanBottomUpRef<'a> { let _ = self.first_span().graph(); self.first_span().extra().graph.get().unwrap().clone() } else { - let mut map: FxIndexMap<&str, (Vec, Vec)> = - FxIndexMap::default(); + let mut map: GroupNameToDirectAndRecusiveSpans = FxIndexMap::default(); let mut queue = VecDeque::with_capacity(8); for child in self.spans() { let name = child.group_name(); diff --git a/turbopack/crates/turbopack-trace-server/src/span_graph_ref.rs b/turbopack/crates/turbopack-trace-server/src/span_graph_ref.rs index 31e25659e3a00a..3538f68a40aaf1 100644 --- a/turbopack/crates/turbopack-trace-server/src/span_graph_ref.rs +++ b/turbopack/crates/turbopack-trace-server/src/span_graph_ref.rs @@ -9,9 +9,9 @@ use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; use crate::{ FxIndexMap, bottom_up::build_bottom_up_graph, - span::{SpanGraph, SpanGraphEvent, SpanIndex}, + span::{SpanGraph, SpanGraphEvent}, span_bottom_up_ref::SpanBottomUpRef, - span_ref::SpanRef, + span_ref::{GroupNameToDirectAndRecusiveSpans, SpanRef}, store::{SpanId, Store}, timestamp::Timestamp, }; @@ -40,7 +40,7 @@ impl<'a> SpanGraphRef<'a> { if self.count() == 1 { self.first_span().nice_name() } else { - ("", self.first_span().group_name()) + self.first_span().group_name() } } @@ -87,8 +87,7 @@ impl<'a> SpanGraphRef<'a> { self.first_span().extra().graph.get().unwrap().clone() } else { let self_group = self.first_span().group_name(); - let mut map: FxIndexMap<&str, (Vec, Vec)> = - FxIndexMap::default(); + let mut map: GroupNameToDirectAndRecusiveSpans = FxIndexMap::default(); let mut queue = VecDeque::with_capacity(8); for span in self.recursive_spans() { for span in span.children() { @@ -303,9 +302,7 @@ impl<'a> SpanGraphRef<'a> { } } -pub fn event_map_to_list( - map: FxIndexMap<&str, (Vec, Vec)>, -) -> Vec { +pub fn event_map_to_list(map: GroupNameToDirectAndRecusiveSpans) -> Vec { map.into_iter() .map(|(_, (root_spans, recursive_spans))| { let graph = SpanGraph { diff --git a/turbopack/crates/turbopack-trace-server/src/span_ref.rs b/turbopack/crates/turbopack-trace-server/src/span_ref.rs index 124b94727043f1..25adc8a234cb63 100644 --- a/turbopack/crates/turbopack-trace-server/src/span_ref.rs +++ b/turbopack/crates/turbopack-trace-server/src/span_ref.rs @@ -19,6 +19,9 @@ use crate::{ timestamp::Timestamp, }; +pub type GroupNameToDirectAndRecusiveSpans<'l> = + FxIndexMap<(&'l str, &'l str), (Vec, Vec)>; + #[derive(Copy, Clone)] pub struct SpanRef<'a> { pub(crate) span: &'a Span, @@ -89,18 +92,17 @@ impl<'a> SpanRef<'a> { .find(|&(k, _)| k == "name") .map(|(_, v)| v.as_str()) { - if matches!( + if matches!(self.span.name.as_str(), "turbo_tasks::function") { + (self.span.name.clone(), name.to_string()) + } else if matches!( self.span.name.as_str(), "turbo_tasks::resolve_call" | "turbo_tasks::resolve_trait_call" ) { - ( - format!("{} {}", self.span.name, self.span.category), - format!("*{name}"), - ) + (self.span.name.clone(), format!("*{name}")) } else { ( - format!("{} {}", self.span.name, self.span.category), - name.to_string(), + self.span.category.clone(), + format!("{} {name}", self.span.name), ) } } else { @@ -110,29 +112,37 @@ impl<'a> SpanRef<'a> { (category, title) } - pub fn group_name(&self) -> &'a str { - self.names().group_name.get_or_init(|| { + pub fn group_name(&self) -> (&'a str, &'a str) { + let (category, title) = self.names().group_name.get_or_init(|| { if matches!(self.span.name.as_str(), "turbo_tasks::function") { - self.span + let name = self + .span .args .iter() .find(|&(k, _)| k == "name") .map(|(_, v)| v.to_string()) - .unwrap_or_else(|| self.span.name.to_string()) + .unwrap_or_else(|| self.span.name.to_string()); + (self.span.name.clone(), name) } else if matches!( self.span.name.as_str(), "turbo_tasks::resolve_call" | "turbo_tasks::resolve_trait_call" ) { - self.span + let name = self + .span .args .iter() .find(|&(k, _)| k == "name") .map(|(_, v)| format!("*{v}")) - .unwrap_or_else(|| self.span.name.to_string()) + .unwrap_or_else(|| self.span.name.to_string()); + ( + self.span.category.clone(), + format!("{} {name}", self.span.name), + ) } else { - self.span.name.to_string() + (self.span.category.clone(), self.span.name.clone()) } - }) + }); + (category.as_str(), title.as_str()) } pub fn args(&self) -> impl Iterator { @@ -349,8 +359,7 @@ impl<'a> SpanRef<'a> { Entry { span, recursive } }) .collect_vec_list(); - let mut map: FxIndexMap<&str, (Vec, Vec)> = - FxIndexMap::default(); + let mut map: GroupNameToDirectAndRecusiveSpans = FxIndexMap::default(); for Entry { span, mut recursive, diff --git a/turbopack/crates/turbopack-trace-server/src/string_tuple_ref.rs b/turbopack/crates/turbopack-trace-server/src/string_tuple_ref.rs new file mode 100644 index 00000000000000..4b7a24a754253a --- /dev/null +++ b/turbopack/crates/turbopack-trace-server/src/string_tuple_ref.rs @@ -0,0 +1,28 @@ +use hashbrown::Equivalent; + +#[derive(Hash)] +pub struct StringTupleRef<'a>(pub &'a str, pub &'a str); + +impl<'a> Equivalent<(String, String)> for StringTupleRef<'a> { + fn equivalent(&self, other: &(String, String)) -> bool { + self.0 == other.0 && self.1 == other.1 + } +} + +#[cfg(test)] +mod string_tuple_ref_tests { + use std::hash::RandomState; + + use super::*; + + #[test] + fn test_string_tuple_ref_hash() { + use std::hash::BuildHasher; + + let s = RandomState::new(); + assert_eq!( + s.hash_one(StringTupleRef("abc", "def")), + s.hash_one(&("abc".to_string(), "def".to_string())) + ); + } +}