From 9de394fae95bbf23a4ec6b2f46a52f5b0b862fe0 Mon Sep 17 00:00:00 2001 From: Luke Sandberg Date: Tue, 2 Jun 2026 12:30:12 -0700 Subject: [PATCH 1/9] Devirtualize the backend api inside of turbo_tasks and turbo-tasks-backend (#94007) Methods on the `Backend` trait took a `turbo_tasks: &dyn TurboTasksBackendApi>,` parameter, but they could trivially just name the literal type. That is what this does. Doing this revealed that the `TurboTasksBackendApi` trait was no longer needed so that is also dropped --- .../turbo-tasks-backend/src/backend/mod.rs | 139 ++++++--------- .../src/backend/operation/mod.rs | 8 +- turbopack/crates/turbo-tasks/src/backend.rs | 65 +++---- turbopack/crates/turbo-tasks/src/lib.rs | 8 +- turbopack/crates/turbo-tasks/src/manager.rs | 165 +++--------------- .../crates/turbo-tasks/src/task/local_task.rs | 6 +- 6 files changed, 120 insertions(+), 271 deletions(-) diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs b/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs index 03cfd954f508..9ba4c465d2a3 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/mod.rs @@ -31,8 +31,8 @@ use turbo_bincode::{TurboBincodeBuffer, new_turbo_bincode_decoder, new_turbo_bin use turbo_tasks::{ CellId, RawVc, ReadCellOptions, ReadCellTracking, ReadConsistency, ReadOutputOptions, ReadTracking, SharedReference, StackDynTaskInputs, TRANSIENT_TASK_BIT, TaskExecutionReason, - TaskId, TaskPersistence, TaskPriority, TraitTypeId, TurboTasksBackendApi, TurboTasksPanic, - ValueTypeId, + TaskId, TaskPersistence, TaskPriority, TraitTypeId, TurboTasks, TurboTasksCallApi, + TurboTasksPanic, ValueTypeId, backend::{ Backend, CachedTaskType, CachedTaskTypeArc, CellContent, CellHash, TaskExecutionSpec, TransientTaskType, TurboTaskContextError, TurboTaskLocalContextError, TurboTasksError, @@ -225,7 +225,7 @@ impl TurboTasksBackend { /// Returns `(snapshot_had_new_data, eviction_counts)`. pub fn snapshot_and_evict_for_testing( &self, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> (bool, EvictionCounts) { self.0.snapshot_and_evict_for_testing(turbo_tasks) } @@ -270,7 +270,7 @@ impl TurboTasksBackendInner { fn execute_context<'a>( &'a self, - turbo_tasks: &'a dyn TurboTasksBackendApi>, + turbo_tasks: &'a TurboTasks>, ) -> impl ExecuteContext<'a> { ExecuteContextImpl::new(self, turbo_tasks) } @@ -312,7 +312,7 @@ impl TurboTasksBackendInner { #[doc(hidden)] pub fn snapshot_and_evict_for_testing( &self, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> (bool, EvictionCounts) { assert!( self.should_persist(), @@ -439,7 +439,7 @@ impl TurboTasksBackendInner { task_id: TaskId, reader: Option, options: ReadOutputOptions, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> Result> { self.assert_not_persistent_calling_transient(reader, task_id, /* cell_id */ None); @@ -558,8 +558,7 @@ impl TurboTasksBackendInner { let this = self.clone(); let tt = turbo_tasks.pin(); move || { - let tt: &dyn TurboTasksBackendApi> = &*tt; - let mut ctx = this.execute_context(tt); + let mut ctx = this.execute_context(&tt); let mut visited = FxHashSet::default(); fn indent(s: &str) -> String { s.split_inclusive('\n') @@ -771,7 +770,7 @@ impl TurboTasksBackendInner { reader: Option, cell: CellId, options: ReadCellOptions, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> Result> { self.assert_not_persistent_calling_transient(reader, task_id, Some(cell)); @@ -938,7 +937,7 @@ impl TurboTasksBackendInner { &self, parent_span: Option, reason: &str, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> Result<(Instant, bool), anyhow::Error> { let snapshot_span = tracing::trace_span!(parent: parent_span.clone(), "snapshot", reason = reason) @@ -1376,7 +1375,7 @@ impl TurboTasksBackendInner { Ok((snapshot_time, true)) } - fn startup(&self, turbo_tasks: &dyn TurboTasksBackendApi>) { + fn startup(&self, turbo_tasks: &TurboTasks>) { if self.should_restore() { // Continue all uncompleted operations // They can't be interrupted by a snapshot since the snapshotting job has not been @@ -1409,7 +1408,7 @@ impl TurboTasksBackendInner { } #[allow(unused_variables)] - fn stop(&self, turbo_tasks: &dyn TurboTasksBackendApi>) { + fn stop(&self, turbo_tasks: &TurboTasks>) { #[cfg(feature = "verify_aggregation_graph")] { self.is_idle.store(false, Ordering::Release); @@ -1427,7 +1426,7 @@ impl TurboTasksBackendInner { } #[allow(unused_variables)] - fn idle_start(self: &Arc, turbo_tasks: &dyn TurboTasksBackendApi>) { + fn idle_start(self: &Arc, turbo_tasks: &TurboTasks>) { self.idle_start_event.notify(usize::MAX); #[cfg(feature = "verify_aggregation_graph")] @@ -1467,7 +1466,7 @@ impl TurboTasksBackendInner { arg: &mut dyn StackDynTaskInputs, parent_task: Option, persistence: TaskPersistence, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> TaskId { let transient = matches!(persistence, TaskPersistence::Transient); @@ -1692,11 +1691,7 @@ impl TurboTasksBackendInner { ) } - fn invalidate_task( - &self, - task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi>, - ) { + fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks>) { if !self.should_track_dependencies() { panic!("Dependency tracking is disabled so invalidation is not allowed"); } @@ -1708,11 +1703,7 @@ impl TurboTasksBackendInner { ); } - fn invalidate_tasks( - &self, - tasks: &[TaskId], - turbo_tasks: &dyn TurboTasksBackendApi>, - ) { + fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks>) { if !self.should_track_dependencies() { panic!("Dependency tracking is disabled so invalidation is not allowed"); } @@ -1727,7 +1718,7 @@ impl TurboTasksBackendInner { fn invalidate_tasks_set( &self, tasks: &AutoSet, 2>, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) { if !self.should_track_dependencies() { panic!("Dependency tracking is disabled so invalidation is not allowed"); @@ -1743,7 +1734,7 @@ impl TurboTasksBackendInner { fn invalidate_serialization( &self, task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) { if task_id.is_transient() { return; @@ -1767,7 +1758,7 @@ impl TurboTasksBackendInner { fn get_task_name( &self, task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> String { let mut ctx = self.execute_context(turbo_tasks); let task = ctx.task(task_id, TaskDataCategory::Data); @@ -1788,7 +1779,7 @@ impl TurboTasksBackendInner { fn task_execution_canceled( &self, task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) { let mut ctx = self.execute_context(turbo_tasks); let mut task = ctx.task(task_id, TaskDataCategory::All); @@ -1839,7 +1830,7 @@ impl TurboTasksBackendInner { &self, task_id: TaskId, priority: TaskPriority, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> Option> { let execution_reason; let task_type; @@ -1959,7 +1950,7 @@ impl TurboTasksBackendInner { cell_counters: &AutoMap, 8>, #[cfg(feature = "verify_determinism")] stateful: bool, has_invalidator: bool, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> Option { // Task completion is a 4 step process: // 1. Remove old edges (dependencies, collectibles, children, cells) and update the @@ -2793,7 +2784,7 @@ impl TurboTasksBackendInner { fn run_backend_job<'a>( self: &'a Arc, job: TurboTasksBackendJob, - turbo_tasks: &'a dyn TurboTasksBackendApi>, + turbo_tasks: &'a TurboTasks>, ) -> Pin + Send + 'a>> { Box::pin(async move { match job { @@ -2977,7 +2968,7 @@ impl TurboTasksBackendInner { &self, task_id: TaskId, cell: CellId, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> Result { let mut ctx = self.execute_context(turbo_tasks); let task = ctx.task(task_id, TaskDataCategory::Data); @@ -2993,7 +2984,7 @@ impl TurboTasksBackendInner { task_id: TaskId, collectible_type: TraitTypeId, reader_id: Option, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) -> AutoMap, 1> { let mut ctx = self.execute_context(turbo_tasks); let mut collectibles = AutoMap::default(); @@ -3056,7 +3047,7 @@ impl TurboTasksBackendInner { collectible_type: TraitTypeId, collectible: RawVc, task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) { self.assert_valid_collectible(task_id, collectible); @@ -3084,7 +3075,7 @@ impl TurboTasksBackendInner { collectible: RawVc, count: u32, task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) { self.assert_valid_collectible(task_id, collectible); @@ -3114,7 +3105,7 @@ impl TurboTasksBackendInner { updated_key_hashes: Option>, content_hash: Option, verification_mode: VerificationMode, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) { operation::UpdateCellOperation::run( task_id, @@ -3130,7 +3121,7 @@ impl TurboTasksBackendInner { fn mark_own_task_as_finished( &self, task: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) { let mut ctx = self.execute_context(turbo_tasks); let mut task = ctx.task(task, TaskDataCategory::Data); @@ -3151,7 +3142,7 @@ impl TurboTasksBackendInner { &self, task: TaskId, parent_task: Option, - turbo_tasks: &dyn TurboTasksBackendApi>, + turbo_tasks: &TurboTasks>, ) { self.assert_not_persistent_calling_transient(parent_task, task, None); ConnectChildOperation::run(parent_task, task, self.execute_context(turbo_tasks)); @@ -3168,11 +3159,7 @@ impl TurboTasksBackendInner { task_id } - fn dispose_root_task( - &self, - task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi>, - ) { + fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks>) { #[cfg(feature = "verify_aggregation_graph")] self.root_tasks.lock().remove(&task_id); @@ -3194,11 +3181,7 @@ impl TurboTasksBackendInner { } #[cfg(feature = "verify_aggregation_graph")] - fn verify_aggregation_graph( - &self, - turbo_tasks: &dyn TurboTasksBackendApi>, - idle: bool, - ) { + fn verify_aggregation_graph(&self, turbo_tasks: &TurboTasks>, idle: bool) { if env::var("TURBO_ENGINE_VERIFY_GRAPH").ok().as_deref() == Some("0") { return; } @@ -3456,23 +3439,23 @@ impl TurboTasksBackendInner { } impl Backend for TurboTasksBackend { - fn startup(&self, turbo_tasks: &dyn TurboTasksBackendApi) { + fn startup(&self, turbo_tasks: &TurboTasks) { self.0.startup(turbo_tasks); } - fn stopping(&self, _turbo_tasks: &dyn TurboTasksBackendApi) { + fn stopping(&self, _turbo_tasks: &TurboTasks) { self.0.stopping(); } - fn stop(&self, turbo_tasks: &dyn TurboTasksBackendApi) { + fn stop(&self, turbo_tasks: &TurboTasks) { self.0.stop(turbo_tasks); } - fn idle_start(&self, turbo_tasks: &dyn TurboTasksBackendApi) { + fn idle_start(&self, turbo_tasks: &TurboTasks) { self.0.idle_start(turbo_tasks); } - fn idle_end(&self, _turbo_tasks: &dyn TurboTasksBackendApi) { + fn idle_end(&self, _turbo_tasks: &TurboTasks) { self.0.idle_end(); } @@ -3483,37 +3466,33 @@ impl Backend for TurboTasksBackend { arg: &mut dyn StackDynTaskInputs, parent_task: Option, persistence: TaskPersistence, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> TaskId { self.0 .get_or_create_task(native_fn, this, arg, parent_task, persistence, turbo_tasks) } - fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &dyn TurboTasksBackendApi) { + fn invalidate_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks) { self.0.invalidate_task(task_id, turbo_tasks); } - fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &dyn TurboTasksBackendApi) { + fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks) { self.0.invalidate_tasks(tasks, turbo_tasks); } fn invalidate_tasks_set( &self, tasks: &AutoSet, 2>, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) { self.0.invalidate_tasks_set(tasks, turbo_tasks); } - fn invalidate_serialization( - &self, - task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi, - ) { + fn invalidate_serialization(&self, task_id: TaskId, turbo_tasks: &TurboTasks) { self.0.invalidate_serialization(task_id, turbo_tasks); } - fn task_execution_canceled(&self, task: TaskId, turbo_tasks: &dyn TurboTasksBackendApi) { + fn task_execution_canceled(&self, task: TaskId, turbo_tasks: &TurboTasks) { self.0.task_execution_canceled(task, turbo_tasks) } @@ -3521,7 +3500,7 @@ impl Backend for TurboTasksBackend { &self, task_id: TaskId, priority: TaskPriority, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Option> { self.0 .try_start_task_execution(task_id, priority, turbo_tasks) @@ -3534,7 +3513,7 @@ impl Backend for TurboTasksBackend { cell_counters: &AutoMap, 8>, #[cfg(feature = "verify_determinism")] stateful: bool, has_invalidator: bool, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Option { self.0.task_execution_completed( task_id, @@ -3552,7 +3531,7 @@ impl Backend for TurboTasksBackend { fn run_backend_job<'a>( &'a self, job: Self::BackendJob, - turbo_tasks: &'a dyn TurboTasksBackendApi, + turbo_tasks: &'a TurboTasks, ) -> Pin + Send + 'a>> { self.0.run_backend_job(job, turbo_tasks) } @@ -3562,7 +3541,7 @@ impl Backend for TurboTasksBackend { task_id: TaskId, reader: Option, options: ReadOutputOptions, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Result> { self.0 .try_read_task_output(task_id, reader, options, turbo_tasks) @@ -3574,7 +3553,7 @@ impl Backend for TurboTasksBackend { cell: CellId, reader: Option, options: ReadCellOptions, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Result> { self.0 .try_read_task_cell(task_id, reader, cell, options, turbo_tasks) @@ -3584,7 +3563,7 @@ impl Backend for TurboTasksBackend { &self, task_id: TaskId, cell: CellId, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Result { self.0.try_read_own_task_cell(task_id, cell, turbo_tasks) } @@ -3594,7 +3573,7 @@ impl Backend for TurboTasksBackend { task_id: TaskId, collectible_type: TraitTypeId, reader: Option, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> AutoMap, 1> { self.0 .read_task_collectibles(task_id, collectible_type, reader, turbo_tasks) @@ -3605,7 +3584,7 @@ impl Backend for TurboTasksBackend { collectible_type: TraitTypeId, collectible: RawVc, task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) { self.0 .emit_collectible(collectible_type, collectible, task_id, turbo_tasks) @@ -3617,7 +3596,7 @@ impl Backend for TurboTasksBackend { collectible: RawVc, count: u32, task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) { self.0 .unemit_collectible(collectible_type, collectible, count, task_id, turbo_tasks) @@ -3631,7 +3610,7 @@ impl Backend for TurboTasksBackend { updated_key_hashes: Option>, content_hash: Option, verification_mode: VerificationMode, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) { self.0.update_task_cell( task_id, @@ -3644,11 +3623,7 @@ impl Backend for TurboTasksBackend { ); } - fn mark_own_task_as_finished( - &self, - task_id: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi, - ) { + fn mark_own_task_as_finished(&self, task_id: TaskId, turbo_tasks: &TurboTasks) { self.0.mark_own_task_as_finished(task_id, turbo_tasks); } @@ -3656,7 +3631,7 @@ impl Backend for TurboTasksBackend { &self, task: TaskId, parent_task: Option, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) { self.0.connect_task(task, parent_task, turbo_tasks); } @@ -3664,12 +3639,12 @@ impl Backend for TurboTasksBackend { fn create_transient_task( &self, task_type: TransientTaskType, - _turbo_tasks: &dyn TurboTasksBackendApi, + _turbo_tasks: &TurboTasks, ) -> TaskId { self.0.create_transient_task(task_type) } - fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &dyn TurboTasksBackendApi) { + fn dispose_root_task(&self, task_id: TaskId, turbo_tasks: &TurboTasks) { self.0.dispose_root_task(task_id, turbo_tasks); } @@ -3681,7 +3656,7 @@ impl Backend for TurboTasksBackend { self.0.options.dependency_tracking } - fn get_task_name(&self, task: TaskId, turbo_tasks: &dyn TurboTasksBackendApi) -> String { + fn get_task_name(&self, task: TaskId, turbo_tasks: &TurboTasks) -> String { self.0.get_task_name(task, turbo_tasks) } } diff --git a/turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs b/turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs index fa686e558957..bedb94e8e38b 100644 --- a/turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs +++ b/turbopack/crates/turbo-tasks-backend/src/backend/operation/mod.rs @@ -19,7 +19,7 @@ use tracing::info_span; use tracing::trace_span; use turbo_tasks::{ CellId, DynTaskInputs, FxIndexMap, RawVc, SharedReference, TaskExecutionReason, TaskId, - TaskPriority, TurboTasksBackendApi, TurboTasksCallApi, backend::CachedTaskTypeArc, + TaskPriority, TurboTasks, TurboTasksCallApi, backend::CachedTaskTypeArc, macro_helpers::NativeFunction, }; @@ -168,7 +168,7 @@ impl TaskLockCounter { pub struct ExecuteContextImpl<'e, B: BackingStorage> { backend: &'e TurboTasksBackendInner, - turbo_tasks: &'e dyn TurboTasksBackendApi>, + turbo_tasks: &'e TurboTasks>, _operation_guard: Option>, task_lock_counter: TaskLockCounter, } @@ -176,7 +176,7 @@ pub struct ExecuteContextImpl<'e, B: BackingStorage> { impl<'e, B: BackingStorage> ExecuteContextImpl<'e, B> { pub(super) fn new( backend: &'e TurboTasksBackendInner, - turbo_tasks: &'e dyn TurboTasksBackendApi>, + turbo_tasks: &'e TurboTasks>, ) -> Self { Self { backend, @@ -1018,7 +1018,7 @@ impl<'e, B: BackingStorage> ExecuteContext<'e> for ExecuteContextImpl<'e, B> { struct ChildExecuteContextImpl<'e, B: BackingStorage> { backend: &'e TurboTasksBackendInner, - turbo_tasks: &'e dyn TurboTasksBackendApi>, + turbo_tasks: &'e TurboTasks>, } impl<'e, B: BackingStorage> ChildExecuteContext<'e> for ChildExecuteContextImpl<'e, B> { diff --git a/turbopack/crates/turbo-tasks/src/backend.rs b/turbopack/crates/turbo-tasks/src/backend.rs index a23282829a27..8baecf06cafb 100644 --- a/turbopack/crates/turbo-tasks/src/backend.rs +++ b/turbopack/crates/turbo-tasks/src/backend.rs @@ -35,7 +35,7 @@ use crate::{ dyn_task_inputs::{DynTaskInputs, StackDynTaskInputs}, event::EventListener, macro_helpers::NativeFunction, - manager::{TaskPersistence, TurboTasksBackendApi}, + manager::{TaskPersistence, TurboTasks}, raw_vc::CellId, registry, task::shared_reference::TypedSharedReference, @@ -594,40 +594,35 @@ pub enum VerificationMode { Skip, } -pub trait Backend: Sync + Send { +pub trait Backend: Sized + Sync + Send { #[allow(unused_variables)] - fn startup(&self, turbo_tasks: &dyn TurboTasksBackendApi) {} + fn startup(&self, turbo_tasks: &TurboTasks) {} #[allow(unused_variables)] - fn stop(&self, turbo_tasks: &dyn TurboTasksBackendApi) {} + fn stop(&self, turbo_tasks: &TurboTasks) {} #[allow(unused_variables)] - fn stopping(&self, turbo_tasks: &dyn TurboTasksBackendApi) {} + fn stopping(&self, turbo_tasks: &TurboTasks) {} #[allow(unused_variables)] - fn idle_start(&self, turbo_tasks: &dyn TurboTasksBackendApi) {} + fn idle_start(&self, turbo_tasks: &TurboTasks) {} #[allow(unused_variables)] - fn idle_end(&self, turbo_tasks: &dyn TurboTasksBackendApi) {} + fn idle_end(&self, turbo_tasks: &TurboTasks) {} - fn invalidate_task(&self, task: TaskId, turbo_tasks: &dyn TurboTasksBackendApi); + fn invalidate_task(&self, task: TaskId, turbo_tasks: &TurboTasks); - fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &dyn TurboTasksBackendApi); - fn invalidate_tasks_set(&self, tasks: &TaskIdSet, turbo_tasks: &dyn TurboTasksBackendApi); + fn invalidate_tasks(&self, tasks: &[TaskId], turbo_tasks: &TurboTasks); + fn invalidate_tasks_set(&self, tasks: &TaskIdSet, turbo_tasks: &TurboTasks); - fn invalidate_serialization( - &self, - _task: TaskId, - _turbo_tasks: &dyn TurboTasksBackendApi, - ) { - } + fn invalidate_serialization(&self, _task: TaskId, _turbo_tasks: &TurboTasks) {} fn try_start_task_execution<'a>( &'a self, task: TaskId, priority: TaskPriority, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Option>; - fn task_execution_canceled(&self, task: TaskId, turbo_tasks: &dyn TurboTasksBackendApi); + fn task_execution_canceled(&self, task: TaskId, turbo_tasks: &TurboTasks); /// Called when a task's execution finishes. /// @@ -641,7 +636,7 @@ pub trait Backend: Sync + Send { cell_counters: &AutoMap, 8>, #[cfg(feature = "verify_determinism")] stateful: bool, has_invalidator: bool, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Option; type BackendJob: Send + 'static; @@ -649,7 +644,7 @@ pub trait Backend: Sync + Send { fn run_backend_job<'a>( &'a self, job: Self::BackendJob, - turbo_tasks: &'a dyn TurboTasksBackendApi, + turbo_tasks: &'a TurboTasks, ) -> Pin + Send + 'a>>; /// INVALIDATION: Be careful with this, when reader is None, it will not track dependencies, so @@ -659,7 +654,7 @@ pub trait Backend: Sync + Send { task: TaskId, reader: Option, options: ReadOutputOptions, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Result>; /// INVALIDATION: Be careful with this, when reader is None, it will not track dependencies, so @@ -670,7 +665,7 @@ pub trait Backend: Sync + Send { index: CellId, reader: Option, options: ReadCellOptions, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Result>; /// INVALIDATION: Be careful with this, it will not track dependencies, so @@ -679,7 +674,7 @@ pub trait Backend: Sync + Send { &self, current_task: TaskId, index: CellId, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> Result; /// INVALIDATION: Be careful with this, when reader is None, it will not track dependencies, so @@ -689,7 +684,7 @@ pub trait Backend: Sync + Send { task: TaskId, trait_id: TraitTypeId, reader: Option, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> TaskCollectiblesMap; fn emit_collectible( @@ -697,7 +692,7 @@ pub trait Backend: Sync + Send { trait_type: TraitTypeId, collectible: RawVc, task: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ); fn unemit_collectible( @@ -706,7 +701,7 @@ pub trait Backend: Sync + Send { collectible: RawVc, count: u32, task: TaskId, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ); fn update_task_cell( @@ -717,7 +712,7 @@ pub trait Backend: Sync + Send { updated_key_hashes: Option>, content_hash: Option, verification_mode: VerificationMode, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ); fn get_or_create_task( @@ -727,31 +722,27 @@ pub trait Backend: Sync + Send { arg: &mut dyn StackDynTaskInputs, parent_task: Option, persistence: TaskPersistence, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> TaskId; fn connect_task( &self, task: TaskId, parent_task: Option, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ); - fn mark_own_task_as_finished( - &self, - _task: TaskId, - _turbo_tasks: &dyn TurboTasksBackendApi, - ) { + fn mark_own_task_as_finished(&self, _task: TaskId, _turbo_tasks: &TurboTasks) { // Do nothing by default } fn create_transient_task( &self, task_type: TransientTaskType, - turbo_tasks: &dyn TurboTasksBackendApi, + turbo_tasks: &TurboTasks, ) -> TaskId; - fn dispose_root_task(&self, task: TaskId, turbo_tasks: &dyn TurboTasksBackendApi); + fn dispose_root_task(&self, task: TaskId, turbo_tasks: &TurboTasks); fn task_statistics(&self) -> &TaskStatisticsApi; @@ -759,7 +750,7 @@ pub trait Backend: Sync + Send { /// Returns a human-readable name for the given task. Used by error display formatting /// to lazily resolve task names instead of storing them eagerly in error objects. - fn get_task_name(&self, task: TaskId, turbo_tasks: &dyn TurboTasksBackendApi) -> String; + fn get_task_name(&self, task: TaskId, turbo_tasks: &TurboTasks) -> String; } #[cfg(test)] diff --git a/turbopack/crates/turbo-tasks/src/lib.rs b/turbopack/crates/turbo-tasks/src/lib.rs index 4c5a4bee6a6d..d50c06784a59 100644 --- a/turbopack/crates/turbo-tasks/src/lib.rs +++ b/turbopack/crates/turbo-tasks/src/lib.rs @@ -96,10 +96,10 @@ pub use crate::{ join_iter_ext::{JoinIterExt, TryFlatJoinIterExt, TryJoinIterExt}, manager::{ CurrentCellRef, ReadCellTracking, ReadConsistency, ReadTracking, TaskPersistence, - TaskPriority, TurboTasks, TurboTasksApi, TurboTasksBackendApi, TurboTasksCallApi, Unused, - UpdateInfo, dynamic_call, emit, get_serialization_invalidator, mark_finished, - mark_stateful, mark_top_level_task, prevent_gc, run, run_once, run_once_with_reason, - trait_call, turbo_tasks, turbo_tasks_scope, turbo_tasks_weak, + TaskPriority, TurboTasks, TurboTasksApi, TurboTasksCallApi, Unused, UpdateInfo, + dynamic_call, emit, get_serialization_invalidator, mark_finished, mark_stateful, + mark_top_level_task, prevent_gc, run, run_once, run_once_with_reason, trait_call, + turbo_tasks, turbo_tasks_scope, turbo_tasks_weak, unmark_top_level_task_may_leak_eventually_consistent_state, with_turbo_tasks, }, mapped_read_ref::MappedReadRef, diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index 58e6b3a3edf6..c40a19b0d275 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -37,8 +37,7 @@ use crate::{ capture_future::CaptureFuture, dyn_task_inputs::StackDynTaskInputs, event::{Event, EventListener}, - id::{ExecutionId, LocalTaskId, TRANSIENT_TASK_BIT, TraitTypeId}, - id_factory::IdFactoryWithReuse, + id::{ExecutionId, LocalTaskId, TraitTypeId}, keyed::KeyedEq, local_task_tracker::LocalTaskTracker, macro_helpers::NativeFunction, @@ -53,8 +52,8 @@ use crate::{ util::{IdFactory, StaticOrArc}, }; -/// Common base trait for [`TurboTasksApi`] and [`TurboTasksBackendApi`]. Provides APIs for creating -/// tasks from function calls. +/// Common base trait for [`TurboTasksApi`] and [`TurboTasks`]. Provides APIs for creating tasks +/// from function calls. pub trait TurboTasksCallApi: Sync + Send { /// Calls a native function with arguments. Resolves arguments when needed /// with a wrapper task. @@ -227,46 +226,6 @@ impl Unused { } } -/// A subset of the [`TurboTasks`] API that's exposed to [`Backend`] implementations. -pub trait TurboTasksBackendApi: TurboTasksCallApi + Sync + Send { - fn pin(&self) -> Arc>; - - fn get_fresh_persistent_task_id(&self) -> Unused; - fn get_fresh_transient_task_id(&self) -> Unused; - /// # Safety - /// - /// The caller must ensure that the task id is not used anymore. - unsafe fn reuse_persistent_task_id(&self, id: Unused); - /// # Safety - /// - /// The caller must ensure that the task id is not used anymore. - unsafe fn reuse_transient_task_id(&self, id: Unused); - - /// Schedule a task for execution. - fn schedule(&self, task: TaskId, priority: TaskPriority); - - /// Returns the priority of the current task. - fn get_current_task_priority(&self) -> TaskPriority; - - /// Schedule a foreground backend job for execution. - fn schedule_backend_foreground_job(&self, job: B::BackendJob); - - /// Schedule a background backend job for execution. - /// - /// Background jobs are not counted towards activeness of the system. The system is considered - /// idle even with active background jobs. - fn schedule_backend_background_job(&self, job: B::BackendJob); - - /// Returns the duration from the start of the program to the given instant. - fn program_duration_until(&self, instant: Instant) -> Duration; - - /// Returns true if the system is idle. - fn is_idle(&self) -> bool; - - /// Returns a reference to the backend. - fn backend(&self) -> &B; -} - #[allow(clippy::manual_non_exhaustive)] pub struct UpdateInfo { pub duration: Duration, @@ -478,8 +437,6 @@ enum ScheduledTask { pub struct TurboTasks { this: Weak, backend: B, - task_id_factory: IdFactoryWithReuse, - transient_task_id_factory: IdFactoryWithReuse, execution_id_factory: IdFactory, stopped: AtomicBool, currently_scheduled_foreground_jobs: AtomicUsize, @@ -496,7 +453,6 @@ pub struct TurboTasks { event_foreground_done: Event, /// Event that is triggered when all background jobs are done event_background_done: Event, - program_start: Instant, compilation_events: CompilationEventQueue, } @@ -605,18 +561,10 @@ impl TurboTasks { // so we probably want to make sure that all tasks are joined // when trying to drop turbo tasks pub fn new(backend: B) -> Arc { - let task_id_factory = IdFactoryWithReuse::new( - TaskId::MIN, - TaskId::try_from(TRANSIENT_TASK_BIT - 1).unwrap(), - ); - let transient_task_id_factory = - IdFactoryWithReuse::new(TaskId::try_from(TRANSIENT_TASK_BIT).unwrap(), TaskId::MAX); let execution_id_factory = IdFactory::new(ExecutionId::MIN, ExecutionId::MAX); let this = Arc::new_cyclic(|this| Self { this: this.clone(), backend, - task_id_factory, - transient_task_id_factory, execution_id_factory, stopped: AtomicBool::new(false), currently_scheduled_foreground_jobs: AtomicUsize::new(0), @@ -634,7 +582,6 @@ impl TurboTasks { event_background_done: Event::new(|| { || "TurboTasks::event_background_done".to_string() }), - program_start: Instant::now(), compilation_events: CompilationEventQueue::default(), }); this.backend.startup(&*this); @@ -835,7 +782,7 @@ impl TurboTasks { } #[track_caller] - pub(crate) fn schedule(&self, task_id: TaskId, priority: TaskPriority) { + pub fn schedule(&self, task_id: TaskId, priority: TaskPriority) { self.begin_foreground_job(); self.scheduled_tasks.fetch_add(1, Ordering::AcqRel); @@ -1095,26 +1042,6 @@ impl TurboTasks { .await; } - #[track_caller] - pub(crate) fn schedule_foreground_job(&self, func: T) - where - T: AsyncFnOnce(Arc>) -> Arc> + Send + 'static, - T::CallOnceFuture: Send, - { - let mut this = self.pin(); - this.begin_foreground_job(); - tokio::spawn( - TURBO_TASKS - .scope(this.clone(), async move { - if !this.stopped.load(Ordering::Acquire) { - this = func(this.clone()).await; - } - this.finish_foreground_job(); - }) - .in_current_span(), - ); - } - #[track_caller] pub(crate) fn schedule_background_job(&self, func: T) where @@ -1149,6 +1076,26 @@ impl TurboTasks { pub fn backend(&self) -> &B { &self.backend } + + pub fn get_current_task_priority(&self) -> TaskPriority { + CURRENT_TASK_STATE + .try_with(|task_state| task_state.read().unwrap().priority) + .unwrap_or(TaskPriority::initial()) + } + + pub fn is_idle(&self) -> bool { + self.currently_scheduled_foreground_jobs + .load(Ordering::Acquire) + == 0 + } + + #[track_caller] + pub fn schedule_backend_background_job(&self, job: B::BackendJob) { + self.schedule_background_job(async move |this| { + this.backend.run_backend_job(job, &*this).await; + this + }) + } } struct TurboTasksExecutor; @@ -1622,70 +1569,6 @@ impl TurboTasksApi for TurboTasks { } } -impl TurboTasksBackendApi for TurboTasks { - fn pin(&self) -> Arc> { - self.pin() - } - fn backend(&self) -> &B { - &self.backend - } - - #[track_caller] - fn schedule_backend_background_job(&self, job: B::BackendJob) { - self.schedule_background_job(async move |this| { - this.backend.run_backend_job(job, &*this).await; - this - }) - } - - #[track_caller] - fn schedule_backend_foreground_job(&self, job: B::BackendJob) { - self.schedule_foreground_job(async move |this| { - this.backend.run_backend_job(job, &*this).await; - this - }) - } - - #[track_caller] - fn schedule(&self, task: TaskId, priority: TaskPriority) { - self.schedule(task, priority) - } - - fn get_current_task_priority(&self) -> TaskPriority { - CURRENT_TASK_STATE - .try_with(|task_state| task_state.read().unwrap().priority) - .unwrap_or(TaskPriority::initial()) - } - - fn program_duration_until(&self, instant: Instant) -> Duration { - instant - self.program_start - } - - fn get_fresh_persistent_task_id(&self) -> Unused { - // SAFETY: This is a fresh id from the factory - unsafe { Unused::new_unchecked(self.task_id_factory.get()) } - } - - fn get_fresh_transient_task_id(&self) -> Unused { - // SAFETY: This is a fresh id from the factory - unsafe { Unused::new_unchecked(self.transient_task_id_factory.get()) } - } - - unsafe fn reuse_persistent_task_id(&self, id: Unused) { - unsafe { self.task_id_factory.reuse(id.into()) } - } - - unsafe fn reuse_transient_task_id(&self, id: Unused) { - unsafe { self.transient_task_id_factory.reuse(id.into()) } - } - - fn is_idle(&self) -> bool { - self.currently_scheduled_foreground_jobs - .load(Ordering::Acquire) - == 0 - } -} - async fn wait_for_local_tasks() { let listener = CURRENT_TASK_STATE.with(|ts| ts.read().unwrap().local_tasks.listen_for_in_flight()); diff --git a/turbopack/crates/turbo-tasks/src/task/local_task.rs b/turbopack/crates/turbo-tasks/src/task/local_task.rs index 8aab78684890..0e58472f0862 100644 --- a/turbopack/crates/turbo-tasks/src/task/local_task.rs +++ b/turbopack/crates/turbo-tasks/src/task/local_task.rs @@ -4,7 +4,7 @@ use anyhow::{Result, bail}; use crate::{ CellId, DynTaskInputs, OutputContent, OwnedStackDynTaskInputs, RawVc, TaskPersistence, - TraitMethod, TurboTasksBackendApi, ValueTypeId, backend::Backend, event::Event, + TraitMethod, TurboTasks, ValueTypeId, backend::Backend, event::Event, macro_helpers::NativeFunction, registry, }; @@ -55,7 +55,7 @@ impl LocalTaskType { mut this: Option, arg: &dyn DynTaskInputs, persistence: TaskPersistence, - turbo_tasks: Arc>, + turbo_tasks: Arc>, ) -> Result { if let Some(this) = this.as_mut() { *this = this.resolve().await?; @@ -70,7 +70,7 @@ impl LocalTaskType { this: RawVc, arg: &dyn DynTaskInputs, persistence: TaskPersistence, - turbo_tasks: Arc>, + turbo_tasks: Arc>, ) -> Result { let this = this.resolve().await?; let RawVc::TaskCell(_, CellId { type_id, .. }) = this else { From 8bb6aa9708fb3e4a816e74108d4c6599915af80c Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 2 Jun 2026 21:34:21 +0200 Subject: [PATCH 2/9] Delay Node Fizz piping until after render task (#94347) ### What? Delay dynamic Node Fizz piping by one React render task after `onShellReady()`, and keep the dedicated Node-stream CI jobs scoped to `test/use-node-streams-tests-manifest.json`. ### Why? The Node adapter started `pipeable.pipe(pt)` inside `onShellReady()`, before the existing continuation delay could take effect. In App Router renders backed by RSC, that caused short-lived Suspense boundaries to flush fallback and reveal markup that the web-stream path avoids by waiting before its first pull. The previous test assertion relaxations hid this runtime mismatch. The original assertions pass once piping starts after the settling window, so they have been restored. The workflow correction remains necessary because merging the cache-components manifest into the ordinary Node-stream jobs filtered out relevant e2e coverage. ### How? - Resolve the Node Fizz shell promise from `onShellReady()` without immediately piping. - For dynamic renders, wait one React render task and then start piping into the `PassThrough`. - Preserve the existing `onAllReady()` behavior for static generation. - Use the Node-stream manifest alone for ordinary Node-stream dev and prod CI jobs. ### Verification - `pnpm --filter=next build` - `__NEXT_USE_NODE_STREAMS=true __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts` - `__NEXT_USE_NODE_STREAMS=true __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true NEXT_TEST_PREFER_OFFLINE=1 pnpm test-dev-turbo test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts` --- .github/workflows/build_and_test.yml | 4 ++-- packages/next/src/server/app-render/stream-ops.node.ts | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 799678988275..42c54b6c6bb8 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -1100,7 +1100,7 @@ jobs: with: afterBuild: | export __NEXT_USE_NODE_STREAMS=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/cache-components-tests-manifest.json,test/use-node-streams-tests-manifest.json" + export NEXT_EXTERNAL_TESTS_FILTERS="test/use-node-streams-tests-manifest.json" export NEXT_TEST_MODE=dev export IS_TURBOPACK_TEST=1 export TURBOPACK_DEV=1 @@ -1127,7 +1127,7 @@ jobs: with: afterBuild: | export __NEXT_USE_NODE_STREAMS=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/cache-components-tests-manifest.json,test/use-node-streams-tests-manifest.json" + export NEXT_EXTERNAL_TESTS_FILTERS="test/use-node-streams-tests-manifest.json" export NEXT_TEST_MODE=start export IS_TURBOPACK_TEST=1 export TURBOPACK_BUILD=1 diff --git a/packages/next/src/server/app-render/stream-ops.node.ts b/packages/next/src/server/app-render/stream-ops.node.ts index 5e4e80b26253..90dc34d9aadb 100644 --- a/packages/next/src/server/app-render/stream-ops.node.ts +++ b/packages/next/src/server/app-render/stream-ops.node.ts @@ -614,9 +614,6 @@ export async function renderToNodeFizzStream( onHeaders: streamOptions?.onHeaders, onShellReady() { streamOptions?.onShellReady?.() - if (!deferPipe) { - pipeable.pipe(pt) - } shellReady.resolve() }, onShellError(error: unknown) { @@ -636,6 +633,11 @@ export async function renderToNodeFizzStream( await shellReady.promise + if (!deferPipe) { + await waitAtLeastOneReactRenderTask() + pipeable.pipe(pt) + } + return { stream: pt, allReady: allReady.promise, From b92cf40ee6e4f201f896a9a8be4039e8a5eaf77a Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Tue, 2 Jun 2026 21:34:22 +0200 Subject: [PATCH 3/9] Enable Node streams by default (#94311) ### What? Defaults `experimental.useNodeStreams` to `true` while keeping the experimental config flag and explicit `false` opt-out available. Removes redundant dedicated node-stream CI jobs because the ordinary path exercises Node streams after this change. Depends on the standalone test compatibility update in #94347, which intentionally remains outside the Node streams PR stack. ### Why? Node streams can be exercised through the standard Node.js App Router test path once they are the default, allowing a smaller rollout before the larger follow-up that removes the flag and obsolete plumbing. CI exposed one necessary compatibility fix for this intermediate state: projects with a `next.config.*` received the new default after raw experimental-feature processing, leaving the runtime stream selector on the web implementation while compiled app code selected Node stream helpers. ### How? - Set `defaultConfig.experimental.useNodeStreams` to `true`. - Synchronize `__NEXT_USE_NODE_STREAMS` from the fully resolved config, including cached and standalone resolved configs, so defaults and adapter modifications are reflected at runtime. - Preserve the current public config/schema and explicit `experimental: { useNodeStreams: false }` opt-out; edge bundles continue to define the stream flag as `false`. - Remove the dedicated node-stream test jobs and now-unused manifest; the affected streaming assertion compatibility changes are isolated in #94347. ### Verification Run before extracting the prerequisite assertion changes into #94347: - `pnpm --filter=next types` - `pnpm --filter=next build` - `IS_WEBPACK_TEST=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm test-start-webpack test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts` - `IS_TURBOPACK_TEST=1 TURBOPACK_BUILD=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm test-start-turbo test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts` - `IS_WEBPACK_TEST=1 __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true pnpm test-start-webpack test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts` - `pnpm test-dev-webpack test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts` - `pnpm test-dev-turbo test/e2e/app-dir/ppr-root-param-fallback/ppr-root-param-fallback.test.ts test/e2e/app-dir/use-server-inserted-html/use-server-inserted-html.test.ts` - `pnpm test-start-webpack test/production/app-dir/use-node-streams-env-precedence/use-node-streams-env-precedence.test.ts` - `pnpm test-start-webpack test/e2e/app-dir/app/standalone.test.ts` --- .github/workflows/build_and_test.yml | 132 ---------------------- packages/next-env/index.ts | 12 +- packages/next/src/server/config-shared.ts | 1 + packages/next/src/server/config.ts | 32 ++++-- test/unit/preserve-process-env.test.ts | 22 +++- test/use-node-streams-tests-manifest.json | 13 --- 6 files changed, 55 insertions(+), 157 deletions(-) delete mode 100644 test/use-node-streams-tests-manifest.json diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 42c54b6c6bb8..9fef923b17a4 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -1013,134 +1013,6 @@ jobs: stepName: 'test-cache-components-prod-${{ matrix.group }}' secrets: inherit - test-node-streams-cache-components-dev: - name: test node streams cache components dev - needs: - [ - 'optimize-ci', - 'changes', - 'build-native', - 'build-next', - 'fetch-test-timings', - ] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: | - export __NEXT_USE_NODE_STREAMS=true - export __NEXT_CACHE_COMPONENTS=true - export __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS=true - export __NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/cache-components-tests-manifest.json" - export NEXT_TEST_MODE=dev - export IS_TURBOPACK_TEST=1 - export TURBOPACK_DEV=1 - - node run-tests.js \ - --timings \ - --require-timings \ - -g ${{ matrix.group }} \ - --type development - testTimingsArtifact: 'test-timings' - stepName: 'test-node-streams-cache-components-dev-${{ matrix.group }}' - secrets: inherit - - test-node-streams-cache-components-prod: - name: test node streams cache components prod - needs: - [ - 'optimize-ci', - 'changes', - 'build-native', - 'build-next', - 'fetch-test-timings', - ] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/7, 2/7, 3/7, 4/7, 5/7, 6/7, 7/7] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: | - export __NEXT_USE_NODE_STREAMS=true - export __NEXT_CACHE_COMPONENTS=true - export __NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS=true - export __NEXT_EXPERIMENTAL_APP_NEW_SCROLL_HANDLER=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/cache-components-tests-manifest.json" - export NEXT_TEST_MODE=start - export IS_TURBOPACK_TEST=1 - export TURBOPACK_BUILD=1 - - node run-tests.js \ - --timings \ - --require-timings \ - -g ${{ matrix.group }} \ - --type production - testTimingsArtifact: 'test-timings' - stepName: 'test-node-streams-cache-components-prod-${{ matrix.group }}' - secrets: inherit - - test-node-streams-dev: - name: test node streams dev - needs: ['optimize-ci', 'changes', 'build-native', 'build-next'] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/6, 2/6, 3/6, 4/6, 5/6, 6/6] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: | - export __NEXT_USE_NODE_STREAMS=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/use-node-streams-tests-manifest.json" - export NEXT_TEST_MODE=dev - export IS_TURBOPACK_TEST=1 - export TURBOPACK_DEV=1 - export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - export RUST_BACKTRACE=1 - - node run-tests.js \ - --timings \ - -g ${{ matrix.group }} \ - --type development - stepName: 'test-node-streams-dev-${{ matrix.group }}' - secrets: inherit - - test-node-streams-prod: - name: test node streams prod - needs: ['optimize-ci', 'changes', 'build-native', 'build-next'] - if: ${{ needs.optimize-ci.outputs.skip == 'false' && needs.changes.outputs.docs-only == 'false' }} - - strategy: - fail-fast: false - matrix: - group: [1/7, 2/7, 3/7, 4/7, 5/7, 6/7, 7/7] - uses: ./.github/workflows/build_reusable.yml - with: - afterBuild: | - export __NEXT_USE_NODE_STREAMS=true - export NEXT_EXTERNAL_TESTS_FILTERS="test/use-node-streams-tests-manifest.json" - export NEXT_TEST_MODE=start - export IS_TURBOPACK_TEST=1 - export TURBOPACK_BUILD=1 - export __NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES=true - export RUST_BACKTRACE=1 - - node run-tests.js \ - --timings \ - -g ${{ matrix.group }} \ - --type production - stepName: 'test-node-streams-prod-${{ matrix.group }}' - secrets: inherit - tests-pass: needs: [ @@ -1157,10 +1029,6 @@ jobs: 'test-firefox-safari', 'test-cache-components-dev', 'test-cache-components-prod', - 'test-node-streams-cache-components-dev', - 'test-node-streams-cache-components-prod', - 'test-node-streams-dev', - 'test-node-streams-prod', 'test-cargo-unit', 'rust-check', 'rustdoc-check', diff --git a/packages/next-env/index.ts b/packages/next-env/index.ts index 392670c7817e..5b34e68be666 100644 --- a/packages/next-env/index.ts +++ b/packages/next-env/index.ts @@ -18,7 +18,17 @@ let cachedLoadedEnvFiles: LoadedEnvFiles = [] let previousLoadedEnvFiles: LoadedEnvFiles = [] export function updateInitialEnv(newEnv: Env) { - Object.assign(initialEnv || {}, newEnv) + if (!initialEnv) { + return + } + + for (const [key, value] of Object.entries(newEnv)) { + if (value === undefined) { + delete initialEnv[key] + } else { + initialEnv[key] = value + } + } } type Log = { diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 8da5f855611e..fe771f1b5ff3 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -2040,6 +2040,7 @@ export const defaultConfig = Object.freeze({ gestureTransition: false, inlineCss: false, useCache: undefined, + useNodeStreams: true, slowModuleDetection: undefined, globalNotFound: false, browserDebugInfoInTerminal: 'warn', diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index c61f17984c65..937c481487eb 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1620,9 +1620,28 @@ function finalizeConfig(config: NextConfigComplete): NextConfigComplete { validationLevel: config.experimental.instantInsights?.validationLevel ?? 'manual-warning', } + syncUseNodeStreamsEnv(config) return config } +function syncUseNodeStreamsEnv(config: NextConfig): void { + // This must use resolved config: user configs are inspected before defaults + // are merged, while runtime bundles must select the default implementation. + const useNodeStreams = config.experimental?.useNodeStreams + ? 'true' + : undefined + + if (useNodeStreams) { + process.env.__NEXT_USE_NODE_STREAMS = useNodeStreams + } else { + delete process.env.__NEXT_USE_NODE_STREAMS + } + + // Dev env reloads restore process.env from this snapshot. Preserve the + // resolved runtime selection so a reload cannot mix stream implementations. + updateInitialEnv({ __NEXT_USE_NODE_STREAMS: useNodeStreams }) +} + async function applyModifyConfig( config: NextConfigComplete, phase: PHASE_TYPE, @@ -1750,6 +1769,7 @@ export default async function loadConfig( return cachedResult.rawConfig } + syncUseNodeStreamsEnv(cachedResult.config) return cachedResult.config } else { // Reset next.config errors before loading config @@ -1777,6 +1797,8 @@ export default async function loadConfig( process.env.__NEXT_PRIVATE_STANDALONE_CONFIG ) + syncUseNodeStreamsEnv(standaloneConfig) + // Cache the standalone config configCache.set(cacheKey, { config: standaloneConfig, @@ -2229,16 +2251,6 @@ function enforceExperimentalFeatures( config.experimental.useNodeStreams = true } - // Keep runtime bundle selection env in sync with the resolved config. - // Explicit user config (e.g. useNodeStreams: false) should win over an - // inherited shell env var to avoid selecting nodestream runtime bundles - // while define-env compiled user bundles with node streams disabled. - if (config.experimental.useNodeStreams) { - process.env.__NEXT_USE_NODE_STREAMS = 'true' - } else { - delete process.env.__NEXT_USE_NODE_STREAMS - } - // TODO: Remove this once strictRouteTypes is the default. if ( process.env.__NEXT_EXPERIMENTAL_STRICT_ROUTE_TYPES === 'true' && diff --git a/test/unit/preserve-process-env.test.ts b/test/unit/preserve-process-env.test.ts index cbddc233040e..174ea9f764d2 100644 --- a/test/unit/preserve-process-env.test.ts +++ b/test/unit/preserve-process-env.test.ts @@ -1,4 +1,8 @@ -import { loadEnvConfig } from '../../packages/next-env/' +import { + loadEnvConfig, + resetEnv, + updateInitialEnv, +} from '../../packages/next-env/' describe('preserve process env', () => { it('should not reassign `process.env`', () => { @@ -6,4 +10,20 @@ describe('preserve process env', () => { loadEnvConfig('.') expect(Object.is(originalProcessEnv, process.env)).toBeTrue() }) + + it('should remove values unset in the initial env snapshot', () => { + const key = '__NEXT_TEST_UNSET_INITIAL_ENV' + + try { + loadEnvConfig('.') + process.env[key] = 'changed' + updateInitialEnv({ [key]: undefined }) + + resetEnv() + + expect(process.env[key]).toBeUndefined() + } finally { + delete process.env[key] + } + }) }) diff --git a/test/use-node-streams-tests-manifest.json b/test/use-node-streams-tests-manifest.json deleted file mode 100644 index 46c1c103b37e..000000000000 --- a/test/use-node-streams-tests-manifest.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "version": 2, - "suites": {}, - "rules": { - "include": [ - "test/e2e/**/*.test.{t,j}s{,x}", - "test/production/app-*/**/*.test.{t,j}s{,x}", - "test/development/app-*/**/*.test.{t,j}s{,x}", - "test/development/acceptance-app/**/*.test.{t,j}s{,x}" - ], - "exclude": [] - } -} From ad175bb874038e31ccd046b8ddc722431d52d701 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 2 Jun 2026 21:46:07 +0200 Subject: [PATCH 4/9] [tests] Use snapshots in debug-build-paths (#94352) Make sure that we don't accidentally match too many pages --- .../debug-build-paths.test.ts | 226 ++++++++++-------- 1 file changed, 124 insertions(+), 102 deletions(-) diff --git a/test/production/debug-build-path/debug-build-paths.test.ts b/test/production/debug-build-path/debug-build-paths.test.ts index ac382337ebd2..a70ab6827bb3 100644 --- a/test/production/debug-build-path/debug-build-paths.test.ts +++ b/test/production/debug-build-path/debug-build-paths.test.ts @@ -1,11 +1,33 @@ import path from 'path' import { nextTestSetup } from 'e2e-utils' +function getTreeView(cliOutput: string): string { + let foundStart = false + const lines: string[] = [] + + for (const line of cliOutput.split('\n')) { + foundStart ||= line.startsWith('Route ') + + if (foundStart) { + lines.push(line) + } + + if (line.startsWith('└')) { + foundStart = false + } + } + + return lines.join('\n').trim() +} + describe('debug-build-paths', () => { describe('default fixture', () => { const { next } = nextTestSetup({ files: path.join(__dirname, 'fixtures/default'), skipStart: true, + env: { + __NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT: '1', + }, }) describe('explicit path formats', () => { @@ -17,12 +39,11 @@ describe('debug-build-paths', () => { expect(buildResult.cliOutput).toBeDefined() // Should only build the specified page - expect(buildResult.cliOutput).toContain('Route (pages)') - expect(buildResult.cliOutput).toContain('○ /foo') - // Should not build other pages - expect(buildResult.cliOutput).not.toContain('○ /bar') - // Should not build app routes - expect(buildResult.cliOutput).not.toContain('Route (app)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (pages) + ┌ ○ /404 + └ ○ /foo" + `) }) it('should build multiple pages routes', async () => { @@ -33,11 +54,12 @@ describe('debug-build-paths', () => { expect(buildResult.cliOutput).toBeDefined() // Should build both specified pages - expect(buildResult.cliOutput).toContain('Route (pages)') - expect(buildResult.cliOutput).toContain('○ /foo') - expect(buildResult.cliOutput).toContain('○ /bar') - // Should not build app routes - expect(buildResult.cliOutput).not.toContain('Route (app)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (pages) + ┌ ○ /404 + ├ ○ /bar + └ ○ /foo" + `) }) it('should build dynamic route with literal [slug] path', async () => { @@ -50,14 +72,11 @@ describe('debug-build-paths', () => { expect(buildResult.cliOutput).toBeDefined() // Should build only the blog/[slug] route - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('/blog/[slug]') - // Should not build other app routes - expect(buildResult.cliOutput).not.toMatch(/○ \/\n/) - expect(buildResult.cliOutput).not.toContain('○ /about') - expect(buildResult.cliOutput).not.toContain('○ /dashboard') - // Should not build pages routes - expect(buildResult.cliOutput).not.toContain('Route (pages)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ƒ /blog/[slug]" + `) }) }) @@ -69,17 +88,14 @@ describe('debug-build-paths', () => { expect(buildResult.exitCode).toBe(0) expect(buildResult.cliOutput).toBeDefined() - // Should build pages matching the glob - expect(buildResult.cliOutput).toContain('Route (pages)') - expect(buildResult.cliOutput).toContain('○ /foo') - expect(buildResult.cliOutput).toContain('○ /bar') - - // Should build the specified app route - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('○ /') - // Should not build other app routes - expect(buildResult.cliOutput).not.toContain('○ /about') - expect(buildResult.cliOutput).not.toContain('○ /dashboard') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ / + └ ○ /_not-found + Route (pages) + ┌ ○ /bar + └ ○ /foo" + `) }) it('should match nested routes with app/blog/**/page.tsx pattern', async () => { @@ -90,14 +106,12 @@ describe('debug-build-paths', () => { expect(buildResult.cliOutput).toBeDefined() // Should build the blog route - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('/blog/[slug]') - // Should not build other app routes (check for exact route, not substring) - expect(buildResult.cliOutput).not.toMatch(/○ \/\n/) - expect(buildResult.cliOutput).not.toContain('○ /about') - expect(buildResult.cliOutput).not.toContain('○ /dashboard') - // Should not build pages routes - expect(buildResult.cliOutput).not.toContain('Route (pages)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + ├ ƒ /blog/[slug] + └ ƒ /blog/[slug]/comments" + `) }) it('should match dynamic routes with glob before brackets like app/**/[slug]/page.tsx', async () => { @@ -108,14 +122,11 @@ describe('debug-build-paths', () => { expect(buildResult.cliOutput).toBeDefined() // Should build the blog/[slug] route - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('/blog/[slug]') - // Should not build other app routes - expect(buildResult.cliOutput).not.toMatch(/○ \/\n/) - expect(buildResult.cliOutput).not.toContain('○ /about') - expect(buildResult.cliOutput).not.toContain('○ /dashboard') - // Should not build pages routes - expect(buildResult.cliOutput).not.toContain('Route (pages)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ƒ /blog/[slug]" + `) }) it('should match hybrid pattern with literal [slug] and glob **', async () => { @@ -129,15 +140,12 @@ describe('debug-build-paths', () => { expect(buildResult.cliOutput).toBeDefined() // Should build both blog/[slug] and blog/[slug]/comments routes - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('/blog/[slug]') - expect(buildResult.cliOutput).toContain('/blog/[slug]/comments') - // Should not build other app routes - expect(buildResult.cliOutput).not.toMatch(/○ \/\n/) - expect(buildResult.cliOutput).not.toContain('○ /about') - expect(buildResult.cliOutput).not.toContain('○ /dashboard') - // Should not build pages routes - expect(buildResult.cliOutput).not.toContain('Route (pages)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + ├ ƒ /blog/[slug] + └ ƒ /blog/[slug]/comments" + `) }) it('should match multiple app routes with explicit patterns', async () => { @@ -151,15 +159,15 @@ describe('debug-build-paths', () => { expect(buildResult.cliOutput).toBeDefined() // Should build specified app routes - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('○ /') - expect(buildResult.cliOutput).toContain('○ /about') - expect(buildResult.cliOutput).toContain('○ /dashboard') - expect(buildResult.cliOutput).toContain('/blog/[slug]') - // Should not build routes not specified - expect(buildResult.cliOutput).not.toContain('/with-type-error') - // Should not build pages routes - expect(buildResult.cliOutput).not.toContain('Route (pages)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ / + ├ ○ /_not-found + ├ ○ /about + ├ ƒ /blog/[slug] + ├ ƒ /blog/[slug]/comments + └ ○ /dashboard" + `) }) it('should exclude paths matching negation patterns', async () => { @@ -188,9 +196,11 @@ describe('debug-build-paths', () => { }) expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('/blog/[slug]') - expect(buildResult.cliOutput).not.toContain('/blog/[slug]/comments') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ƒ /blog/[slug]" + `) }) it('should support multiple negation patterns', async () => { @@ -228,12 +238,13 @@ describe('debug-build-paths', () => { args: ['--debug-build-paths', 'app/(group)/**/page.tsx'], }) expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (app)') + // Route groups are stripped from the path, so /nested instead of /(group)/nested - expect(buildResult.cliOutput).toContain('/nested') - // Should not build other routes - expect(buildResult.cliOutput).not.toContain('○ /about') - expect(buildResult.cliOutput).not.toContain('○ /dashboard') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ○ /nested" + `) }) it('should build routes with parallel routes', async () => { @@ -241,12 +252,12 @@ describe('debug-build-paths', () => { args: ['--debug-build-paths', 'app/parallel-test/**/page.tsx'], }) expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (app)') // Parallel route segments (@sidebar) are stripped from the path - expect(buildResult.cliOutput).toContain('/parallel-test') - // Should not build other routes - expect(buildResult.cliOutput).not.toContain('○ /about') - expect(buildResult.cliOutput).not.toContain('○ /dashboard') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ○ /parallel-test" + `) }) }) @@ -258,10 +269,11 @@ describe('debug-build-paths', () => { }) // Build should succeed because the file with type error is not checked expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (pages)') - expect(buildResult.cliOutput).toContain('○ /foo') - // Should not include app routes - expect(buildResult.cliOutput).not.toContain('Route (app)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (pages) + ┌ ○ /404 + └ ○ /foo" + `) }) it('should fail typechecking when route with type error is included', async () => { @@ -281,6 +293,9 @@ describe('debug-build-paths', () => { const { next } = nextTestSetup({ files: path.join(__dirname, 'fixtures/src-dir'), skipStart: true, + env: { + __NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT: '1', + }, }) it('should resolve app patterns with explicit src/ prefix', async () => { @@ -288,11 +303,11 @@ describe('debug-build-paths', () => { args: ['--debug-build-paths', 'src/app/blog/[slug]/page.tsx'], }) expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('/blog/[slug]') - // Should not build other app routes - expect(buildResult.cliOutput).not.toMatch(/○ \/\n/) - expect(buildResult.cliOutput).not.toContain('Route (pages)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ƒ /blog/[slug]" + `) }) it('should resolve app patterns without src/ prefix when project uses src/app', async () => { @@ -300,10 +315,11 @@ describe('debug-build-paths', () => { args: ['--debug-build-paths', 'app/blog/[slug]/page.tsx'], }) expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('/blog/[slug]') - expect(buildResult.cliOutput).not.toMatch(/○ \/\n/) - expect(buildResult.cliOutput).not.toContain('Route (pages)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ƒ /blog/[slug]" + `) }) it('should resolve pages patterns without src/ prefix when project uses src/pages', async () => { @@ -311,9 +327,11 @@ describe('debug-build-paths', () => { args: ['--debug-build-paths', 'pages/foo.tsx'], }) expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (pages)') - expect(buildResult.cliOutput).toContain('○ /foo') - expect(buildResult.cliOutput).not.toContain('Route (app)') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (pages) + ┌ ○ /404 + └ ○ /foo" + `) }) it('should resolve glob patterns without src/ prefix when project uses src/app', async () => { @@ -321,10 +339,11 @@ describe('debug-build-paths', () => { args: ['--debug-build-paths', 'app/(group)/**/page.tsx'], }) expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (app)') - // Route groups are stripped from the path - expect(buildResult.cliOutput).toContain('/nested') - expect(buildResult.cliOutput).not.toContain('/blog/[slug]') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ○ /nested" + `) }) }) @@ -332,6 +351,9 @@ describe('debug-build-paths', () => { const { next } = nextTestSetup({ files: path.join(__dirname, 'fixtures/with-compile-error'), skipStart: true, + env: { + __NEXT_PRIVATE_DETERMINISTIC_BUILD_OUTPUT: '1', + }, }) it('should skip compilation of excluded routes with compile errors', async () => { @@ -340,11 +362,11 @@ describe('debug-build-paths', () => { args: ['--debug-build-paths', 'app/valid/page.tsx'], }) // Build should succeed because the broken page is not compiled - expect(buildResult.exitCode).toBe(0) - expect(buildResult.cliOutput).toContain('Route (app)') - expect(buildResult.cliOutput).toContain('○ /valid') - // Should not include the broken route - expect(buildResult.cliOutput).not.toContain('/broken') + expect(getTreeView(buildResult.cliOutput)).toMatchInlineSnapshot(` + "Route (app) + ┌ ○ /_not-found + └ ○ /valid" + `) }) it('should fail compilation when route with compile error is included', async () => { From d4db8c9f2198982668c5fcc89042c850c4c90797 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Tue, 2 Jun 2026 22:11:16 +0200 Subject: [PATCH 5/9] Turbopack: support turbopackIgnore on more traced calls (#94361) This already worked: ```js fs.readFileSync(/*turbopackIgnore: true*/ x) ``` but this didn't: ```js fs.readdirSync(/*turbopackIgnore: true*/ x) ``` --- turbopack/crates/turbopack-ecmascript/src/references/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs index e6085bda4888..a2335d8d8220 100644 --- a/turbopack/crates/turbopack-ecmascript/src/references/mod.rs +++ b/turbopack/crates/turbopack-ecmascript/src/references/mod.rs @@ -3541,7 +3541,10 @@ async fn value_visitor_inner( JsValue::WellKnownFunction( WellKnownFunctionKind::PathJoin | WellKnownFunctionKind::PathResolve(_) - | WellKnownFunctionKind::FsReadMethod(_), + | WellKnownFunctionKind::FsReadMethod(_) + | WellKnownFunctionKind::FsReadDir + | WellKnownFunctionKind::ChildProcessSpawnMethod(_) + | WellKnownFunctionKind::ChildProcessFork, ) => { if ignore { return Ok(( From f4086d562759c338d6559d7b725f76acd485f090 Mon Sep 17 00:00:00 2001 From: Sam Poder Date: Tue, 2 Jun 2026 13:46:30 -0700 Subject: [PATCH 6/9] [turbopack] Treat assignments to `module.exports` as side-effect free (#94293) I noticed this section of the documentation in `turbopack/crates/turbopack-ecmascript/src/analyzer/side_effects.rs`: https://github.com/vercel/next.js/blob/89562628d27b672352d212ecd8c6941873e83222/turbopack/crates/turbopack-ecmascript/src/analyzer/side_effects.rs#L36 This PR implements this change; as part of a stack of PRs that attempts to work towards tracking local variable mutations. --- .../src/analyzer/side_effects.rs | 331 +++++++++++++++++- ...merged-unicode_input_index_0nlrkdv.js.map} | 0 ...aps_merged-unicode_input_index_0nlrkdv.js} | 4 +- ...ce_maps_merged-unicode_input_1tq4mpj._.js} | 9 +- ...aps_merged-unicode_input_1tq4mpj._.js.map} | 9 +- 5 files changed, 329 insertions(+), 24 deletions(-) rename turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/{0_9x_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_1j3m-pr.js.map => 0_9x_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_0nlrkdv.js.map} (100%) rename turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/{0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_1j3m-pr.js => 0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_0nlrkdv.js} (77%) rename turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/{1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js => 1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js} (85%) rename turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/{1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js.map => 1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js.map} (76%) diff --git a/turbopack/crates/turbopack-ecmascript/src/analyzer/side_effects.rs b/turbopack/crates/turbopack-ecmascript/src/analyzer/side_effects.rs index 74af9fd40d62..95c3e5929b08 100644 --- a/turbopack/crates/turbopack-ecmascript/src/analyzer/side_effects.rs +++ b/turbopack/crates/turbopack-ecmascript/src/analyzer/side_effects.rs @@ -267,13 +267,161 @@ static KNOWN_PURE_REGEXP_PROTOTYPE_METHODS: phf::Set<&'static str> = phf_set! { "test", "exec", }; +/// True if `prop` is the non-computed member property `.`. +fn prop_is(prop: &MemberProp, name: &str) -> bool { + matches!(prop, MemberProp::Ident(i) if i.sym.as_ref() == name) +} + +/// True if `identifier` is the named, unshadowed module-scope (unresolved) +/// binding — e.g. the real CommonJS `module`/`exports`/`require`, not a local +/// shadow. Prevents `let exports = {}; exports.foo = 'a'` from being treated as +/// a write to the global `exports`. +fn is_global(identifier: &Ident, name: &str, unresolved_mark: Mark) -> bool { + identifier.ctxt.outer() == unresolved_mark && identifier.sym.as_ref() == name +} + +/// A freshly-allocated object/array literal (evaluates to a brand-new value). +fn is_object_or_array_literal(expr: &Expr) -> bool { + matches!(unparen(expr), Expr::Object(_) | Expr::Array(_)) +} + +/// Whether `expr`'s object graph contains a getter or setter. An accessor makes +/// member access (read *or* write) potentially effectful — e.g. `o.foo = 1` +/// invokes a `set foo` — so a value carrying one can't be attached to the +/// exports object and then mutated as if it were plain data. Function/method +/// bodies are not descended into: an accessor declared inside a nested function +/// isn't part of this value's own shape. +fn contains_getters_or_setters(expr: &Expr) -> bool { + match unparen(expr) { + Expr::Object(obj) => obj.props.iter().any(|prop| match prop { + PropOrSpread::Prop(prop) => match &**prop { + Prop::Getter(_) | Prop::Setter(_) => true, + Prop::KeyValue(kv) => contains_getters_or_setters(&kv.value), + Prop::Method(_) | Prop::Shorthand(_) | Prop::Assign(_) => false, + }, + PropOrSpread::Spread(spread) => contains_getters_or_setters(&spread.expr), + }), + Expr::Array(arr) => arr + .elems + .iter() + .flatten() + .any(|elem| contains_getters_or_setters(&elem.expr)), + _ => false, + } +} + +/// `module.exports` (the real, unshadowed `module` binding). +fn is_module_dot_exports(member: &MemberExpr, unresolved_mark: Mark) -> bool { + matches!(unparen(&member.obj), Expr::Ident(o) if is_global(o, "module", unresolved_mark)) + && prop_is(&member.prop, "exports") +} + +/// `module.exports` or a property chain rooted at it (`module.exports.a.b`). +fn is_module_exports_chain(expr: &Expr, unresolved_mark: Mark) -> bool { + match unparen(expr) { + Expr::Member(m) => { + is_module_dot_exports(m, unresolved_mark) + || is_module_exports_chain(&m.obj, unresolved_mark) + } + _ => false, + } +} + +/// Whether `member` writes the module's own CommonJS exports: `exports.`, +/// `module.exports`, or `module.exports.`. +fn is_cjs_export_member(member: &MemberExpr, unresolved_mark: Mark) -> bool { + match unparen(&member.obj) { + // `exports.`, or `module.exports` + Expr::Ident(obj) => { + is_global(obj, "exports", unresolved_mark) + || is_module_dot_exports(member, unresolved_mark) + } + // `module.exports.` + Expr::Member(inner) => is_cjs_export_member(inner, unresolved_mark), + _ => false, + } +} + +/// Calls `f` for every assignment that executes during module evaluation. +/// +/// This descends through all expressions — conditionals, logical/binary +/// operators, sequences, assignment chains, call arguments, etc. — so an +/// assignment hidden in `cond && (module.exports = …)` or `a ? (b = …) : c` is +/// still seen. It does *not* descend into function/method bodies: those don't +/// run at module-evaluation time (calling such a function would itself be a side +/// effect), so assignments inside them are irrelevant here. +fn for_each_top_level_assign(program: &Program, f: impl FnMut(&AssignExpr)) { + struct Collector { + f: F, + } + impl Visit for Collector { + noop_visit_type!(); + fn visit_assign_expr(&mut self, n: &AssignExpr) { + (self.f)(n); + n.visit_children_with(self); + } + // Function/method bodies do not execute during module evaluation. + fn visit_function(&mut self, _: &Function) {} + fn visit_arrow_expr(&mut self, _: &ArrowExpr) {} + fn visit_constructor(&mut self, _: &Constructor) {} + } + program.visit_with(&mut Collector { f }); +} + +/// Whether `module.exports` is ever reassigned to a value that isn't safe. +/// +/// A reassignment to an alias (`module.exports = require('./x')`, +/// `module.exports = other`) would make later changes to properties on +/// `module.exports` have side effects. +fn module_exports_is_tainted(program: &Program, unresolved_mark: Mark) -> bool { + let mut tainted = false; + for_each_top_level_assign(program, |assign| { + if assign.op == AssignOp::Assign + && let AssignTarget::Simple(SimpleAssignTarget::Member(member)) = &assign.left + && is_module_dot_exports(member, unresolved_mark) + && !is_object_or_array_literal(&assign.right) + { + tainted = true; + } + }); + tainted +} + +/// Whether any value assigned to the module's CommonJS exports carries a getter +/// or setter. +/// +/// Attaching an accessor to the exports object makes later member access (read or +/// write) potentially effectful — a subsequent `module.exports.foo = 1` could +/// invoke a `set foo` — so once one is present, writes to the exports can no +/// longer be treated as plain data assignments. +fn module_exports_has_accessor(program: &Program, unresolved_mark: Mark) -> bool { + let mut found = false; + for_each_top_level_assign(program, |assign| { + if assign.op == AssignOp::Assign + && let AssignTarget::Simple(SimpleAssignTarget::Member(member)) = &assign.left + && is_cjs_export_member(member, unresolved_mark) + && contains_getters_or_setters(&assign.right) + { + found = true; + } + }); + found +} + /// Analyzes a program to determine if it contains side effects at the top level. pub fn compute_module_evaluation_side_effects( program: &Program, comments: &dyn Comments, unresolved_mark: Mark, ) -> ModuleSideEffects { - let mut visitor = SideEffectVisitor::new(comments, unresolved_mark); + let module_exports_tainted = module_exports_is_tainted(program, unresolved_mark); + let module_exports_has_accessor = module_exports_has_accessor(program, unresolved_mark); + let mut visitor = SideEffectVisitor::new( + comments, + unresolved_mark, + module_exports_tainted, + module_exports_has_accessor, + ); program.visit_with(&mut visitor); if visitor.has_side_effects { ModuleSideEffects::SideEffectful @@ -287,16 +435,29 @@ pub fn compute_module_evaluation_side_effects( struct SideEffectVisitor<'a> { comments: &'a dyn Comments, unresolved_mark: Mark, + /// Whether `module.exports` was reassigned to a non-safe value, making member + /// writes to `module.exports.*` potentially observable. + module_exports_tainted: bool, + /// Whether a getter or setter is attached to the exports object, making any + /// write to the CommonJS exports potentially observable. + module_exports_has_accessor: bool, has_side_effects: bool, will_invoke_fn_exprs: bool, has_imports: bool, } impl<'a> SideEffectVisitor<'a> { - fn new(comments: &'a dyn Comments, unresolved_mark: Mark) -> Self { + fn new( + comments: &'a dyn Comments, + unresolved_mark: Mark, + module_exports_tainted: bool, + module_exports_has_accessor: bool, + ) -> Self { Self { comments, unresolved_mark, + module_exports_tainted, + module_exports_has_accessor, has_side_effects: false, will_invoke_fn_exprs: false, has_imports: false, @@ -345,7 +506,7 @@ impl<'a> SideEffectVisitor<'a> { Callee::Expr(expr) => { let expr = unparen(expr); if let Expr::Ident(ident) = expr { - ident.ctxt.outer() == self.unresolved_mark && ident.sym.as_ref() == "require" + is_global(ident, "require", self.unresolved_mark) } else { false } @@ -355,6 +516,32 @@ impl<'a> SideEffectVisitor<'a> { _ => false, } } + + /// Returns true if `target` writes to the module's own CommonJS exports + /// (`exports.x`, `module.exports`, or `module.exports.x`) in a way that is + /// the CJS equivalent of an ESM `export`, rather than a side effect. + fn is_safe_cjs_export_target(&self, target: &AssignTarget) -> bool { + let AssignTarget::Simple(SimpleAssignTarget::Member(member)) = target else { + return false; + }; + if !is_cjs_export_member(member, self.unresolved_mark) { + return false; + } + // If a getter/setter is attached to the exports object, any write to its + // members could invoke an accessor, so conservatively none are safe. + if self.module_exports_has_accessor { + return false; + } + // If `module.exports` was reassigned to a non-safe value, writing its + // members may invoke a setter or mutate another module's object, so it + // is not safe even though it targets the CJS exports. + if self.module_exports_tainted && is_module_exports_chain(&member.obj, self.unresolved_mark) + { + return false; + } + true + } + /// Check if an expression is a known pure built-in function. /// /// This checks for: @@ -735,10 +922,23 @@ impl<'a> Visit for SideEffectVisitor<'a> { self.mark_side_effect(); } } - Expr::Assign(_) => { - // Assignments have side effects - // TODO: allow assignments to module level variables - self.mark_side_effect(); + Expr::Assign(assign) => { + // Assigning to the module's own CommonJS exports (`exports.x`, + // `module.exports`, `module.exports.x`) is the CJS equivalent of an + // ESM `export` declaration. + // + // If a getter/setter is attached to the exports object (detected + // by the `module_exports_has_accessor` pass), `is_safe_cjs_export_target` + // conservatively rejects every write to the exports, since a + // member write could invoke the accessor. + if assign.op == AssignOp::Assign && self.is_safe_cjs_export_target(&assign.left) { + // Still check the assigned value, and the target's computed + // property keys (e.g. `exports[sideEffect()] = …`). + assign.left.visit_with(self); + assign.right.visit_with(self); + } else { + self.mark_side_effect(); + } } Expr::Update(_) => { // Updates (++, --) have side effects @@ -2372,8 +2572,119 @@ mod tests { mod common_js_modules_tests { use super::*; - side_effects!(test_common_js_exports, "exports.foo = 'a'"); - side_effects!(test_common_js_exports_module, "module.exports.foo = 'a'"); - side_effects!(test_common_js_exports_assignment, "module.exports = {}"); + // Writing the module's own CommonJS exports with a pure value is the CJS + // equivalent of an ESM `export` and is not a module-evaluation side effect. + no_side_effects!(test_common_js_exports, "exports.foo = 'a'"); + no_side_effects!(test_common_js_exports_module, "module.exports.foo = 'a'"); + no_side_effects!(test_common_js_exports_assignment, "module.exports = {}"); + no_side_effects!( + test_common_js_function_exports, + "exports.foo = function () { return 1; }; exports.bar = 2;" + ); + + module_evaluation_is_side_effect_free!( + test_common_js_reexport, + "module.exports = require('./other');" + ); + module_evaluation_is_side_effect_free!( + test_common_js_named_reexports, + "exports.a = require('./a'); exports.b = require('./b');" + ); + + // a side effect in a computed value + side_effects!( + test_common_js_export_impure_value, + "exports.foo = sideEffect();" + ); + // a side effect in a computed export key, + side_effects!( + test_common_js_export_computed_side_effect, + "exports[sideEffect()] = 'a';" + ); + // writing a non-`exports` property of `module`, + side_effects!(test_module_non_export_assignment, "module.foo = 'a';"); + // and a locally-shadowed `exports`. + side_effects!( + test_shadowed_exports_assignment, + "let exports = {}; exports.foo = 'a';" + ); + + // A getter/setter attached to the exports object makes member writes + // potentially invoke an accessor, so it is conservatively flagged as a + // side effect as soon as the accessor is attached. + side_effects!( + test_cjs_export_setter_invoked, + "module.exports = { set foo(v) { sideEffect() } }; module.exports.foo = 1;" + ); + + // Reassigning `module.exports` to a fresh literal keeps later member + // writes pure (the common incremental-exports pattern). + no_side_effects!( + test_cjs_export_fresh_then_write, + "module.exports = {}; module.exports.foo = 1;" + ); + // But reassigning it to an alias (a re-export, or any non-literal) taints + // it: a later `module.exports.*` write may mutate that other object, so + // it is a side effect. + side_effects!( + test_cjs_export_reexport_then_write, + "module.exports = require('./other'); module.exports.extra = 1;" + ); + side_effects!( + test_cjs_export_alias_then_write, + "module.exports = other; module.exports.foo = 1;" + ); + // The reassignment is also detected when hidden inside a top-level + // comma-sequence expression rather than a standalone statement. + side_effects!( + test_cjs_export_reexport_then_write_sequence, + "module.exports = require('./other'), module.exports.extra = 1;" + ); + // …and when nested in a conditional/logical expression that still runs at + // module evaluation (the scan descends every evaluated expression, just + // not function bodies). + side_effects!( + test_cjs_export_reexport_in_logical_then_write, + "x && (module.exports = require('./other')); module.exports.extra = 1;" + ); + side_effects!( + test_cjs_export_setter_attached_in_conditional, + "x ? (module.exports = { set foo(v) { sideEffect() } }) : 0;" + ); + // A reassignment inside a function body does not run during module + // evaluation, so it is not a taint on its own. + no_side_effects!( + test_cjs_export_reassign_in_function_body_is_pure, + "function f() { module.exports = require('./other'); } module.exports.foo = 1;" + ); + + // A class `static` block executes at module evaluation (when the class + // definition is evaluated), so an accessor attached to the exports object + // inside one is detected — mirroring the "top level" assignment in: + // class C { static { foo = bar; } } + side_effects!( + test_cjs_export_setter_in_static_block, + "class C { static { module.exports = { set foo(v) { sideEffect() } }; \ + module.exports.foo = 1; } }" + ); + // …but a constructor body only runs when the class is instantiated, not at + // module evaluation, so the same attachment there is *not* a top-level + // assignment and is not detected — mirroring the "not top level" + // assignment in: + // class C { constructor() { baz = quux; } } + no_side_effects!( + test_cjs_export_setter_in_constructor_is_pure, + "class C { constructor() { module.exports = { set foo(v) { sideEffect() } }; } } \ + module.exports.foo = 1;" + ); + + // A `static` property initializer also runs at module evaluation (when the + // class definition is evaluated), so an accessor attached to the exports + // object inside one is detected. + side_effects!( + test_cjs_export_setter_in_static_property, + "class C { static x = (module.exports = { set foo(v) { sideEffect() } }); } \ + module.exports.foo = 1;" + ); } } diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0_9x_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_1j3m-pr.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0_9x_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_0nlrkdv.js.map similarity index 100% rename from turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0_9x_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_1j3m-pr.js.map rename to turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0_9x_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_0nlrkdv.js.map diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_1j3m-pr.js b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_0nlrkdv.js similarity index 77% rename from turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_1j3m-pr.js rename to turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_0nlrkdv.js index b849a8714b65..f64366809d5d 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_1j3m-pr.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_0nlrkdv.js @@ -1,5 +1,5 @@ (globalThis["TURBOPACK"] || (globalThis["TURBOPACK"] = [])).push([ - "output/0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_1j3m-pr.js", - {"otherChunks":["output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js"],"runtimeModuleIds":["[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/index.js [test] (ecmascript)"]} + "output/0rv8_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_index_0nlrkdv.js", + {"otherChunks":["output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js"],"runtimeModuleIds":["[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/index.js [test] (ecmascript)"]} ]); // Dummy runtime \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js similarity index 85% rename from turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js rename to turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js index ab4a0fb57e28..c0a9b5a47c29 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js @@ -1,8 +1,4 @@ -(globalThis["TURBOPACK"] || (globalThis["TURBOPACK"] = [])).push(["output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js", -"[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/jsx-runtime.js [test] (ecmascript)", ((__turbopack_context__, module, exports) => { - -module.exports = {}; -}), +(globalThis["TURBOPACK"] || (globalThis["TURBOPACK"] = [])).push(["output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js", "[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/entry-base.js [test] (ecmascript)", ((__turbopack_context__) => { "use strict"; @@ -14,7 +10,6 @@ __turbopack_context__.s([], "[project]/turbopack/crates/turbopack-tests/tests/sn ; // MERGED MODULE: [project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/collect-segment-data.js [test] (ecmascript) ; -var __TURBOPACK__imported__module__$5b$project$5d2f$turbopack$2f$crates$2f$turbopack$2d$tests$2f$tests$2f$snapshot$2f$source_maps$2f$merged$2d$unicode$2f$input$2f$jsx$2d$runtime$2e$js__$5b$test$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/jsx-runtime.js [test] (ecmascript)"); ; // eslint-disable-next-line import/no-extraneous-dependencies const createFromReadableStream = 123; //import { createFromReadableStream } from 'react-server-dom-webpack/client.edge'; @@ -63,4 +58,4 @@ if (Date.now() > 0) { }), ]); -//# sourceMappingURL=1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js.map \ No newline at end of file +//# sourceMappingURL=1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js.map \ No newline at end of file diff --git a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js.map b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js.map similarity index 76% rename from turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js.map rename to turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js.map index 686d92189782..b719ef6f5a9d 100644 --- a/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_0b1pxfd._.js.map +++ b/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/output/1do3_crates_turbopack-tests_tests_snapshot_source_maps_merged-unicode_input_1tq4mpj._.js.map @@ -2,9 +2,8 @@ "version": 3, "sources": [], "sections": [ - {"offset": {"line": 3, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/jsx-runtime.js"],"sourcesContent":["module.exports = {};\n"],"names":["module","exports"],"mappings":"AAAAA,OAAOC,OAAO,GAAG,CAAC"}}, - {"offset": {"line": 8, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/collect-segment-data.js"],"sourcesContent":["import { jsx as _jsx } from \"./jsx-runtime\";\n// eslint-disable-next-line import/no-extraneous-dependencies\nconst createFromReadableStream = 123; //import { createFromReadableStream } from 'react-server-dom-webpack/client.edge';\n// eslint-disable-next-line import/no-extraneous-dependencies\nconst prerender = 123; // import { prerender } from 'react-server-dom-webpack/static.edge';\nconst streamFromBuffer=1,streamToBuffer=1; // import { streamFromBuffer, streamToBuffer } from '../stream-utils/node-web-streams-helper';\nconst waitAtLeastOneReactRenderTask=1; //import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler';\n// import './segment-value-encoding';\n"],"names":[],"mappings":";;;;;;;;AAAA;;AACA,6DAA6D;AAC7D,MAAM,2BAA2B,KAAK,kFAAkF;AACxH,6DAA6D;AAC7D,MAAM,YAAY,KAAK,oEAAoE;AAC3F,MAAM,mBAAiB,GAAE,iBAAe,GAAG,8FAA8F;AACzI,MAAM,gCAA8B,GAAG,sEAAsE;CAC7G,sCAAsC"}}, - {"offset": {"line": 31, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/index1.js"],"sourcesContent":["import \"./entry-base.js\";\n"],"names":[],"mappings":";AAAA"}}, - {"offset": {"line": 38, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/index2.js"],"sourcesContent":["import \"./entry-base.js\";\n"],"names":[],"mappings":";AAAA"}}, - {"offset": {"line": 44, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/index.js"],"sourcesContent":["// \"entry-base.js [app-rsc] (ecmascript) \" with\n// [\"reflect-utils.js [app-rsc] (ecmascript)\",\n// \"params.js [app-rsc] (ecmascript)\",\n// \"segment-value-encoding.js [app-rsc] (ecmascript)\",\n// \"collect-segment-data.js [app-rsc] (ecmascript)\",\n// \"entry-base.js [app-rsc] (ecmascript) \"]\n\n// \"entry-base.js [test] (ecmascript) \" with\n// [\"reflect-utils.js [test] (ecmascript)\",\n// \"params.js [test] (ecmascript)\",\n// \"segment-value-encoding.js [test] (ecmascript)\",\n// \"collect-segment-data.js [test] (ecmascript)\",\n// \"entry-base.js [test] (ecmascript) \"]\n\nif (Date.now() > 0) {\n require('./index1.js')\n}\nif (Date.now() > 0) {\n require('./index2.js')\n}\n\n"],"names":["Date","now"],"mappings":"AAAA,kEAAkE;AAClE,8CAA8C;AAC9C,sCAAsC;AACtC,sDAAsD;AACtD,oDAAoD;AACpD,mDAAmD;AAEnD,+DAA+D;AAC/D,2CAA2C;AAC3C,mCAAmC;AACnC,mDAAmD;AACnD,kDAAkD;AAClD,iDAAiD;AAEjD,IAAIA,KAAKC,GAAG,KAAK,GAAG;;AAEpB;AACA,IAAID,KAAKC,GAAG,KAAK,GAAG;;AAEpB"}}] + {"offset": {"line": 4, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/collect-segment-data.js"],"sourcesContent":["import { jsx as _jsx } from \"./jsx-runtime\";\n// eslint-disable-next-line import/no-extraneous-dependencies\nconst createFromReadableStream = 123; //import { createFromReadableStream } from 'react-server-dom-webpack/client.edge';\n// eslint-disable-next-line import/no-extraneous-dependencies\nconst prerender = 123; // import { prerender } from 'react-server-dom-webpack/static.edge';\nconst streamFromBuffer=1,streamToBuffer=1; // import { streamFromBuffer, streamToBuffer } from '../stream-utils/node-web-streams-helper';\nconst waitAtLeastOneReactRenderTask=1; //import { waitAtLeastOneReactRenderTask } from '../../lib/scheduler';\n// import './segment-value-encoding';\n"],"names":[],"mappings":";;;;;;;;;AACA,6DAA6D;AAC7D,MAAM,2BAA2B,KAAK,kFAAkF;AACxH,6DAA6D;AAC7D,MAAM,YAAY,KAAK,oEAAoE;AAC3F,MAAM,mBAAiB,GAAE,iBAAe,GAAG,8FAA8F;AACzI,MAAM,gCAA8B,GAAG,sEAAsE;CAC7G,sCAAsC"}}, + {"offset": {"line": 26, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/index1.js"],"sourcesContent":["import \"./entry-base.js\";\n"],"names":[],"mappings":";AAAA"}}, + {"offset": {"line": 33, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/index2.js"],"sourcesContent":["import \"./entry-base.js\";\n"],"names":[],"mappings":";AAAA"}}, + {"offset": {"line": 39, "column": 0}, "map": {"version":3,"sources":["turbopack:///[project]/turbopack/crates/turbopack-tests/tests/snapshot/source_maps/merged-unicode/input/index.js"],"sourcesContent":["// \"entry-base.js [app-rsc] (ecmascript) \" with\n// [\"reflect-utils.js [app-rsc] (ecmascript)\",\n// \"params.js [app-rsc] (ecmascript)\",\n// \"segment-value-encoding.js [app-rsc] (ecmascript)\",\n// \"collect-segment-data.js [app-rsc] (ecmascript)\",\n// \"entry-base.js [app-rsc] (ecmascript) \"]\n\n// \"entry-base.js [test] (ecmascript) \" with\n// [\"reflect-utils.js [test] (ecmascript)\",\n// \"params.js [test] (ecmascript)\",\n// \"segment-value-encoding.js [test] (ecmascript)\",\n// \"collect-segment-data.js [test] (ecmascript)\",\n// \"entry-base.js [test] (ecmascript) \"]\n\nif (Date.now() > 0) {\n require('./index1.js')\n}\nif (Date.now() > 0) {\n require('./index2.js')\n}\n\n"],"names":["Date","now"],"mappings":"AAAA,kEAAkE;AAClE,8CAA8C;AAC9C,sCAAsC;AACtC,sDAAsD;AACtD,oDAAoD;AACpD,mDAAmD;AAEnD,+DAA+D;AAC/D,2CAA2C;AAC3C,mCAAmC;AACnC,mDAAmD;AACnD,kDAAkD;AAClD,iDAAiD;AAEjD,IAAIA,KAAKC,GAAG,KAAK,GAAG;;AAEpB;AACA,IAAID,KAAKC,GAAG,KAAK,GAAG;;AAEpB"}}] } \ No newline at end of file From a0c6a6140ecdb2967e32f879c889a6b6340a1db3 Mon Sep 17 00:00:00 2001 From: Aurora Scharff <66901228+aurorascharff@users.noreply.github.com> Date: Wed, 3 Jun 2026 01:26:11 +0200 Subject: [PATCH 7/9] instant: enable navigation validation by default (#94312) ### What? Flips the default `experimental.instantInsights.validationLevel` from `'manual-warning'` to `'warning'` so Cache Components apps get instant-navigation validation across all pages by default. ### Why? `'manual-warning'` only validates pages that explicitly export `unstable_instant`, so apps see nothing unless they opt in. `'warning'` is what users have been turning on manually to actually see the feature (`v0`, `vercel-site`). ### Test coverage - Unit: `instant-config-normalization.test.ts` pins the framework default at `'warning'`. - Integration: new `instant-validation-level-default/` fixture (no `instantInsights` in config) asserts implicit dev validation fires, and that build is unaffected. ### Collateral test fixtures Tests whose intent is unrelated to instant validation (router-autoscroll, owner-stack, hmr-iframe, next-image, server-source-maps, etc.) now hit new redboxes/console warnings because their fixtures incidentally use dynamic data. Each opts out with `experimental: { instantInsights: { validationLevel: 'manual-warning' } }` in `next.config`. ### Open question With no `'off'` / `'info'` tier today, silencing Insights after this change requires setting `validationLevel: 'manual-warning'`. Is that acceptable? Should we inform of this anywhere? --- packages/next/src/server/config-shared.ts | 8 +- packages/next/src/server/config.ts | 2 +- .../next.config.js | 5 + .../error-ignored-frames/next.config.js | 8 +- .../app-dir/hmr-iframe/next.config.js | 12 +++ .../next.config.js | 8 +- .../app-dir/owner-stack/next.config.js | 8 +- .../fixtures/default/next.config.js | 5 + .../next.config.js | 3 + .../app-dir/cache-components/next.config.js | 5 + .../app/bare/page.tsx | 16 ++++ .../app/explicit-false/page.tsx | 16 ++++ .../app/layout.tsx | 23 +++++ .../instant-validation-level-default.test.ts | 93 +++++++++++++++++++ .../next.config.ts | 10 ++ .../next.config.js | 5 + .../apps/web/next.config.js | 8 +- .../app-dir/router-autoscroll/next.config.js | 8 +- .../fixtures/default/next.config.js | 3 + .../fixtures/edge/next.config.js | 3 + test/e2e/legacy-link-behavior/next.config.js | 5 + .../app-dir-localpatterns/next.config.js | 5 + .../unit/instant-config-normalization.test.ts | 8 +- 23 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 test/development/app-dir/hmr-iframe/next.config.js create mode 100644 test/e2e/app-dir/instant-validation-level-default/app/bare/page.tsx create mode 100644 test/e2e/app-dir/instant-validation-level-default/app/explicit-false/page.tsx create mode 100644 test/e2e/app-dir/instant-validation-level-default/app/layout.tsx create mode 100644 test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts create mode 100644 test/e2e/app-dir/instant-validation-level-default/next.config.ts diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index fe771f1b5ff3..077f90d8eca5 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -36,7 +36,7 @@ export type NextConfigComplete = Required> & { prefetchInlining?: PrefetchInliningConfig // Normalized by config.ts: defaulted to 90% of staticPageGenerationTimeout useCacheTimeout: number - // Normalized by config.ts `finalizeConfig`: defaulted to `'manual-warning'` + // Normalized by config.ts `finalizeConfig`: defaulted to `'warning'` instantInsights: { validationLevel: ValidationLevel } } // The root directory of the distDir. In development mode, this is the parent directory of `distDir` @@ -1080,10 +1080,10 @@ export interface ExperimentalConfig { /** * Controls the validation behavior of Instant Insights * - * - `'warning'`: Validates all navigations for Instant UI in development - * - `'manual-warning'`: Validates navigations for Instant UI in development when configured with `unstable_instant` in Pages and Layouts + * - `'warning'` (default): Validates all navigations for Instant UI in development + * - `'manual-warning'`: Validates navigations for Instant UI in development only when configured with `unstable_instant` in Pages and Layouts * - `'experimental-error'`: Validates all navigations for Instant in development and build. Use with caution. - * - `'experimental-manual-error'`: Validates navigations for Instant UI in developement and build when configured with `unstable_instant` in Pages and Layouts. Use with caution. + * - `'experimental-manual-error'`: Validates navigations for Instant UI in development and build when configured with `unstable_instant` in Pages and Layouts. Use with caution. */ validationLevel?: ValidationLevel } diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 937c481487eb..3d1e14ffb0a5 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -1618,7 +1618,7 @@ function assignDefaultsAndValidate( function finalizeConfig(config: NextConfigComplete): NextConfigComplete { config.experimental.instantInsights = { validationLevel: - config.experimental.instantInsights?.validationLevel ?? 'manual-warning', + config.experimental.instantInsights?.validationLevel ?? 'warning', } syncUseNodeStreamsEnv(config) return config diff --git a/test/development/app-dir/cache-components-dev-errors/next.config.js b/test/development/app-dir/cache-components-dev-errors/next.config.js index e64bae22d658..94fcd455df28 100644 --- a/test/development/app-dir/cache-components-dev-errors/next.config.js +++ b/test/development/app-dir/cache-components-dev-errors/next.config.js @@ -3,6 +3,11 @@ */ const nextConfig = { cacheComponents: true, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/development/app-dir/error-overlay/error-ignored-frames/next.config.js b/test/development/app-dir/error-overlay/error-ignored-frames/next.config.js index 807126e4cf0b..3f1322dc956a 100644 --- a/test/development/app-dir/error-overlay/error-ignored-frames/next.config.js +++ b/test/development/app-dir/error-overlay/error-ignored-frames/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = nextConfig diff --git a/test/development/app-dir/hmr-iframe/next.config.js b/test/development/app-dir/hmr-iframe/next.config.js new file mode 100644 index 000000000000..3f1322dc956a --- /dev/null +++ b/test/development/app-dir/hmr-iframe/next.config.js @@ -0,0 +1,12 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} + +module.exports = nextConfig diff --git a/test/development/app-dir/owner-stack-invalid-element-type/next.config.js b/test/development/app-dir/owner-stack-invalid-element-type/next.config.js index 807126e4cf0b..3f1322dc956a 100644 --- a/test/development/app-dir/owner-stack-invalid-element-type/next.config.js +++ b/test/development/app-dir/owner-stack-invalid-element-type/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = nextConfig diff --git a/test/development/app-dir/owner-stack/next.config.js b/test/development/app-dir/owner-stack/next.config.js index 807126e4cf0b..3f1322dc956a 100644 --- a/test/development/app-dir/owner-stack/next.config.js +++ b/test/development/app-dir/owner-stack/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = nextConfig diff --git a/test/e2e/app-dir/cache-components-errors/fixtures/default/next.config.js b/test/e2e/app-dir/cache-components-errors/fixtures/default/next.config.js index e64bae22d658..94fcd455df28 100644 --- a/test/e2e/app-dir/cache-components-errors/fixtures/default/next.config.js +++ b/test/e2e/app-dir/cache-components-errors/fixtures/default/next.config.js @@ -3,6 +3,11 @@ */ const nextConfig = { cacheComponents: true, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/next.config.js b/test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/next.config.js index 34aa2b5aed26..e561e049a1eb 100644 --- a/test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/next.config.js +++ b/test/e2e/app-dir/cache-components-errors/fixtures/http-access-fallback-prerender/next.config.js @@ -5,6 +5,9 @@ const nextConfig = { cacheComponents: true, experimental: { authInterrupts: true, + instantInsights: { + validationLevel: 'manual-warning', + }, }, } diff --git a/test/e2e/app-dir/cache-components/next.config.js b/test/e2e/app-dir/cache-components/next.config.js index 86f364bdd422..e83f1cf6305a 100644 --- a/test/e2e/app-dir/cache-components/next.config.js +++ b/test/e2e/app-dir/cache-components/next.config.js @@ -5,6 +5,11 @@ const nextConfig = { cacheComponents: true, adapterPath: process.env.NEXT_ADAPTER_PATH ?? require.resolve('./my-adapter.mjs'), + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/e2e/app-dir/instant-validation-level-default/app/bare/page.tsx b/test/e2e/app-dir/instant-validation-level-default/app/bare/page.tsx new file mode 100644 index 000000000000..307c49df3a08 --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/app/bare/page.tsx @@ -0,0 +1,16 @@ +// Bare page (no `unstable_instant` config). Under the framework default +// (`'warning'`), implicit validation fires on this page in dev. The runtime +// data accessed at the top of the page is the "Suspense too high for instant +// navigation" violation that instant validation specifically flags. The root +// layout's Suspense satisfies static-shell validation, so the only error in +// dev is the instant one. +import { connection } from 'next/server' + +export default async function Page() { + await connection() + return ( +
+

bare page (no unstable_instant), runtime data at the top.

+
+ ) +} diff --git a/test/e2e/app-dir/instant-validation-level-default/app/explicit-false/page.tsx b/test/e2e/app-dir/instant-validation-level-default/app/explicit-false/page.tsx new file mode 100644 index 000000000000..27e70e2c2631 --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/app/explicit-false/page.tsx @@ -0,0 +1,16 @@ +// Page explicitly opts out of instant validation. Under the framework +// default (`'warning'`), this segment-level override suppresses the +// implicit validation that would otherwise fire on a bare page, so no +// redbox appears. +import { connection } from 'next/server' + +export const unstable_instant = false + +export default async function Page() { + await connection() + return ( +
+

