From 8f54f35e90ca143690f54796119244c3f98abb54 Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Fri, 26 Aug 2022 18:30:04 +0200 Subject: [PATCH] HMR support + React Refresh (vercel/turbo#252) This PR implements HMR support with React Refresh built-in. For now, in order for React Refresh to be enabled, you'll need the `@next/react-refresh-utils` package to be resolveable: `yarn add @next/react-refresh-utils` in your app folder. * Depends on vercel/turbo#266 * Integrated both HMR-and-React-Refresh-specific logic directly into the ES chunks' runtime. Webpack has more complex setup here, but for now this makes the logic much more easy to follow since everything is in one place. I have yet to implement the "dependencies" signature for `hot.accept`/`hot.dispose`, since React Refresh does not depend on them. We'll have to see if they're even used in the wild or if we should deprecate them. * Only implemented the [module API](https://webpack.js.org/api/hot-module-replacement/#module-api), not the [management API](https://webpack.js.org/api/hot-module-replacement/#management-api). We apply all updates as soon as we receive them. * Added support for "runtime entries" to ES chunks. These are assets that will be executed *before* the main entry of an ES chunk. They'll be useful for polyfills in the future, but for now they're here to evaluate the react refresh runtime before any module is instantiated. Next steps for HMR: * Implement CSS HMR * Implement (or decide to deprecate) the [dependencies form](https://webpack.js.org/api/hot-module-replacement/#accept) of `hot.accept`/`hot.dispose` * Clean up `runtime.js` some more: switch to TypeScript, split into multiple files, etc. It'd be nice if all of this could be done at compile time, but how to achieve this is unclear at the moment. _Can we run turbopack to compile turbopack?_ --- packages/next-swc/crates/next-core/src/lib.rs | 1 + .../crates/next-core/src/react_refresh.rs | 108 ++++++++++++++++++ .../next-core/src/server_render/asset.rs | 4 +- .../next-core/src/server_rendered_source.rs | 1 + .../crates/next-core/src/web_entry_source.rs | 53 ++++++--- packages/next-swc/crates/next-dev/src/lib.rs | 2 +- packages/next-swc/crates/next-dev/src/main.rs | 5 +- 7 files changed, 156 insertions(+), 18 deletions(-) create mode 100644 packages/next-swc/crates/next-core/src/react_refresh.rs diff --git a/packages/next-swc/crates/next-core/src/lib.rs b/packages/next-swc/crates/next-core/src/lib.rs index 6d5bcfa8d08c3..d3e2e419d8a80 100644 --- a/packages/next-swc/crates/next-core/src/lib.rs +++ b/packages/next-swc/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/packages/next-swc/crates/next-core/src/react_refresh.rs b/packages/next-swc/crates/next-core/src/react_refresh.rs new file mode 100644 index 0000000000000..b365103f43db4 --- /dev/null +++ b/packages/next-swc/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/packages/next-swc/crates/next-core/src/server_render/asset.rs b/packages/next-swc/crates/next-core/src/server_render/asset.rs index 5d5e18d5cf4fc..ddc75b26bb257 100644 --- a/packages/next-swc/crates/next-core/src/server_render/asset.rs +++ b/packages/next-swc/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/packages/next-swc/crates/next-core/src/server_rendered_source.rs b/packages/next-swc/crates/next-core/src/server_rendered_source.rs index 683519087712f..aab60e665751d 100644 --- a/packages/next-swc/crates/next-core/src/server_rendered_source.rs +++ b/packages/next-swc/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/packages/next-swc/crates/next-core/src/web_entry_source.rs b/packages/next-swc/crates/next-core/src/web_entry_source.rs index b98f47ac7af0e..f17d3db300563 100644 --- a/packages/next-swc/crates/next-core/src/web_entry_source.rs +++ b/packages/next-swc/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/packages/next-swc/crates/next-dev/src/lib.rs b/packages/next-swc/crates/next-dev/src/lib.rs index 98a692fdc9dc7..5da5181ae6e43 100644 --- a/packages/next-swc/crates/next-dev/src/lib.rs +++ b/packages/next-swc/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/packages/next-swc/crates/next-dev/src/main.rs b/packages/next-swc/crates/next-dev/src/main.rs index 40021ddf1da8b..3ead12545a355 100644 --- a/packages/next-swc/crates/next-dev/src/main.rs +++ b/packages/next-swc/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?;