diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 84e0181e8a..b66d15cb72 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -59,7 +59,7 @@ use turbopack_binding::{ turbopack::{ module_options::ModuleOptionsContext, resolve_options_context::ResolveOptionsContext, - transition::{ContextTransition, FullContextTransition}, + transition::{ContextTransition, FullContextTransition, Transition}, ModuleAssetContext, }, }, @@ -118,7 +118,7 @@ impl AppProject { } } -const ECMASCRIPT_CLIENT_TRANSITION_NAME: &str = "next-ecmascript-client-reference"; +pub(crate) const ECMASCRIPT_CLIENT_TRANSITION_NAME: &str = "next-ecmascript-client-reference"; #[turbo_tasks::value_impl] impl AppProject { @@ -166,7 +166,7 @@ impl AppProject { } #[turbo_tasks::function] - fn client_transition_name(self: Vc) -> Vc { + pub(crate) fn client_transition_name(self: Vc) -> Vc { Vc::cell(ECMASCRIPT_CLIENT_TRANSITION_NAME.into()) } @@ -270,15 +270,28 @@ impl AppProject { )) } + #[turbo_tasks::function] + pub fn client_reference_transition(self: Vc) -> Vc> { + Vc::upcast(NextEcmascriptClientReferenceTransition::new( + Vc::upcast(self.client_transition()), + self.ssr_transition(), + )) + } + + #[turbo_tasks::function] + pub fn edge_client_reference_transition(self: Vc) -> Vc> { + Vc::upcast(NextEcmascriptClientReferenceTransition::new( + Vc::upcast(self.client_transition()), + self.edge_ssr_transition(), + )) + } + #[turbo_tasks::function] fn rsc_module_context(self: Vc) -> Vc { let transitions = [ ( ECMASCRIPT_CLIENT_TRANSITION_NAME.into(), - Vc::upcast(NextEcmascriptClientReferenceTransition::new( - Vc::upcast(self.client_transition()), - self.ssr_transition(), - )), + self.client_reference_transition(), ), ( "next-dynamic".into(), @@ -305,10 +318,7 @@ impl AppProject { let transitions = [ ( ECMASCRIPT_CLIENT_TRANSITION_NAME.into(), - Vc::upcast(NextEcmascriptClientReferenceTransition::new( - Vc::upcast(self.client_transition()), - self.edge_ssr_transition(), - )), + self.edge_client_reference_transition(), ), ( "next-dynamic".into(), @@ -338,10 +348,7 @@ impl AppProject { let transitions = [ ( ECMASCRIPT_CLIENT_TRANSITION_NAME.into(), - Vc::upcast(NextEcmascriptClientReferenceTransition::new( - Vc::upcast(self.client_transition()), - self.ssr_transition(), - )), + self.client_reference_transition(), ), ( "next-dynamic".into(), @@ -369,10 +376,7 @@ impl AppProject { let transitions = [ ( ECMASCRIPT_CLIENT_TRANSITION_NAME.into(), - Vc::upcast(NextEcmascriptClientReferenceTransition::new( - Vc::upcast(self.client_transition()), - self.edge_ssr_transition(), - )), + self.edge_client_reference_transition(), ), ( "next-dynamic".into(), diff --git a/packages/next-swc/crates/next-api/src/instrumentation.rs b/packages/next-swc/crates/next-api/src/instrumentation.rs index c229ba9632..5f657e1ccc 100644 --- a/packages/next-swc/crates/next-api/src/instrumentation.rs +++ b/packages/next-swc/crates/next-api/src/instrumentation.rs @@ -6,9 +6,9 @@ use next_core::{ next_server::{get_server_runtime_entries, ServerContextType}, }; use tracing::Instrument; -use turbo_tasks::{Completion, Value, Vc}; +use turbo_tasks::{Completion, RcStr, Value, Vc}; use turbopack_binding::{ - turbo::tasks_fs::{File, FileContent}, + turbo::tasks_fs::{File, FileContent, FileSystemPath}, turbopack::{ core::{ asset::AssetContent, @@ -41,6 +41,9 @@ pub struct InstrumentationEndpoint { context: Vc>, source: Vc>, is_edge: bool, + + app_dir: Option>, + ecmascript_client_reference_transition_name: Option>, } #[turbo_tasks::value_impl] @@ -51,12 +54,16 @@ impl InstrumentationEndpoint { context: Vc>, source: Vc>, is_edge: bool, + app_dir: Option>, + ecmascript_client_reference_transition_name: Option>, ) -> Vc { Self { project, context, source, is_edge, + app_dir, + ecmascript_client_reference_transition_name, } .cell() } @@ -79,7 +86,11 @@ impl InstrumentationEndpoint { ); let mut evaluatable_assets = get_server_runtime_entries( - Value::new(ServerContextType::Middleware), + Value::new(ServerContextType::Instrumentation { + app_dir: self.app_dir, + ecmascript_client_reference_transition_name: self + .ecmascript_client_reference_transition_name, + }), self.project.next_mode(), ) .resolve_entries(self.context) @@ -131,7 +142,11 @@ impl InstrumentationEndpoint { .join("server/instrumentation.js".into()), module, get_server_runtime_entries( - Value::new(ServerContextType::Instrumentation), + Value::new(ServerContextType::Instrumentation { + app_dir: self.app_dir, + ecmascript_client_reference_transition_name: self + .ecmascript_client_reference_transition_name, + }), self.project.next_mode(), ) .resolve_entries(self.context), diff --git a/packages/next-swc/crates/next-api/src/middleware.rs b/packages/next-swc/crates/next-api/src/middleware.rs index d2df5a768c..b7909e7bb2 100644 --- a/packages/next-swc/crates/next-api/src/middleware.rs +++ b/packages/next-swc/crates/next-api/src/middleware.rs @@ -8,9 +8,9 @@ use next_core::{ util::{parse_config_from_source, MiddlewareMatcherKind}, }; use tracing::Instrument; -use turbo_tasks::{Completion, Value, Vc}; +use turbo_tasks::{Completion, RcStr, Value, Vc}; use turbopack_binding::{ - turbo::tasks_fs::{File, FileContent}, + turbo::tasks_fs::{File, FileContent, FileSystemPath}, turbopack::{ core::{ asset::AssetContent, @@ -40,6 +40,8 @@ pub struct MiddlewareEndpoint { project: Vc, context: Vc>, source: Vc>, + app_dir: Option>, + ecmascript_client_reference_transition_name: Option>, } #[turbo_tasks::value_impl] @@ -49,11 +51,15 @@ impl MiddlewareEndpoint { project: Vc, context: Vc>, source: Vc>, + app_dir: Option>, + ecmascript_client_reference_transition_name: Option>, ) -> Vc { Self { project, context, source, + app_dir, + ecmascript_client_reference_transition_name, } .cell() } @@ -79,7 +85,11 @@ impl MiddlewareEndpoint { ); let mut evaluatable_assets = get_server_runtime_entries( - Value::new(ServerContextType::Middleware), + Value::new(ServerContextType::Middleware { + app_dir: self.app_dir, + ecmascript_client_reference_transition_name: self + .ecmascript_client_reference_transition_name, + }), self.project.next_mode(), ) .resolve_entries(self.context) diff --git a/packages/next-swc/crates/next-api/src/project.rs b/packages/next-swc/crates/next-api/src/project.rs index 797a3d52a4..61e7df6d8d 100644 --- a/packages/next-swc/crates/next-api/src/project.rs +++ b/packages/next-swc/crates/next-api/src/project.rs @@ -57,7 +57,7 @@ use turbopack_binding::{ }; use crate::{ - app::{AppProject, OptionAppProject}, + app::{AppProject, OptionAppProject, ECMASCRIPT_CLIENT_TRANSITION_NAME}, build, entrypoints::Entrypoints, instrumentation::InstrumentationEndpoint, @@ -875,27 +875,49 @@ impl Project { } #[turbo_tasks::function] - fn middleware_context(self: Vc) -> Vc> { - Vc::upcast(ModuleAssetContext::new( - Default::default(), + async fn middleware_context(self: Vc) -> Result>> { + let mut transitions = vec![]; + + let app_dir = *find_app_dir(self.project_path()).await?; + let app_project = *self.app_project().await?; + + let ecmascript_client_reference_transition_name = app_project + .as_ref() + .map(|app_project| app_project.client_transition_name()); + + if let Some(app_project) = app_project { + transitions.push(( + ECMASCRIPT_CLIENT_TRANSITION_NAME.into(), + app_project.edge_client_reference_transition(), + )); + } + + Ok(Vc::upcast(ModuleAssetContext::new( + Vc::cell(transitions.into_iter().collect()), self.edge_compile_time_info(), get_server_module_options_context( self.project_path(), self.execution_context(), - Value::new(ServerContextType::Middleware), + Value::new(ServerContextType::Middleware { + app_dir, + ecmascript_client_reference_transition_name, + }), self.next_mode(), self.next_config(), NextRuntime::Edge, ), get_edge_resolve_options_context( self.project_path(), - Value::new(ServerContextType::Middleware), + Value::new(ServerContextType::Middleware { + app_dir, + ecmascript_client_reference_transition_name, + }), self.next_mode(), self.next_config(), self.execution_context(), ), Vc::cell("middleware".into()), - )) + ))) } #[turbo_tasks::function] @@ -903,48 +925,139 @@ impl Project { self: Vc, source: Vc>, ) -> Result> { + let app_dir = *find_app_dir(self.project_path()).await?; + let ecmascript_client_reference_transition_name = (*self.app_project().await?) + .as_ref() + .map(|app_project| app_project.client_transition_name()); + let context = self.middleware_context(); - Ok(MiddlewareEndpoint::new(self, context, source)) + Ok(MiddlewareEndpoint::new( + self, + context, + source, + app_dir, + ecmascript_client_reference_transition_name, + )) } #[turbo_tasks::function] - fn node_instrumentation_context(self: Vc) -> Vc> { - Vc::upcast(ModuleAssetContext::new( - Default::default(), + async fn node_instrumentation_context(self: Vc) -> Result>> { + let mut transitions = vec![]; + + let app_dir = *find_app_dir(self.project_path()).await?; + let app_project = &*self.app_project().await?; + + let ecmascript_client_reference_transition_name = app_project + .as_ref() + .map(|app_project| app_project.client_transition_name()); + + if let Some(app_project) = app_project { + transitions.push(( + ECMASCRIPT_CLIENT_TRANSITION_NAME.into(), + app_project.client_reference_transition(), + )); + } + + Ok(Vc::upcast(ModuleAssetContext::new( + Vc::cell(transitions.into_iter().collect()), self.server_compile_time_info(), get_server_module_options_context( self.project_path(), self.execution_context(), - Value::new(ServerContextType::Instrumentation), + Value::new(ServerContextType::Instrumentation { + app_dir, + ecmascript_client_reference_transition_name, + }), self.next_mode(), self.next_config(), NextRuntime::NodeJs, ), get_server_resolve_options_context( self.project_path(), - Value::new(ServerContextType::Instrumentation), + Value::new(ServerContextType::Instrumentation { + app_dir, + ecmascript_client_reference_transition_name, + }), + self.next_mode(), + self.next_config(), + self.execution_context(), + ), + Vc::cell("instrumentation-edge".into()), + ))) + } + + #[turbo_tasks::function] + async fn edge_instrumentation_context(self: Vc) -> Result>> { + let mut transitions = vec![]; + + let app_dir = *find_app_dir(self.project_path()).await?; + let app_project = &*self.app_project().await?; + + let ecmascript_client_reference_transition_name = app_project + .as_ref() + .map(|app_project| app_project.client_transition_name()); + + if let Some(app_project) = app_project { + transitions.push(( + ECMASCRIPT_CLIENT_TRANSITION_NAME.into(), + app_project.edge_client_reference_transition(), + )); + } + + Ok(Vc::upcast(ModuleAssetContext::new( + Vc::cell(transitions.into_iter().collect()), + self.edge_compile_time_info(), + get_server_module_options_context( + self.project_path(), + self.execution_context(), + Value::new(ServerContextType::Instrumentation { + app_dir, + ecmascript_client_reference_transition_name, + }), + self.next_mode(), + self.next_config(), + NextRuntime::Edge, + ), + get_edge_resolve_options_context( + self.project_path(), + Value::new(ServerContextType::Instrumentation { + app_dir, + ecmascript_client_reference_transition_name, + }), self.next_mode(), self.next_config(), self.execution_context(), ), Vc::cell("instrumentation".into()), - )) + ))) } #[turbo_tasks::function] - fn instrumentation_endpoint( + async fn instrumentation_endpoint( self: Vc, source: Vc>, is_edge: bool, - ) -> Vc { + ) -> Result> { + let app_dir = *find_app_dir(self.project_path()).await?; + let ecmascript_client_reference_transition_name = (*self.app_project().await?) + .as_ref() + .map(|app_project| app_project.client_transition_name()); + let context = if is_edge { - self.middleware_context() + self.edge_instrumentation_context() } else { self.node_instrumentation_context() }; - InstrumentationEndpoint::new(self, context, source, is_edge) + Ok(InstrumentationEndpoint::new( + self, + context, + source, + is_edge, + app_dir, + ecmascript_client_reference_transition_name, + )) } #[turbo_tasks::function] diff --git a/packages/next-swc/crates/next-core/src/next_edge/context.rs b/packages/next-swc/crates/next-core/src/next_edge/context.rs index 85547a8809..1b35e8f025 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/context.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/context.rs @@ -98,7 +98,7 @@ pub async fn get_edge_resolve_options_context( let next_edge_import_map = get_next_edge_import_map(project_path, ty, next_config, execution_context); - let ty = ty.into_value(); + let ty: ServerContextType = ty.into_value(); let mut before_resolve_plugins = vec![Vc::upcast(ModuleFeatureReportResolvePlugin::new( project_path, @@ -110,7 +110,7 @@ pub async fn get_edge_resolve_options_context( | ServerContextType::AppRSC { .. } ) { before_resolve_plugins.push(Vc::upcast(NextFontLocalResolvePlugin::new(project_path))); - } + }; if matches!( ty, @@ -118,7 +118,7 @@ pub async fn get_edge_resolve_options_context( | ServerContextType::AppRoute { .. } | ServerContextType::PagesData { .. } | ServerContextType::Middleware { .. } - | ServerContextType::Instrumentation + | ServerContextType::Instrumentation { .. } ) { before_resolve_plugins.push(Vc::upcast(get_invalid_client_only_resolve_plugin( project_path, diff --git a/packages/next-swc/crates/next-core/src/next_import_map.rs b/packages/next-swc/crates/next-core/src/next_import_map.rs index 27cc8b0216..5044ccee27 100644 --- a/packages/next-swc/crates/next-core/src/next_import_map.rs +++ b/packages/next-swc/crates/next-core/src/next_import_map.rs @@ -345,7 +345,7 @@ pub async fn get_next_server_import_map( request_to_import_mapping(project_path, "next/dist/shared/lib/app-dynamic"), ); } - ServerContextType::Middleware | ServerContextType::Instrumentation => {} + ServerContextType::Middleware { .. } | ServerContextType::Instrumentation { .. } => {} } insert_next_server_special_aliases( @@ -433,7 +433,9 @@ pub async fn get_next_edge_import_map( match ty { ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } - | ServerContextType::PagesApi { .. } => {} + | ServerContextType::PagesApi { .. } + | ServerContextType::Middleware { .. } + | ServerContextType::Instrumentation { .. } => {} ServerContextType::AppSSR { .. } | ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } => { @@ -446,7 +448,6 @@ pub async fn get_next_edge_import_map( request_to_import_mapping(project_path, "next/dist/shared/lib/app-dynamic"), ); } - ServerContextType::Middleware | ServerContextType::Instrumentation => {} } insert_next_server_special_aliases( @@ -465,6 +466,7 @@ pub async fn get_next_edge_import_map( | ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } | ServerContextType::Middleware { .. } + | ServerContextType::Instrumentation { .. } | ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } | ServerContextType::PagesApi { .. } => { @@ -474,7 +476,6 @@ pub async fn get_next_edge_import_map( execution_context, ); } - _ => {} } Ok(import_map.cell()) @@ -576,10 +577,9 @@ async fn insert_next_server_special_aliases( rsc_aliases(import_map, project_path, ty, runtime, next_config).await?; } - ServerContextType::Middleware => { + ServerContextType::Middleware { .. } | ServerContextType::Instrumentation { .. } => { rsc_aliases(import_map, project_path, ty, runtime, next_config).await?; } - ServerContextType::Instrumentation => {} } // see https://github.com/vercel/next.js/blob/8013ef7372fc545d49dbd060461224ceb563b454/packages/next/src/build/webpack-config.ts#L1449-L1531 @@ -605,7 +605,7 @@ async fn insert_next_server_special_aliases( | ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } | ServerContextType::Middleware { .. } - | ServerContextType::Instrumentation => { + | ServerContextType::Instrumentation { .. } => { insert_exact_alias_map( import_map, project_path, @@ -650,19 +650,29 @@ async fn rsc_aliases( let taint = *next_config.enable_taint().await?; let react_channel = if ppr || taint { "-experimental" } else { "" }; - let mut alias = indexmap! { - "react" => format!("next/dist/compiled/react{react_channel}"), - "react-dom" => format!("next/dist/compiled/react-dom{react_channel}"), - "react/jsx-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-runtime"), - "react/jsx-dev-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-dev-runtime"), - "react/compiler-runtime" => format!("next/dist/compiled/react{react_channel}/compiler-runtime"), - "react-dom/client" => format!("next/dist/compiled/react-dom{react_channel}/client"), - "react-dom/static" => format!("next/dist/compiled/react-dom-experimental/static"), - "react-dom/static.edge" => format!("next/dist/compiled/react-dom-experimental/static.edge"), - "react-dom/static.browser" => format!("next/dist/compiled/react-dom-experimental/static.browser"), - "react-dom/server" => format!("next/dist/compiled/react-dom{react_channel}/server"), - "react-dom/server.edge" => format!("next/dist/compiled/react-dom{react_channel}/server.edge"), - "react-dom/server.browser" => format!("next/dist/compiled/react-dom{react_channel}/server.browser"), + let mut alias = IndexMap::new(); + if matches!( + ty, + ServerContextType::AppSSR { .. } + | ServerContextType::AppRSC { .. } + | ServerContextType::AppRoute { .. } + ) { + alias.extend(indexmap! { + "react" => format!("next/dist/compiled/react{react_channel}"), + "react-dom" => format!("next/dist/compiled/react-dom{react_channel}"), + "react/jsx-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-runtime"), + "react/jsx-dev-runtime" => format!("next/dist/compiled/react{react_channel}/jsx-dev-runtime"), + "react/compiler-runtime" => format!("next/dist/compiled/react{react_channel}/compiler-runtime"), + "react-dom/client" => format!("next/dist/compiled/react-dom{react_channel}/client"), + "react-dom/static" => format!("next/dist/compiled/react-dom-experimental/static"), + "react-dom/static.edge" => format!("next/dist/compiled/react-dom-experimental/static.edge"), + "react-dom/static.browser" => format!("next/dist/compiled/react-dom-experimental/static.browser"), + "react-dom/server" => format!("next/dist/compiled/react-dom{react_channel}/server"), + "react-dom/server.edge" => format!("next/dist/compiled/react-dom{react_channel}/server.edge"), + "react-dom/server.browser" => format!("next/dist/compiled/react-dom{react_channel}/server.browser"), + }); + } + alias.extend(indexmap! { "react-server-dom-webpack/client" => format!("next/dist/compiled/react-server-dom-turbopack{react_channel}/client"), "react-server-dom-webpack/client.edge" => format!("next/dist/compiled/react-server-dom-turbopack{react_channel}/client.edge"), "react-server-dom-webpack/server.edge" => format!("next/dist/compiled/react-server-dom-turbopack{react_channel}/server.edge"), @@ -671,7 +681,7 @@ async fn rsc_aliases( "react-server-dom-turbopack/client.edge" => format!("next/dist/compiled/react-server-dom-turbopack{react_channel}/client.edge"), "react-server-dom-turbopack/server.edge" => format!("next/dist/compiled/react-server-dom-turbopack{react_channel}/server.edge"), "react-server-dom-turbopack/server.node" => format!("next/dist/compiled/react-server-dom-turbopack{react_channel}/server.node"), - }; + }); if runtime == NextRuntime::NodeJs { match ty { @@ -684,9 +694,12 @@ async fn rsc_aliases( "react-dom" => format!("next/dist/server/route-modules/app-page/vendored/ssr/react-dom"), "react-server-dom-webpack/client.edge" => format!("next/dist/server/route-modules/app-page/vendored/ssr/react-server-dom-turbopack-client-edge"), "react-server-dom-turbopack/client.edge" => format!("next/dist/server/route-modules/app-page/vendored/ssr/react-server-dom-turbopack-client-edge"), - }) + }); } - ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } => { + ServerContextType::AppRSC { .. } + | ServerContextType::AppRoute { .. } + | ServerContextType::Middleware { .. } + | ServerContextType::Instrumentation { .. } => { alias.extend(indexmap! { "react/jsx-runtime" => format!("next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-runtime"), "react/jsx-dev-runtime" => format!("next/dist/server/route-modules/app-page/vendored/rsc/react-jsx-dev-runtime"), @@ -701,7 +714,7 @@ async fn rsc_aliases( // Needed to make `react-dom/server` work. "next/dist/compiled/react" => format!("next/dist/compiled/react/index.js"), - }) + }); } _ => {} } diff --git a/packages/next-swc/crates/next-core/src/next_server/context.rs b/packages/next-swc/crates/next-core/src/next_server/context.rs index 13127c3ebf..9022efc48d 100644 --- a/packages/next-swc/crates/next-core/src/next_server/context.rs +++ b/packages/next-swc/crates/next-core/src/next_server/context.rs @@ -18,7 +18,10 @@ use turbopack_binding::{ free_var_references, }, ecmascript::{references::esm::UrlRewriteBehavior, TreeShakingMode}, - ecmascript_plugin::transform::directives::client::ClientDirectiveTransformer, + ecmascript_plugin::transform::directives::{ + client::ClientDirectiveTransformer, + client_disallowed::ClientDisallowedDirectiveTransformer, + }, node::{ execution_context::ExecutionContext, transforms::postcss::{PostCssConfigLocation, PostCssTransformOptions}, @@ -96,8 +99,14 @@ pub enum ServerContextType { app_dir: Vc, ecmascript_client_reference_transition_name: Option>, }, - Middleware, - Instrumentation, + Middleware { + app_dir: Option>, + ecmascript_client_reference_transition_name: Option>, + }, + Instrumentation { + app_dir: Option>, + ecmascript_client_reference_transition_name: Option>, + }, } impl ServerContextType { @@ -108,6 +117,7 @@ impl ServerContextType { | ServerContextType::AppRoute { .. } | ServerContextType::PagesApi { .. } | ServerContextType::Middleware { .. } + | ServerContextType::Instrumentation { .. } ) } } @@ -214,7 +224,7 @@ pub async fn get_server_resolve_options_context( | ServerContextType::PagesApi { .. } | ServerContextType::AppRoute { .. } | ServerContextType::Middleware { .. } - | ServerContextType::Instrumentation => { + | ServerContextType::Instrumentation { .. } => { vec![Vc::upcast(module_feature_report_resolve_plugin)] } }; @@ -264,7 +274,7 @@ pub async fn get_server_resolve_options_context( | ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } | ServerContextType::Middleware { .. } - | ServerContextType::Instrumentation => { + | ServerContextType::Instrumentation { .. } => { before_resolve_plugins.push(Vc::upcast(invalid_client_only_resolve_plugin)); before_resolve_plugins.push(Vc::upcast(invalid_styled_jsx_client_only_resolve_plugin)); } @@ -731,13 +741,46 @@ pub async fn get_server_module_options_context( ..module_options_context } } - ServerContextType::Middleware | ServerContextType::Instrumentation => { - let custom_source_transform_rules: Vec = + ServerContextType::Middleware { + app_dir, + ecmascript_client_reference_transition_name, + } + | ServerContextType::Instrumentation { + app_dir, + ecmascript_client_reference_transition_name, + } => { + let mut custom_source_transform_rules: Vec = vec![styled_components_transform_rule, styled_jsx_transform_rule] .into_iter() .flatten() .collect(); + if let Some(ecmascript_client_reference_transition_name) = + ecmascript_client_reference_transition_name + { + custom_source_transform_rules.push(get_ecma_transform_rule( + Box::new(ClientDirectiveTransformer::new( + ecmascript_client_reference_transition_name, + )), + enable_mdx_rs.is_some(), + true, + )); + } else { + custom_source_transform_rules.push(get_ecma_transform_rule( + Box::new(ClientDisallowedDirectiveTransformer::new( + "next/dist/client/use-client-disallowed.js".to_string(), + )), + enable_mdx_rs.is_some(), + true, + )); + } + + custom_source_transform_rules.push( + get_next_react_server_components_transform_rule(next_config, true, app_dir).await?, + ); + + internal_custom_rules.extend(custom_source_transform_rules.iter().cloned()); + next_server_rules.extend(custom_source_transform_rules); next_server_rules.extend(source_transform_rules); diff --git a/packages/next-swc/crates/next-core/src/next_shared/resolve.rs b/packages/next-swc/crates/next-core/src/next_shared/resolve.rs index c0ced49ee2..9214f07636 100644 --- a/packages/next-swc/crates/next-core/src/next_shared/resolve.rs +++ b/packages/next-swc/crates/next-core/src/next_shared/resolve.rs @@ -293,10 +293,10 @@ impl AfterResolvePlugin for NextNodeSharedRuntimeResolvePlugin { let resource_request = format!( "next/dist/server/route-modules/{}/vendored/contexts/{}.js", match self.context { - ServerContextType::Pages { .. } => "pages", ServerContextType::AppRoute { .. } => "app-route", ServerContextType::AppSSR { .. } | ServerContextType::AppRSC { .. } => "app-page", - _ => "unknown", + // Use default pages context for all other contexts. + _ => "pages", }, stem ); diff --git a/packages/next/src/build/create-compiler-aliases.ts b/packages/next/src/build/create-compiler-aliases.ts index 6434c208e8..6c065b7874 100644 --- a/packages/next/src/build/create-compiler-aliases.ts +++ b/packages/next/src/build/create-compiler-aliases.ts @@ -235,6 +235,16 @@ export function createAppRouterApiAliases(isServerOnlyLayer: boolean) { return aliasMap } +export function createRSCRendererAliases(bundledReactChannel: string) { + return { + // react-server-dom-webpack alias + 'react-server-dom-webpack/client$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client`, + 'react-server-dom-webpack/client.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client.edge`, + 'react-server-dom-webpack/server.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.edge`, + 'react-server-dom-webpack/server.node$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.node`, + } +} + export function createRSCAliases( bundledReactChannel: string, { @@ -262,10 +272,7 @@ export function createRSCAliases( 'react-dom/server.edge$': `next/dist/build/webpack/alias/react-dom-server-edge${bundledReactChannel}.js`, 'react-dom/server.browser$': `next/dist/build/webpack/alias/react-dom-server-browser${bundledReactChannel}.js`, // react-server-dom-webpack alias - 'react-server-dom-webpack/client$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client`, - 'react-server-dom-webpack/client.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/client.edge`, - 'react-server-dom-webpack/server.edge$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.edge`, - 'react-server-dom-webpack/server.node$': `next/dist/compiled/react-server-dom-webpack${bundledReactChannel}/server.node`, + ...createRSCRendererAliases(bundledReactChannel), } if (!isEdgeServer) { diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index cafedb2fb3..8f2d6db974 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -838,8 +838,9 @@ export function finalizeEntrypoint({ } case COMPILER_NAMES.edgeServer: { return { - layer: - isMiddlewareFilename(name) || isApi || isInstrumentation + layer: isApi + ? WEBPACK_LAYERS.api + : isMiddlewareFilename(name) || isInstrumentation ? WEBPACK_LAYERS.middleware : undefined, library: { name: ['_ENTRIES', `middleware_[name]`], type: 'assign' }, diff --git a/packages/next/src/build/handle-externals.ts b/packages/next/src/build/handle-externals.ts index 9ca9421403..5b38dd26e0 100644 --- a/packages/next/src/build/handle-externals.ts +++ b/packages/next/src/build/handle-externals.ts @@ -10,7 +10,7 @@ import { NODE_ESM_RESOLVE_OPTIONS, NODE_RESOLVE_OPTIONS, } from './webpack-config' -import { isWebpackAppLayer, isWebpackServerOnlyLayer } from './utils' +import { isWebpackBundledLayer, isWebpackServerOnlyLayer } from './utils' import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' const reactPackagesRegex = /^(react|react-dom|react-server-dom-webpack)($|\/)/ @@ -174,7 +174,7 @@ export function makeExternalHandler({ return `commonjs next/dist/lib/import-next-warning` } - const isAppLayer = isWebpackAppLayer(layer) + const isAppLayer = isWebpackBundledLayer(layer) // Relative requires don't need custom resolution, because they // are relative to requests we've already resolved here. diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 73dac41f7b..d46e817d80 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -2276,8 +2276,8 @@ export function isWebpackDefaultLayer( return layer === null || layer === undefined } -export function isWebpackAppLayer( +export function isWebpackBundledLayer( layer: WebpackLayerName | null | undefined ): boolean { - return Boolean(layer && WEBPACK_LAYERS.GROUP.app.includes(layer as any)) + return Boolean(layer && WEBPACK_LAYERS.GROUP.bundled.includes(layer as any)) } diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 9f4812b1bc..40dfdf4a71 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -9,7 +9,7 @@ import { escapeStringRegexp } from '../shared/lib/escape-regexp' import { WEBPACK_LAYERS, WEBPACK_RESOURCE_QUERIES } from '../lib/constants' import type { WebpackLayerName } from '../lib/constants' import { - isWebpackAppLayer, + isWebpackBundledLayer, isWebpackClientOnlyLayer, isWebpackDefaultLayer, isWebpackServerOnlyLayer, @@ -81,6 +81,7 @@ import { createRSCAliases, createNextApiEsmAliases, createAppRouterApiAliases, + createRSCRendererAliases, } from './create-compiler-aliases' import { hasCustomExportOutput } from '../export/utils' import { CssChunkingPlugin } from './webpack/plugins/css-chunking-plugin' @@ -529,6 +530,7 @@ export default async function getBaseWebpackConfig( : [] const instrumentLayerLoaders = [ + 'next-flight-loader', // When using Babel, we will have to add the SWC loader // as an additional pass to handle RSC correctly. // This will cause some performance overhead but @@ -538,12 +540,13 @@ export default async function getBaseWebpackConfig( ].filter(Boolean) const middlewareLayerLoaders = [ + 'next-flight-loader', // When using Babel, we will have to use SWC to do the optimization // for middleware to tree shake the unused default optimized imports like "next/server". // This will cause some performance overhead but // acceptable as Babel will not be recommended. getSwcLoader({ - serverComponents: false, + serverComponents: true, bundleLayer: WEBPACK_LAYERS.middleware, }), babelLoader, @@ -592,13 +595,12 @@ export default async function getBaseWebpackConfig( // Loader for API routes needs to be differently configured as it shouldn't // have RSC transpiler enabled, so syntax checks such as invalid imports won't // be performed. - const apiRoutesLayerLoaders = - hasAppDir && useSWCLoader - ? getSwcLoader({ - serverComponents: false, - bundleLayer: WEBPACK_LAYERS.api, - }) - : defaultLoaders.babel + const apiRoutesLayerLoaders = useSWCLoader + ? getSwcLoader({ + serverComponents: false, + bundleLayer: WEBPACK_LAYERS.api, + }) + : defaultLoaders.babel const pageExtensions = config.pageExtensions @@ -1304,7 +1306,7 @@ export default async function getBaseWebpackConfig( test: /next[\\/]dist[\\/](esm[\\/])?server[\\/]route-modules[\\/]app-page[\\/]module/, }, { - issuerLayer: isWebpackAppLayer, + issuerLayer: isWebpackBundledLayer, resolve: { alias: createNextApiEsmAliases(), }, @@ -1460,8 +1462,11 @@ export default async function getBaseWebpackConfig( ...codeCondition, issuerLayer: WEBPACK_LAYERS.api, parser: { - // Switch back to normal URL handling - url: true, + // In Node.js, switch back to normal URL handling. + // In Edge runtime, we should disable parser.url handling in webpack so URLDependency is not added. + // Then there's browser code won't be injected into the edge runtime chunk. + // x-ref: https://github.com/webpack/webpack/blob/d9ce3b1f87e63c809d8a19bbd92257d65922e81f/lib/web/JsonpChunkLoadingRuntimeModule.js#L69 + url: !isEdgeServer, }, use: apiRoutesLayerLoaders, }, @@ -1469,11 +1474,23 @@ export default async function getBaseWebpackConfig( test: codeCondition.test, issuerLayer: WEBPACK_LAYERS.middleware, use: middlewareLayerLoaders, + resolve: { + mainFields: getMainField(compilerType, true), + conditionNames: reactServerCondition, + // Always use default channels when use installed react + alias: createRSCRendererAliases(''), + }, }, { test: codeCondition.test, issuerLayer: WEBPACK_LAYERS.instrument, use: instrumentLayerLoaders, + resolve: { + mainFields: getMainField(compilerType, true), + conditionNames: reactServerCondition, + // Always use default channels when use installed react + alias: createRSCRendererAliases(''), + }, }, ...(hasAppDir ? [ diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index 8e0f53f724..66c6cc90ef 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -155,6 +155,10 @@ export function getDefineEnv({ * the runtime they are running with, if it's not using `edge-runtime` */ process.env.NEXT_EDGE_RUNTIME_PROVIDER ?? 'edge-runtime', + + // process should be only { env: {...} } for edge runtime. + // For ignore avoid warn on `process.emit` usage but directly omit it. + 'process.emit': false, }), 'process.turbopack': isTurbopack, 'process.env.TURBOPACK': isTurbopack, diff --git a/packages/next/src/build/webpack/plugins/middleware-plugin.ts b/packages/next/src/build/webpack/plugins/middleware-plugin.ts index 866aff2ae1..e6bdccd6da 100644 --- a/packages/next/src/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/src/build/webpack/plugins/middleware-plugin.ts @@ -28,7 +28,10 @@ import type { Telemetry } from '../../../telemetry/storage' import { traceGlobals } from '../../../trace/shared' import { EVENT_BUILD_FEATURE_USAGE } from '../../../telemetry/events' import { normalizeAppPath } from '../../../shared/lib/router/utils/app-paths' -import { INSTRUMENTATION_HOOK_FILENAME } from '../../../lib/constants' +import { + INSTRUMENTATION_HOOK_FILENAME, + WEBPACK_LAYERS, +} from '../../../lib/constants' import type { CustomRoutes } from '../../../lib/load-custom-routes' import { isInterceptionRouteRewrite } from '../../../lib/generate-interception-routes-rewrites' import { getDynamicCodeEvaluationError } from './wellknown-errors-plugin/parse-dynamic-code-evaluation-error' @@ -272,7 +275,8 @@ function buildWebpackError({ } function isInMiddlewareLayer(parser: webpack.javascript.JavascriptParser) { - return parser.state.module?.layer === 'middleware' + const layer = parser.state.module?.layer + return layer === WEBPACK_LAYERS.middleware || layer === WEBPACK_LAYERS.api } function isNodeJsModule(moduleName: string) { @@ -849,7 +853,8 @@ export async function handleWebpackExternalForEdgeRuntime({ getResolve: () => any }) { if ( - contextInfo.issuerLayer === 'middleware' && + (contextInfo.issuerLayer === WEBPACK_LAYERS.middleware || + contextInfo.issuerLayer === WEBPACK_LAYERS.api) && isNodeJsModule(request) && !supportedEdgePolyfills.has(request) ) { diff --git a/packages/next/src/client/use-client-disallowed.ts b/packages/next/src/client/use-client-disallowed.ts new file mode 100644 index 0000000000..71c898def8 --- /dev/null +++ b/packages/next/src/client/use-client-disallowed.ts @@ -0,0 +1,19 @@ +const error = new Proxy( + {}, + { + get(_target) { + throw new Error( + 'Using client components is not allowed in this environment.' + ) + }, + } +) +export default new Proxy( + {}, + { + get: (_target, p) => { + if (p === '__esModule') return true + return error + }, + } +) diff --git a/packages/next/src/lib/constants.ts b/packages/next/src/lib/constants.ts index c250a65b4a..c098c8da62 100644 --- a/packages/next/src/lib/constants.ts +++ b/packages/next/src/lib/constants.ts @@ -159,6 +159,11 @@ export type WebpackLayerName = const WEBPACK_LAYERS = { ...WEBPACK_LAYERS_NAMES, GROUP: { + builtinReact: [ + WEBPACK_LAYERS_NAMES.reactServerComponents, + WEBPACK_LAYERS_NAMES.actionBrowser, + WEBPACK_LAYERS_NAMES.appMetadataRoute, + ], serverOnly: [ WEBPACK_LAYERS_NAMES.reactServerComponents, WEBPACK_LAYERS_NAMES.actionBrowser, @@ -174,7 +179,7 @@ const WEBPACK_LAYERS = { WEBPACK_LAYERS_NAMES.serverSideRendering, WEBPACK_LAYERS_NAMES.appPagesBrowser, ], - app: [ + bundled: [ WEBPACK_LAYERS_NAMES.reactServerComponents, WEBPACK_LAYERS_NAMES.actionBrowser, WEBPACK_LAYERS_NAMES.appMetadataRoute, diff --git a/test/e2e/module-layer/lib/mixed-lib/shared-module.js b/test/e2e/module-layer/lib/mixed-lib/shared-module.js new file mode 100644 index 0000000000..89966a446a --- /dev/null +++ b/test/e2e/module-layer/lib/mixed-lib/shared-module.js @@ -0,0 +1,4 @@ +import Link from 'next/link' + +export const textValue = 'text-value' +export const TestLink = Link diff --git a/test/e2e/module-layer/middleware.js b/test/e2e/module-layer/middleware.js index 8a4d11761d..51bfdfc909 100644 --- a/test/e2e/module-layer/middleware.js +++ b/test/e2e/module-layer/middleware.js @@ -1,11 +1,20 @@ import 'server-only' -import React from 'react' +import * as React from 'react' import { NextResponse } from 'next/server' // import './lib/mixed-lib' export function middleware(request) { - if (React.useState) { + // To avoid webpack ESM exports checking warning + const ReactObject = Object(React) + if (ReactObject.useState) { throw new Error('React.useState should not be defined in server layer') } + + if (request.nextUrl.pathname === '/middleware') { + return Response.json({ + React: Object.keys(ReactObject), + }) + } + return NextResponse.next() } diff --git a/test/e2e/module-layer/module-layer.test.ts b/test/e2e/module-layer/module-layer.test.ts index bf665e1428..8dca3ba952 100644 --- a/test/e2e/module-layer/module-layer.test.ts +++ b/test/e2e/module-layer/module-layer.test.ts @@ -2,7 +2,7 @@ import { nextTestSetup } from 'e2e-utils' import { getRedboxSource, hasRedbox, retry } from 'next-test-utils' describe('module layer', () => { - const { next, isNextStart, isNextDev, isTurbopack } = nextTestSetup({ + const { next, isNextStart, isNextDev } = nextTestSetup({ files: __dirname, }) @@ -18,8 +18,10 @@ describe('module layer', () => { '/app/route', '/app/route-edge', // pages/api - '/api/hello', - '/api/hello-edge', + '/api/default', + '/api/default-edge', + '/api/server-only', + '/api/server-only-edge', '/api/mixed', ] @@ -30,6 +32,35 @@ describe('module layer', () => { }) } + it('should render installed react-server condition for middleware', async () => { + const json = await next.fetch('/middleware').then((res) => res.json()) + expect(json.React).toContain('version') // basic react-server export + expect(json.React).not.toContain('useEffect') // no client api export + }) + + // This is for backward compatibility, don't change react usage in existing pages/api + it('should contain client react exports for pages api', async () => { + async function verifyReactExports(route, isEdge) { + const json = await next.fetch(route).then((res) => res.json()) + // contain all react-server and default condition exports + expect(json.React).toContain('version') + expect(json.React).toContain('useEffect') + + // contain react-dom-server default condition exports + expect(json.ReactDomServer).toContain('version') + expect(json.ReactDomServer).toContain('renderToString') + expect(json.ReactDomServer).toContain('renderToStaticMarkup') + expect(json.ReactDomServer).toContain( + isEdge ? 'renderToReadableStream' : 'renderToPipeableStream' + ) + } + + await verifyReactExports('/api/default', false) + await verifyReactExports('/api/default-edge', true) + await verifyReactExports('/api/server-only', false) + await verifyReactExports('/api/server-only-edge', true) + }) + if (isNextStart) { it('should log the build info properly', async () => { const cliOutput = next.cliOutput @@ -40,7 +71,8 @@ describe('module layer', () => { ) expect(functionsManifest.functions).toContainKeys([ '/app/route-edge', - '/api/hello-edge', + '/api/default-edge', + '/api/server-only-edge', '/app/client-edge', '/app/server-edge', ]) @@ -52,9 +84,10 @@ describe('module layer', () => { ) expect(middlewareManifest.middleware).toBeTruthy() expect(pagesManifest).toContainKeys([ - '/api/hello-edge', + '/api/default-edge', '/pages-ssr', - '/api/hello', + '/api/default', + '/api/server-only', ]) }) } @@ -81,22 +114,13 @@ describe('module layer', () => { .replace("// import './lib/mixed-lib'", "import './lib/mixed-lib'") ) - const existingCliOutputLength = next.cliOutput.length await retry(async () => { expect(await hasRedbox(browser)).toBe(true) const source = await getRedboxSource(browser) expect(source).toContain( - `'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component.` + `You're importing a component that imports client-only. It only works in a Client Component but none of its parents are marked with "use client"` ) }) - - if (!isTurbopack) { - const newCliOutput = next.cliOutput.slice(existingCliOutputLength) - expect(newCliOutput).toContain('./middleware.js') - expect(newCliOutput).toContain( - `'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component` - ) - } }) }) } diff --git a/test/e2e/module-layer/pages/api/default-edge.js b/test/e2e/module-layer/pages/api/default-edge.js new file mode 100644 index 0000000000..f4621f2500 --- /dev/null +++ b/test/e2e/module-layer/pages/api/default-edge.js @@ -0,0 +1,11 @@ +import * as ReactDomServer from 'react-dom/server' +import * as React from 'react' + +export default async (_req) => { + return Response.json({ + React: Object.keys(Object(React)), + ReactDomServer: Object.keys(Object(ReactDomServer)), + }) +} + +export const runtime = 'edge' diff --git a/test/e2e/module-layer/pages/api/default.js b/test/e2e/module-layer/pages/api/default.js new file mode 100644 index 0000000000..2b13630aa4 --- /dev/null +++ b/test/e2e/module-layer/pages/api/default.js @@ -0,0 +1,9 @@ +import * as ReactDomServer from 'react-dom/server' +import * as React from 'react' + +export default async (_req, res) => { + return res.json({ + React: Object.keys(Object(React)), + ReactDomServer: Object.keys(Object(ReactDomServer)), + }) +} diff --git a/test/e2e/module-layer/pages/api/hello-edge.js b/test/e2e/module-layer/pages/api/hello-edge.js deleted file mode 100644 index dce0100296..0000000000 --- a/test/e2e/module-layer/pages/api/hello-edge.js +++ /dev/null @@ -1,7 +0,0 @@ -import 'server-only' - -export default function handler() { - return new Response('pages/api/hello-edge.js:') -} - -export const runtime = 'edge' diff --git a/test/e2e/module-layer/pages/api/hello.js b/test/e2e/module-layer/pages/api/hello.js deleted file mode 100644 index d1fe5339d8..0000000000 --- a/test/e2e/module-layer/pages/api/hello.js +++ /dev/null @@ -1,5 +0,0 @@ -import 'server-only' - -export default function handler(req, res) { - return res.send('pages/api/hello.js') -} diff --git a/test/e2e/module-layer/pages/api/server-only-edge.js b/test/e2e/module-layer/pages/api/server-only-edge.js new file mode 100644 index 0000000000..17e682c015 --- /dev/null +++ b/test/e2e/module-layer/pages/api/server-only-edge.js @@ -0,0 +1,12 @@ +import 'server-only' +import * as ReactDomServer from 'react-dom/server' +import * as React from 'react' + +export default async (_req) => { + return Response.json({ + React: Object.keys(Object(React)), + ReactDomServer: Object.keys(Object(ReactDomServer)), + }) +} + +export const runtime = 'edge' diff --git a/test/e2e/module-layer/pages/api/server-only.js b/test/e2e/module-layer/pages/api/server-only.js new file mode 100644 index 0000000000..7f276bdf9c --- /dev/null +++ b/test/e2e/module-layer/pages/api/server-only.js @@ -0,0 +1,10 @@ +import 'server-only' +import * as ReactDomServer from 'react-dom/server' +import * as React from 'react' + +export default async (_req, res) => { + return res.json({ + React: Object.keys(Object(React)), + ReactDomServer: Object.keys(Object(ReactDomServer)), + }) +} diff --git a/test/e2e/react-version/app/app/client-edge/page.js b/test/e2e/react-version/app/app/client-edge/page.js new file mode 100644 index 0000000000..1bb1e68bdc --- /dev/null +++ b/test/e2e/react-version/app/app/client-edge/page.js @@ -0,0 +1,5 @@ +'use client' + +export { default } from '../client/page' + +export const runtime = 'edge' diff --git a/test/e2e/react-version/app/app/client/page.js b/test/e2e/react-version/app/app/client/page.js new file mode 100644 index 0000000000..0aafacbc62 --- /dev/null +++ b/test/e2e/react-version/app/app/client/page.js @@ -0,0 +1,7 @@ +'use client' + +import { ReactConditionUI } from '../../../lib/react-version' + +export default function Page() { + return +} diff --git a/test/e2e/react-version/app/app/route-edge/route.js b/test/e2e/react-version/app/app/route-edge/route.js new file mode 100644 index 0000000000..7db938b384 --- /dev/null +++ b/test/e2e/react-version/app/app/route-edge/route.js @@ -0,0 +1,3 @@ +export { GET } from '../route/route' + +export const runtime = 'edge' diff --git a/test/e2e/react-version/app/app/route/route.js b/test/e2e/react-version/app/app/route/route.js new file mode 100644 index 0000000000..255fb2ffd3 --- /dev/null +++ b/test/e2e/react-version/app/app/route/route.js @@ -0,0 +1,5 @@ +import { getReactConditionJson } from '../../../lib/react-version' + +export function GET() { + return Response.json(getReactConditionJson()) +} diff --git a/test/e2e/react-version/app/app/server-edge/page.js b/test/e2e/react-version/app/app/server-edge/page.js new file mode 100644 index 0000000000..d4bb5d67b4 --- /dev/null +++ b/test/e2e/react-version/app/app/server-edge/page.js @@ -0,0 +1,3 @@ +export { default } from '../server/page' + +export const runtime = 'edge' diff --git a/test/e2e/react-version/app/app/server/page.js b/test/e2e/react-version/app/app/server/page.js new file mode 100644 index 0000000000..289dd851aa --- /dev/null +++ b/test/e2e/react-version/app/app/server/page.js @@ -0,0 +1,5 @@ +import { ReactConditionUI } from '../../../lib/react-version' + +export default function Page() { + return +} diff --git a/test/e2e/react-version/app/layout.js b/test/e2e/react-version/app/layout.js new file mode 100644 index 0000000000..4ee00a2185 --- /dev/null +++ b/test/e2e/react-version/app/layout.js @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/react-version/lib/react-validate.js b/test/e2e/react-version/lib/react-validate.js new file mode 100644 index 0000000000..0c14777cb0 --- /dev/null +++ b/test/e2e/react-version/lib/react-validate.js @@ -0,0 +1,27 @@ +import React from 'react' +import ReactDOM from 'react-dom' + +function getReactConditionByModule(React_) { + const React = Object(React_) + const isReactServer = + React.useState === undefined && + React.useEffect === undefined && + React.version !== undefined && + React.useId !== undefined + return isReactServer ? 'react-server' : 'default' +} + +function getReactDomConditionByModule(ReactDOM_) { + const ReactDOM = Object(ReactDOM_) + const isReactServer = + ReactDOM.useFormState === undefined && ReactDOM.preload !== undefined + return isReactServer ? 'react-server' : 'default' +} + +export function getReactCondition() { + return getReactConditionByModule(React) +} + +export function getReactDomCondition() { + return getReactDomConditionByModule(ReactDOM) +} diff --git a/test/e2e/react-version/lib/react-version.js b/test/e2e/react-version/lib/react-version.js new file mode 100644 index 0000000000..74919e2075 --- /dev/null +++ b/test/e2e/react-version/lib/react-version.js @@ -0,0 +1,17 @@ +import { getReactCondition, getReactDomCondition } from './react-validate' + +export function ReactConditionUI() { + return ( +
+