explicit-false page (segment opts out of validation).

+
+ ) +} diff --git a/test/e2e/app-dir/instant-validation-level-default/app/layout.tsx b/test/e2e/app-dir/instant-validation-level-default/app/layout.tsx new file mode 100644 index 000000000000..a32d71748e1c --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/app/layout.tsx @@ -0,0 +1,23 @@ +import { Suspense, type ReactNode } from 'react' + +// Validation level is not set in next.config.ts, so the framework default +// applies. The default is 'warning' — implicit validation fires on bare +// page/default segments in dev only (build is unaffected unless a segment +// explicitly escalates with `level: 'experimental-error'`). +// +// Children are wrapped in Suspense so that pages with runtime data +// accessed at the top of the page don't fail static-shell validation +// (the Suspense fallback renders into the static shell). Instant +// validation flags "Suspense too high for instant navigation" as an +// instant-specific violation when it runs. +export const unstable_instant = false + +export default function RootLayout({ children }: { children: ReactNode }) { + return ( + + + loading…

}>{children}
+ + + ) +} diff --git a/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts b/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts new file mode 100644 index 000000000000..461bf9067e0d --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/instant-validation-level-default.test.ts @@ -0,0 +1,93 @@ +import { nextTestSetup } from 'e2e-utils' +import { expectBuildValidationSkipped } from 'e2e-utils/instant-validation' +import { waitForNoErrorToast } from '../../../lib/next-test-utils' + +// This fixture intentionally omits `experimental.instantInsights` from +// next.config.ts. It pins the framework default for `validationLevel` — +// the framework should resolve the default to `'warning'`, which means +// implicit validation fires on bare pages in dev. If the framework default +// ever changes, this test should fail, alerting whoever changes it. +// +// For exhaustive coverage of explicit levels and per-segment overrides, +// see the sibling `instant-validation-level-{warning,manual-warning,error, +// manual-error}` fixtures. +describe('instant validation - default level', () => { + const { next, skipped, isNextDev, isNextStart, isTurbopack } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + env: { + NEXT_TEST_LOG_VALIDATION: '1', + }, + }) + if (skipped) return + + if (isNextStart && !isTurbopack) { + it.skip('TODO: snapshot tests for webpack', () => {}) + return + } + + if (isNextStart) { + beforeAll(async () => { + await next.build({ args: ['--experimental-build-mode', 'compile'] }) + }) + afterEach(async () => { + await next.stop() + }) + } else { + beforeAll(async () => { + await next.start() + }) + } + + const prerender = async (pathname: string) => { + return await next.build({ + args: [ + '--experimental-build-mode', + 'generate', + '--debug-build-paths', + `app${pathname}/page.tsx`, + ], + }) + } + + if (isNextDev) { + describe('dev', () => { + it('bare page: framework default matches `warning`, implicit validation fires', async () => { + const browser = await next.browser('/bare') + await expect(browser).toDisplayCollapsedRedbox(` + { + "code": "E1264", + "description": "Next.js encountered uncached data during a navigation.", + "environmentLabel": "Server", + "label": "Instant", + "source": "app/bare/page.tsx (10:19) @ Page + > 10 | await connection() + | ^", + "stack": [ + "Page app/bare/page.tsx (10:19)", + ], + } + `) + }) + + it('explicit-false page: per-segment opt-out still works under default', async () => { + const browser = await next.browser('/explicit-false') + await browser.elementByCss('main') + await waitForNoErrorToast(browser, { waitInMs: 500 }) + }) + }) + } else { + describe('build', () => { + it('bare page: framework default is dev-only, build skips validation', async () => { + const result = await prerender('/bare') + expectBuildValidationSkipped(result) + }) + + it('explicit-false page: per-segment opt-out keeps build clean', async () => { + const result = await prerender('/explicit-false') + expectBuildValidationSkipped(result) + }) + }) + } +}) diff --git a/test/e2e/app-dir/instant-validation-level-default/next.config.ts b/test/e2e/app-dir/instant-validation-level-default/next.config.ts new file mode 100644 index 000000000000..9fd0c7a083d4 --- /dev/null +++ b/test/e2e/app-dir/instant-validation-level-default/next.config.ts @@ -0,0 +1,10 @@ +import type { NextConfig } from 'next' + +const nextConfig: NextConfig = { + cacheComponents: true, + typescript: { + ignoreBuildErrors: true, + }, +} + +export default nextConfig diff --git a/test/e2e/app-dir/loader-file-named-export-custom-loader-error/next.config.js b/test/e2e/app-dir/loader-file-named-export-custom-loader-error/next.config.js index 77961243627b..b625c77b5dca 100644 --- a/test/e2e/app-dir/loader-file-named-export-custom-loader-error/next.config.js +++ b/test/e2e/app-dir/loader-file-named-export-custom-loader-error/next.config.js @@ -5,6 +5,11 @@ const nextConfig = { images: { loaderFile: '/dummy-loader.ts', }, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/e2e/app-dir/non-root-project-monorepo/apps/web/next.config.js b/test/e2e/app-dir/non-root-project-monorepo/apps/web/next.config.js index 807126e4cf0b..3f1322dc956a 100644 --- a/test/e2e/app-dir/non-root-project-monorepo/apps/web/next.config.js +++ b/test/e2e/app-dir/non-root-project-monorepo/apps/web/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const nextConfig = {} +const nextConfig = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = nextConfig diff --git a/test/e2e/app-dir/router-autoscroll/next.config.js b/test/e2e/app-dir/router-autoscroll/next.config.js index 36e8d82ea1af..ad60a12885cd 100644 --- a/test/e2e/app-dir/router-autoscroll/next.config.js +++ b/test/e2e/app-dir/router-autoscroll/next.config.js @@ -1,6 +1,12 @@ /** * @type {import('next').NextConfig} */ -const config = {} +const config = { + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, +} module.exports = config diff --git a/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js b/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js index 8a61ee02d1f4..19c0893004fa 100644 --- a/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js +++ b/test/e2e/app-dir/server-source-maps/fixtures/default/next.config.js @@ -6,6 +6,9 @@ const nextConfig = { experimental: { cpus: 1, serverSourceMaps: true, + instantInsights: { + validationLevel: 'manual-warning', + }, }, serverExternalPackages: ['external-pkg'], } diff --git a/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js b/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js index 86773e2ae9a6..0341e872150f 100644 --- a/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js +++ b/test/e2e/app-dir/server-source-maps/fixtures/edge/next.config.js @@ -5,6 +5,9 @@ const nextConfig = { experimental: { cpus: 1, serverSourceMaps: true, + instantInsights: { + validationLevel: 'manual-warning', + }, }, } diff --git a/test/e2e/legacy-link-behavior/next.config.js b/test/e2e/legacy-link-behavior/next.config.js index e64bae22d658..94fcd455df28 100644 --- a/test/e2e/legacy-link-behavior/next.config.js +++ b/test/e2e/legacy-link-behavior/next.config.js @@ -3,6 +3,11 @@ */ const nextConfig = { cacheComponents: true, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } module.exports = nextConfig diff --git a/test/e2e/next-image-new/app-dir-localpatterns/next.config.js b/test/e2e/next-image-new/app-dir-localpatterns/next.config.js index 10c28b1a185c..635050bf29c1 100644 --- a/test/e2e/next-image-new/app-dir-localpatterns/next.config.js +++ b/test/e2e/next-image-new/app-dir-localpatterns/next.config.js @@ -7,4 +7,9 @@ module.exports = { }, ], }, + experimental: { + instantInsights: { + validationLevel: 'manual-warning', + }, + }, } diff --git a/test/unit/instant-config-normalization.test.ts b/test/unit/instant-config-normalization.test.ts index e33a8c7811db..670a6d0bbe12 100644 --- a/test/unit/instant-config-normalization.test.ts +++ b/test/unit/instant-config-normalization.test.ts @@ -14,7 +14,7 @@ function uniqueDir(tag: string) { // `experimental.instantInsights.validationLevel` to a concrete value in // one place so consumers don't each need to know the current framework default. describe('experimental.instantInsights validationLevel normalization', () => { - it('defaults to manual-warning when the instantInsights config is absent', async () => { + it('defaults to warning when the instantInsights config is absent', async () => { const config = await loadConfig( PHASE_PRODUCTION_SERVER, uniqueDir('absent'), @@ -23,11 +23,11 @@ describe('experimental.instantInsights validationLevel normalization', () => { } ) expect(config.experimental.instantInsights).toEqual({ - validationLevel: 'manual-warning', + validationLevel: 'warning', }) }) - it('defaults to manual-warning when experimental.instantInsights is an empty object', async () => { + it('defaults to warning when experimental.instantInsights is an empty object', async () => { const config = await loadConfig( PHASE_PRODUCTION_SERVER, uniqueDir('empty'), @@ -36,7 +36,7 @@ describe('experimental.instantInsights validationLevel normalization', () => { } ) expect(config.experimental.instantInsights).toEqual({ - validationLevel: 'manual-warning', + validationLevel: 'warning', }) }) From 4c3cdf61de11ad995ae273d3f2a311d379b0f3f2 Mon Sep 17 00:00:00 2001 From: Benjamin Woodruff Date: Tue, 2 Jun 2026 16:38:30 -0700 Subject: [PATCH 8/9] Turbopack: Move turbo-tasks/src/raw_vc.rs to turbo-tasks/src/vc/raw.rs (#94239) This has slightly annoyed me for a long time. --- turbopack/crates/turbo-tasks/src/backend.rs | 3 +-- turbopack/crates/turbo-tasks/src/lib.rs | 13 ++++++------- .../crates/turbo-tasks/src/local_task_tracker.rs | 2 +- turbopack/crates/turbo-tasks/src/manager.rs | 7 +++---- turbopack/crates/turbo-tasks/src/vc/mod.rs | 3 ++- .../crates/turbo-tasks/src/{raw_vc.rs => vc/raw.rs} | 0 6 files changed, 13 insertions(+), 15 deletions(-) rename turbopack/crates/turbo-tasks/src/{raw_vc.rs => vc/raw.rs} (100%) diff --git a/turbopack/crates/turbo-tasks/src/backend.rs b/turbopack/crates/turbo-tasks/src/backend.rs index 8baecf06cafb..904015babf77 100644 --- a/turbopack/crates/turbo-tasks/src/backend.rs +++ b/turbopack/crates/turbo-tasks/src/backend.rs @@ -29,14 +29,13 @@ use turbo_rcstr::RcStr; use turbo_tasks_hash::DeterministicHasher; use crate::{ - RawVc, ReadCellOptions, ReadOutputOptions, ReadRef, SharedReference, TaskId, TaskIdSet, + CellId, RawVc, ReadCellOptions, ReadOutputOptions, ReadRef, SharedReference, TaskId, TaskIdSet, TaskPriority, TraitRef, TraitTypeId, TurboTasksCallApi, TurboTasksPanic, ValueTypeId, ValueTypePersistence, VcValueTrait, VcValueType, dyn_task_inputs::{DynTaskInputs, StackDynTaskInputs}, event::EventListener, macro_helpers::NativeFunction, manager::{TaskPersistence, TurboTasks}, - raw_vc::CellId, registry, task::shared_reference::TypedSharedReference, task_statistics::TaskStatisticsApi, diff --git a/turbopack/crates/turbo-tasks/src/lib.rs b/turbopack/crates/turbo-tasks/src/lib.rs index d50c06784a59..592d1c9ed276 100644 --- a/turbopack/crates/turbo-tasks/src/lib.rs +++ b/turbopack/crates/turbo-tasks/src/lib.rs @@ -43,7 +43,6 @@ pub mod panic_hooks; pub mod parallel; pub mod primitives; mod priority_runner; -mod raw_vc; mod read_options; mod read_ref; pub mod registry; @@ -104,7 +103,6 @@ pub use crate::{ }, mapped_read_ref::MappedReadRef, output::OutputContent, - raw_vc::{CellId, RawVc, ReadRawVcFuture, ResolveRawVcFuture}, read_options::{ReadCellOptions, ReadOutputOptions}, read_ref::ReadRef, serialization_invalidation::SerializationInvalidator, @@ -120,11 +118,12 @@ pub use crate::{ value::{TransientInstance, TransientValue}, value_type::{Evictability, TraitMethod, TraitType, ValueType, ValueTypePersistence}, vc::{ - Dynamic, NonLocalValue, OperationValue, OperationVc, OptionVcExt, ReadVcFuture, - ResolveOperationVcFuture, ResolveVcFuture, ResolvedVc, ToResolvedVcFuture, Upcast, - UpcastStrict, ValueDefault, Vc, VcCast, VcCellCompareMode, VcCellHashedCompareMode, - VcCellKeyedCompareMode, VcCellNewMode, VcDefaultRead, VcRead, VcTransparentRead, - VcValueTrait, VcValueTraitCast, VcValueType, VcValueTypeCast, + CellId, Dynamic, NonLocalValue, OperationValue, OperationVc, OptionVcExt, RawVc, + ReadRawVcFuture, ReadVcFuture, ResolveOperationVcFuture, ResolveRawVcFuture, + ResolveVcFuture, ResolvedVc, ToResolvedVcFuture, Upcast, UpcastStrict, ValueDefault, Vc, + VcCast, VcCellCompareMode, VcCellHashedCompareMode, VcCellKeyedCompareMode, VcCellNewMode, + VcDefaultRead, VcRead, VcTransparentRead, VcValueTrait, VcValueTraitCast, VcValueType, + VcValueTypeCast, }, }; diff --git a/turbopack/crates/turbo-tasks/src/local_task_tracker.rs b/turbopack/crates/turbo-tasks/src/local_task_tracker.rs index b9fe571b74b8..5380c6cdf83a 100644 --- a/turbopack/crates/turbo-tasks/src/local_task_tracker.rs +++ b/turbopack/crates/turbo-tasks/src/local_task_tracker.rs @@ -119,7 +119,7 @@ mod tests { use super::*; fn dummy_output() -> OutputContent { - OutputContent::Link(crate::raw_vc::RawVc::TaskOutput( + OutputContent::Link(crate::RawVc::TaskOutput( crate::TaskId::try_from(1).unwrap(), )) } diff --git a/turbopack/crates/turbo-tasks/src/manager.rs b/turbopack/crates/turbo-tasks/src/manager.rs index c40a19b0d275..153fa1c278b6 100644 --- a/turbopack/crates/turbo-tasks/src/manager.rs +++ b/turbopack/crates/turbo-tasks/src/manager.rs @@ -27,9 +27,9 @@ use tracing::{Instrument, Span, instrument}; use turbo_tasks_hash::{DeterministicHash, hash_xxh3_hash128}; use crate::{ - Completion, InvalidationReason, InvalidationReasonSet, OutputContent, ReadCellOptions, - ReadOutputOptions, ResolvedVc, SharedReference, TaskId, TraitMethod, ValueTypeId, Vc, VcRead, - VcValueTrait, VcValueType, + CellId, Completion, InvalidationReason, InvalidationReasonSet, OutputContent, RawVc, + ReadCellOptions, ReadOutputOptions, ResolvedVc, SharedReference, TaskId, TraitMethod, + ValueTypeId, Vc, VcRead, VcValueTrait, VcValueType, backend::{ Backend, CellContent, CellHash, TaskCollectiblesMap, TaskExecutionSpec, TransientTaskType, TurboTasksExecutionError, TypedCellContent, VerificationMode, @@ -43,7 +43,6 @@ use crate::{ macro_helpers::NativeFunction, message_queue::{CompilationEvent, CompilationEventQueue}, priority_runner::{Executor, PriorityRunner}, - raw_vc::{CellId, RawVc}, registry, serialization_invalidation::SerializationInvalidator, task::local_task::{LocalTask, LocalTaskSpec, LocalTaskType}, diff --git a/turbopack/crates/turbo-tasks/src/vc/mod.rs b/turbopack/crates/turbo-tasks/src/vc/mod.rs index 4b2d6ecb522f..0f708f8b0898 100644 --- a/turbopack/crates/turbo-tasks/src/vc/mod.rs +++ b/turbopack/crates/turbo-tasks/src/vc/mod.rs @@ -3,6 +3,7 @@ mod cell_mode; pub(crate) mod default; mod local; pub(crate) mod operation; +mod raw; mod read; pub(crate) mod resolved; mod traits; @@ -32,6 +33,7 @@ pub use self::{ default::ValueDefault, local::NonLocalValue, operation::{OperationValue, OperationVc, ResolveOperationVcFuture}, + raw::{CellId, RawVc, ReadRawVcFuture, ResolveRawVcFuture}, read::{ReadOwnedVcFuture, ReadVcFuture, VcDefaultRead, VcRead, VcTransparentRead}, resolved::ResolvedVc, traits::{Dynamic, Upcast, UpcastStrict, VcValueTrait, VcValueType}, @@ -39,7 +41,6 @@ pub use self::{ #[cfg(debug_assertions)] use crate::debug::{ValueDebug, ValueDebugFormat, ValueDebugFormatString}; use crate::{ - CellId, RawVc, ResolveRawVcFuture, keyed::{KeyedAccess, KeyedEq}, registry, trace::{TraceRawVcs, TraceRawVcsContext}, diff --git a/turbopack/crates/turbo-tasks/src/raw_vc.rs b/turbopack/crates/turbo-tasks/src/vc/raw.rs similarity index 100% rename from turbopack/crates/turbo-tasks/src/raw_vc.rs rename to turbopack/crates/turbo-tasks/src/vc/raw.rs From c479036924222e1c665f54d59cb6ee0b373e3461 Mon Sep 17 00:00:00 2001 From: "next-js-bot[bot]" <279046576+next-js-bot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 23:51:40 +0000 Subject: [PATCH 9/9] v16.3.0-canary.38 --- lerna.json | 2 +- packages/create-next-app/package.json | 2 +- packages/eslint-config-next/package.json | 4 ++-- packages/eslint-plugin-internal/package.json | 2 +- packages/eslint-plugin-next/package.json | 2 +- packages/font/package.json | 2 +- packages/next-bundle-analyzer/package.json | 2 +- packages/next-codemod/package.json | 2 +- packages/next-env/package.json | 2 +- packages/next-mdx/package.json | 2 +- packages/next-playwright/package.json | 2 +- packages/next-plugin-storybook/package.json | 2 +- packages/next-polyfill-module/package.json | 2 +- packages/next-polyfill-nomodule/package.json | 2 +- packages/next-routing/package.json | 2 +- packages/next-rspack/package.json | 2 +- packages/next-swc/package.json | 2 +- packages/next/package.json | 14 +++++++------- packages/react-refresh-utils/package.json | 2 +- packages/third-parties/package.json | 4 ++-- pnpm-lock.yaml | 16 ++++++++-------- 21 files changed, 36 insertions(+), 36 deletions(-) diff --git a/lerna.json b/lerna.json index 9acef58d795f..98302bb874e3 100644 --- a/lerna.json +++ b/lerna.json @@ -15,5 +15,5 @@ "registry": "https://registry.npmjs.org/" } }, - "version": "16.3.0-canary.37" + "version": "16.3.0-canary.38" } \ No newline at end of file diff --git a/packages/create-next-app/package.json b/packages/create-next-app/package.json index 0552ff3a1317..c31434c22d2a 100644 --- a/packages/create-next-app/package.json +++ b/packages/create-next-app/package.json @@ -1,6 +1,6 @@ { "name": "create-next-app", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "keywords": [ "react", "next", diff --git a/packages/eslint-config-next/package.json b/packages/eslint-config-next/package.json index 5ae58b51f2dc..492fe300daaa 100644 --- a/packages/eslint-config-next/package.json +++ b/packages/eslint-config-next/package.json @@ -1,6 +1,6 @@ { "name": "eslint-config-next", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "description": "ESLint configuration used by Next.js.", "license": "MIT", "repository": { @@ -12,7 +12,7 @@ "dist" ], "dependencies": { - "@next/eslint-plugin-next": "16.3.0-canary.37", + "@next/eslint-plugin-next": "16.3.0-canary.38", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 071fa49c464c..225325e6d68d 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": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "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 bb64794693b0..a63a8afaa447 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": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "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 7e5e92b07790..d2e8ac784459 100644 --- a/packages/font/package.json +++ b/packages/font/package.json @@ -1,7 +1,7 @@ { "name": "@next/font", "private": true, - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "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 d100769667b2..a985d1222498 100644 --- a/packages/next-bundle-analyzer/package.json +++ b/packages/next-bundle-analyzer/package.json @@ -1,6 +1,6 @@ { "name": "@next/bundle-analyzer", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "main": "index.js", "types": "index.d.ts", "license": "MIT", diff --git a/packages/next-codemod/package.json b/packages/next-codemod/package.json index fba401d9fa72..f875fac75015 100644 --- a/packages/next-codemod/package.json +++ b/packages/next-codemod/package.json @@ -1,6 +1,6 @@ { "name": "@next/codemod", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "license": "MIT", "repository": { "type": "git", diff --git a/packages/next-env/package.json b/packages/next-env/package.json index 91f7abeb9ed2..faa73740af0f 100644 --- a/packages/next-env/package.json +++ b/packages/next-env/package.json @@ -1,6 +1,6 @@ { "name": "@next/env", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "keywords": [ "react", "next", diff --git a/packages/next-mdx/package.json b/packages/next-mdx/package.json index b133ae0ceda5..68afc5d7341a 100644 --- a/packages/next-mdx/package.json +++ b/packages/next-mdx/package.json @@ -1,6 +1,6 @@ { "name": "@next/mdx", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "main": "index.js", "license": "MIT", "repository": { diff --git a/packages/next-playwright/package.json b/packages/next-playwright/package.json index fe0bbd9562f7..c8e23bf0f234 100644 --- a/packages/next-playwright/package.json +++ b/packages/next-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@next/playwright", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "repository": { "url": "vercel/next.js", "directory": "packages/next-playwright" diff --git a/packages/next-plugin-storybook/package.json b/packages/next-plugin-storybook/package.json index 6ec1529c33a4..06c2fd30ae63 100644 --- a/packages/next-plugin-storybook/package.json +++ b/packages/next-plugin-storybook/package.json @@ -1,6 +1,6 @@ { "name": "@next/plugin-storybook", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "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 68e02644914d..8596f6300312 100644 --- a/packages/next-polyfill-module/package.json +++ b/packages/next-polyfill-module/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-module", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "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 f2f21d15b13c..752e7b5128eb 100644 --- a/packages/next-polyfill-nomodule/package.json +++ b/packages/next-polyfill-nomodule/package.json @@ -1,6 +1,6 @@ { "name": "@next/polyfill-nomodule", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "description": "A polyfill for non-dead, nomodule browsers.", "main": "dist/polyfill-nomodule.js", "license": "MIT", diff --git a/packages/next-routing/package.json b/packages/next-routing/package.json index 5ab5910f388d..b5110dd01451 100644 --- a/packages/next-routing/package.json +++ b/packages/next-routing/package.json @@ -1,6 +1,6 @@ { "name": "@next/routing", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "keywords": [ "react", "next", diff --git a/packages/next-rspack/package.json b/packages/next-rspack/package.json index 91a5a17105fe..f81659571ee0 100644 --- a/packages/next-rspack/package.json +++ b/packages/next-rspack/package.json @@ -1,6 +1,6 @@ { "name": "next-rspack", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "repository": { "url": "vercel/next.js", "directory": "packages/next-rspack" diff --git a/packages/next-swc/package.json b/packages/next-swc/package.json index b7073cd5208c..e1fc35d0380c 100644 --- a/packages/next-swc/package.json +++ b/packages/next-swc/package.json @@ -1,6 +1,6 @@ { "name": "@next/swc", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "private": true, "files": [ "native/" diff --git a/packages/next/package.json b/packages/next/package.json index 0c06e3f121de..495b02117b35 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "next", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "description": "The React Framework", "main": "./dist/server/next.js", "license": "MIT", @@ -101,7 +101,7 @@ ] }, "dependencies": { - "@next/env": "16.3.0-canary.37", + "@next/env": "16.3.0-canary.38", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", @@ -165,11 +165,11 @@ "@modelcontextprotocol/sdk": "1.18.1", "@mswjs/interceptors": "0.23.0", "@napi-rs/triples": "1.2.0", - "@next/font": "16.3.0-canary.37", - "@next/polyfill-module": "16.3.0-canary.37", - "@next/polyfill-nomodule": "16.3.0-canary.37", - "@next/react-refresh-utils": "16.3.0-canary.37", - "@next/swc": "16.3.0-canary.37", + "@next/font": "16.3.0-canary.38", + "@next/polyfill-module": "16.3.0-canary.38", + "@next/polyfill-nomodule": "16.3.0-canary.38", + "@next/react-refresh-utils": "16.3.0-canary.38", + "@next/swc": "16.3.0-canary.38", "@opentelemetry/api": "1.6.0", "@playwright/test": "1.58.2", "@rspack/core": "1.6.7", diff --git a/packages/react-refresh-utils/package.json b/packages/react-refresh-utils/package.json index 9a7664759eeb..423e598566e3 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": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "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 16793b4f18ce..9f486741dc3e 100644 --- a/packages/third-parties/package.json +++ b/packages/third-parties/package.json @@ -1,6 +1,6 @@ { "name": "@next/third-parties", - "version": "16.3.0-canary.37", + "version": "16.3.0-canary.38", "repository": { "url": "vercel/next.js", "directory": "packages/third-parties" @@ -27,7 +27,7 @@ "third-party-capital": "1.0.20" }, "devDependencies": { - "next": "16.3.0-canary.37", + "next": "16.3.0-canary.38", "outdent": "0.8.0", "prettier": "2.5.1", "typescript": "6.0.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 636661a0b069..ef3d6815d204 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -986,7 +986,7 @@ importers: packages/eslint-config-next: dependencies: '@next/eslint-plugin-next': - specifier: 16.3.0-canary.37 + specifier: 16.3.0-canary.38 version: link:../eslint-plugin-next eslint: specifier: '>=9.0.0' @@ -1063,7 +1063,7 @@ importers: packages/next: dependencies: '@next/env': - specifier: 16.3.0-canary.37 + specifier: 16.3.0-canary.38 version: link:../next-env '@swc/helpers': specifier: 0.5.15 @@ -1184,19 +1184,19 @@ importers: specifier: 1.2.0 version: 1.2.0 '@next/font': - specifier: 16.3.0-canary.37 + specifier: 16.3.0-canary.38 version: link:../font '@next/polyfill-module': - specifier: 16.3.0-canary.37 + specifier: 16.3.0-canary.38 version: link:../next-polyfill-module '@next/polyfill-nomodule': - specifier: 16.3.0-canary.37 + specifier: 16.3.0-canary.38 version: link:../next-polyfill-nomodule '@next/react-refresh-utils': - specifier: 16.3.0-canary.37 + specifier: 16.3.0-canary.38 version: link:../react-refresh-utils '@next/swc': - specifier: 16.3.0-canary.37 + specifier: 16.3.0-canary.38 version: link:../next-swc '@opentelemetry/api': specifier: 1.6.0 @@ -1930,7 +1930,7 @@ importers: version: 1.0.20 devDependencies: next: - specifier: 16.3.0-canary.37 + specifier: 16.3.0-canary.38 version: link:../next outdent: specifier: 0.8.0