Skip to content

Commit

Permalink
HMR support + React Refresh (vercel/turborepo#252)
Browse files Browse the repository at this point in the history
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/turborepo#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?_
  • Loading branch information
alexkirsz authored Aug 26, 2022
1 parent 4eb6df7 commit cc96dd6
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 18 deletions.
1 change: 1 addition & 0 deletions crates/next-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
108 changes: 108 additions & 0 deletions crates/next-core/src/react_refresh.rs
Original file line number Diff line number Diff line change
@@ -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<BoolVc> {
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<EcmascriptChunkPlaceableVc> {
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<StringVc> {
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
}
}
4 changes: 2 additions & 2 deletions crates/next-core/src/server_render/asset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
1 change: 1 addition & 0 deletions crates/next-core/src/server_rendered_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ pub async fn create_server_rendered_source(
)),
Value::new(EnvironmentIntention::Client),
),
Default::default(),
)
.into();

Expand Down
53 changes: 39 additions & 14 deletions crates/next-core/src/web_entry_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -19,28 +23,41 @@ 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,
entry_requests: Vec<RequestVc>,
dev_server_fs: FileSystemVc,
eager_compile: bool,
) -> Result<ContentSourceVc> {
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();

Expand All @@ -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())
Expand All @@ -63,7 +88,7 @@ pub async fn create_web_entry_source(
.flat_map(|assets| assets.iter().copied().collect::<Vec<_>>());
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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/next-dev/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Expand Down
5 changes: 4 additions & 1 deletion crates/next-dev/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;

Expand Down

0 comments on commit cc96dd6

Please sign in to comment.