diff --git a/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts b/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts index b6dae3bbfabcf..be18e806548e9 100644 --- a/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-metadata-route-loader.ts @@ -41,7 +41,7 @@ const cacheHeader = { revalidate: 'public, max-age=0, must-revalidate', } -type MetadataRouteLoaderOptions = { +export type MetadataRouteLoaderOptions = { // Using separate argument to avoid json being parsed and hit error // x-ref: https://github.com/vercel/next.js/pull/62615 filePath: string diff --git a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts index 7268afdd1a3ec..005a609070bcc 100644 --- a/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts @@ -45,6 +45,7 @@ import { getModuleBuildInfo } from '../loaders/get-module-build-info' import { getAssumedSourceType } from '../loaders/next-flight-loader' import { isAppRouteRoute } from '../../../lib/is-app-route-route' import { isMetadataRoute } from '../../../lib/metadata/is-metadata-route' +import type { MetadataRouteLoaderOptions } from '../loaders/next-metadata-route-loader' interface Options { dev: boolean @@ -305,7 +306,12 @@ export class FlightClientEntryPlugin { ).request if (entryRequest.endsWith(WEBPACK_RESOURCE_QUERIES.metadataRoute)) { - entryRequest = getMetadataRouteResource(entryRequest) + const { filePath, isDynamicRouteExtension } = + getMetadataRouteResource(entryRequest) + + if (isDynamicRouteExtension === '1') { + entryRequest = filePath + } } const { clientComponentImports, actionImports, cssImports } = @@ -1110,14 +1116,15 @@ function getModuleResource(mod: webpack.NormalModule): string { } if (mod.resource === `?${WEBPACK_RESOURCE_QUERIES.metadataRoute}`) { - return getMetadataRouteResource(mod.rawRequest) + return getMetadataRouteResource(mod.rawRequest).filePath } return modResource } -function getMetadataRouteResource(request: string): string { - const query = request.split('next-metadata-route-loader?')[1] +function getMetadataRouteResource(request: string): MetadataRouteLoaderOptions { + // e.g. next-metadata-route-loader?filePath=&isDynamicRouteExtension=1!?__next_metadata_route__ + const query = request.split('!')[0].split('next-metadata-route-loader?')[1] - return parse(query).filePath as string + return parse(query) as MetadataRouteLoaderOptions } diff --git a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts index 38dba137b1dcc..e331683c92b8d 100644 --- a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts +++ b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts @@ -19,6 +19,7 @@ import picomatch from 'next/dist/compiled/picomatch' import { getModuleBuildInfo } from '../loaders/get-module-build-info' import { getPageFilePath } from '../../entries' import { resolveExternal } from '../../handle-externals' +import { isStaticMetadataRoute } from '../../../lib/metadata/is-metadata-route' const PLUGIN_NAME = 'TraceEntryPointsPlugin' export const TRACE_IGNORES = [ @@ -115,6 +116,7 @@ export interface BuildTraceContext { outputPath: string depModArray: string[] entryNameMap: Record + absolutePathByEntryName: Record } chunksTrace?: { action: TurbotraceAction @@ -241,19 +243,28 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { nodePath.join(outputPath, `${outputPrefix}${entrypoint.name}.js`) ) - if (entrypoint.name.startsWith('app/')) { - // include the client reference manifest - const clientManifestsForEntrypoint = nodePath.join( - outputPath, - outputPrefix, - entrypoint.name.replace(/%5F/g, '_') + - '_' + - CLIENT_REFERENCE_MANIFEST + - '.js' - ) + if (entrypoint.name.startsWith('app/') && this.appDir) { + const appDirRelativeEntryPath = + this.buildTraceContext.entriesTrace?.absolutePathByEntryName[ + entrypoint.name + ]?.replace(this.appDir, '') - if (clientManifestsForEntrypoint !== null) { - entryFiles.add(clientManifestsForEntrypoint) + // Include the client reference manifest in the trace, but not for + // static metadata routes, for which we don't generate those. + if ( + appDirRelativeEntryPath && + !isStaticMetadataRoute(appDirRelativeEntryPath) + ) { + entryFiles.add( + nodePath.join( + outputPath, + outputPrefix, + entrypoint.name.replace(/%5F/g, '_') + + '_' + + CLIENT_REFERENCE_MANIFEST + + '.js' + ) + ) } } @@ -315,6 +326,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { const entryNameMap = new Map() const entryModMap = new Map() const additionalEntries = new Map>() + const absolutePathByEntryName = new Map() const depModMap = new Map() @@ -356,6 +368,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { ) { entryModMap.set(absolutePath, entryMod) entryNameMap.set(absolutePath, name) + absolutePathByEntryName.set(name, absolutePath) } } @@ -441,6 +454,9 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { appDir: this.rootDir, depModArray: Array.from(depModMap.keys()), entryNameMap: Object.fromEntries(entryNameMap), + absolutePathByEntryName: Object.fromEntries( + absolutePathByEntryName + ), outputPath: compilation.outputOptions.path!, } diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/favicon.ico b/test/e2e/app-dir/use-cache-metadata-route-handler/app/favicon.ico new file mode 100644 index 0000000000000..4965832f2c9b0 Binary files /dev/null and b/test/e2e/app-dir/use-cache-metadata-route-handler/app/favicon.ico differ diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/use-cache-metadata-route-handler.test.ts b/test/e2e/app-dir/use-cache-metadata-route-handler/use-cache-metadata-route-handler.test.ts index 3c144e42f29e5..31de5a4c82c96 100644 --- a/test/e2e/app-dir/use-cache-metadata-route-handler/use-cache-metadata-route-handler.test.ts +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/use-cache-metadata-route-handler.test.ts @@ -1,7 +1,7 @@ import { nextTestSetup } from 'e2e-utils' describe('use-cache-metadata-route-handler', () => { - const { next, isNextDev, isNextStart } = nextTestSetup({ + const { next, isNextDev, isNextStart, isTurbopack } = nextTestSetup({ files: __dirname, }) @@ -131,4 +131,31 @@ describe('use-cache-metadata-route-handler', () => { expect(body).toEqual({ name: 'buildtime' }) } }) + + if (isNextStart && !isTurbopack) { + it('should include the client reference manifest in the route.js.nft.json files of dynamic metadata routes', async () => { + for (const filename of [ + 'icon', + 'manifest.webmanifest', + 'opengraph-image', + 'products/sitemap/[__metadata_id__]', + 'robots.txt', + 'sitemap.xml', + ]) { + const { files } = await next.readJSON( + `/.next/server/app/${filename}/route.js.nft.json` + ) + + expect(files).toInclude('route_client-reference-manifest.js') + } + }) + + it('should not include the client reference manifest in the route.js.nft.json files of static metadata routes', async () => { + const { files } = await next.readJSON( + '/.next/server/app/favicon.ico/route.js.nft.json' + ) + + expect(files).not.toInclude('route_client-reference-manifest.js') + }) + } })