diff --git a/crates/next-core/src/lib.rs b/crates/next-core/src/lib.rs index 6d5bcfa8d08c3..d3e2e419d8a80 100644 --- a/crates/next-core/src/lib.rs +++ b/crates/next-core/src/lib.rs @@ -1,6 +1,7 @@ #![feature(min_specialization)] pub mod next_client; +pub mod react_refresh; mod server_render; mod server_rendered_source; mod web_entry_source; diff --git a/crates/next-core/src/react_refresh.rs b/crates/next-core/src/react_refresh.rs new file mode 100644 index 0000000000000..b365103f43db4 --- /dev/null +++ b/crates/next-core/src/react_refresh.rs @@ -0,0 +1,108 @@ +use anyhow::{anyhow, Result}; +use turbo_tasks::primitives::{BoolVc, StringVc}; +use turbo_tasks_fs::FileSystemPathVc; +use turbopack::ecmascript::{ + chunk::EcmascriptChunkPlaceableVc, + resolve::{apply_cjs_specific_options, cjs_resolve}, +}; +use turbopack_core::{ + context::AssetContextVc, + environment::EnvironmentVc, + issue::{Issue, IssueSeverity, IssueSeverityVc, IssueVc}, + resolve::{parse::RequestVc, ResolveResult}, +}; + +#[turbo_tasks::function] +fn react_refresh_request() -> RequestVc { + RequestVc::parse_string("@next/react-refresh-utils/dist/runtime".to_string()) +} + +/// Checks whether we can resolve the React Refresh runtime module from the +/// given path. Emits an issue if we can't. +/// +/// Differs from `resolve_react_refresh` in that we don't have access to an +/// [AssetContextVc] when we first want to check for RR. +#[turbo_tasks::function] +pub async fn assert_can_resolve_react_refresh( + path: FileSystemPathVc, + environment: EnvironmentVc, +) -> Result { + let resolve_options = apply_cjs_specific_options(turbopack::resolve_options(path, environment)); + let result = turbopack_core::resolve::resolve(path, react_refresh_request(), resolve_options); + + Ok(match &*result.await? { + ResolveResult::Single(_, _) => BoolVc::cell(true), + _ => { + ReactRefreshResolvingIssue { + path, + description: StringVc::cell( + "could not resolve the `@next/react-refresh-utils/dist/runtime` module" + .to_string(), + ), + } + .cell() + .as_issue() + .emit(); + BoolVc::cell(false) + } + }) +} + +/// Resolves the React Refresh runtime module from the given [AssetContextVc]. +#[turbo_tasks::function] +pub async fn resolve_react_refresh(context: AssetContextVc) -> Result { + match &*cjs_resolve(react_refresh_request(), context).await? { + ResolveResult::Single(asset, _) => { + if let Some(placeable) = EcmascriptChunkPlaceableVc::resolve_from(asset).await? { + Ok(placeable) + } else { + Err(anyhow!("React Refresh runtime asset is not placeable")) + } + } + // The react-refresh-runtime module is not installed. + ResolveResult::Unresolveable(_) => Err(anyhow!( + "could not resolve the `@next/react-refresh-utils/dist/runtime` module" + )), + _ => Err(anyhow!("invalid React Refresh runtime asset")), + } +} + +/// An issue that occurred while resolving the React Refresh runtime module. +#[turbo_tasks::value(shared)] +pub struct ReactRefreshResolvingIssue { + path: FileSystemPathVc, + description: StringVc, +} + +#[turbo_tasks::value_impl] +impl Issue for ReactRefreshResolvingIssue { + #[turbo_tasks::function] + fn severity(&self) -> IssueSeverityVc { + IssueSeverity::Warning.into() + } + + #[turbo_tasks::function] + async fn title(&self) -> Result { + Ok(StringVc::cell( + "An issue occurred while resolving the React Refresh runtime. React Refresh will be \ + disabled.\nTo enable React Refresh, install the `react-refresh` and \ + `@next/react-refresh-utils` modules." + .to_string(), + )) + } + + #[turbo_tasks::function] + fn category(&self) -> StringVc { + StringVc::cell("other".to_string()) + } + + #[turbo_tasks::function] + fn context(&self) -> FileSystemPathVc { + self.path + } + + #[turbo_tasks::function] + fn description(&self) -> StringVc { + self.description + } +} diff --git a/crates/next-core/src/server_render/asset.rs b/crates/next-core/src/server_render/asset.rs index 5d5e18d5cf4fc..ddc75b26bb257 100644 --- a/crates/next-core/src/server_render/asset.rs +++ b/crates/next-core/src/server_render/asset.rs @@ -168,10 +168,10 @@ async fn get_intermediate_asset( WrapperAssetVc::new(entry_asset, "server-renderer.js", get_server_renderer()).into(), context.with_context_path(entry_asset.path()), Value::new(ModuleAssetType::Ecmascript), - EcmascriptInputTransformsVc::cell(vec![EcmascriptInputTransform::JSX]), + EcmascriptInputTransformsVc::cell(vec![EcmascriptInputTransform::React { refresh: false }]), context.environment(), ); - let chunk = module.as_evaluated_chunk(chunking_context.into()); + let chunk = module.as_evaluated_chunk(chunking_context.into(), None); let chunk_group = ChunkGroupVc::from_chunk(chunk); Ok(NodeJsBootstrapAsset { path: intermediate_output_path.join("index.js"), diff --git a/crates/next-core/src/server_rendered_source.rs b/crates/next-core/src/server_rendered_source.rs index 683519087712f..aab60e665751d 100644 --- a/crates/next-core/src/server_rendered_source.rs +++ b/crates/next-core/src/server_rendered_source.rs @@ -84,6 +84,7 @@ pub async fn create_server_rendered_source( )), Value::new(EnvironmentIntention::Client), ), + Default::default(), ) .into(); diff --git a/crates/next-core/src/web_entry_source.rs b/crates/next-core/src/web_entry_source.rs index b98f47ac7af0e..f17d3db300563 100644 --- a/crates/next-core/src/web_entry_source.rs +++ b/crates/next-core/src/web_entry_source.rs @@ -3,7 +3,11 @@ use std::{collections::HashMap, future::IntoFuture}; use anyhow::{anyhow, Result}; use turbo_tasks::{util::try_join_all, Value}; use turbo_tasks_fs::{FileSystemPathVc, FileSystemVc}; -use turbopack::{ecmascript::EcmascriptModuleAssetVc, ModuleAssetContextVc}; +use turbopack::{ + ecmascript::{chunk::EcmascriptChunkPlaceablesVc, EcmascriptModuleAssetVc}, + module_options::ModuleOptionsContext, + ModuleAssetContextVc, +}; use turbopack_core::{ chunk::{ dev::{DevChunkingContext, DevChunkingContextVc}, @@ -19,6 +23,8 @@ use turbopack_dev_server::{ source::{asset_graph::AssetGraphContentSourceVc, ContentSourceVc}, }; +use crate::react_refresh::{assert_can_resolve_react_refresh, resolve_react_refresh}; + #[turbo_tasks::function] pub async fn create_web_entry_source( root: FileSystemPathVc, @@ -26,21 +32,32 @@ pub async fn create_web_entry_source( dev_server_fs: FileSystemVc, eager_compile: bool, ) -> Result { + let environment = EnvironmentVc::new( + Value::new(ExecutionEnvironment::Browser( + BrowserEnvironment { + dom: true, + web_worker: false, + service_worker: false, + browser_version: 0, + } + .into(), + )), + Value::new(EnvironmentIntention::Client), + ); + + let can_resolve_react_refresh = *assert_can_resolve_react_refresh(root, environment).await?; + let context: AssetContextVc = ModuleAssetContextVc::new( TransitionsByNameVc::cell(HashMap::new()), root, - EnvironmentVc::new( - Value::new(ExecutionEnvironment::Browser( - BrowserEnvironment { - dom: true, - web_worker: false, - service_worker: false, - browser_version: 0, - } - .into(), - )), - Value::new(EnvironmentIntention::Client), - ), + environment, + ModuleOptionsContext { + // We don't need to resolve React Refresh for each module. Instead, + // we try resolve it once at the root and pass down a context to all + // the modules. + enable_react_refresh: can_resolve_react_refresh, + } + .into(), ) .into(); @@ -51,6 +68,14 @@ pub async fn create_web_entry_source( } .into(); + let runtime_entries = if can_resolve_react_refresh { + Some(EcmascriptChunkPlaceablesVc::cell(vec![ + resolve_react_refresh(context), + ])) + } else { + None + }; + let modules = try_join_all(entry_requests.into_iter().map(|r| { context .resolve_asset(context.context_path(), r, context.resolve_options()) @@ -63,7 +88,7 @@ pub async fn create_web_entry_source( .flat_map(|assets| assets.iter().copied().collect::>()); let chunks = try_join_all(modules.map(|module| async move { if let Some(ecmascript) = EcmascriptModuleAssetVc::resolve_from(module).await? { - Ok(ecmascript.as_evaluated_chunk(chunking_context.into())) + Ok(ecmascript.as_evaluated_chunk(chunking_context.into(), runtime_entries)) } else if let Some(chunkable) = ChunkableAssetVc::resolve_from(module).await? { Ok(chunkable.as_chunk(chunking_context.into())) } else { diff --git a/crates/next-dev/src/lib.rs b/crates/next-dev/src/lib.rs index 98a692fdc9dc7..5da5181ae6e43 100644 --- a/crates/next-dev/src/lib.rs +++ b/crates/next-dev/src/lib.rs @@ -50,7 +50,7 @@ impl NextDevServerBuilder { eager_compile: false, hostname: None, port: None, - log_level: IssueSeverity::Error, + log_level: IssueSeverity::Warning, show_all: false, log_detail: false, } diff --git a/crates/next-dev/src/main.rs b/crates/next-dev/src/main.rs index 40021ddf1da8b..3ead12545a355 100644 --- a/crates/next-dev/src/main.rs +++ b/crates/next-dev/src/main.rs @@ -102,7 +102,10 @@ async fn main() -> Result<()> { .port(args.port) .log_detail(args.log_detail) .show_all(args.show_all) - .log_level(args.log_level.map_or_else(|| IssueSeverity::Error, |l| l.0)) + .log_level( + args.log_level + .map_or_else(|| IssueSeverity::Warning, |l| l.0), + ) .build() .await?;