Skip to content

Commit

Permalink
Move server HMR handling into hot-middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
ijjk committed Oct 24, 2024
1 parent 6d02322 commit 3af4d91
Show file tree
Hide file tree
Showing 2 changed files with 249 additions and 215 deletions.
246 changes: 240 additions & 6 deletions packages/next/src/server/dev/hot-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import type { webpack } from 'next/dist/compiled/webpack/webpack'
import { type webpack, StringXor } from 'next/dist/compiled/webpack/webpack'
import type ws from 'next/dist/compiled/ws'
import { isMiddlewareFilename } from '../../build/utils'
import { difference, isMiddlewareFilename } from '../../build/utils'
import type { VersionInfo } from './parse-version-info'
import type { HMR_ACTION_TYPES } from './hot-reloader-types'
import { HMR_ACTIONS_SENT_TO_BROWSER } from './hot-reloader-types'
import { WEBPACK_LAYERS } from '../../lib/constants'
import { denormalizePagePath } from '../../shared/lib/page-path/denormalize-page-path'

function isMiddlewareStats(stats: webpack.Stats) {
for (const key of stats.compilation.entrypoints.keys()) {
Expand Down Expand Up @@ -107,19 +109,27 @@ export class WebpackHotMiddleware {
closed: boolean
versionInfo: VersionInfo
devtoolsFrontendUrl: string | undefined
pendingStats = []
reloadAfterInvalidation?: boolean
resetFetch: () => void

constructor(
compilers: webpack.Compiler[],
multiCompiler: webpack.MultiCompiler,
versionInfo: VersionInfo,
devtoolsFrontendUrl: string | undefined
devtoolsFrontendUrl: string | undefined,
pageExtensions: string[],
resetFetch: () => void
) {
const compilers = multiCompiler.compilers
this.eventStream = new EventStream()
this.clientLatestStats = null
this.middlewareLatestStats = null
this.serverLatestStats = null
this.closed = false
this.versionInfo = versionInfo
this.devtoolsFrontendUrl = devtoolsFrontendUrl
this.pendingStats = []
this.resetFetch = resetFetch

compilers[0].hooks.invalid.tap(
'webpack-hot-middleware',
Expand All @@ -136,9 +146,235 @@ export class WebpackHotMiddleware {
'webpack-hot-middleware',
this.onEdgeServerInvalid
)
// Watch for changes to client/server page files so we can tell when just
// the server file changes and trigger a reload for GS(S)P pages
const changedClientPages = new Set<string>()
const changedServerPages = new Set<string>()
const changedEdgeServerPages = new Set<string>()

const changedServerComponentPages = new Set<string>()
const changedCSSImportPages = new Set<string>()

const prevClientPageHashes = new Map<string, string>()
const prevServerPageHashes = new Map<string, string>()
const prevEdgeServerPageHashes = new Map<string, string>()
const prevCSSImportModuleHashes = new Map<string, string>()

const pageExtensionRegex = new RegExp(`\\.(?:${pageExtensions.join('|')})$`)

const trackPageChanges =
(
pageHashMap: Map<string, string>,
changedItems: Set<string>,
serverComponentChangedItems?: Set<string>
) =>
(stats: webpack.Compilation) => {
try {
stats.entrypoints.forEach((entry, key) => {
if (
key.startsWith('pages/') ||
key.startsWith('app/') ||
isMiddlewareFilename(key)
) {
// TODO this doesn't handle on demand loaded chunks
entry.chunks.forEach((chunk) => {
if (chunk.id === key) {
const modsIterable: any =
stats.chunkGraph.getChunkModulesIterable(chunk)

let hasCSSModuleChanges = false
let chunksHash = new StringXor()
let chunksHashServerLayer = new StringXor()

modsIterable.forEach((mod: any) => {
if (
mod.resource &&
mod.resource.replace(/\\/g, '/').includes(key) &&
// Shouldn't match CSS modules, etc.
pageExtensionRegex.test(mod.resource)
) {
// use original source to calculate hash since mod.hash
// includes the source map in development which changes
// every time for both server and client so we calculate
// the hash without the source map for the page module
const hash = require('crypto')
.createHash('sha1')
.update(mod.originalSource().buffer())
.digest()
.toString('hex')

if (
mod.layer === WEBPACK_LAYERS.reactServerComponents &&
mod?.buildInfo?.rsc?.type !== 'client'
) {
chunksHashServerLayer.add(hash)
}

chunksHash.add(hash)
} else {
// for non-pages we can use the module hash directly
const hash = stats.chunkGraph.getModuleHash(
mod,
chunk.runtime
)

if (
mod.layer === WEBPACK_LAYERS.reactServerComponents &&
mod?.buildInfo?.rsc?.type !== 'client'
) {
chunksHashServerLayer.add(hash)
}

chunksHash.add(hash)

// Both CSS import changes from server and client
// components are tracked.
if (
key.startsWith('app/') &&
/\.(css|scss|sass)$/.test(mod.resource || '')
) {
const resourceKey = mod.layer + ':' + mod.resource
const prevHash =
prevCSSImportModuleHashes.get(resourceKey)
if (prevHash && prevHash !== hash) {
hasCSSModuleChanges = true
}
prevCSSImportModuleHashes.set(resourceKey, hash)
}
}
})

const prevHash = pageHashMap.get(key)
const curHash = chunksHash.toString()
if (prevHash && prevHash !== curHash) {
changedItems.add(key)
}
pageHashMap.set(key, curHash)

if (serverComponentChangedItems) {
const serverKey =
WEBPACK_LAYERS.reactServerComponents + ':' + key
const prevServerHash = pageHashMap.get(serverKey)
const curServerHash = chunksHashServerLayer.toString()
if (prevServerHash && prevServerHash !== curServerHash) {
serverComponentChangedItems.add(key)
}
pageHashMap.set(serverKey, curServerHash)
}

if (hasCSSModuleChanges) {
changedCSSImportPages.add(key)
}
}
})
}
})
} catch (err) {
console.error(err)
}
}

compilers[0].hooks.emit.tap(
'NextjsHotReloaderForClient',
trackPageChanges(prevClientPageHashes, changedClientPages)
)
compilers[1].hooks.emit.tap(
'NextjsHotReloaderForServer',
trackPageChanges(
prevServerPageHashes,
changedServerPages,
changedServerComponentPages
)
)
compilers[2].hooks.emit.tap(
'NextjsHotReloaderForServer',
trackPageChanges(
prevEdgeServerPageHashes,
changedEdgeServerPages,
changedServerComponentPages
)
)
let lastSentClientStats = 0
let lastSentServerStats = 0

multiCompiler.hooks.done.tap('NextjsHotReloaderForServer', () => {
const reloadAfterInvalidation = this.reloadAfterInvalidation
this.reloadAfterInvalidation = false

if (
this.clientLatestStats &&
this.clientLatestStats.ts > lastSentClientStats
) {
lastSentClientStats = Date.now()
this.publishStats(this.clientLatestStats.stats)
}
if (
this.serverLatestStats &&
this.serverLatestStats.ts > lastSentServerStats
) {
lastSentServerStats = Date.now()
this.publishStats(this.serverLatestStats.stats)
}
const serverOnlyChanges = difference<string>(
changedServerPages,
changedClientPages
)

const edgeServerOnlyChanges = difference<string>(
changedEdgeServerPages,
changedClientPages
)

const pageChanges = serverOnlyChanges
.concat(edgeServerOnlyChanges)
.filter((key) => key.startsWith('pages/'))
const middlewareChanges = Array.from(changedEdgeServerPages).filter(
(name) => isMiddlewareFilename(name)
)

if (middlewareChanges.length > 0) {
this.publish({
event: HMR_ACTIONS_SENT_TO_BROWSER.MIDDLEWARE_CHANGES,
})
}

if (pageChanges.length > 0) {
this.publish({
event: HMR_ACTIONS_SENT_TO_BROWSER.SERVER_ONLY_CHANGES,
pages: serverOnlyChanges.map((pg) =>
denormalizePagePath(pg.slice('pages'.length))
),
})
}

if (
changedServerComponentPages.size ||
changedCSSImportPages.size ||
reloadAfterInvalidation
) {
this.resetFetch()

// This event must come after the stats have been sent above
// or else the client might try to trigger the HMR RSC request
// before we send built event clearing any errors that are still
// valid from the HMR RSC request
this.publish({
action: HMR_ACTIONS_SENT_TO_BROWSER.SERVER_COMPONENT_CHANGES,
// TODO: granular reloading of changes
// entrypoints: serverComponentChanges,
})
}

changedClientPages.clear()
changedServerPages.clear()
changedEdgeServerPages.clear()
changedServerComponentPages.clear()
changedCSSImportPages.clear()
})
}

onClientInvalid = () => {
this.clientLatestStats = null
if (this.closed || this.serverLatestStats?.stats.hasErrors()) return
this.publish({
action: HMR_ACTIONS_SENT_TO_BROWSER.BUILDING,
Expand All @@ -148,7 +384,6 @@ export class WebpackHotMiddleware {
onClientDone = (statsResult: webpack.Stats) => {
this.clientLatestStats = { ts: Date.now(), stats: statsResult }
if (this.closed || this.serverLatestStats?.stats.hasErrors()) return
this.publishStats(statsResult)
}

onServerInvalid = () => {
Expand All @@ -163,7 +398,6 @@ export class WebpackHotMiddleware {
if (this.closed) return
if (statsResult.hasErrors()) {
this.serverLatestStats = { ts: Date.now(), stats: statsResult }
this.publishStats(statsResult)
}
}

Expand Down
Loading

0 comments on commit 3af4d91

Please sign in to comment.