Repository: vercel/next.js
Pull request: Turbopack: exclude metadata routes from server HMR

### What?

Metadata routes (`manifest.ts`, `robots.ts`, `sitemap.ts`, `icon.tsx`, `apple-icon.tsx`, etc.) were not being hot-reloaded in Turbopack dev mode — changes to those files would not be reflected on subsequent requests until a full server restart.

### Why?

PR #91466 extended `usesServerHmr = true` in `clearRequireCache()` (in `hot-reloader-turbopack.ts`) from `app-page` entries only to **all** `app`-type entries (pages + route handlers). The motivation was correct: regular route handlers like `app/api/hello/route.ts` use Turbopack's in-place module update model and benefit from server HMR.

However, metadata routes (`/manifest.webmanifest/route`, `/robots.txt/route`, etc.) are also `app`-type entries but they are **not** suitable for in-place server HMR. When `usesServerHmr = true` for a metadata route, `clearRequireCache()` skips two critical invalidation steps:

1. Deleting the compiled chunk from `require.cache`
2. Calling `__next__clear_chunk_cache__()`

Without those steps, the old module stays in-memory and all subsequent requests to `/manifest.webmanifest` (etc.) return the stale content.

### How?

Added an `!isMetadataRoute(entryPage)` guard to the `usesServerHmr` expression in `clearRequireCache()`. This restores full cache invalidation for metadata routes on every rebuild while leaving regular route handler server HMR (added in #91466) intact.

```ts
// Before
const usesServerHmr =
  serverFastRefresh &&
  entryType === 'app' &&
  writtenEndpoint.type !== 'edge'

// After
const usesServerHmr =
  serverFastRefresh &&
  entryType === 'app' &&
  writtenEndpoint.type !== 'edge' &&
  !isMetadataRoute(entryPage)   // ← metadata routes always clear the cache
```

`isMetadataRoute('/manifest.webmanifest/route')` → `true` (excluded from server HMR)  
`isMetadataRoute('/api/hello/route')` → `false` (keeps server HMR, no regression)

Also added a regression test: `metadata route hmr > reflects manifest.ts changes on fetch/refresh` in the `server-hmr` test suite, with a `manifest.ts` fixture that starts at `name: 'Version 0'`. The test patches the file and asserts the updated JSON is returned on the next fetch.

Fixes #91981

Changed files:
filename: packages/next/src/server/dev/hot-reloader-turbopack.ts
patch: @@ -82,7 +82,10 @@ import { isAppPageRouteDefinition } from '../route-definitions/app-page-route-de
 import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths'
 import type { ModernSourceMapPayload } from '../lib/source-maps'
 import { isDeferredEntry } from '../../build/entries'
-import { isMetadataRouteFile } from '../../lib/metadata/is-metadata-route'
+import {
+  isMetadataRoute,
+  isMetadataRouteFile,
+} from '../../lib/metadata/is-metadata-route'
 import { setBundlerFindSourceMapImplementation } from '../patch-error-inspect'
 import { getNextErrorFeedbackMiddleware } from '../../next-devtools/server/get-next-error-feedback-middleware'
 import {
@@ -602,16 +605,20 @@ export async function createHotReloaderTurbopack(
       join(distDir, p)
     )
 
-    const { type: entryType } = splitEntryKey(key)
+    const { type: entryType, page: entryPage } = splitEntryKey(key)
 
-    // Server HMR applies to all App Router entries built with the Turbopack
-    // Node.js runtime: both app pages and route handlers. Edge routes,
-    // Pages Router pages, and middleware/instrumentation do not use the
-    // Turbopack Node.js dev runtime and are excluded.
+    // Server HMR applies to App Router entries built with the Turbopack Node.js
+    // runtime: app pages and regular route handlers. Edge routes, Pages Router
+    // pages, middleware/instrumentation, and metadata routes (manifest.ts,
+    // robots.ts, sitemap.ts, icon.tsx, etc.) are excluded. Metadata routes are
+    // excluded because they serve HTTP responses directly and must re-execute
+    // on every request to pick up file changes; the in-place module update
+    // model of Server HMR does not apply to them.
     const usesServerHmr =
       serverFastRefresh &&
       entryType === 'app' &&
-      writtenEndpoint.type !== 'edge'
+      writtenEndpoint.type !== 'edge' &&
+      !isMetadataRoute(entryPage)
 
     const filesToDelete: string[] = []
     for (const file of serverPaths) {

filename: test/development/app-dir/server-hmr/app/manifest.ts
patch: @@ -0,0 +1,10 @@
+import type { MetadataRoute } from 'next'
+
+export default function manifest(): MetadataRoute.Manifest {
+  return {
+    name: 'Version 0',
+    short_name: 'v0',
+    start_url: '/',
+    display: 'standalone',
+  }
+}

filename: test/development/app-dir/server-hmr/server-hmr.test.ts
patch: @@ -155,6 +155,26 @@ describe('server-hmr', () => {
     )
   })
 
+  describe('metadata route hmr', () => {
+    it('reflects manifest.ts changes on fetch/refresh', async () => {
+      const initial = await next
+        .fetch('/manifest.webmanifest')
+        .then((res) => res.json())
+      expect(initial.name).toBe('Version 0')
+
+      await next.patchFile('app/manifest.ts', (content) =>
+        content.replace('Version 0', 'Version 1')
+      )
+
+      await retry(async () => {
+        const updated = await next
+          .fetch('/manifest.webmanifest')
+          .then((res) => res.json())
+        expect(updated.name).toBe('Version 1')
+      })
+    })
+  })
+
   describe('route handler hmr', () => {
     function getText(res: Response) {
       return res.ok