import { useCallback, useContext, useEffect, useRef, // @ts-expect-error TODO-APP: startTransition exists startTransition, } from 'react' import { GlobalLayoutRouterContext } from '../../shared/lib/app-router-context' import { register, unregister, onBuildError, onBuildOk, onRefresh, } from 'next/dist/compiled/@next/react-dev-overlay/dist/client' import stripAnsi from 'next/dist/compiled/strip-ansi' import formatWebpackMessages from '../dev/error-overlay/format-webpack-messages' import { useRouter } from './hooks-client' function getSocketProtocol(assetPrefix: string): string { let protocol = window.location.protocol try { // assetPrefix is a url protocol = new URL(assetPrefix).protocol } catch (_) {} return protocol === 'http:' ? 'ws' : 'wss' } // const TIMEOUT = 5000 // TODO-APP: add actual type type PongEvent = any let mostRecentCompilationHash: any = null let __nextDevClientId = Math.round(Math.random() * 100 + Date.now()) let hadRuntimeError = false // let startLatency = undefined function onFastRefresh(hasUpdates: boolean) { onBuildOk() if (hasUpdates) { onRefresh() } // if (startLatency) { // const endLatency = Date.now() // const latency = endLatency - startLatency // console.log(`[Fast Refresh] done in ${latency}ms`) // sendMessage( // JSON.stringify({ // event: 'client-hmr-latency', // id: __nextDevClientId, // startTime: startLatency, // endTime: endLatency, // }) // ) // // if (self.__NEXT_HMR_LATENCY_CB) { // // self.__NEXT_HMR_LATENCY_CB(latency) // // } // } } // There is a newer version of the code available. function handleAvailableHash(hash: string) { // Update last known compilation hash. mostRecentCompilationHash = hash } // Is there a newer version of this code available? function isUpdateAvailable() { /* globals __webpack_hash__ */ // __webpack_hash__ is the hash of the current compilation. // It's a global variable injected by Webpack. // @ts-expect-error __webpack_hash__ exists return mostRecentCompilationHash !== __webpack_hash__ } // Webpack disallows updates in other states. function canApplyUpdates() { // @ts-expect-error module.hot exists return module.hot.status() === 'idle' } // function afterApplyUpdates(fn: any) { // if (canApplyUpdates()) { // fn() // } else { // function handler(status: any) { // if (status === 'idle') { // // @ts-expect-error module.hot exists // module.hot.removeStatusHandler(handler) // fn() // } // } // // @ts-expect-error module.hot exists // module.hot.addStatusHandler(handler) // } // } // Attempt to update code on the fly, fall back to a hard reload. function tryApplyUpdates(onHotUpdateSuccess: any, sendMessage: any) { // @ts-expect-error module.hot exists if (!module.hot) { // HotModuleReplacementPlugin is not in Webpack configuration. console.error('HotModuleReplacementPlugin is not in Webpack configuration.') // window.location.reload(); return } if (!isUpdateAvailable() || !canApplyUpdates()) { onBuildOk() return } function handleApplyUpdates(err: any, updatedModules: any) { if (err || hadRuntimeError || !updatedModules) { if (err) { console.warn( '[Fast Refresh] performing full reload\n\n' + "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree.\n" + 'You might have a file which exports a React component but also exports a value that is imported by a non-React component file.\n' + 'Consider migrating the non-React component export to a separate file and importing it into both files.\n\n' + 'It is also possible the parent component of the component you edited is a class component, which disables Fast Refresh.\n' + 'Fast Refresh requires at least one parent function component in your React tree.' ) } else if (hadRuntimeError) { console.warn( '[Fast Refresh] performing full reload because your application had an unrecoverable error' ) } performFullReload(err, sendMessage) return } const hasUpdates = Boolean(updatedModules.length) if (typeof onHotUpdateSuccess === 'function') { // Maybe we want to do something. onHotUpdateSuccess(hasUpdates) } if (isUpdateAvailable()) { // While we were updating, there was a new update! Do it again. tryApplyUpdates(hasUpdates ? onBuildOk : onHotUpdateSuccess, sendMessage) } else { onBuildOk() // if (process.env.__NEXT_TEST_MODE) { // afterApplyUpdates(() => { // if (self.__NEXT_HMR_CB) { // self.__NEXT_HMR_CB() // self.__NEXT_HMR_CB = null // } // }) // } } } // https://webpack.js.org/api/hot-module-replacement/#check // @ts-expect-error module.hot exists module.hot.check(/* autoApply */ true).then( (updatedModules: any) => { handleApplyUpdates(null, updatedModules) }, (err: any) => { handleApplyUpdates(err, null) } ) } function performFullReload(err: any, sendMessage: any) { const stackTrace = err && ((err.stack && err.stack.split('\n').slice(0, 5).join('\n')) || err.message || err + '') sendMessage( JSON.stringify({ event: 'client-full-reload', stackTrace, }) ) window.location.reload() } function processMessage( e: any, sendMessage: any, router: ReturnType ) { const obj = JSON.parse(e.data) switch (obj.action) { case 'building': { // startLatency = Date.now() console.log('[Fast Refresh] rebuilding') break } case 'built': case 'sync': { if (obj.hash) { handleAvailableHash(obj.hash) } const { errors, warnings } = obj const hasErrors = Boolean(errors && errors.length) // Compilation with errors (e.g. syntax error or missing modules). if (hasErrors) { sendMessage( JSON.stringify({ event: 'client-error', errorCount: errors.length, clientId: __nextDevClientId, }) ) // "Massage" webpack messages. var formatted = formatWebpackMessages({ errors: errors, warnings: [], }) // Only show the first error. onBuildError(formatted.errors[0]) // Also log them to the console. for (let i = 0; i < formatted.errors.length; i++) { console.error(stripAnsi(formatted.errors[i])) } // Do not attempt to reload now. // We will reload on next success instead. // if (process.env.__NEXT_TEST_MODE) { // if (self.__NEXT_HMR_CB) { // self.__NEXT_HMR_CB(formatted.errors[0]) // self.__NEXT_HMR_CB = null // } // } return } const hasWarnings = Boolean(warnings && warnings.length) if (hasWarnings) { sendMessage( JSON.stringify({ event: 'client-warning', warningCount: warnings.length, clientId: __nextDevClientId, }) ) // Compilation with warnings (e.g. ESLint). const isHotUpdate = obj.action !== 'sync' // Print warnings to the console. const formattedMessages = formatWebpackMessages({ warnings: warnings, errors: [], }) for (let i = 0; i < formattedMessages.warnings.length; i++) { if (i === 5) { console.warn( 'There were more warnings in other files.\n' + 'You can find a complete log in the terminal.' ) break } console.warn(stripAnsi(formattedMessages.warnings[i])) } // Attempt to apply hot updates or reload. if (isHotUpdate) { tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates: any) { // Only dismiss it when we're sure it's a hot update. // Otherwise it would flicker right before the reload. onFastRefresh(hasUpdates) }, sendMessage) } return } sendMessage( JSON.stringify({ event: 'client-success', clientId: __nextDevClientId, }) ) const isHotUpdate = obj.action !== 'sync' || ((!window.__NEXT_DATA__ || window.__NEXT_DATA__.page !== '/_error') && isUpdateAvailable()) // Attempt to apply hot updates or reload. if (isHotUpdate) { tryApplyUpdates(function onSuccessfulHotUpdate(hasUpdates: any) { // Only dismiss it when we're sure it's a hot update. // Otherwise it would flicker right before the reload. onFastRefresh(hasUpdates) }, sendMessage) } return } // TODO-APP: make server component change more granular case 'serverComponentChanges': { sendMessage( JSON.stringify({ event: 'server-component-reload-page', clientId: __nextDevClientId, }) ) if (hadRuntimeError) { return window.location.reload() } startTransition(() => { router.reload() onRefresh() }) return } case 'reloadPage': { sendMessage( JSON.stringify({ event: 'client-reload-page', clientId: __nextDevClientId, }) ) return window.location.reload() } case 'removedPage': { // const [page] = obj.data // if (page === window.next.router.pathname) { // sendMessage( // JSON.stringify({ // event: 'client-removed-page', // clientId: window.__nextDevClientId, // page, // }) // ) // return window.location.reload() // } return } case 'addedPage': { // const [page] = obj.data // if ( // page === window.next.router.pathname && // typeof window.next.router.components[page] === 'undefined' // ) { // sendMessage( // JSON.stringify({ // event: 'client-added-page', // clientId: window.__nextDevClientId, // page, // }) // ) // return window.location.reload() // } return } case 'pong': { const { invalid } = obj if (invalid) { // Payload can be invalid even if the page does exist. // So, we check if it can be created. fetch(location.href, { credentials: 'same-origin', }).then((pageRes) => { if (pageRes.status === 200) { // Page exists now, reload location.reload() } else { // TODO-APP: fix this // Page doesn't exist // if ( // self.__NEXT_DATA__.page === Router.pathname && // Router.pathname !== '/_error' // ) { // // We are still on the page, // // reload to show 404 error page // location.reload() // } } }) } return } default: { throw new Error('Unexpected action ' + obj.action) } } } export default function HotReload({ assetPrefix }: { assetPrefix: string }) { const { tree } = useContext(GlobalLayoutRouterContext) const router = useRouter() const webSocketRef = useRef() const sendMessage = useCallback((data) => { const socket = webSocketRef.current if (!socket || socket.readyState !== socket.OPEN) return return socket.send(data) }, []) useEffect(() => { register() const onError = () => { hadRuntimeError = true } window.addEventListener('error', onError) window.addEventListener('unhandledrejection', onError) return () => { unregister() window.removeEventListener('error', onError) window.removeEventListener('unhandledrejection', onError) } }, []) useEffect(() => { if (webSocketRef.current) { return } const { hostname, port } = window.location const protocol = getSocketProtocol(assetPrefix || '') const normalizedAssetPrefix = assetPrefix.replace(/^\/+/, '') let url = `${protocol}://${hostname}:${port}${ normalizedAssetPrefix ? `/${normalizedAssetPrefix}` : '' }` if (normalizedAssetPrefix.startsWith('http')) { url = `${protocol}://${normalizedAssetPrefix.split('://')[1]}` } webSocketRef.current = new window.WebSocket(`${url}/_next/webpack-hmr`) }, [assetPrefix]) useEffect(() => { // Taken from on-demand-entries-client.js // TODO-APP: check 404 case const interval = setInterval(() => { sendMessage( JSON.stringify({ event: 'ping', // TODO-APP: fix case for dynamic parameters, this will be resolved wrong currently. tree, appDirRoute: true, }) ) }, 2500) return () => clearInterval(interval) }, [tree, sendMessage]) useEffect(() => { const handler = (event: MessageEvent) => { if ( event.data.indexOf('action') === -1 && // TODO-APP: clean this up for consistency event.data.indexOf('pong') === -1 ) { return } try { processMessage(event, sendMessage, router) } catch (ex) { console.warn('Invalid HMR message: ' + event.data + '\n', ex) } } if (webSocketRef.current) { webSocketRef.current.addEventListener('message', handler) } return () => webSocketRef.current && webSocketRef.current.removeEventListener('message', handler) }, [sendMessage, router]) // useEffect(() => { // const interval = setInterval(function () { // if ( // lastActivityRef.current && // Date.now() - lastActivityRef.current > TIMEOUT // ) { // handleDisconnect() // } // }, 2500) // return () => clearInterval(interval) // }) return null }