Reland "Middleware to use react-server condition" (#66534)

This commit is contained in:
Jiachi Liu 2024-06-14 17:41:12 +02:00 committed by GitHub
parent e4d107cc93
commit ce69888af1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 723 additions and 136 deletions

View file

@ -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<Self>) -> Vc<RcStr> {
pub(crate) fn client_transition_name(self: Vc<Self>) -> Vc<RcStr> {
Vc::cell(ECMASCRIPT_CLIENT_TRANSITION_NAME.into())
}
@ -270,15 +270,28 @@ impl AppProject {
))
}
#[turbo_tasks::function]
pub fn client_reference_transition(self: Vc<Self>) -> Vc<Box<dyn Transition>> {
Vc::upcast(NextEcmascriptClientReferenceTransition::new(
Vc::upcast(self.client_transition()),
self.ssr_transition(),
))
}
#[turbo_tasks::function]
pub fn edge_client_reference_transition(self: Vc<Self>) -> Vc<Box<dyn Transition>> {
Vc::upcast(NextEcmascriptClientReferenceTransition::new(
Vc::upcast(self.client_transition()),
self.edge_ssr_transition(),
))
}
#[turbo_tasks::function]
fn rsc_module_context(self: Vc<Self>) -> Vc<ModuleAssetContext> {
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(),

View file

@ -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<Box<dyn AssetContext>>,
source: Vc<Box<dyn Source>>,
is_edge: bool,
app_dir: Option<Vc<FileSystemPath>>,
ecmascript_client_reference_transition_name: Option<Vc<RcStr>>,
}
#[turbo_tasks::value_impl]
@ -51,12 +54,16 @@ impl InstrumentationEndpoint {
context: Vc<Box<dyn AssetContext>>,
source: Vc<Box<dyn Source>>,
is_edge: bool,
app_dir: Option<Vc<FileSystemPath>>,
ecmascript_client_reference_transition_name: Option<Vc<RcStr>>,
) -> Vc<Self> {
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),

View file

@ -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<Project>,
context: Vc<Box<dyn AssetContext>>,
source: Vc<Box<dyn Source>>,
app_dir: Option<Vc<FileSystemPath>>,
ecmascript_client_reference_transition_name: Option<Vc<RcStr>>,
}
#[turbo_tasks::value_impl]
@ -49,11 +51,15 @@ impl MiddlewareEndpoint {
project: Vc<Project>,
context: Vc<Box<dyn AssetContext>>,
source: Vc<Box<dyn Source>>,
app_dir: Option<Vc<FileSystemPath>>,
ecmascript_client_reference_transition_name: Option<Vc<RcStr>>,
) -> Vc<Self> {
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)

View file

@ -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<Self>) -> Vc<Box<dyn AssetContext>> {
Vc::upcast(ModuleAssetContext::new(
Default::default(),
async fn middleware_context(self: Vc<Self>) -> Result<Vc<Box<dyn AssetContext>>> {
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<Self>,
source: Vc<Box<dyn Source>>,
) -> Result<Vc<MiddlewareEndpoint>> {
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<Self>) -> Vc<Box<dyn AssetContext>> {
Vc::upcast(ModuleAssetContext::new(
Default::default(),
async fn node_instrumentation_context(self: Vc<Self>) -> Result<Vc<Box<dyn AssetContext>>> {
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<Self>) -> Result<Vc<Box<dyn AssetContext>>> {
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<Self>,
source: Vc<Box<dyn Source>>,
is_edge: bool,
) -> Vc<InstrumentationEndpoint> {
) -> Result<Vc<InstrumentationEndpoint>> {
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]

View file

@ -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,

View file

@ -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,7 +650,14 @@ async fn rsc_aliases(
let taint = *next_config.enable_taint().await?;
let react_channel = if ppr || taint { "-experimental" } else { "" };
let mut alias = indexmap! {
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"),
@ -663,6 +670,9 @@ async fn rsc_aliases(
"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"),
})
});
}
_ => {}
}

View file

@ -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<FileSystemPath>,
ecmascript_client_reference_transition_name: Option<Vc<RcStr>>,
},
Middleware,
Instrumentation,
Middleware {
app_dir: Option<Vc<FileSystemPath>>,
ecmascript_client_reference_transition_name: Option<Vc<RcStr>>,
},
Instrumentation {
app_dir: Option<Vc<FileSystemPath>>,
ecmascript_client_reference_transition_name: Option<Vc<RcStr>>,
},
}
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<ModuleRule> =
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<ModuleRule> =
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);