{getReactCondition()}

+

{getReactDomCondition()}

+
+ ) +} + +export function getReactConditionJson() { + return { + react: getReactCondition(), + reactDom: getReactDomCondition(), + } +} diff --git a/test/e2e/react-version/middleware.js b/test/e2e/react-version/middleware.js new file mode 100644 index 0000000000..48257ff2fa --- /dev/null +++ b/test/e2e/react-version/middleware.js @@ -0,0 +1,10 @@ +import { NextResponse } from 'next/server' +import { getReactConditionJson } from './lib/react-version' + +export function middleware(request) { + if (request.nextUrl.pathname === '/middleware') { + return Response.json(getReactConditionJson()) + } + + return NextResponse.next() +} diff --git a/test/e2e/react-version/pages/api/pages-api-edge-url-dep.js b/test/e2e/react-version/pages/api/pages-api-edge-url-dep.js new file mode 100644 index 0000000000..12c4679ba2 --- /dev/null +++ b/test/e2e/react-version/pages/api/pages-api-edge-url-dep.js @@ -0,0 +1,13 @@ +import { getReactConditionJson } from '../../lib/react-version' + +// Adding URL dependency to edge api, it shouldn't break the build +console.log( + 'TEST_URL_DEPENDENCY', + import(new URL('./style.css', import.meta.url).href) +) + +export default async (_req) => { + return Response.json(getReactConditionJson()) +} + +export const runtime = 'experimental-edge' diff --git a/test/e2e/react-version/pages/api/pages-api-edge.js b/test/e2e/react-version/pages/api/pages-api-edge.js new file mode 100644 index 0000000000..cceae822c1 --- /dev/null +++ b/test/e2e/react-version/pages/api/pages-api-edge.js @@ -0,0 +1,7 @@ +import { getReactConditionJson } from '../../lib/react-version' + +export default async (_req) => { + return Response.json(getReactConditionJson()) +} + +export const runtime = 'experimental-edge' diff --git a/test/e2e/react-version/pages/api/pages-api.js b/test/e2e/react-version/pages/api/pages-api.js new file mode 100644 index 0000000000..9ea402cc74 --- /dev/null +++ b/test/e2e/react-version/pages/api/pages-api.js @@ -0,0 +1,5 @@ +import { getReactConditionJson } from '../../lib/react-version' + +export default async (_req, res) => { + return res.json(getReactConditionJson()) +} diff --git a/test/e2e/react-version/pages/api/style.css b/test/e2e/react-version/pages/api/style.css new file mode 100644 index 0000000000..a15c877ac0 --- /dev/null +++ b/test/e2e/react-version/pages/api/style.css @@ -0,0 +1,3 @@ +.foo { + color: red; +} diff --git a/test/e2e/react-version/pages/pages-ssr-edge.js b/test/e2e/react-version/pages/pages-ssr-edge.js new file mode 100644 index 0000000000..907fd5f21a --- /dev/null +++ b/test/e2e/react-version/pages/pages-ssr-edge.js @@ -0,0 +1,9 @@ +import { ReactConditionUI } from '../lib/react-version' + +export default function Page() { + return +} + +export const config = { + runtime: 'experimental-edge', +} diff --git a/test/e2e/react-version/pages/pages-ssr.js b/test/e2e/react-version/pages/pages-ssr.js new file mode 100644 index 0000000000..5ab9cf1056 --- /dev/null +++ b/test/e2e/react-version/pages/pages-ssr.js @@ -0,0 +1,5 @@ +import { ReactConditionUI } from '../lib/react-version' + +export default function Page() { + return +} diff --git a/test/e2e/react-version/react-version.test.ts b/test/e2e/react-version/react-version.test.ts new file mode 100644 index 0000000000..00fd3a3a98 --- /dev/null +++ b/test/e2e/react-version/react-version.test.ts @@ -0,0 +1,63 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('react version', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should use react-server condition for app router server components pages', async () => { + const rscPagesRoutes = ['/app/server', '/app/server-edge'] + + for (const route of rscPagesRoutes) { + const $ = await next.render$(route) + expect($('#react-export-condition').text()).toBe('react-server') + expect($('#react-dom-export-condition').text()).toBe('react-server') + } + }) + + it('should use react-server condition for app router client components pages', async () => { + const rscPagesRoutes = ['/app/client', '/app/client-edge'] + + for (const route of rscPagesRoutes) { + const $ = await next.render$(route) + expect($('#react-export-condition').text()).toBe('default') + expect($('#react-dom-export-condition').text()).toBe('default') + } + }) + + it('should use react-server condition for app router custom routes', async () => { + const customRoutes = ['/app/route', '/app/route-edge'] + + for (const route of customRoutes) { + const res = await next.fetch(route) + const json = await res.json() + expect(json.react).toBe('react-server') + expect(json.reactDom).toBe('react-server') + } + }) + + it('should use default react condition for pages router pages', async () => { + const pagesRoutes = ['/pages-ssr', '/pages-ssr-edge'] + + for (const route of pagesRoutes) { + const $ = await next.render$(route) + expect($('#react-export-condition').text()).toBe('default') + expect($('#react-dom-export-condition').text()).toBe('default') + } + }) + + it('should use default react condition for pages router apis', async () => { + const pagesRoutes = [ + '/api/pages-api', + '/api/pages-api-edge', + '/api/pages-api-edge-url-dep', + ] + + for (const route of pagesRoutes) { + const res = await next.fetch(route) + const json = await res.json() + expect(json.react).toBe('default') + expect(json.reactDom).toBe('default') + } + }) +}) diff --git a/test/e2e/rsc-layers-transform/app/layout.js b/test/e2e/rsc-layers-transform/app/layout.js new file mode 100644 index 0000000000..750eb927b1 --- /dev/null +++ b/test/e2e/rsc-layers-transform/app/layout.js @@ -0,0 +1,7 @@ +export default function Layout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/rsc-layers-transform/app/page.js b/test/e2e/rsc-layers-transform/app/page.js new file mode 100644 index 0000000000..9c4a210081 --- /dev/null +++ b/test/e2e/rsc-layers-transform/app/page.js @@ -0,0 +1,3 @@ +export default function Page() { + return 'page' +} diff --git a/test/e2e/rsc-layers-transform/instrumentation.js b/test/e2e/rsc-layers-transform/instrumentation.js new file mode 100644 index 0000000000..cff7a8b10b --- /dev/null +++ b/test/e2e/rsc-layers-transform/instrumentation.js @@ -0,0 +1,10 @@ +import React from 'react' +import { textValue } from './lib/shared-module' + +export async function register() { + if (Object(React).useState) { + throw new Error('instrumentation is not working correctly in server layer') + } + console.log('instrumentation:register') + console.log('instrumentation:text:' + textValue) +} diff --git a/test/e2e/rsc-layers-transform/lib/shared-module.js b/test/e2e/rsc-layers-transform/lib/shared-module.js new file mode 100644 index 0000000000..89966a446a --- /dev/null +++ b/test/e2e/rsc-layers-transform/lib/shared-module.js @@ -0,0 +1,4 @@ +import Link from 'next/link' + +export const textValue = 'text-value' +export const TestLink = Link diff --git a/test/e2e/rsc-layers-transform/middleware.js b/test/e2e/rsc-layers-transform/middleware.js new file mode 100644 index 0000000000..96d0427e04 --- /dev/null +++ b/test/e2e/rsc-layers-transform/middleware.js @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server' +import { textValue, TestLink } from './lib/shared-module' + +export function middleware(request) { + if (request.nextUrl.pathname === '/middleware') { + let testLink + try { + testLink = TestLink.$$typeof.toString() + } catch (e) { + testLink = e.message + } + return Response.json({ + clientReference: testLink, + textValue, + }) + } + + return NextResponse.next() +} diff --git a/test/e2e/rsc-layers-transform/next.config.js b/test/e2e/rsc-layers-transform/next.config.js new file mode 100644 index 0000000000..c4cf84a765 --- /dev/null +++ b/test/e2e/rsc-layers-transform/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + experimental: { + instrumentationHook: true, + }, +} diff --git a/test/e2e/rsc-layers-transform/rsc-layers-transform.test.ts b/test/e2e/rsc-layers-transform/rsc-layers-transform.test.ts new file mode 100644 index 0000000000..dd1d3ddab5 --- /dev/null +++ b/test/e2e/rsc-layers-transform/rsc-layers-transform.test.ts @@ -0,0 +1,22 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('rsc layers transform', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should render installed react-server condition for middleware', async () => { + const json = await next.fetch('/middleware').then((res) => res.json()) + + expect(json).toEqual({ + textValue: 'text-value', + clientReference: 'Symbol(react.client.reference)', + }) + }) + + it('should call instrumentation hook without errors', async () => { + const output = next.cliOutput + expect(output).toContain('instrumentation:register') + expect(output).toContain('instrumentation:text:text-value') + }) +})