Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix turbopack HMR, fix disconnect detection #55361

Merged
merged 4 commits into from Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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() {},
},
Expand Down
22 changes: 13 additions & 9 deletions packages/next/src/client/dev/error-overlay/hot-dev-client.ts
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
25 changes: 4 additions & 21 deletions packages/next/src/client/dev/error-overlay/websocket.ts
Expand Up @@ -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
Expand All @@ -27,45 +26,28 @@ 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) {
eventCallback(msg)
}
}

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

Expand Down
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions packages/next/src/client/index.tsx
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 4 additions & 5 deletions packages/next/src/client/next-dev-turbopack.ts
@@ -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'
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/next/src/client/next-dev.ts
@@ -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'
Expand All @@ -15,8 +15,8 @@ window.next = {
emitter,
}

const webpackHMR = initWebpackHMR()
initialize({ webpackHMR })
const devClient = initHMR('webpack')
initialize({ devClient })
.then(({ assetPrefix }) => {
return pageBootrap(assetPrefix)
})
Expand Down
20 changes: 14 additions & 6 deletions packages/next/src/server/lib/router-utils/setup-dev.ts
Expand Up @@ -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>>()
Expand Down Expand Up @@ -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, {
Expand All @@ -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()
}
Expand All @@ -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()
}

Expand Down Expand Up @@ -822,7 +829,7 @@ async function startWatcher(opts: SetupOpts) {
// The subscription will always emit once, which is the initial
// computation. This is not a change, so swallow it.
try {
await subscription.next()
console.log('first event', await subscription.next())
} catch (e) {
// The client is using an HMR session from a previous server, tell them
// to fully reload the page to resolve the issue. We can't use
Expand All @@ -837,6 +844,7 @@ async function startWatcher(opts: SetupOpts) {
}

for await (const data of subscription) {
console.log('event', data)
processIssues(id, data)
sendHmr('hmr-event', id, {
type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_MESSAGE,
Expand Down