View file

@ -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
);

View file

@ -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) {

View file

@ -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' },

View file

@ -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.

View file

@ -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))
}

View file

@ -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,8 +595,7 @@ 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
const apiRoutesLayerLoaders = useSWCLoader
? getSwcLoader({
serverComponents: false,
bundleLayer: WEBPACK_LAYERS.api,
@ -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
? [

View file

@ -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,

View file

@ -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)
) {

View file

@ -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
},
}
)

View file

@ -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,

View file

@ -0,0 +1,4 @@
import Link from 'next/link'
export const textValue = 'text-value'
export const TestLink = Link

View file

@ -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()
}

View file

@ -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`
)
}
})
})
}

View file

@ -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'

View file

@ -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)),
})
}

View file

@ -1,7 +0,0 @@
import 'server-only'
export default function handler() {
return new Response('pages/api/hello-edge.js:')
}
export const runtime = 'edge'

View file

@ -1,5 +0,0 @@
import 'server-only'
export default function handler(req, res) {
return res.send('pages/api/hello.js')
}

View file

@ -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'

View file

@ -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)),
})
}

View file

@ -0,0 +1,5 @@
'use client'
export { default } from '../client/page'
export const runtime = 'edge'

View file

@ -0,0 +1,7 @@
'use client'
import { ReactConditionUI } from '../../../lib/react-version'
export default function Page() {
return <ReactConditionUI />
}

View file

@ -0,0 +1,3 @@
export { GET } from '../route/route'
export const runtime = 'edge'

View file

@ -0,0 +1,5 @@
import { getReactConditionJson } from '../../../lib/react-version'
export function GET() {
return Response.json(getReactConditionJson())
}

View file

@ -0,0 +1,3 @@
export { default } from '../server/page'
export const runtime = 'edge'

View file

@ -0,0 +1,5 @@
import { ReactConditionUI } from '../../../lib/react-version'
export default function Page() {
return <ReactConditionUI />
}

7
test/e2e/react-version/app/layout.js vendored Normal file
View file

@ -0,0 +1,7 @@
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}

View file

@ -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)
}

View file

@ -0,0 +1,17 @@
import { getReactCondition, getReactDomCondition } from './react-validate'
export function ReactConditionUI() {
return (
<div>
<p id="react-export-condition">{getReactCondition()}</p>
<p id="react-dom-export-condition">{getReactDomCondition()}</p>
</div>
)
}
export function getReactConditionJson() {
return {
react: getReactCondition(),
reactDom: getReactDomCondition(),
}
}

10
test/e2e/react-version/middleware.js vendored Normal file
View file

@ -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()
}

View file

@ -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'

View file

@ -0,0 +1,7 @@
import { getReactConditionJson } from '../../lib/react-version'
export default async (_req) => {
return Response.json(getReactConditionJson())
}
export const runtime = 'experimental-edge'

View file

@ -0,0 +1,5 @@
import { getReactConditionJson } from '../../lib/react-version'
export default async (_req, res) => {
return res.json(getReactConditionJson())
}

View file

@ -0,0 +1,3 @@
.foo {
color: red;
}

View file

@ -0,0 +1,9 @@
import { ReactConditionUI } from '../lib/react-version'
export default function Page() {
return <ReactConditionUI />
}
export const config = {
runtime: 'experimental-edge',
}

View file

@ -0,0 +1,5 @@
import { ReactConditionUI } from '../lib/react-version'
export default function Page() {
return <ReactConditionUI />
}

View file

@ -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')
}
})
})

View file

@ -0,0 +1,7 @@
export default function Layout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}

View file

@ -0,0 +1,3 @@
export default function Page() {
return 'page'
}

View file

@ -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)
}

View file

@ -0,0 +1,4 @@
import Link from 'next/link'
export const textValue = 'text-value'
export const TestLink = Link

View file

@ -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()
}

View file

@ -0,0 +1,5 @@
module.exports = {
experimental: {
instrumentationHook: true,
},
}

View file

@ -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')
})
})