diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index aa80e4c6fbff4..9878809347361 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -477,6 +477,10 @@ function createRSCAliases( ] = `next/dist/compiled/scheduler${bundledReactChannel}/tracing-profiling` } + alias[ + '@vercel/turbopack-ecmascript-runtime/dev/client/hmr-client.ts' + ] = `next/dist/client/dev/noop-turbopack-hmr` + return alias } diff --git a/packages/next/src/client/app-next-dev-turbopack.ts b/packages/next/src/client/app-next-dev-turbopack.ts index 12bc07d9e3894..8af1e233b4ef2 100644 --- a/packages/next/src/client/app-next-dev-turbopack.ts +++ b/packages/next/src/client/app-next-dev-turbopack.ts @@ -3,6 +3,7 @@ import { appBootstrap } from './app-bootstrap' window.next.version += '-turbo' +;(self as any).__webpack_hash__ = '' appBootstrap(() => { require('./app-turbopack') diff --git a/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx b/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx index 84c3569834abb..04f8c4ecbce63 100644 --- a/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx +++ b/packages/next/src/client/components/react-dev-overlay/hot-reloader-client.tsx @@ -1,4 +1,4 @@ -import type { ReactNode } from 'react' +import { ReactNode } from 'react' import React, { useCallback, useEffect, @@ -30,6 +30,7 @@ import { } from './internal/helpers/use-error-handler' import { useSendMessage, + useTurbopack, useWebsocket, useWebsocketPing, } from './internal/helpers/use-websocket' @@ -204,17 +205,12 @@ function tryApplyUpdates( } function processMessage( - e: any, + obj: HMR_ACTION_TYPES, sendMessage: any, router: ReturnType, dispatcher: Dispatcher ) { - let obj: HMR_ACTION_TYPES | undefined - try { - obj = JSON.parse(e.data) - } catch {} - - if (!obj || !('action' in obj)) { + if (!('action' in obj)) { return } @@ -475,13 +471,18 @@ export default function HotReload({ const webSocketRef = useWebsocket(assetPrefix) useWebsocketPing(webSocketRef) const sendMessage = useSendMessage(webSocketRef) + const processTurbopackMessage = useTurbopack(sendMessage) const router = useRouter() useEffect(() => { const handler = (event: MessageEvent) => { try { - processMessage(event, sendMessage, router, dispatcher) + const obj = JSON.parse(event.data) + const handledByTurbopack = processTurbopackMessage?.(obj) + if (!handledByTurbopack) { + processMessage(obj, sendMessage, router, dispatcher) + } } catch (err: any) { console.warn( '[HMR] Invalid message: ' + event.data + '\n' + (err?.stack ?? '') @@ -495,7 +496,7 @@ export default function HotReload({ } return () => websocket && websocket.removeEventListener('message', handler) - }, [sendMessage, router, webSocketRef, dispatcher]) + }, [sendMessage, router, webSocketRef, dispatcher, processTurbopackMessage]) return ( diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-websocket.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-websocket.ts index d37fce9851e91..f2c3188b9e6ff 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-websocket.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/use-websocket.ts @@ -1,6 +1,11 @@ import { useCallback, useContext, useEffect, useRef } from 'react' import { GlobalLayoutRouterContext } from '../../../../../shared/lib/app-router-context.shared-runtime' import { getSocketUrl } from './get-socket-url' +import type { + HMR_ACTION_TYPES, + TurbopackConnectedAction, + TurbopackMessageAction, +} from '../../../../../server/dev/hot-reloader-types' export function useWebsocket(assetPrefix: string) { const webSocketRef = useRef() @@ -32,6 +37,62 @@ export function useSendMessage(webSocketRef: ReturnType) { return sendMessage } +export function useTurbopack(sendMessage: ReturnType) { + const turbopackState = useRef<{ + init: boolean + queue: Array | undefined + callback: ((msg: HMR_ACTION_TYPES) => void) | undefined + }>({ + init: false, + // Until the dynamic import resolves, queue any turbopack messages which will be replayed. + queue: [], + callback: undefined, + }) + + const processTurbopackMessage = useCallback((msg: HMR_ACTION_TYPES) => { + if ('type' in msg && msg.type?.startsWith('turbopack-')) { + const { callback, queue } = turbopackState.current + if (callback) { + callback(msg) + } else { + queue!.push(msg) + } + return true + } + return false + }, []) + + useEffect(() => { + const { current: initCurrent } = turbopackState + // TODO(WEB-1589): only install if `process.turbopack` set. + if (initCurrent.init) { + return + } + initCurrent.init = true + + import( + // @ts-expect-error requires "moduleResolution": "node16" in tsconfig.json and not .ts extension + '@vercel/turbopack-ecmascript-runtime/dev/client/hmr-client.ts' + ).then(({ connect }) => { + const { current } = turbopackState + connect({ + addMessageListener(cb: (msg: HMR_ACTION_TYPES) => void) { + current.callback = cb + + // Replay all Turbopack messages before we were able to establish the HMR client. + for (const msg of current.queue!) { + cb(msg) + } + current.queue = undefined + }, + sendMessage, + }) + }) + }, [sendMessage]) + + return processTurbopackMessage +} + export function useWebsocketPing( websocketRef: ReturnType ) { diff --git a/packages/next/src/client/dev/noop-turbopack-hmr.ts b/packages/next/src/client/dev/noop-turbopack-hmr.ts new file mode 100644 index 0000000000000..6b81e0b7deea2 --- /dev/null +++ b/packages/next/src/client/dev/noop-turbopack-hmr.ts @@ -0,0 +1,3 @@ +// The Turbopack HMR client can't be properly omitted at the moment (WEB-1589), +// so instead we remap its import to this file in webpack builds. +export function connect() {} diff --git a/packages/next/src/server/dev/hot-reloader-types.ts b/packages/next/src/server/dev/hot-reloader-types.ts index 7428e426001ee..fb4f3f03e2470 100644 --- a/packages/next/src/server/dev/hot-reloader-types.ts +++ b/packages/next/src/server/dev/hot-reloader-types.ts @@ -28,7 +28,7 @@ interface ServerErrorAction { errorJSON: string } -interface TurboPackMessageAction { +export interface TurbopackMessageAction { type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_MESSAGE data: TurbopackUpdate | TurbopackUpdate[] } @@ -87,12 +87,13 @@ interface DevPagesManifestUpdateAction { ] } -export interface TurboPackConnectedAction { +export interface TurbopackConnectedAction { type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED } export type HMR_ACTION_TYPES = - | TurboPackMessageAction + | TurbopackMessageAction + | TurbopackConnectedAction | BuildingAction | SyncAction | BuiltAction diff --git a/packages/next/src/server/lib/router-utils/setup-dev.ts b/packages/next/src/server/lib/router-utils/setup-dev.ts index 8db318f8b2037..77c345b06ad02 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev.ts @@ -100,7 +100,7 @@ import { HMR_ACTION_TYPES, NextJsHotReloaderInterface, ReloadPageAction, - TurboPackConnectedAction, + TurbopackConnectedAction, } from '../../dev/hot-reloader-types' import type { Update as TurbopackUpdate } from '../../../build/swc' import { debounce } from '../../utils' @@ -356,10 +356,14 @@ async function startWatcher(opts: SetupOpts) { } hotReloader.send({ - action: HMR_ACTIONS_SENT_TO_BROWSER.BUILT, + action: HMR_ACTIONS_SENT_TO_BROWSER.SYNC, hash: String(++hmrHash), errors: [...errors.values()], warnings: [], + versionInfo: { + installed: '0.0.0', + staleness: 'unknown', + }, }) hmrBuilding = false @@ -984,7 +988,7 @@ async function startWatcher(opts: SetupOpts) { } }) - const turbopackConnected: TurboPackConnectedAction = { + const turbopackConnected: TurbopackConnectedAction = { type: HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED, } client.send(JSON.stringify(turbopackConnected))