Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion packages/next/src/build/swc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,14 @@ export interface NodeJsPartialHmrUpdate extends BaseUpdate {
}
}

export type NodeJsHmrUpdate = IssuesUpdate | NodeJsPartialHmrUpdate
export interface NodeJsRestartHmrUpdate {
type: 'restart'
}

export type NodeJsHmrUpdate =
| IssuesUpdate
| NodeJsPartialHmrUpdate
| NodeJsRestartHmrUpdate

export interface HmrChunkNames {
/** Relative paths to output chunks that can receive HMR updates (e.g., "server/chunks/ssr/..._.js") */
Expand Down
3 changes: 1 addition & 2 deletions packages/next/src/server/app-render/entry-base.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { NodeJsPartialHmrUpdate } from '../../build/swc/types'

// eslint-disable-next-line import/no-extraneous-dependencies
export {
createTemporaryReferenceSet,
Expand Down Expand Up @@ -49,6 +47,7 @@ export const InstantValidation =
? (require('./instant-validation/instant-validation') as typeof import('./instant-validation/instant-validation'))
: undefined

import type { NodeJsPartialHmrUpdate } from '../../build/swc/types'
import { workAsyncStorage } from '../app-render/work-async-storage.external'
import { workUnitAsyncStorage } from './work-unit-async-storage.external'
import { patchFetch as _patchFetch } from '../lib/patch-fetch'
Expand Down
65 changes: 43 additions & 22 deletions packages/next/src/server/dev/hot-reloader-turbopack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ type ServerHmrSubscriptions = Map<
function setupServerHmr(
project: Project,
{
onUpdateFailed,
clear,
}: {
onUpdateFailed: () => void | Promise<void>
clear: () => void | Promise<void>
}
) {
const serverHmrSubscriptions: ServerHmrSubscriptions = new Map()
Expand All @@ -184,6 +184,14 @@ function setupServerHmr(

for await (const result of subscription) {
const update = result as NodeJsHmrUpdate

// Fully re-evaluate all chunks from disk. Clears the module cache and
// notifies browsers to refetch RSC.
if (update.type === 'restart') {
await clear()
continue
}

if (update.type !== 'partial') {
continue
}
Expand All @@ -196,14 +204,14 @@ function setupServerHmr(
if (typeof __turbopack_server_hmr_apply__ === 'function') {
const applied = __turbopack_server_hmr_apply__(update)
if (!applied) {
await onUpdateFailed()
await clear()
}
}
}
})().catch(async (err) => {
console.error('[Server HMR] Subscription error:', err)
serverHmrSubscriptions.delete(chunkPath)
await onUpdateFailed()
await clear()
})
}

Expand Down Expand Up @@ -580,33 +588,38 @@ export async function createHotReloaderTurbopack(
}
}

resetFetch()

const serverPaths = writtenEndpoint.serverPaths.map(({ path: p }) =>
join(distDir, p)
)

const { type: entryType } = splitEntryKey(key)
// Server HMR only applies to App Router Node.js runtime endpoints.
// Server HMR only applies to App Router.
// Pages Router uses Node's require(), root entries (middleware/instrumentation)
// use the edge runtime, and App Router edge routes all don't support server HMR.
const usesServerHmr = entryType === 'app' && writtenEndpoint.type !== 'edge'
// use the edge runtime.
const usesServerHmr =
experimentalServerFastRefresh &&
entryType === 'app' &&
writtenEndpoint.type !== 'edge'

for (const file of serverPaths) {
const relativePath = relative(distDir, file)
clearModuleContext(file)

if (usesServerHmr && serverHmrSubscriptions?.has(relativePath)) {
// Skip deleteCache for server HMR module chunks.
// Pages Router entries are excluded by usesServerHmr (always false for
// pages), so they always get deleteCache regardless of subscriptions.
continue
const relativePath = relative(distDir, file)
if (
// For Pages Router, edge routes, middleware, and manifest files
// (e.g., *_client-reference-manifest.js): clear the sharedCache in
// evalManifest(), Node.js require.cache, and edge runtime module contexts.
force ||
!usesServerHmr ||
!serverHmrSubscriptions?.has(relativePath)
) {
deleteCache(file)
}
}

clearModuleContext(file)
// For Pages Router, edge routes, middleware, and manifest files
// (e.g., *_client-reference-manifest.js): clear the sharedCache in
// evalManifest(), Node.js require.cache, and edge runtime module contexts.
deleteCache(file)
// Reset the fetch patch so patchFetch() can re-wrap on the next request.
if (serverPaths.length > 0) {
resetFetch()
}

// Clear Turbopack's chunk-loading cache so chunks are re-required from disk on
Expand Down Expand Up @@ -1808,14 +1821,22 @@ export async function createHotReloaderTurbopack(

if (experimentalServerFastRefresh) {
serverHmrSubscriptions = setupServerHmr(project, {
onUpdateFailed: async () => {
clear: async () => {
// Clear Node's require cache of all Turbopack-built modules
for (const chunkPath of serverHmrSubscriptions?.keys() ?? []) {
deleteCache(join(distDir, chunkPath))
}

// Clear Turbopack's runtime caches
if (typeof __next__clear_chunk_cache__ === 'function') {
__next__clear_chunk_cache__()
}

// Clear all module contexts so they're re-evaluated on next request
// Clear all edge contexts
await clearAllModuleContexts()

resetFetch()

// Tell browsers to refetch RSC (soft refresh, not full page reload)
hotReloader.send({
type: HMR_MESSAGE_SENT_TO_BROWSER.SERVER_COMPONENT_CHANGES,
Expand Down
8 changes: 7 additions & 1 deletion test/development/app-dir/dev-fetch-hmr/dev-fetch-hmr.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,16 @@ describe('dev-fetch-hmr', () => {
const update = cheerio.load(html2)('#update').text()
expect(update).toBe('touch to trigger HMR')

// trigger HMR
await next.patchFile('app/page.tsx', (content) =>
content.replace('touch to trigger HMR', 'touch to trigger HMR 2')
)
// For server hmr, we must touch the exact module to trigger re-evaluation
await next.patchFile('app/layout.tsx', (content) =>
content.replace(
'const magicNumber = Math.random()',
'// hmr trigger\nconst magicNumber = Math.random()'
)
)

await retry(async () => {
const html3 = await next.render('/')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ interface NodeJsPartialHmrUpdate {
chunks?: Record<string, { type: 'partial' }>
}
}

interface NodeJsRestartHmrUpdate {
type: 'restart'
}
Loading