From 52f25edd98080c5d87bf51e92ff3146a32d00bae Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 19 Dec 2024 18:54:21 +0100 Subject: [PATCH 1/8] Add a failing test for a metadata route handler with `"use cache"` --- .../app/layout.tsx | 8 +++++ .../app/opengraph-image.tsx | 34 +++++++++++++++++++ .../app/page.tsx | 3 ++ .../next.config.js | 10 ++++++ .../use-cache-metadata-route-handler.test.ts | 13 +++++++ 5 files changed, 68 insertions(+) create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/layout.tsx create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/opengraph-image.tsx create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/page.tsx create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/next.config.js create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/use-cache-metadata-route-handler.test.ts diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/layout.tsx b/test/e2e/app-dir/use-cache-metadata-route-handler/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/opengraph-image.tsx b/test/e2e/app-dir/use-cache-metadata-route-handler/app/opengraph-image.tsx new file mode 100644 index 0000000000000..5c368305c8dab --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/opengraph-image.tsx @@ -0,0 +1,34 @@ +import { ImageResponse } from 'next/og' + +export const alt = 'About Acme' +export const size = { width: 1200, height: 630 } +export const contentType = 'image/png' + +async function fetchPostData() { + 'use cache' + + return { title: 'Test' } +} + +export default async function Image() { + const post = await fetchPostData() + + return new ImageResponse( + ( +
+ {post.title} +
+ ), + size + ) +} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/page.tsx b/test/e2e/app-dir/use-cache-metadata-route-handler/app/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/next.config.js b/test/e2e/app-dir/use-cache-metadata-route-handler/next.config.js new file mode 100644 index 0000000000000..ac4afcf432196 --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + dynamicIO: true, + }, +} + +module.exports = nextConfig 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 new file mode 100644 index 0000000000000..a6629c403f48b --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/use-cache-metadata-route-handler.test.ts @@ -0,0 +1,13 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('use-cache-metadata-route-handler', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should generate an opengraph image with a metadata route handler that uses "use cache"', async () => { + const res = await next.fetch('/opengraph-image') + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('image/png') + }) +}) From 04297fa99a86b6297cc2d97b14a67f2db07bca8c Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Thu, 19 Dec 2024 18:59:16 +0100 Subject: [PATCH 2/8] Revert "Do not create client reference manifest for metadata route handlers (#71225)" This reverts commit 39dfa3affd5369ea138b46f7a677b917188f5967. --- .../webpack/plugins/flight-manifest-plugin.ts | 7 +++--- .../plugins/next-trace-entrypoints-plugin.ts | 22 ++++++++----------- packages/next/src/build/webpack/utils.ts | 7 +----- packages/next/src/server/load-components.ts | 6 +---- .../app-dir/hello-world/hello-world.test.ts | 4 ++-- 5 files changed, 16 insertions(+), 30 deletions(-) diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts index c6e62cf27d4dd..be012833e6b7b 100644 --- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts @@ -26,7 +26,6 @@ import { } from '../utils' import type { ChunkGroup } from 'webpack' import { encodeURIPath } from '../../../shared/lib/encode-uri-path' -import { isMetadataRoute } from '../../../lib/metadata/is-metadata-route' import type { ModuleInfo } from './flight-client-entry-plugin' interface Options { @@ -559,9 +558,9 @@ export class ClientReferenceManifestPlugin { manifestEntryFiles.push(entryName.replace(/\/page(\.[^/]+)?$/, '/page')) } - // We also need to create manifests for route handler entrypoints - // (excluding metadata route handlers) to enable `'use cache'`. - if (/\/route$/.test(entryName) && !isMetadataRoute(entryName)) { + // We also need to create manifests for route handler entrypoints to + // enable `'use cache'`. + if (/\/route$/.test(entryName)) { manifestEntryFiles.push(entryName) } 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 8d2056380ce62..38dba137b1dcc 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,7 +19,6 @@ 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 { isMetadataRoute } from '../../../lib/metadata/is-metadata-route' const PLUGIN_NAME = 'TraceEntryPointsPlugin' export const TRACE_IGNORES = [ @@ -243,18 +242,15 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { ) if (entrypoint.name.startsWith('app/')) { - // Include the client reference manifest for pages and route handlers, - // excluding metadata route handlers. - const clientManifestsForEntrypoint = isMetadataRoute(entrypoint.name) - ? null - : nodePath.join( - outputPath, - outputPrefix, - entrypoint.name.replace(/%5F/g, '_') + - '_' + - CLIENT_REFERENCE_MANIFEST + - '.js' - ) + // include the client reference manifest + const clientManifestsForEntrypoint = nodePath.join( + outputPath, + outputPrefix, + entrypoint.name.replace(/%5F/g, '_') + + '_' + + CLIENT_REFERENCE_MANIFEST + + '.js' + ) if (clientManifestsForEntrypoint !== null) { entryFiles.add(clientManifestsForEntrypoint) diff --git a/packages/next/src/build/webpack/utils.ts b/packages/next/src/build/webpack/utils.ts index 981559871021a..0ea11a65bb52d 100644 --- a/packages/next/src/build/webpack/utils.ts +++ b/packages/next/src/build/webpack/utils.ts @@ -7,7 +7,6 @@ import type { ModuleGraph, } from 'webpack' import type { ModuleGraphConnection } from 'webpack' -import { isMetadataRoute } from '../../lib/metadata/is-metadata-route' export function traverseModules( compilation: Compilation, @@ -48,11 +47,7 @@ export function forEachEntryModule( ) { for (const [name, entry] of compilation.entries.entries()) { // Skip for entries under pages/ - if ( - name.startsWith('pages/') || - // Skip for metadata route handlers - (name.startsWith('app/') && isMetadataRoute(name)) - ) { + if (name.startsWith('pages/')) { continue } diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index 499f6afcaf9d7..40c6f6d670708 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -32,7 +32,6 @@ import { wait } from '../lib/wait' import { setReferenceManifestsSingleton } from './app-render/encryption-utils' import { createServerModuleMap } from './app-render/action-utils' import type { DeepReadonly } from '../shared/lib/deep-readonly' -import { isMetadataRoute } from '../lib/metadata/is-metadata-route' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' export type ManifestItem = { @@ -169,9 +168,6 @@ async function loadComponentsImpl({ ]) } - // Make sure to avoid loading the manifest for metadata route handlers. - const hasClientManifest = isAppPath && !isMetadataRoute(page) - // In dev mode we retry loading a manifest file to handle a race condition // that can occur while app and pages are compiling at the same time, and the // build-manifest is still being written to disk while an app path is @@ -227,7 +223,7 @@ async function loadComponentsImpl({ join(distDir, `${DYNAMIC_CSS_MANIFEST}.json`), manifestLoadAttempts ).catch(() => undefined), - hasClientManifest + isAppPath ? tryLoadClientReferenceManifest( join( distDir, diff --git a/test/e2e/app-dir/hello-world/hello-world.test.ts b/test/e2e/app-dir/hello-world/hello-world.test.ts index f7a390baacb65..9f9aeae6fcc10 100644 --- a/test/e2e/app-dir/hello-world/hello-world.test.ts +++ b/test/e2e/app-dir/hello-world/hello-world.test.ts @@ -11,7 +11,7 @@ describe('hello-world', () => { expect($('p').text()).toBe('hello world') }) - // Recommended for tests that need a full browser. + // Recommended for tests that need a full browser it('should work using browser', async () => { const browser = await next.browser('/') expect(await browser.elementByCss('p').text()).toBe('hello world') @@ -23,7 +23,7 @@ describe('hello-world', () => { expect(html).toContain('hello world') }) - // In case you need to test the response object. + // In case you need to test the response object it('should work with fetch', async () => { const res = await next.fetch('/') const html = await res.text() From 9993433b2107568b1ff23d4c2fa2692c2ff7a018 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Tue, 7 Jan 2025 15:26:21 +0100 Subject: [PATCH 3/8] Generate manifests for metadata routes (Webpack) --- packages/next/src/build/webpack-config.ts | 6 ++++ .../plugins/flight-client-entry-plugin.ts | 35 +++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 884be1b1f6c6e..62472d162d519 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1309,6 +1309,12 @@ export default async function getBaseWebpackConfig( }, ], }, + resourceQuery: { + not: [ + new RegExp(WEBPACK_RESOURCE_QUERIES.metadata), + new RegExp(WEBPACK_RESOURCE_QUERIES.metadataImageMeta), + ], + }, resolve: { mainFields: getMainField(compilerType, true), conditionNames: reactServerCondition, 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 d917c1c344105..cad2f7aa7f831 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 @@ -4,7 +4,7 @@ import type { } from '../loaders/next-flight-client-entry-loader' import { webpack } from 'next/dist/compiled/webpack/webpack' -import { stringify } from 'querystring' +import { parse, stringify } from 'querystring' import path from 'path' import { sources } from 'next/dist/compiled/webpack/webpack' import { @@ -13,7 +13,10 @@ import { EntryTypes, getEntryKey, } from '../../../server/dev/on-demand-entry-handler' -import { WEBPACK_LAYERS } from '../../../lib/constants' +import { + WEBPACK_LAYERS, + WEBPACK_RESOURCE_QUERIES, +} from '../../../lib/constants' import { APP_CLIENT_INTERNALS, BARREL_OPTIMIZATION_PREFIX, @@ -41,6 +44,7 @@ import { PAGE_TYPES } from '../../../lib/page-types' import { getModuleBuildInfo } from '../loaders/get-module-build-info' import { getAssumedSourceType } from '../loaders/next-flight-loader' import { isAppRouteRoute } from '../../../lib/is-app-route-route' +import { normalizeMetadataRoute } from '../../../lib/metadata/get-metadata-route' interface Options { dev: boolean @@ -296,10 +300,14 @@ export class FlightClientEntryPlugin { compilation.moduleGraph )) { // Entry can be any user defined entry files such as layout, page, error, loading, etc. - const entryRequest = ( + let entryRequest = ( connection.dependency as unknown as webpack.NormalModule ).request + if (entryRequest.endsWith(WEBPACK_RESOURCE_QUERIES.metadataRoute)) { + entryRequest = getMetadataRouteResource(entryRequest) + } + const { clientComponentImports, actionImports, cssImports } = this.collectComponentInfoFromServerEntryDependency({ entryRequest, @@ -331,9 +339,13 @@ export class FlightClientEntryPlugin { ? path.relative(compilation.options.context!, entryRequest) : entryRequest - // Replace file suffix as `.js` will be added. - const bundlePath = normalizePathSep( - relativeRequest.replace(/\.[^.\\/]+$/, '').replace(/^src[\\/]/, '') + const bundlePath = normalizeMetadataRoute( + normalizePathSep( + relativeRequest + // Replace file suffix as `.js` will be added. + .replace(/\.[^.\\/]+$/, '') + .replace(/^src[\\/]/, '') + ) ) Object.assign(mergedCSSimports, cssImports) @@ -1094,5 +1106,16 @@ function getModuleResource(mod: webpack.NormalModule): string { if (mod.matchResource?.startsWith(BARREL_OPTIMIZATION_PREFIX)) { modResource = mod.matchResource + ':' + modResource } + + if (mod.resource === `?${WEBPACK_RESOURCE_QUERIES.metadataRoute}`) { + return getMetadataRouteResource(mod.rawRequest) + } + return modResource } + +function getMetadataRouteResource(request: string): string { + const query = request.split('next-metadata-route-loader?')[1] + + return parse(query).filePath as string +} From 3781ceb0cb9b13845be13df97b5fb5de73990d4c Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Tue, 7 Jan 2025 15:39:56 +0100 Subject: [PATCH 4/8] Generate manifests for metadata routes (Turbopack) --- crates/next-api/src/app.rs | 591 +++++++++++++++++-------------------- 1 file changed, 271 insertions(+), 320 deletions(-) diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index 92ca5b69cd80d..72dc705e52256 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -67,9 +67,7 @@ use turbopack_core::{ use turbopack_ecmascript::resolve::cjs_resolve; use crate::{ - dynamic_imports::{ - collect_next_dynamic_chunks, DynamicImportedChunks, NextDynamicChunkAvailability, - }, + dynamic_imports::{collect_next_dynamic_chunks, NextDynamicChunkAvailability}, font::create_font_manifest, loadable_manifest::create_react_loadable_manifest, module_graph::get_reduced_graphs_for_endpoint, @@ -917,17 +915,15 @@ impl AppEndpoint { let app_entry = self.app_endpoint_entry().await?; - let (process_client_components, process_client_assets, process_ssr, emit_manifests) = - match this.ty { - AppEndpointType::Page { ty, .. } => ( - true, - true, - matches!(ty, AppPageEndpointType::Html), - matches!(ty, AppPageEndpointType::Html), - ), - AppEndpointType::Route { .. } => (true, false, false, true), - AppEndpointType::Metadata { .. } => (false, false, false, true), - }; + let (process_client_assets, process_ssr, emit_manifests) = match this.ty { + AppEndpointType::Page { ty, .. } => ( + true, + matches!(ty, AppPageEndpointType::Html), + matches!(ty, AppPageEndpointType::Html), + ), + AppEndpointType::Route { .. } => (false, false, true), + AppEndpointType::Metadata { .. } => (false, false, true), + }; let node_root = this.app_project.project().node_root(); @@ -960,223 +956,197 @@ impl AppEndpoint { None }; - let (next_dynamic_imports, client_references, client_references_chunks) = - if process_client_components { - let client_shared_chunk_group = get_app_client_shared_chunk_group( - AssetIdent::from_path(this.app_project.project().project_path()) - .with_modifier(client_shared_chunks_modifier()), - this.app_project.client_runtime_entries(), - client_chunking_context, - ) - .await?; - - let mut client_shared_chunks = vec![]; - for chunk in client_shared_chunk_group.assets.await?.iter().copied() { - client_assets.insert(chunk); + let client_shared_chunk_group = get_app_client_shared_chunk_group( + AssetIdent::from_path(this.app_project.project().project_path()) + .with_modifier(client_shared_chunks_modifier()), + this.app_project.client_runtime_entries(), + client_chunking_context, + ) + .await?; - let chunk_path = chunk.ident().path().await?; - if chunk_path.extension_ref() == Some("js") { - client_shared_chunks.push(chunk); - } - } - let client_shared_availability_info = client_shared_chunk_group.availability_info; + let mut client_shared_chunks = vec![]; + for chunk in client_shared_chunk_group.assets.await?.iter().copied() { + client_assets.insert(chunk); - let reduced_graphs = - get_reduced_graphs_for_endpoint(this.app_project.project(), *rsc_entry); - let next_dynamic_imports = reduced_graphs - .get_next_dynamic_imports_for_endpoint(*rsc_entry) - .await?; + let chunk_path = chunk.ident().path().await?; + if chunk_path.extension_ref() == Some("js") { + client_shared_chunks.push(chunk); + } + } + let client_shared_availability_info = client_shared_chunk_group.availability_info; - let client_references_cell = - reduced_graphs.get_client_references_for_endpoint(*rsc_entry); - - let client_references_chunks = get_app_client_references_chunks( - client_references_cell, - client_chunking_context, - Value::new(client_shared_availability_info), - ssr_chunking_context, - ); - let client_references_chunks_ref = client_references_chunks.await?; - - let mut entry_client_chunks = FxIndexSet::default(); - // TODO(alexkirsz) In which manifest does this go? - let mut entry_ssr_chunks = FxIndexSet::default(); - for chunks in client_references_chunks_ref - .layout_segment_client_chunks - .values() - { - entry_client_chunks.extend(chunks.await?.iter().copied()); - } - for (chunks, _) in client_references_chunks_ref - .client_component_client_chunks - .values() - { - client_assets.extend(chunks.await?.iter().copied()); - } - for (chunks, _) in client_references_chunks_ref - .client_component_ssr_chunks - .values() - { - entry_ssr_chunks.extend(chunks.await?.iter().copied()); - } + let reduced_graphs = + get_reduced_graphs_for_endpoint(this.app_project.project(), *rsc_entry); + let next_dynamic_imports = reduced_graphs + .get_next_dynamic_imports_for_endpoint(*rsc_entry) + .await?; - client_assets.extend(entry_client_chunks.iter().copied()); - server_assets.extend(entry_ssr_chunks.iter().copied()); + let client_references = reduced_graphs.get_client_references_for_endpoint(*rsc_entry); + + let client_references_chunks = get_app_client_references_chunks( + client_references, + client_chunking_context, + Value::new(client_shared_availability_info), + ssr_chunking_context, + ); + let client_references_chunks_ref = client_references_chunks.await?; + + let mut entry_client_chunks = FxIndexSet::default(); + // TODO(alexkirsz) In which manifest does this go? + let mut entry_ssr_chunks = FxIndexSet::default(); + for chunks in client_references_chunks_ref + .layout_segment_client_chunks + .values() + { + entry_client_chunks.extend(chunks.await?.iter().copied()); + } + for (chunks, _) in client_references_chunks_ref + .client_component_client_chunks + .values() + { + client_assets.extend(chunks.await?.iter().copied()); + } + for (chunks, _) in client_references_chunks_ref + .client_component_ssr_chunks + .values() + { + entry_ssr_chunks.extend(chunks.await?.iter().copied()); + } - let manifest_path_prefix = &app_entry.original_name; + client_assets.extend(entry_client_chunks.iter().copied()); + server_assets.extend(entry_ssr_chunks.iter().copied()); - if emit_manifests { - let app_build_manifest = AppBuildManifest { - pages: fxindexmap!( - app_entry.original_name.clone() => Vc::cell(entry_client_chunks - .iter() - .chain(client_shared_chunks.iter()) - .copied() - .collect()) - ), - }; - let app_build_manifest_output = - app_build_manifest - .build_output( - node_root.join( - format!( - "server/app{manifest_path_prefix}/app-build-manifest.json", - ) - .into(), - ), - client_relative_path, - ) - .await? - .to_resolved() - .await?; + let manifest_path_prefix = &app_entry.original_name; - server_assets.insert(app_build_manifest_output); - } + if emit_manifests { + let app_build_manifest = AppBuildManifest { + pages: fxindexmap!( + app_entry.original_name.clone() => Vc::cell(entry_client_chunks + .iter() + .chain(client_shared_chunks.iter()) + .copied() + .collect()) + ), + }; + let app_build_manifest_output = app_build_manifest + .build_output( + node_root.join( + format!("server/app{manifest_path_prefix}/app-build-manifest.json",).into(), + ), + client_relative_path, + ) + .await? + .to_resolved() + .await?; - // polyfill-nomodule.js is a pre-compiled asset distributed as part of next, - // load it as a RawModule. - let next_package = get_next_package(this.app_project.project().project_path()); - let polyfill_source = FileSource::new( - next_package.join("dist/build/polyfills/polyfill-nomodule.js".into()), - ); - let polyfill_output_path = - client_chunking_context.chunk_path(polyfill_source.ident(), ".js".into()); - let polyfill_output_asset = ResolvedVc::upcast( - RawOutput::new(polyfill_output_path, Vc::upcast(polyfill_source)) - .to_resolved() - .await?, - ); - client_assets.insert(polyfill_output_asset); + server_assets.insert(app_build_manifest_output); + } - if emit_manifests { - if *this - .app_project - .project() - .should_create_webpack_stats() - .await? - { - let webpack_stats = - generate_webpack_stats(app_entry.original_name.clone(), &client_assets) - .await?; - let stats_output = VirtualOutputAsset::new( - node_root.join( - format!("server/app{manifest_path_prefix}/webpack-stats.json",) - .into(), - ), - AssetContent::file( - File::from(serde_json::to_string_pretty(&webpack_stats)?).into(), - ), - ) - .to_resolved() - .await?; - server_assets.insert(ResolvedVc::upcast(stats_output)); - } + // polyfill-nomodule.js is a pre-compiled asset distributed as part of next, + // load it as a RawModule. + let next_package = get_next_package(this.app_project.project().project_path()); + let polyfill_source = + FileSource::new(next_package.join("dist/build/polyfills/polyfill-nomodule.js".into())); + let polyfill_output_path = + client_chunking_context.chunk_path(polyfill_source.ident(), ".js".into()); + let polyfill_output_asset = ResolvedVc::upcast( + RawOutput::new(polyfill_output_path, Vc::upcast(polyfill_source)) + .to_resolved() + .await?, + ); + client_assets.insert(polyfill_output_asset); - let build_manifest = BuildManifest { - root_main_files: client_shared_chunks, - polyfill_files: vec![polyfill_output_asset], - ..Default::default() - }; - let build_manifest_output = - ResolvedVc::upcast( - build_manifest - .build_output( - node_root.join( - format!( - "server/app{manifest_path_prefix}/build-manifest.json", - ) - .into(), - ), - client_relative_path, - ) - .await? - .to_resolved() - .await?, - ); - server_assets.insert(build_manifest_output); - } + if emit_manifests { + if *this + .app_project + .project() + .should_create_webpack_stats() + .await? + { + let webpack_stats = + generate_webpack_stats(app_entry.original_name.clone(), &client_assets).await?; + let stats_output = VirtualOutputAsset::new( + node_root.join( + format!("server/app{manifest_path_prefix}/webpack-stats.json",).into(), + ), + AssetContent::file( + File::from(serde_json::to_string_pretty(&webpack_stats)?).into(), + ), + ) + .to_resolved() + .await?; + server_assets.insert(ResolvedVc::upcast(stats_output)); + } - if runtime == NextRuntime::Edge { - // as the edge runtime doesn't support chunk loading we need to add all client - // references to the middleware manifest so they get loaded during runtime - // initialization - let client_references_chunks = &*client_references_chunks.await?; + let build_manifest = BuildManifest { + root_main_files: client_shared_chunks, + polyfill_files: vec![polyfill_output_asset], + ..Default::default() + }; + let build_manifest_output = ResolvedVc::upcast( + build_manifest + .build_output( + node_root.join( + format!("server/app{manifest_path_prefix}/build-manifest.json",).into(), + ), + client_relative_path, + ) + .await? + .to_resolved() + .await?, + ); + server_assets.insert(build_manifest_output); + } - for (ssr_chunks, _) in client_references_chunks - .client_component_ssr_chunks - .values() - { - let ssr_chunks = ssr_chunks.await?; + if runtime == NextRuntime::Edge { + // as the edge runtime doesn't support chunk loading we need to add all client + // references to the middleware manifest so they get loaded during runtime + // initialization + let client_references_chunks = &*client_references_chunks.await?; - middleware_assets.extend(ssr_chunks); - } - } + for (ssr_chunks, _) in client_references_chunks + .client_component_ssr_chunks + .values() + { + let ssr_chunks = ssr_chunks.await?; - ( - Some(next_dynamic_imports), - Some(client_references_cell), - Some(client_references_chunks), - ) - } else { - (None, None, None) - }; + middleware_assets.extend(ssr_chunks); + } + } - let server_action_manifest_loader = if process_client_components { - let reduced_graphs = - get_reduced_graphs_for_endpoint(this.app_project.project(), *rsc_entry); - let actions = reduced_graphs.get_server_actions_for_endpoint( - *rsc_entry, - match runtime { - NextRuntime::Edge => Vc::upcast(this.app_project.edge_rsc_module_context()), - NextRuntime::NodeJs => Vc::upcast(this.app_project.rsc_module_context()), - }, - ); + let reduced_graphs = + get_reduced_graphs_for_endpoint(this.app_project.project(), *rsc_entry); + let actions = reduced_graphs.get_server_actions_for_endpoint( + *rsc_entry, + match runtime { + NextRuntime::Edge => Vc::upcast(this.app_project.edge_rsc_module_context()), + NextRuntime::NodeJs => Vc::upcast(this.app_project.rsc_module_context()), + }, + ); + + let server_action_manifest = create_server_actions_manifest( + actions, + this.app_project.project().project_path(), + node_root, + app_entry.original_name.clone(), + runtime, + match runtime { + NextRuntime::Edge => Vc::upcast(this.app_project.edge_rsc_module_context()), + NextRuntime::NodeJs => Vc::upcast(this.app_project.rsc_module_context()), + }, + this.app_project + .project() + .runtime_chunking_context(process_client_assets, runtime), + ) + .await?; + server_assets.insert(server_action_manifest.manifest); - let server_action_manifest = create_server_actions_manifest( - actions, - this.app_project.project().project_path(), - node_root, - app_entry.original_name.clone(), - runtime, - match runtime { - NextRuntime::Edge => Vc::upcast(this.app_project.edge_rsc_module_context()), - NextRuntime::NodeJs => Vc::upcast(this.app_project.rsc_module_context()), - }, - this.app_project - .project() - .runtime_chunking_context(process_client_assets, runtime), - ) - .await?; - server_assets.insert(server_action_manifest.manifest); - Some(server_action_manifest.loader) - } else { - None - }; + let server_action_manifest_loader = server_action_manifest.loader; let (app_entry_chunks, app_entry_chunks_availability) = &*self .app_entry_chunks( client_references, - server_action_manifest_loader.map(|v| *v), + *server_action_manifest_loader, server_path, process_client_assets, ) @@ -1192,31 +1162,27 @@ impl AppEndpoint { let mut client_reference_manifest = None; if emit_manifests { - if let (Some(client_references), Some(client_references_chunks)) = - (client_references, client_references_chunks) - { - let entry_manifest = ClientReferenceManifest::build_output( - node_root, - client_relative_path, - app_entry.original_name.clone(), - client_references, - client_references_chunks, - **app_entry_chunks, - Value::new(*app_entry_chunks_availability), - client_chunking_context, - ssr_chunking_context, - this.app_project.project().next_config(), - runtime, - this.app_project.project().next_mode(), - ) - .to_resolved() - .await?; - server_assets.insert(entry_manifest); - if runtime == NextRuntime::Edge { - middleware_assets.push(entry_manifest); - } - client_reference_manifest = Some(entry_manifest) + let entry_manifest = ClientReferenceManifest::build_output( + node_root, + client_relative_path, + app_entry.original_name.clone(), + client_references, + client_references_chunks, + **app_entry_chunks, + Value::new(*app_entry_chunks_availability), + client_chunking_context, + ssr_chunking_context, + this.app_project.project().next_config(), + runtime, + this.app_project.project().next_mode(), + ) + .to_resolved() + .await?; + server_assets.insert(entry_manifest); + if runtime == NextRuntime::Edge { + middleware_assets.push(entry_manifest); } + client_reference_manifest = Some(entry_manifest); let next_font_manifest_output = create_font_manifest( this.app_project.project().client_root(), @@ -1266,21 +1232,15 @@ impl AppEndpoint { let entry_file = "app-edge-has-no-entrypoint".into(); if emit_manifests { - let dynamic_import_entries = - if let (Some(next_dynamic_imports), Some(client_references_chunks)) = - (next_dynamic_imports, client_references_chunks) - { - collect_next_dynamic_chunks( - Vc::upcast(client_chunking_context), - next_dynamic_imports, - NextDynamicChunkAvailability::ClientReferences( - &*(client_references_chunks.await?), - ), - ) - .await? - } else { - DynamicImportedChunks::default().resolved_cell() - }; + let dynamic_import_entries = collect_next_dynamic_chunks( + Vc::upcast(client_chunking_context), + next_dynamic_imports, + NextDynamicChunkAvailability::ClientReferences( + &*(client_references_chunks.await?), + ), + ) + .await?; + let loadable_manifest_output = create_react_loadable_manifest( *dynamic_import_entries, client_relative_path, @@ -1294,6 +1254,7 @@ impl AppEndpoint { NextRuntime::Edge, ) .await?; + server_assets.extend(loadable_manifest_output.iter().copied()); file_paths_from_root.extend( get_js_paths_from_root(&node_root_value, &loadable_manifest_output).await?, @@ -1385,21 +1346,15 @@ impl AppEndpoint { server_assets.insert(app_paths_manifest_output); // create react-loadable-manifest for next/dynamic - let dynamic_import_entries = - if let (Some(next_dynamic_imports), Some(client_references_chunks)) = - (next_dynamic_imports, client_references_chunks) - { - collect_next_dynamic_chunks( - Vc::upcast(client_chunking_context), - next_dynamic_imports, - NextDynamicChunkAvailability::ClientReferences( - &*(client_references_chunks.await?), - ), - ) - .await? - } else { - DynamicImportedChunks::default().resolved_cell() - }; + let dynamic_import_entries = collect_next_dynamic_chunks( + Vc::upcast(client_chunking_context), + next_dynamic_imports, + NextDynamicChunkAvailability::ClientReferences( + &*(client_references_chunks.await?), + ), + ) + .await?; + let loadable_manifest_output = create_react_loadable_manifest( *dynamic_import_entries, client_relative_path, @@ -1413,6 +1368,7 @@ impl AppEndpoint { NextRuntime::NodeJs, ) .await?; + server_assets.extend(loadable_manifest_output.iter().copied()); Some(loadable_manifest_output) } else { @@ -1459,8 +1415,8 @@ impl AppEndpoint { #[turbo_tasks::function] async fn app_entry_chunks( self: Vc, - client_references: Option>, - server_action_manifest_loader: Option>>, + client_references: Vc, + server_action_manifest_loader: ResolvedVc>, server_path: Vc, process_client_assets: bool, ) -> Result> { @@ -1484,10 +1440,7 @@ impl AppEndpoint { .await? .context("Entry module must be evaluatable")?; evaluatable_assets.push(evaluatable); - - if let Some(server_action_manifest_loader) = server_action_manifest_loader { - evaluatable_assets.push(server_action_manifest_loader.to_resolved().await?); - } + evaluatable_assets.push(server_action_manifest_loader); { let _span = tracing::info_span!("Server Components"); @@ -1508,9 +1461,7 @@ impl AppEndpoint { let mut evaluatable_assets = this.app_project.rsc_runtime_entries().await?.clone_value(); - if let Some(server_action_manifest_loader) = server_action_manifest_loader { - evaluatable_assets.push(server_action_manifest_loader.to_resolved().await?); - } + evaluatable_assets.push(server_action_manifest_loader); let EntryChunkGroupResult { asset: rsc_chunk, @@ -1518,20 +1469,54 @@ impl AppEndpoint { } = *(async { let mut current_chunks = OutputAssets::empty(); let mut current_availability_info = AvailabilityInfo::Root; - if let Some(client_references) = client_references { - let client_references = client_references.await?; - let span = tracing::trace_span!("server utils"); - async { - let utils_module = IncludeModulesModule::new( - AssetIdent::from_path(this.app_project.project().project_path()) - .with_modifier(server_utils_modifier()), - client_references.server_utils.iter().map(|v| **v).collect(), - ); + let client_references = client_references.await?; + let span = tracing::trace_span!("server utils"); + async { + let utils_module = IncludeModulesModule::new( + AssetIdent::from_path(this.app_project.project().project_path()) + .with_modifier(server_utils_modifier()), + client_references.server_utils.iter().map(|v| **v).collect(), + ); + + let chunk_group = chunking_context + .chunk_group( + utils_module.ident(), + Vc::upcast(utils_module), + Value::new(current_availability_info), + ) + .await?; + + current_chunks = current_chunks + .concatenate(*chunk_group.assets) + .resolve() + .await?; + current_availability_info = chunk_group.availability_info; + + anyhow::Ok(()) + } + .instrument(span) + .await?; + for server_component in client_references + .server_component_entries + .iter() + .copied() + .take( + client_references + .server_component_entries + .len() + .saturating_sub(1), + ) + { + let span = tracing::trace_span!( + "layout segment", + name = server_component.ident().to_string().await?.as_str() + ); + async { let chunk_group = chunking_context .chunk_group( - utils_module.ident(), - Vc::upcast(utils_module), + server_component.ident(), + *ResolvedVc::upcast(server_component), Value::new(current_availability_info), ) .await?; @@ -1546,42 +1531,8 @@ impl AppEndpoint { } .instrument(span) .await?; - for server_component in client_references - .server_component_entries - .iter() - .copied() - .take( - client_references - .server_component_entries - .len() - .saturating_sub(1), - ) - { - let span = tracing::trace_span!( - "layout segment", - name = server_component.ident().to_string().await?.as_str() - ); - async { - let chunk_group = chunking_context - .chunk_group( - server_component.ident(), - *ResolvedVc::upcast(server_component), - Value::new(current_availability_info), - ) - .await?; - - current_chunks = current_chunks - .concatenate(*chunk_group.assets) - .resolve() - .await?; - current_availability_info = chunk_group.availability_info; - - anyhow::Ok(()) - } - .instrument(span) - .await?; - } } + chunking_context .entry_chunk_group( server_path.join( From fc20b378cce8f79de6cee399286cceecd9089893 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 13 Jan 2025 21:17:21 +0100 Subject: [PATCH 5/8] Fix `bundlePath` for other metadata routes, and add more tests --- .../plugins/flight-client-entry-plugin.ts | 18 +-- .../app/icon.tsx | 38 ++++++ .../app/layout.tsx | 8 -- .../app/manifest.ts | 12 ++ .../app/opengraph-image.tsx | 8 +- .../app/page.tsx | 3 - .../app/products/sitemap.ts | 20 +++ .../app/robots.ts | 14 ++ .../app/sentinel.ts | 7 + .../app/sitemap.ts | 12 ++ .../use-cache-metadata-route-handler.test.ts | 123 +++++++++++++++++- 11 files changed, 241 insertions(+), 22 deletions(-) create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/icon.tsx delete mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/layout.tsx create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/manifest.ts delete mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/page.tsx create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/products/sitemap.ts create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/robots.ts create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/sentinel.ts create mode 100644 test/e2e/app-dir/use-cache-metadata-route-handler/app/sitemap.ts 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 cad2f7aa7f831..7268afdd1a3ec 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 @@ -44,7 +44,7 @@ import { PAGE_TYPES } from '../../../lib/page-types' import { getModuleBuildInfo } from '../loaders/get-module-build-info' import { getAssumedSourceType } from '../loaders/next-flight-loader' import { isAppRouteRoute } from '../../../lib/is-app-route-route' -import { normalizeMetadataRoute } from '../../../lib/metadata/get-metadata-route' +import { isMetadataRoute } from '../../../lib/metadata/is-metadata-route' interface Options { dev: boolean @@ -339,15 +339,17 @@ export class FlightClientEntryPlugin { ? path.relative(compilation.options.context!, entryRequest) : entryRequest - const bundlePath = normalizeMetadataRoute( - normalizePathSep( - relativeRequest - // Replace file suffix as `.js` will be added. - .replace(/\.[^.\\/]+$/, '') - .replace(/^src[\\/]/, '') - ) + // Replace file suffix as `.js` will be added. + let bundlePath = normalizePathSep( + relativeRequest.replace(/\.[^.\\/]+$/, '').replace(/^src[\\/]/, '') ) + // For metadata routes, the entry name can be used as the bundle path, + // as it has been normalized already. + if (isMetadataRoute(bundlePath)) { + bundlePath = name + } + Object.assign(mergedCSSimports, cssImports) clientEntriesToInject.push({ compiler, diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/icon.tsx b/test/e2e/app-dir/use-cache-metadata-route-handler/app/icon.tsx new file mode 100644 index 0000000000000..7a1a1cca1656f --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/icon.tsx @@ -0,0 +1,38 @@ +import { ImageResponse } from 'next/og' +import { setTimeout } from 'timers/promises' + +export const size = { width: 32, height: 32 } +export const contentType = 'image/png' + +async function fetchIconLetter() { + 'use cache' + + // Simulate I/O + await setTimeout(100) + + return 'N' +} + +export default async function Icon() { + const letter = await fetchIconLetter() + + return new ImageResponse( + ( +
+ {letter} +
+ ), + { ...size } + ) +} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/layout.tsx b/test/e2e/app-dir/use-cache-metadata-route-handler/app/layout.tsx deleted file mode 100644 index 888614deda3ba..0000000000000 --- a/test/e2e/app-dir/use-cache-metadata-route-handler/app/layout.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { ReactNode } from 'react' -export default function Root({ children }: { children: ReactNode }) { - return ( - - {children} - - ) -} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/manifest.ts b/test/e2e/app-dir/use-cache-metadata-route-handler/app/manifest.ts new file mode 100644 index 0000000000000..138a6ba1b483f --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/manifest.ts @@ -0,0 +1,12 @@ +import type { MetadataRoute } from 'next' +import { getSentinelValue } from './sentinel' +import { setTimeout } from 'timers/promises' + +export default async function manifest(): Promise { + 'use cache' + + // Simulate I/O + await setTimeout(100) + + return { name: getSentinelValue() } +} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/opengraph-image.tsx b/test/e2e/app-dir/use-cache-metadata-route-handler/app/opengraph-image.tsx index 5c368305c8dab..1913162fbc4ff 100644 --- a/test/e2e/app-dir/use-cache-metadata-route-handler/app/opengraph-image.tsx +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/opengraph-image.tsx @@ -7,7 +7,7 @@ export const contentType = 'image/png' async function fetchPostData() { 'use cache' - return { title: 'Test' } + return { title: 'Test', created: Date.now() } } export default async function Image() { @@ -24,9 +24,13 @@ export default async function Image() { display: 'flex', alignItems: 'center', justifyContent: 'center', + flexDirection: 'column', }} > - {post.title} +

{post.title}

+

+ {new Date(post.created).toLocaleTimeString()} +

), size diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/page.tsx b/test/e2e/app-dir/use-cache-metadata-route-handler/app/page.tsx deleted file mode 100644 index ff7159d9149fe..0000000000000 --- a/test/e2e/app-dir/use-cache-metadata-route-handler/app/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page() { - return

hello world

-} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/products/sitemap.ts b/test/e2e/app-dir/use-cache-metadata-route-handler/app/products/sitemap.ts new file mode 100644 index 0000000000000..147ed3deb90d4 --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/products/sitemap.ts @@ -0,0 +1,20 @@ +import type { MetadataRoute } from 'next' +import { getSentinelValue } from '../sentinel' +import { setTimeout } from 'timers/promises' + +export async function generateSitemaps() { + return [{ id: 0 }, { id: 1 }] +} + +export default async function sitemap({ + id, +}: { + id: number +}): Promise { + 'use cache' + + // Simulate I/O + await setTimeout(100) + + return [{ url: `https://acme.com/${id}?sentinel=${getSentinelValue()}` }] +} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/robots.ts b/test/e2e/app-dir/use-cache-metadata-route-handler/app/robots.ts new file mode 100644 index 0000000000000..f48d0d95bb0a9 --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/robots.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from 'next' +import { getSentinelValue } from './sentinel' +import { setTimeout } from 'timers/promises' + +export default async function robots(): Promise { + 'use cache' + + // Simulate I/O + await setTimeout(100) + + return { + rules: { userAgent: '*', allow: `/${getSentinelValue()}` }, + } +} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/sentinel.ts b/test/e2e/app-dir/use-cache-metadata-route-handler/app/sentinel.ts new file mode 100644 index 0000000000000..4571ba8f47bb8 --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/sentinel.ts @@ -0,0 +1,7 @@ +const { PHASE_PRODUCTION_BUILD } = require('next/constants') + +export function getSentinelValue() { + return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD + ? 'buildtime' + : 'runtime' +} diff --git a/test/e2e/app-dir/use-cache-metadata-route-handler/app/sitemap.ts b/test/e2e/app-dir/use-cache-metadata-route-handler/app/sitemap.ts new file mode 100644 index 0000000000000..05ccea5bbf45b --- /dev/null +++ b/test/e2e/app-dir/use-cache-metadata-route-handler/app/sitemap.ts @@ -0,0 +1,12 @@ +import type { MetadataRoute } from 'next' +import { getSentinelValue } from './sentinel' +import { setTimeout } from 'timers/promises' + +export default async function sitemap(): Promise { + 'use cache' + + // Simulate I/O + await setTimeout(100) + + return [{ url: `https://acme.com?sentinel=${getSentinelValue()}` }] +} 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 a6629c403f48b..0a69749b2d122 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 } = nextTestSetup({ + const { next, isNextStart } = nextTestSetup({ files: __dirname, }) @@ -9,5 +9,126 @@ describe('use-cache-metadata-route-handler', () => { const res = await next.fetch('/opengraph-image') expect(res.status).toBe(200) expect(res.headers.get('content-type')).toBe('image/png') + + if (isNextStart) { + const [buildStatus] = next.cliOutput.match(/. \/opengraph-image/) + + // TODO: Should always be `○ /opengraph-image`. + expect(buildStatus).toBeOneOf([ + '○ /opengraph-image', + 'ƒ /opengraph-image', + ]) + } + }) + + it('should generate an icon image with a metadata route handler that uses "use cache"', async () => { + const res = await next.fetch('/icon') + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('image/png') + + if (isNextStart) { + const [buildStatus] = next.cliOutput.match(/. \/icon/) + + // TODO: Should always be `○ /icon`. + expect(buildStatus).toBeOneOf(['○ /icon', 'ƒ /icon']) + } + }) + + it('should generate sitemaps with a metadata route handler that uses "use cache"', async () => { + const res = await next.fetch('/sitemap.xml') + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('application/xml') + + const body = await res.text() + + if (isNextStart) { + expect(body).toMatchInlineSnapshot(` + " + + + https://acme.com?sentinel=buildtime + + + " + `) + } else { + expect(body).toMatchInlineSnapshot(` + " + + + https://acme.com?sentinel=runtime + + + " + `) + } + }) + + it('should generate multiple sitemaps with a metadata route handler that uses "use cache"', async () => { + const res = await next.fetch('/products/sitemap/1.xml') + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('application/xml') + + const body = await res.text() + + if (isNextStart) { + expect(body).toMatchInlineSnapshot(` + " + + + https://acme.com/1?sentinel=buildtime + + + " + `) + } else { + expect(body).toMatchInlineSnapshot(` + " + + + https://acme.com/1?sentinel=runtime + + + " + `) + } + }) + + it('should generate robots.txt with a metadata route handler that uses "use cache"', async () => { + const res = await next.fetch('/robots.txt') + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('text/plain') + + const body = await res.text() + + if (isNextStart) { + expect(body).toMatchInlineSnapshot(` + "User-Agent: * + Allow: /buildtime + + " + `) + } else { + expect(body).toMatchInlineSnapshot(` + "User-Agent: * + Allow: /runtime + + " + `) + } + }) + + it('should generate manifest.json with a metadata route handler that uses "use cache"', async () => { + const res = await next.fetch('/manifest.webmanifest') + expect(res.status).toBe(200) + expect(res.headers.get('content-type')).toBe('application/manifest+json') + + const body = await res.json() + + if (isNextStart) { + expect(body).toEqual({ name: 'buildtime' }) + } else { + expect(body).toEqual({ name: 'runtime' }) + } }) }) From f746b4e6284c146f0c2faabf8cedaa9901625a5c Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 13 Jan 2025 21:21:52 +0100 Subject: [PATCH 6/8] Revert accidental change in unrelated test file --- test/e2e/app-dir/hello-world/hello-world.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/app-dir/hello-world/hello-world.test.ts b/test/e2e/app-dir/hello-world/hello-world.test.ts index 9f9aeae6fcc10..f7a390baacb65 100644 --- a/test/e2e/app-dir/hello-world/hello-world.test.ts +++ b/test/e2e/app-dir/hello-world/hello-world.test.ts @@ -11,7 +11,7 @@ describe('hello-world', () => { expect($('p').text()).toBe('hello world') }) - // Recommended for tests that need a full browser + // Recommended for tests that need a full browser. it('should work using browser', async () => { const browser = await next.browser('/') expect(await browser.elementByCss('p').text()).toBe('hello world') @@ -23,7 +23,7 @@ describe('hello-world', () => { expect(html).toContain('hello world') }) - // In case you need to test the response object + // In case you need to test the response object. it('should work with fetch', async () => { const res = await next.fetch('/') const html = await res.text() From e9e182dcec25114fadf9d176e0a09b0e515beef7 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 13 Jan 2025 22:26:45 +0100 Subject: [PATCH 7/8] Fix expectations for deploy tests --- .../use-cache-metadata-route-handler.test.ts | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) 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 0a69749b2d122..3c144e42f29e5 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, isNextStart } = nextTestSetup({ + const { next, isNextDev, isNextStart } = nextTestSetup({ files: __dirname, }) @@ -41,12 +41,12 @@ describe('use-cache-metadata-route-handler', () => { const body = await res.text() - if (isNextStart) { + if (isNextDev) { expect(body).toMatchInlineSnapshot(` " - https://acme.com?sentinel=buildtime + https://acme.com?sentinel=runtime " @@ -56,7 +56,7 @@ describe('use-cache-metadata-route-handler', () => { " - https://acme.com?sentinel=runtime + https://acme.com?sentinel=buildtime " @@ -71,12 +71,12 @@ describe('use-cache-metadata-route-handler', () => { const body = await res.text() - if (isNextStart) { + if (isNextDev) { expect(body).toMatchInlineSnapshot(` " - https://acme.com/1?sentinel=buildtime + https://acme.com/1?sentinel=runtime " @@ -86,7 +86,7 @@ describe('use-cache-metadata-route-handler', () => { " - https://acme.com/1?sentinel=runtime + https://acme.com/1?sentinel=buildtime " @@ -101,17 +101,17 @@ describe('use-cache-metadata-route-handler', () => { const body = await res.text() - if (isNextStart) { + if (isNextDev) { expect(body).toMatchInlineSnapshot(` - "User-Agent: * - Allow: /buildtime - - " - `) + "User-Agent: * + Allow: /runtime + + " + `) } else { expect(body).toMatchInlineSnapshot(` "User-Agent: * - Allow: /runtime + Allow: /buildtime " `) @@ -125,10 +125,10 @@ describe('use-cache-metadata-route-handler', () => { const body = await res.json() - if (isNextStart) { - expect(body).toEqual({ name: 'buildtime' }) - } else { + if (isNextDev) { expect(body).toEqual({ name: 'runtime' }) + } else { + expect(body).toEqual({ name: 'buildtime' }) } }) }) From e848a90242df9c06a67efcdfba215cc43842b23a Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Mon, 13 Jan 2025 23:25:04 +0100 Subject: [PATCH 8/8] Add clarifying comment --- packages/next/src/build/webpack-config.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 62472d162d519..af8bcd9dcdf7a 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1310,6 +1310,11 @@ export default async function getBaseWebpackConfig( ], }, resourceQuery: { + // Do not apply next-flight-loader to imports generated by the + // next-metadata-image-loader, to avoid generating unnecessary + // and conflicting entries in the flight client entry plugin. + // These are already covered by the next-metadata-route-loader + // entries. not: [ new RegExp(WEBPACK_RESOURCE_QUERIES.metadata), new RegExp(WEBPACK_RESOURCE_QUERIES.metadataImageMeta),