Skip to content

Commit 91fab9c

Browse files
wbinnssmithclaude
andcommitted
fix(server-hmr): re-require metadata route handlers per-request in Turbopack dev
Dynamic metadata routes (manifest, robots, sitemap, images) are generated as virtual wrapper modules with a static top-level import of the user's handler. When server HMR patches devModuleCache in-place for the user's file, the wrapper's captured binding still references the old function. Fix by adding a conditional require() inside the GET handler when running under Turbopack dev server, mirroring the pattern already used for app-route handlers. In production and webpack builds the branch is dead code and is eliminated. Fixes #91981 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent d84e3bb commit 91fab9c

File tree

4 files changed

+77
-17
lines changed

4 files changed

+77
-17
lines changed

crates/next-core/src/next_app/metadata/route.rs

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,14 @@ async fn dynamic_text_route_source(path: FileSystemPath) -> Result<Vc<Box<dyn So
237237
}}
238238
239239
export async function GET() {{
240-
const data = await handler()
240+
// In Turbopack dev mode, re-require the user's module on every
241+
// request so that server HMR updates are reflected immediately.
242+
// The static `handler` import above is kept for the type check
243+
// and re-exports; at runtime in dev we bypass it here.
244+
const _handler = (process.env.TURBOPACK && process.env.__NEXT_DEV_SERVER)
245+
? require({resource_path}).default
246+
: handler
247+
const data = await _handler()
241248
const content = resolveRouteData(data, fileType)
242249
243250
return new NextResponse(content, {{
@@ -293,7 +300,14 @@ async fn dynamic_sitemap_route_with_generate_source(
293300
294301
const id = await idPromise
295302
const hasXmlExtension = id ? id.endsWith('.xml') : false
296-
const sitemaps = await generateSitemaps()
303+
// In Turbopack dev mode, re-require the user's module on every
304+
// request so that server HMR updates are reflected immediately.
305+
const _mod = (process.env.TURBOPACK && process.env.__NEXT_DEV_SERVER)
306+
? require({resource_path})
307+
: null
308+
const _handler = _mod ? _mod.default : handler
309+
const _generateSitemaps = _mod ? _mod.generateSitemaps : generateSitemaps
310+
const sitemaps = await _generateSitemaps()
297311
let foundId
298312
for (const item of sitemaps) {{
299313
if (item?.id == null) {{
@@ -314,7 +328,7 @@ async fn dynamic_sitemap_route_with_generate_source(
314328
const hasXmlExtension = id ? id.endsWith('.xml') : false
315329
return id && hasXmlExtension ? id.slice(0, -4) : undefined
316330
}})
317-
const data = await handler({{ id: targetIdPromise }})
331+
const data = await _handler({{ id: targetIdPromise }})
318332
const content = resolveRouteData(data, fileType)
319333
320334
return new NextResponse(content, {{
@@ -378,7 +392,12 @@ async fn dynamic_sitemap_route_without_generate_source(
378392
}}
379393
380394
export async function GET() {{
381-
const data = await handler()
395+
// In Turbopack dev mode, re-require the user's module on every
396+
// request so that server HMR updates are reflected immediately.
397+
const _handler = (process.env.TURBOPACK && process.env.__NEXT_DEV_SERVER)
398+
? require({resource_path}).default
399+
: handler
400+
const data = await _handler()
382401
const content = resolveRouteData(data, fileType)
383402
384403
return new NextResponse(content, {{
@@ -443,9 +462,16 @@ async fn dynamic_image_route_with_metadata_source(
443462
return rest
444463
}})
445464
465+
// In Turbopack dev mode, re-require the user's module on every
466+
// request so that server HMR updates are reflected immediately.
467+
const _mod = (process.env.TURBOPACK && process.env.__NEXT_DEV_SERVER)
468+
? require({resource_path})
469+
: null
470+
const _handler = _mod ? _mod.default : handler
471+
const _generateImageMetadata = _mod ? _mod.generateImageMetadata : generateImageMetadata
446472
const restParams = await restParamsPromise
447473
const __metadata_id__ = await idPromise
448-
const imageMetadata = await generateImageMetadata({{ params: restParams }})
474+
const imageMetadata = await _generateImageMetadata({{ params: restParams }})
449475
const id = imageMetadata.find((item) => {{
450476
if (item?.id == null) {{
451477
throw new Error('id property is required for every item returned from generateImageMetadata')
@@ -460,7 +486,7 @@ async fn dynamic_image_route_with_metadata_source(
460486
}})
461487
}}
462488
463-
return handler({{ params: restParamsPromise, id: idPromise }})
489+
return _handler({{ params: restParamsPromise, id: idPromise }})
464490
}}
465491
466492
export * from {resource_path}
@@ -507,7 +533,12 @@ async fn dynamic_image_route_without_metadata_source(
507533
}}
508534
509535
export async function GET(_, ctx) {{
510-
return handler({{ params: ctx.params }})
536+
// In Turbopack dev mode, re-require the user's module on every
537+
// request so that server HMR updates are reflected immediately.
538+
const _handler = (process.env.TURBOPACK && process.env.__NEXT_DEV_SERVER)
539+
? require({resource_path}).default
540+
: handler
541+
return _handler({{ params: ctx.params }})
511542
}}
512543
513544
export * from {resource_path}

packages/next/src/build/swc/generated-native.d.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ export declare function minify(
153153
export declare function minifySync(input: Buffer, opts: Buffer): TransformOutput
154154
export interface NapiEndpointConfig {}
155155
export interface NapiAssetPath {
156-
path: string
157-
contentHash: string
156+
path: RcStr
157+
contentHash: RcStr
158158
}
159159
export interface NapiWrittenEndpoint {
160160
type: string
@@ -363,7 +363,7 @@ export interface AppPageNapiRoute {
363363
}
364364
export interface NapiRoute {
365365
/** The router path */
366-
pathname: string
366+
pathname: RcStr
367367
/** The relative path from project_path to the route file */
368368
originalName?: RcStr
369369
/** The type of route, eg a Page or App */
@@ -522,13 +522,13 @@ export declare function rootTaskDispose(rootTask: {
522522
export interface NapiIssue {
523523
severity: string
524524
stage: string
525-
filePath: string
525+
filePath: RcStr
526526
title: any
527527
description?: any
528528
detail?: any
529529
source?: NapiIssueSource
530530
additionalSources: Array<NapiAdditionalIssueSource>
531-
documentationLink: string
531+
documentationLink: RcStr
532532
importTraces: any
533533
/**
534534
* Pre-rendered code frame for the issue's source location, if available.
@@ -537,7 +537,7 @@ export interface NapiIssue {
537537
codeFrame?: string
538538
}
539539
export interface NapiAdditionalIssueSource {
540-
description: string
540+
description: RcStr
541541
source: NapiIssueSource
542542
/** Pre-rendered code frame for this additional source location, if available. */
543543
codeFrame?: string
@@ -551,16 +551,16 @@ export interface NapiIssueSourceRange {
551551
end: NapiSourcePos
552552
}
553553
export interface NapiSource {
554-
ident: string
555-
filePath: string
554+
ident: RcStr
555+
filePath: RcStr
556556
}
557557
export interface NapiSourcePos {
558558
line: number
559559
column: number
560560
}
561561
export interface NapiDiagnostic {
562-
category: string
563-
name: string
562+
category: RcStr
563+
name: RcStr
564564
payload: Record<string, string>
565565
}
566566
export declare function expandNextJsTemplate(
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { MetadataRoute } from 'next'
2+
3+
export default function manifest(): MetadataRoute.Manifest {
4+
return {
5+
name: 'My App v1',
6+
start_url: '/',
7+
display: 'standalone',
8+
}
9+
}

test/development/app-dir/server-hmr/server-hmr.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,24 @@ describe('server-hmr', () => {
206206
}
207207
)
208208
})
209+
210+
describe('metadata route hmr', () => {
211+
itTurbopackDev('reflects manifest.ts changes on fetch', async () => {
212+
const initial = await next
213+
.fetch('/manifest.webmanifest')
214+
.then((res) => res.json())
215+
expect(initial.name).toBe('My App v1')
216+
217+
await next.patchFile('app/manifest.ts', (content) =>
218+
content.replace('My App v1', 'My App v2')
219+
)
220+
221+
await retry(async () => {
222+
const updated = await next
223+
.fetch('/manifest.webmanifest')
224+
.then((res) => res.json())
225+
expect(updated.name).toBe('My App v2')
226+
})
227+
})
228+
})
209229
})

0 commit comments

Comments
 (0)