fix turbopack HMR, fix disconnect detection (#55361)
### What? Fix reload in turbopack on every change Fix loosing of events due to debouncing Fix writing of CSS files for CSS HMR ### Why? Since we removed the pong event, the websocket impl would cause reconnects every 5 seconds loosing HMR events... ### How? Closes WEB-1555
This commit is contained in:
parent
423d66b086
commit
1ffc40ac6d
12 changed files with 151 additions and 75 deletions
|
@ -597,6 +597,18 @@ impl Project {
|
|||
.get(self.client_relative_path().join(identifier)))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn hmr_content_and_write(
|
||||
self: Vc<Self>,
|
||||
identifier: String,
|
||||
) -> Result<Vc<Box<dyn VersionedContent>>> {
|
||||
Ok(self.await?.versioned_content_map.get_and_write(
|
||||
self.client_relative_path().join(identifier),
|
||||
self.client_relative_path(),
|
||||
self.node_root(),
|
||||
))
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
async fn hmr_version(self: Vc<Self>, identifier: String) -> Result<Vc<Box<dyn Version>>> {
|
||||
let content = self.hmr_content(identifier);
|
||||
|
@ -633,7 +645,7 @@ impl Project {
|
|||
from: Vc<VersionState>,
|
||||
) -> Result<Vc<Update>> {
|
||||
let from = from.get();
|
||||
Ok(self.hmr_content(identifier).update(from))
|
||||
Ok(self.hmr_content_and_write(identifier).update(from))
|
||||
}
|
||||
|
||||
/// Gets a list of all HMR identifiers that can be subscribed to. This is
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use next_core::emit_client_assets;
|
||||
use turbo_tasks::{State, TryFlatJoinIterExt, TryJoinIterExt, ValueDefault, ValueToString, Vc};
|
||||
use turbopack_binding::{
|
||||
turbo::tasks_fs::FileSystemPath,
|
||||
|
@ -53,7 +54,7 @@ impl VersionedContentMap {
|
|||
let assets = assets_operation.await?;
|
||||
let entries: Vec<_> = assets
|
||||
.iter()
|
||||
.map(|asset| async move {
|
||||
.map(|&asset| async move {
|
||||
// NOTE(alexkirsz) `.versioned_content()` should not be resolved, to ensure that
|
||||
// it always points to the task that computes the versioned
|
||||
// content.
|
||||
|
@ -73,21 +74,20 @@ impl VersionedContentMap {
|
|||
|
||||
#[turbo_tasks::function]
|
||||
pub async fn get(&self, path: Vc<FileSystemPath>) -> Result<Vc<Box<dyn VersionedContent>>> {
|
||||
let result = {
|
||||
// NOTE(alexkirsz) This is to avoid Rust marking this method as !Send because a
|
||||
// StateRef to the map is captured across an await boundary below, even though
|
||||
// it does not look like it would.
|
||||
// I think this is a similar issue as https://fasterthanli.me/articles/a-rust-match-made-in-hell
|
||||
let map = self.map.get();
|
||||
map.get(&path).copied()
|
||||
};
|
||||
let Some((content, assets_operation)) = result else {
|
||||
let path = path.to_string().await?;
|
||||
bail!("could not find versioned content for path {}", path);
|
||||
};
|
||||
// NOTE(alexkirsz) This is necessary to mark the task as active again.
|
||||
Vc::connect(assets_operation);
|
||||
Vc::connect(content);
|
||||
let (content, _) = self.get_internal(path).await?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
pub async fn get_and_write(
|
||||
&self,
|
||||
path: Vc<FileSystemPath>,
|
||||
client_relative_path: Vc<FileSystemPath>,
|
||||
client_output_path: Vc<FileSystemPath>,
|
||||
) -> Result<Vc<Box<dyn VersionedContent>>> {
|
||||
let (content, assets_operation) = self.get_internal(path).await?;
|
||||
// Make sure all written client assets are up-to-date
|
||||
emit_client_assets(assets_operation, client_relative_path, client_output_path).await?;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
|
@ -106,3 +106,27 @@ impl VersionedContentMap {
|
|||
Ok(Vc::cell(keys))
|
||||
}
|
||||
}
|
||||
|
||||
impl VersionedContentMap {
|
||||
async fn get_internal(
|
||||
&self,
|
||||
path: Vc<FileSystemPath>,
|
||||
) -> Result<(Vc<Box<dyn VersionedContent>>, Vc<OutputAssets>)> {
|
||||
let result = {
|
||||
// NOTE(alexkirsz) This is to avoid Rust marking this method as !Send because a
|
||||
// StateRef to the map is captured across an await boundary below, even though
|
||||
// it does not look like it would.
|
||||
// I think this is a similar issue as https://fasterthanli.me/articles/a-rust-match-made-in-hell
|
||||
let map = self.map.get();
|
||||
map.get(&path).copied()
|
||||
};
|
||||
let Some((content, assets_operation)) = result else {
|
||||
let path = path.to_string().await?;
|
||||
bail!("could not find versioned content for path {}", path);
|
||||
};
|
||||
// NOTE(alexkirsz) This is necessary to mark the task as active again.
|
||||
Vc::connect(assets_operation);
|
||||
Vc::connect(content);
|
||||
Ok((content, assets_operation))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ async function loadPageChunk(assetPrefix: string, chunkData: ChunkData) {
|
|||
}
|
||||
|
||||
const { assetPrefix } = await initialize({
|
||||
webpackHMR: {
|
||||
devClient: {
|
||||
// Expected when `process.env.NODE_ENV === 'development'`
|
||||
onUnrecoverableError() {},
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use anyhow::Result;
|
||||
use turbo_tasks::{
|
||||
graph::{AdjacencyMap, GraphTraversal},
|
||||
Completion, Completions, TryJoinIterExt, Vc,
|
||||
Completion, Completions, TryFlatJoinIterExt, TryJoinIterExt, Vc,
|
||||
};
|
||||
use turbo_tasks_fs::{rebase, FileSystemPath};
|
||||
use turbopack_binding::turbopack::core::{
|
||||
|
@ -64,7 +64,7 @@ pub async fn emit_assets(
|
|||
client_relative_path: Vc<FileSystemPath>,
|
||||
client_output_path: Vc<FileSystemPath>,
|
||||
) -> Result<Vc<Completion>> {
|
||||
Ok(Completions::all(
|
||||
Ok(Vc::<Completions>::cell(
|
||||
assets
|
||||
.await?
|
||||
.iter()
|
||||
|
@ -76,7 +76,7 @@ pub async fn emit_assets(
|
|||
.await?
|
||||
.is_inside_ref(&*node_root.await?)
|
||||
{
|
||||
return Ok(emit(asset));
|
||||
return Ok(Some(emit(asset)));
|
||||
} else if asset
|
||||
.ident()
|
||||
.path()
|
||||
|
@ -85,14 +85,59 @@ pub async fn emit_assets(
|
|||
{
|
||||
// Client assets are emitted to the client output path, which is prefixed with
|
||||
// _next. We need to rebase them to remove that prefix.
|
||||
return Ok(emit_rebase(asset, client_relative_path, client_output_path));
|
||||
return Ok(Some(emit_rebase(
|
||||
asset,
|
||||
client_relative_path,
|
||||
client_output_path,
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(Completion::immutable())
|
||||
Ok(None)
|
||||
})
|
||||
.try_join()
|
||||
.try_flat_join()
|
||||
.await?,
|
||||
))
|
||||
)
|
||||
.completed())
|
||||
}
|
||||
|
||||
/// Emits all assets transitively reachable from the given chunks, that are
|
||||
/// inside the client root.
|
||||
///
|
||||
/// Assets inside the given client root are rebased to the given client output
|
||||
/// path.
|
||||
#[turbo_tasks::function]
|
||||
pub async fn emit_client_assets(
|
||||
assets: Vc<OutputAssets>,
|
||||
client_relative_path: Vc<FileSystemPath>,
|
||||
client_output_path: Vc<FileSystemPath>,
|
||||
) -> Result<Vc<Completion>> {
|
||||
Ok(Vc::<Completions>::cell(
|
||||
assets
|
||||
.await?
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|asset| async move {
|
||||
if asset
|
||||
.ident()
|
||||
.path()
|
||||
.await?
|
||||
.is_inside_ref(&*client_relative_path.await?)
|
||||
{
|
||||
// Client assets are emitted to the client output path, which is prefixed with
|
||||
// _next. We need to rebase them to remove that prefix.
|
||||
return Ok(Some(emit_rebase(
|
||||
asset,
|
||||
client_relative_path,
|
||||
client_output_path,
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
})
|
||||
.try_flat_join()
|
||||
.await?,
|
||||
)
|
||||
.completed())
|
||||
}
|
||||
|
||||
#[turbo_tasks::function]
|
||||
|
|
|
@ -56,7 +56,9 @@ pub use app_segment_config::{
|
|||
parse_segment_config_from_loader_tree, parse_segment_config_from_source,
|
||||
};
|
||||
pub use app_source::create_app_source;
|
||||
pub use emit::{all_assets_from_entries, all_server_paths, emit_all_assets, emit_assets};
|
||||
pub use emit::{
|
||||
all_assets_from_entries, all_server_paths, emit_all_assets, emit_assets, emit_client_assets,
|
||||
};
|
||||
pub use next_edge::context::{
|
||||
get_edge_chunking_context, get_edge_compile_time_info, get_edge_resolve_options_context,
|
||||
};
|
||||
|
|
|
@ -64,7 +64,9 @@ window.__nextDevClientId = Math.round(Math.random() * 100 + Date.now())
|
|||
|
||||
let hadRuntimeError = false
|
||||
let customHmrEventHandler: any
|
||||
export default function connect() {
|
||||
let MODE: 'webpack' | 'turbopack' = 'webpack'
|
||||
export default function connect(mode: 'webpack' | 'turbopack') {
|
||||
MODE = mode
|
||||
register()
|
||||
|
||||
addMessageListener((payload) => {
|
||||
|
@ -109,15 +111,17 @@ function clearOutdatedErrors() {
|
|||
function handleSuccess() {
|
||||
clearOutdatedErrors()
|
||||
|
||||
const isHotUpdate =
|
||||
!isFirstCompilation ||
|
||||
(window.__NEXT_DATA__.page !== '/_error' && isUpdateAvailable())
|
||||
isFirstCompilation = false
|
||||
hasCompileErrors = false
|
||||
if (MODE === 'webpack') {
|
||||
const isHotUpdate =
|
||||
!isFirstCompilation ||
|
||||
(window.__NEXT_DATA__.page !== '/_error' && isUpdateAvailable())
|
||||
isFirstCompilation = false
|
||||
hasCompileErrors = false
|
||||
|
||||
// Attempt to apply hot updates or reload.
|
||||
if (isHotUpdate) {
|
||||
tryApplyUpdates(onBeforeFastRefresh, onFastRefresh)
|
||||
// Attempt to apply hot updates or reload.
|
||||
if (isHotUpdate) {
|
||||
tryApplyUpdates(onBeforeFastRefresh, onFastRefresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ let source: WebSocket
|
|||
type ActionCallback = (action: HMR_ACTION_TYPES) => void
|
||||
|
||||
const eventCallbacks: Array<ActionCallback> = []
|
||||
let lastActivity = Date.now()
|
||||
|
||||
function getSocketProtocol(assetPrefix: string): string {
|
||||
let protocol = location.protocol
|
||||
|
@ -27,26 +26,15 @@ export function sendMessage(data: string) {
|
|||
return source.send(data)
|
||||
}
|
||||
|
||||
export function connectHMR(options: {
|
||||
path: string
|
||||
assetPrefix: string
|
||||
timeout?: number
|
||||
}) {
|
||||
if (!options.timeout) {
|
||||
options.timeout = 5 * 1000
|
||||
}
|
||||
|
||||
export function connectHMR(options: { path: string; assetPrefix: string }) {
|
||||
function init() {
|
||||
if (source) source.close()
|
||||
|
||||
function handleOnline() {
|
||||
window.console.log('[HMR] connected')
|
||||
lastActivity = Date.now()
|
||||
}
|
||||
|
||||
function handleMessage(event: MessageEvent<string>) {
|
||||
lastActivity = Date.now()
|
||||
|
||||
// Coerce into HMR_ACTION_TYPES as that is the format.
|
||||
const msg: HMR_ACTION_TYPES = JSON.parse(event.data)
|
||||
for (const eventCallback of eventCallbacks) {
|
||||
|
@ -54,18 +42,12 @@ export function connectHMR(options: {
|
|||
}
|
||||
}
|
||||
|
||||
let timer: NodeJS.Timeout
|
||||
function handleDisconnect() {
|
||||
clearInterval(timer)
|
||||
source.onerror = null
|
||||
source.onclose = null
|
||||
source.close()
|
||||
setTimeout(init, options.timeout)
|
||||
init()
|
||||
}
|
||||
timer = setInterval(function () {
|
||||
if (Date.now() - lastActivity > (options.timeout as any)) {
|
||||
handleDisconnect()
|
||||
}
|
||||
}, (options.timeout as any) / 2)
|
||||
|
||||
const { hostname, port } = location
|
||||
const protocol = getSocketProtocol(options.assetPrefix || '')
|
||||
|
@ -82,6 +64,7 @@ export function connectHMR(options: {
|
|||
source = new window.WebSocket(`${url}${options.path}`)
|
||||
source.onopen = handleOnline
|
||||
source.onerror = handleDisconnect
|
||||
source.onclose = handleDisconnect
|
||||
source.onmessage = handleMessage
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import connect from './error-overlay/hot-dev-client'
|
||||
import { sendMessage } from './error-overlay/websocket'
|
||||
|
||||
export default () => {
|
||||
const devClient = connect()
|
||||
export default (mode: 'webpack' | 'turbopack') => {
|
||||
const devClient = connect(mode)
|
||||
|
||||
devClient.subscribeToHmrEvent((obj: any) => {
|
||||
// if we're on an error/404 page, we can't reliably tell if the newly added/removed page
|
|
@ -89,7 +89,7 @@ let initialMatchesMiddleware = false
|
|||
let lastAppProps: AppProps
|
||||
|
||||
let lastRenderReject: (() => void) | null
|
||||
let webpackHMR: any
|
||||
let devClient: any
|
||||
|
||||
let CachedApp: AppComponent, onPerfEntry: (metric: any) => void
|
||||
let CachedComponent: React.ComponentType
|
||||
|
@ -185,14 +185,14 @@ class Container extends React.Component<{
|
|||
}
|
||||
}
|
||||
|
||||
export async function initialize(opts: { webpackHMR?: any } = {}): Promise<{
|
||||
export async function initialize(opts: { devClient?: any } = {}): Promise<{
|
||||
assetPrefix: string
|
||||
}> {
|
||||
tracer.onSpanEnd(reportToSocket)
|
||||
|
||||
// This makes sure this specific lines are removed in production
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
webpackHMR = opts.webpackHMR
|
||||
devClient = opts.devClient
|
||||
}
|
||||
|
||||
initialData = JSON.parse(
|
||||
|
@ -357,7 +357,7 @@ function renderError(renderErrorProps: RenderErrorProps): Promise<any> {
|
|||
if (process.env.NODE_ENV !== 'production') {
|
||||
// A Next.js rendering runtime error is always unrecoverable
|
||||
// FIXME: let's make this recoverable (error in GIP client-transition)
|
||||
webpackHMR.onUnrecoverableError()
|
||||
devClient.onUnrecoverableError()
|
||||
|
||||
// We need to render an empty <App> so that the `<ReactDevOverlay>` can
|
||||
// render itself.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// TODO: Remove use of `any` type.
|
||||
import { initialize, version, router, emitter } from './'
|
||||
import initWebpackHMR from './dev/webpack-hot-middleware-client'
|
||||
import initHMR from './dev/hot-middleware-client'
|
||||
|
||||
import './setup-hydration-warning'
|
||||
import { pageBootrap } from './page-bootstrap'
|
||||
|
@ -18,15 +18,14 @@ window.next = {
|
|||
emitter,
|
||||
}
|
||||
;(self as any).__next_set_public_path__ = () => {}
|
||||
;(self as any).__webpack_hash__ = 0
|
||||
;(self as any).__webpack_hash__ = ''
|
||||
|
||||
// for the page loader
|
||||
declare let __turbopack_load__: any
|
||||
|
||||
const webpackHMR = initWebpackHMR()
|
||||
const devClient = initHMR('turbopack')
|
||||
initialize({
|
||||
// TODO the prop name is confusing as related to webpack
|
||||
webpackHMR,
|
||||
devClient,
|
||||
})
|
||||
.then(({ assetPrefix }) => {
|
||||
// for the page loader
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// TODO: Remove use of `any` type.
|
||||
import './webpack'
|
||||
import { initialize, version, router, emitter } from './'
|
||||
import initWebpackHMR from './dev/webpack-hot-middleware-client'
|
||||
import initHMR from './dev/hot-middleware-client'
|
||||
import { pageBootrap } from './page-bootstrap'
|
||||
|
||||
import './setup-hydration-warning'
|
||||
|
@ -15,8 +15,8 @@ window.next = {
|
|||
emitter,
|
||||
}
|
||||
|
||||
const webpackHMR = initWebpackHMR()
|
||||
initialize({ webpackHMR })
|
||||
const devClient = initHMR('webpack')
|
||||
initialize({ devClient })
|
||||
.then(({ assetPrefix }) => {
|
||||
return pageBootrap(assetPrefix)
|
||||
})
|
||||
|
|
|
@ -227,7 +227,7 @@ async function startWatcher(opts: SetupOpts) {
|
|||
let currentEntriesHandling = new Promise(
|
||||
(resolve) => (currentEntriesHandlingResolve = resolve)
|
||||
)
|
||||
const hmrPayloads = new Map<string, HMR_ACTION_TYPES>()
|
||||
const hmrPayloads = new Map<string, HMR_ACTION_TYPES[]>()
|
||||
let hmrBuilding = false
|
||||
|
||||
const issues = new Map<string, Map<string, Issue>>()
|
||||
|
@ -344,7 +344,6 @@ async function startWatcher(opts: SetupOpts) {
|
|||
for (const [key, issue] of issueMap) {
|
||||
if (errors.has(key)) continue
|
||||
|
||||
console.log(issue)
|
||||
const message = formatIssue(issue)
|
||||
|
||||
errors.set(key, {
|
||||
|
@ -363,8 +362,10 @@ async function startWatcher(opts: SetupOpts) {
|
|||
hmrBuilding = false
|
||||
|
||||
if (errors.size === 0) {
|
||||
for (const payload of hmrPayloads.values()) {
|
||||
hotReloader.send(payload)
|
||||
for (const payloads of hmrPayloads.values()) {
|
||||
for (const payload of payloads) {
|
||||
hotReloader.send(payload)
|
||||
}
|
||||
}
|
||||
hmrPayloads.clear()
|
||||
}
|
||||
|
@ -378,7 +379,13 @@ async function startWatcher(opts: SetupOpts) {
|
|||
hotReloader.send({ action: HMR_ACTIONS_SENT_TO_BROWSER.BUILDING })
|
||||
hmrBuilding = true
|
||||
}
|
||||
hmrPayloads.set(`${key}:${id}`, payload)
|
||||
let k = `${key}:${id}`
|
||||
let list = hmrPayloads.get(k)
|
||||
if (!list) {
|
||||
list = []
|
||||
hmrPayloads.set(k, list)
|
||||
}
|
||||
list.push(payload)
|
||||
sendHmrDebounce()
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue