diff --git a/apps/bundle-analyzer/app/page.tsx b/apps/bundle-analyzer/app/page.tsx index 4811c03ba4fc..d7333dc72908 100644 --- a/apps/bundle-analyzer/app/page.tsx +++ b/apps/bundle-analyzer/app/page.tsx @@ -89,6 +89,7 @@ export default function Home() { size: number server?: boolean client?: boolean + traced?: boolean } | null>(null) const [searchQuery, setSearchQuery] = useState('') @@ -265,6 +266,9 @@ export default function Home() { {hoveredNodeInfo.server && ( server )} + {hoveredNodeInfo.traced && ( + traced + )} )} diff --git a/apps/bundle-analyzer/components/import-chain.tsx b/apps/bundle-analyzer/components/import-chain.tsx index cf101fbd19e8..25ba500c5f64 100644 --- a/apps/bundle-analyzer/components/import-chain.tsx +++ b/apps/bundle-analyzer/components/import-chain.tsx @@ -11,6 +11,7 @@ import { Server, Globe, MessageCircleQuestion, + Package, } from 'lucide-react' import { useMemo, useState } from 'react' import type { @@ -52,6 +53,7 @@ interface DependentInfo { sourceIndex: number | undefined ident: string isAsync: boolean + isTraced: boolean depth: number } @@ -219,6 +221,7 @@ export function ImportChain({ .map((index: number) => ({ index, async: false, + traced: false, depth: depthMap.get(index) ?? Infinity, })), ...modulesData @@ -226,6 +229,15 @@ export function ImportChain({ .map((index: number) => ({ index, async: true, + traced: false, + depth: depthMap.get(index) ?? Infinity, + })), + ...modulesData + .tracedModuleDependents(currentModuleIndex) + .map((index: number) => ({ + index, + async: false, + traced: true, depth: depthMap.get(index) ?? Infinity, })), ] @@ -243,7 +255,7 @@ export function ImportChain({ // Build info for each dependent const dependentsInfo: DependentInfo[] = validDependents.map( - ({ index: moduleIndex, async: isAsync, depth }) => { + ({ index: moduleIndex, async: isAsync, traced: isTraced, depth }) => { const sourceIndex = getSourceIndexFromModuleIndex(moduleIndex) let ident = modulesData.module(moduleIndex)?.ident || '' return { @@ -251,6 +263,7 @@ export function ImportChain({ sourceIndex, ident, isAsync, + isTraced, depth, } } @@ -376,6 +389,11 @@ export function ImportChain({ (async) )} + {currentItemInfo?.isTraced && ( + + (traced) + + )} {index > 0 ? ( ) : undefined} @@ -406,19 +424,7 @@ export function ImportChain({ {currentItemInfo?.isAsync &&
}
- {!level.layer ? ( -
- -
- ) : /app/.test(level.layer || '') ? ( -
- -
- ) : ( -
- -
- )} +
@@ -536,3 +542,31 @@ export function ImportChain({
) } + +function LayerIcon({ layer }: { layer: string | undefined }) { + if (!layer || layer === 'external') { + return ( +
+ +
+ ) + } else if (layer.includes('app')) { + return ( +
+ +
+ ) + } else if (layer === 'externals-tracing') { + return ( +
+ +
+ ) + } else { + return ( +
+ +
+ ) + } +} diff --git a/apps/bundle-analyzer/components/treemap-visualizer.tsx b/apps/bundle-analyzer/components/treemap-visualizer.tsx index 1f0190011985..5f6f0a2d137c 100644 --- a/apps/bundle-analyzer/components/treemap-visualizer.tsx +++ b/apps/bundle-analyzer/components/treemap-visualizer.tsx @@ -947,6 +947,7 @@ export function TreemapVisualizer({ size: node.size, server: node.server, client: node.client, + traced: node.traced, } if (node.type === 'directory') { diff --git a/apps/bundle-analyzer/components/ui/badge.tsx b/apps/bundle-analyzer/components/ui/badge.tsx index 85cca5f7fc83..e5c74fb30862 100644 --- a/apps/bundle-analyzer/components/ui/badge.tsx +++ b/apps/bundle-analyzer/components/ui/badge.tsx @@ -19,6 +19,8 @@ const badgeVariants = cva( 'border-transparent bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 ring-1 ring-inset ring-blue-700/10 dark:ring-blue-300/20', server: 'border-transparent bg-purple-50 dark:bg-purple-950 text-purple-700 dark:text-purple-300 ring-1 ring-inset ring-purple-700/10 dark:ring-purple-300/20', + traced: + 'border-transparent bg-grey-50 dark:bg-grey-950 text-grey-700 dark:text-grey-300 ring-1 ring-inset ring-grey-700/10 dark:ring-grey-300/20', polyfill: 'border-transparent bg-polyfill/10 dark:bg-polyfill/30 text-polyfill dark:text-polyfill-foreground ring-1 ring-inset ring-polyfill/20', }, diff --git a/apps/bundle-analyzer/lib/analyze-data.ts b/apps/bundle-analyzer/lib/analyze-data.ts index bdd8b7e41693..98a4a955d11b 100644 --- a/apps/bundle-analyzer/lib/analyze-data.ts +++ b/apps/bundle-analyzer/lib/analyze-data.ts @@ -48,8 +48,10 @@ interface ModulesDataHeader { modules: AnalyzeModule[] module_dependents: EdgesDataReference async_module_dependents: EdgesDataReference + traced_module_dependents: EdgesDataReference module_dependencies: EdgesDataReference async_module_dependencies: EdgesDataReference + traced_module_dependencies: EdgesDataReference } /** @@ -167,6 +169,13 @@ export class ModulesData { ) } + tracedModuleDependents(index: ModuleIndex): ModuleIndex[] { + return this.readEdgesDataAtIndex( + this.modulesHeader.traced_module_dependents, + index + ) + } + moduleDependencies(index: ModuleIndex): ModuleIndex[] { return this.readEdgesDataAtIndex( this.modulesHeader.module_dependencies, @@ -181,6 +190,13 @@ export class ModulesData { ) } + tracedModuleDependencies(index: ModuleIndex): ModuleIndex[] { + return this.readEdgesDataAtIndex( + this.modulesHeader.traced_module_dependencies, + index + ) + } + getRawModulesHeader(): ModulesDataHeader { return this.modulesHeader } @@ -447,10 +463,15 @@ export class AnalyzeData { client = true } else if (outputFile.filename.startsWith('[project]/')) { traced = true + server = true } else { server = true } - if (outputFile.filename.endsWith('.js')) { + if ( + outputFile.filename.endsWith('.js') || + outputFile.filename.endsWith('.mjs') || + outputFile.filename.endsWith('.cjs') + ) { js = true } else if (outputFile.filename.endsWith('.css')) { css = true diff --git a/apps/bundle-analyzer/lib/module-graph.ts b/apps/bundle-analyzer/lib/module-graph.ts index cfaa53486b14..98760b3f13e1 100644 --- a/apps/bundle-analyzer/lib/module-graph.ts +++ b/apps/bundle-analyzer/lib/module-graph.ts @@ -117,6 +117,14 @@ export function computeModuleDepthMap( } } + // Process traced dependencies + const tracedDependencies = modulesData.tracedModuleDependencies(moduleIndex) + for (const depIndex of tracedDependencies) { + if (!depthMap.has(depIndex)) { + depthMap.set(depIndex, newDepth) + } + } + i++ // Check if we need to process the next delayed queue to insert its items into the depth map diff --git a/apps/bundle-analyzer/lib/treemap-layout.ts b/apps/bundle-analyzer/lib/treemap-layout.ts index 6a39d7dc34a7..228a295b9839 100644 --- a/apps/bundle-analyzer/lib/treemap-layout.ts +++ b/apps/bundle-analyzer/lib/treemap-layout.ts @@ -15,6 +15,7 @@ export interface LayoutNodeInfo { size: number server?: boolean client?: boolean + traced?: boolean } export interface LayoutNode extends LayoutNodeInfo { @@ -25,7 +26,6 @@ export interface LayoutNode extends LayoutNodeInfo { titleBarHeight?: number children?: LayoutNode[] itemCount?: number - traced?: boolean js?: boolean css?: boolean json?: boolean @@ -112,7 +112,6 @@ function computeTreemapLayoutFromAnalyzeInternal( foldedPath: string, rect: LayoutRect, metadata: SourceMetadata[], - filterSource: ((sourceIndex: SourceIndex) => boolean) | undefined, sizeMode: SizeMode ): LayoutNode { const source = analyzeData.source(sourceIndex) @@ -139,7 +138,6 @@ function computeTreemapLayoutFromAnalyzeInternal( foldedPath + source.path, rect, metadata, - filterSource, sizeMode ) } @@ -253,7 +251,6 @@ function computeTreemapLayoutFromAnalyzeInternal( '', childRects[i], metadata, - filterSource, sizeMode ) ) @@ -288,7 +285,6 @@ export function computeTreemapLayoutFromAnalyze( '', rect, metadata, - filterSource, sizeMode ) } diff --git a/apps/bundle-analyzer/next.config.mjs b/apps/bundle-analyzer/next.config.mjs index a04884622e9e..f4b0f78f8482 100644 --- a/apps/bundle-analyzer/next.config.mjs +++ b/apps/bundle-analyzer/next.config.mjs @@ -1,7 +1,18 @@ +const developmentRewrites = () => { + return [ + { + source: '/data/:path*', + destination: 'http://localhost:4000/data/:path*', + }, + ] +} + /** @type {import('next').NextConfig} */ const nextConfig = { output: 'export', distDir: 'dist', + rewrites: + process.env.NODE_ENV === 'development' ? developmentRewrites : undefined, } export default nextConfig diff --git a/crates/next-api/src/analyze.rs b/crates/next-api/src/analyze.rs index 920d60c2bbc4..46a5b554b202 100644 --- a/crates/next-api/src/analyze.rs +++ b/crates/next-api/src/analyze.rs @@ -2,7 +2,9 @@ use std::{borrow::Cow, io::Write}; use anyhow::Result; use byteorder::{BE, WriteBytesExt}; -use rustc_hash::FxHashMap; +use either::Either; +use next_core::app_structure::FileSystemPathVec; +use rustc_hash::{FxHashMap, FxHashSet}; use serde::Serialize; use turbo_rcstr::RcStr; use turbo_tasks::{ @@ -12,18 +14,17 @@ use turbo_tasks_fs::{ File, FileContent, FileSystemPath, rope::{Rope, RopeBuilder}, }; -use turbopack_analyze::split_chunk::split_output_asset_into_parts; +use turbopack_analyze::split_chunk::{split_output_asset_into_parts, split_traced_file_into_parts}; use turbopack_core::{ SOURCE_URL_PROTOCOL, asset::{Asset, AssetContent}, - chunk::ChunkingType, + chunk::{ChunkingType, TracedMode}, module::Module, + module_graph::{GraphTraversalAction, ModuleGraph}, output::{OutputAsset, OutputAssets, OutputAssetsReference}, reference::all_assets_from_entries, }; -use crate::route::ModuleGraphs; - pub struct EdgesData { pub offsets: Vec, pub data: Vec, @@ -114,9 +115,13 @@ struct ModulesDataHeader { /// Edges from modules to modules pub async_module_dependents: EdgesDataReference, /// Edges from modules to modules + pub traced_module_dependents: EdgesDataReference, + /// Edges from modules to modules pub module_dependencies: EdgesDataReference, /// Edges from modules to modules pub async_module_dependencies: EdgesDataReference, + /// Edges from modules to modules + pub traced_module_dependencies: EdgesDataReference, } struct AnalyzeOutputFileBuilder { @@ -134,8 +139,10 @@ struct AnalyzeModuleBuilder { module: AnalyzeModule, dependencies: FxIndexSet, async_dependencies: FxIndexSet, + traced_dependencies: FxIndexSet, dependents: FxIndexSet, async_dependents: FxIndexSet, + traced_dependents: FxIndexSet, } struct AnalyzeDataBuilder { @@ -299,8 +306,10 @@ impl ModulesDataBuilder { module: AnalyzeModule { ident, path }, dependencies: FxIndexSet::default(), async_dependencies: FxIndexSet::default(), + traced_dependencies: FxIndexSet::default(), dependents: FxIndexSet::default(), async_dependents: FxIndexSet::default(), + traced_dependents: FxIndexSet::default(), }); (&mut self.modules[index as usize], index) } @@ -316,6 +325,11 @@ impl ModulesDataBuilder { .iter() .map(|s| s.async_dependencies.iter().copied().collect()) .collect(); + let traced_module_dependencies_vecs: Vec> = self + .modules + .iter() + .map(|s| s.traced_dependencies.iter().copied().collect()) + .collect(); let module_dependents_vecs: Vec> = self .modules .iter() @@ -326,11 +340,18 @@ impl ModulesDataBuilder { .iter() .map(|s| s.async_dependents.iter().copied().collect()) .collect(); + let traced_module_dependents_vecs: Vec> = self + .modules + .iter() + .map(|s| s.traced_dependents.iter().copied().collect()) + .collect(); let module_dependencies = EdgesData::from_iterator(&module_dependencies_vecs); let async_module_dependencies = EdgesData::from_iterator(&async_module_dependencies_vecs); + let traced_module_dependencies = EdgesData::from_iterator(&traced_module_dependencies_vecs); let module_dependents = EdgesData::from_iterator(&module_dependents_vecs); let async_module_dependents = EdgesData::from_iterator(&async_module_dependents_vecs); + let traced_module_dependents = EdgesData::from_iterator(&traced_module_dependents_vecs); let mut binary_section = EdgesDataSectionBuilder::new(); @@ -338,8 +359,10 @@ impl ModulesDataBuilder { modules: self.modules.into_iter().map(|s| s.module).collect(), module_dependents: binary_section.add_edges(&module_dependents), async_module_dependents: binary_section.add_edges(&async_module_dependents), + traced_module_dependents: binary_section.add_edges(&traced_module_dependents), module_dependencies: binary_section.add_edges(&module_dependencies), async_module_dependencies: binary_section.add_edges(&async_module_dependencies), + traced_module_dependencies: binary_section.add_edges(&traced_module_dependencies), }; let header_json = serde_json::to_vec(&header).unwrap(); @@ -366,8 +389,23 @@ pub async fn combine_output_assets( Ok(Vc::cell(combined)) } +/// Merges two sets of traced modules into one. Used to combine per-route traced +/// modules with shared modules (e.g. `_app`, `_document`) at report generation time. +#[turbo_tasks::function] +pub async fn combine_traced_files( + primary: Vc, + extra: Vc, +) -> Result> { + let mut combined: Vec = primary.await?.iter().cloned().collect(); + combined.extend(extra.await?.iter().cloned()); + Ok(Vc::cell(combined)) +} + #[turbo_tasks::function] -pub async fn analyze_output_assets(output_assets: Vc) -> Result> { +pub async fn analyze_output_assets( + output_assets: Vc, + traced_files: Vc, +) -> Result> { let output_assets = all_assets_from_entries(output_assets); let mut builder = AnalyzeDataBuilder::new(); @@ -376,19 +414,44 @@ pub async fn analyze_output_assets(output_assets: Vc) -> Result Either::Left(asset.path().await?), + Either::Right(path) => Either::Right(path), + }; + let path = match &file_system_path { + Either::Left(path) => &path.path, + Either::Right(path) => &path.path, + }; + if path.ends_with(".map") || path.ends_with(".nft.json") { // Skip source maps. continue; } - let output_file_index = builder.add_output_file(AnalyzeOutputFile { filename }); - let chunk_parts = split_output_asset_into_parts(*asset).await?; + let filename = match &file_system_path { + Either::Left(path) => path.to_string_ref().await?, + Either::Right(path) => path.to_string_ref().await?, + }; + + let output_file_index = builder.add_output_file(AnalyzeOutputFile { + filename: filename.clone(), + }); + let chunk_parts = match asset { + Either::Left(asset) => split_output_asset_into_parts(*asset).await?, + Either::Right(path) => split_traced_file_into_parts(path).await?, + }; for chunk_part in &chunk_parts { let decoded_source = urlencoding::decode(&chunk_part.source)?; let source = if let Some(stripped) = decoded_source.strip_prefix(&prefix) { Cow::Borrowed(stripped) + } else if decoded_source.starts_with("[project]/") { + decoded_source } else { Cow::Owned(format!( "[project]/{}", @@ -396,11 +459,12 @@ pub async fn analyze_output_assets(output_assets: Vc) -> Result) -> Result) -> Result> { +pub async fn analyze_module_graphs(module_graph: Vc) -> Result> { let mut builder = ModulesDataBuilder::new(); let mut all_modules = FxIndexSet::default(); let mut all_edges = FxIndexSet::default(); let mut all_async_edges = FxIndexSet::default(); - for module_graph in module_graphs.await? { - let module_graph = module_graph.await?; - module_graph.traverse_edges_unordered(|parent, node| { - if let Some((parent_node, reference)) = parent { - all_modules.insert(parent_node); - all_modules.insert(node); - match reference.chunking_type { - ChunkingType::Async => { - all_async_edges.insert((parent_node, node)); - } - _ => { - all_edges.insert((parent_node, node)); - } + let mut all_traced_edges = FxIndexSet::default(); + let mut traced_modules = FxHashSet::default(); + + let module_graph = module_graph.await?; + module_graph.traverse_edges_dfs( + module_graph.graphs.iter().flat_map(|g| g.entry_modules()), + &mut (), + |parent, node, _| { + all_modules.insert(node); + let Some((parent_node, reference)) = parent else { + return Ok(GraphTraversalAction::Continue); + }; + + // ChunkingType::Traced{TracedMode::Entry} => target is always traced + // ChunkingType::Traced{TracedMode::Transitive}=> target only traced if parent is traced + // ChunkingType::* => target only traced if parent is traced + if matches!( + reference.chunking_type, + ChunkingType::Traced { + mode: TracedMode::Entry + } + ) || traced_modules.contains(&parent_node) + { + traced_modules.insert(node); + all_traced_edges.insert((parent_node, node)); + return Ok(GraphTraversalAction::Continue); + }; + + match reference.chunking_type { + ChunkingType::Async => { + all_async_edges.insert((parent_node, node)); + } + _ => { + all_edges.insert((parent_node, node)); } } - Ok(()) - })?; - } + Ok(GraphTraversalAction::Continue) + }, + |_, _, _| Ok(()), + true, + )?; type ModulePair = (ResolvedVc>, ResolvedVc>); async fn mapper((from, to): ModulePair) -> Result> { @@ -479,8 +566,8 @@ pub async fn analyze_module_graphs(module_graphs: Vc) -> Result) -> Result) -> Result) -> Result, + pub traced_files: ResolvedVc, } #[turbo_tasks::value_impl] @@ -538,10 +645,12 @@ impl AnalyzeDataOutputAsset { pub async fn new( path: FileSystemPath, output_assets: ResolvedVc, + traced_files: ResolvedVc, ) -> Result> { Ok(Self { path, output_assets, + traced_files, } .cell()) } @@ -551,7 +660,7 @@ impl AnalyzeDataOutputAsset { impl Asset for AnalyzeDataOutputAsset { #[turbo_tasks::function] fn content(&self) -> Vc { - let file_content = analyze_output_assets(*self.output_assets); + let file_content = analyze_output_assets(*self.output_assets, *self.traced_files); AssetContent::file(file_content) } } @@ -570,18 +679,17 @@ impl OutputAsset for AnalyzeDataOutputAsset { #[turbo_tasks::value] pub struct ModulesDataOutputAsset { pub path: FileSystemPath, - pub module_graphs: ResolvedVc, + pub module_graph: ResolvedVc, } #[turbo_tasks::value_impl] impl ModulesDataOutputAsset { #[turbo_tasks::function] - pub async fn new(path: FileSystemPath, module_graphs: Vc) -> Result> { - Ok(Self { - path, - module_graphs: module_graphs.to_resolved().await?, - } - .cell()) + pub async fn new( + path: FileSystemPath, + module_graph: ResolvedVc, + ) -> Result> { + Ok(Self { path, module_graph }.cell()) } } @@ -589,7 +697,7 @@ impl ModulesDataOutputAsset { impl Asset for ModulesDataOutputAsset { #[turbo_tasks::function] fn content(&self) -> Vc { - let file_content = analyze_module_graphs(*self.module_graphs); + let file_content = analyze_module_graphs(*self.module_graph); AssetContent::file(file_content) } } diff --git a/crates/next-napi-bindings/src/next_api/analyze.rs b/crates/next-napi-bindings/src/next_api/analyze.rs index 0611cf26a517..6f9649103005 100644 --- a/crates/next-napi-bindings/src/next_api/analyze.rs +++ b/crates/next-napi-bindings/src/next_api/analyze.rs @@ -2,11 +2,14 @@ use std::{iter::once, sync::Arc}; use anyhow::Result; use next_api::{ - analyze::{AnalyzeDataOutputAsset, ModulesDataOutputAsset, combine_output_assets}, + analyze::{ + AnalyzeDataOutputAsset, ModulesDataOutputAsset, combine_output_assets, combine_traced_files, + }, project::ProjectContainer, route::EndpointGroupKey, }; use turbo_tasks::{Effects, ReadRef, ResolvedVc, TryJoinIterExt, Vc}; +use turbo_tasks_fs::FileSystemPath; use turbopack_core::{ issue::PlainIssue, output::{OutputAsset, OutputAssets}, @@ -70,21 +73,24 @@ async fn get_analyze_data_operation( // Collect output assets from _app and _document to merge into each route's // analyze.data so their modules are visible in every route's treemap. let mut combined_output_assets: Vec>> = vec![]; + let mut combined_traced_files: Vec = vec![]; for (key, endpoint_group) in endpoint_groups.iter() { if matches!( key, EndpointGroupKey::PagesApp | EndpointGroupKey::PagesDocument ) { combined_output_assets.extend(endpoint_group.output_assets().await?.iter().copied()); + combined_traced_files.extend(endpoint_group.traced_files().await?.iter().cloned()); } } let has_combined = !combined_output_assets.is_empty(); - let combined_vc = Vc::cell(combined_output_assets); + let combined_assets_vc = Vc::cell(combined_output_assets); + let combined_traced_vc = Vc::cell(combined_traced_files); let analyze_data = endpoint_groups .iter() - .map(|(key, endpoint_group)| async move { + .map(async |(key, endpoint_group)| { let output_assets = if has_combined && !matches!( key, @@ -92,15 +98,27 @@ async fn get_analyze_data_operation( ) { // Combine route output assets with _app and _document output assets so // the generated analyze.data already includes their modules. - combine_output_assets(endpoint_group.output_assets(), combined_vc) + combine_output_assets(endpoint_group.output_assets(), combined_assets_vc) } else { endpoint_group.output_assets() }; + let traced_files = if has_combined + && !matches!( + key, + EndpointGroupKey::PagesApp | EndpointGroupKey::PagesDocument + ) { + // Combine route traced files with _app and _document traced modules so + // the generated analyze.data already includes their modules. + combine_traced_files(endpoint_group.traced_files(), combined_traced_vc) + } else { + endpoint_group.traced_files() + }; let analyze_data = AnalyzeDataOutputAsset::new( analyze_output_root .join(&key.to_string())? .join("analyze.data")?, output_assets, + traced_files, ) .to_resolved() .await?; @@ -113,7 +131,7 @@ async fn get_analyze_data_operation( let modules_data = ResolvedVc::upcast( ModulesDataOutputAsset::new( analyze_output_root.join("modules.data")?, - Vc::cell(vec![whole_app_module_graphs.await?.full]), + *whole_app_module_graphs.await?.full, ) .to_resolved() .await?, diff --git a/docs/01-app/02-guides/draft-mode.mdx b/docs/01-app/02-guides/draft-mode.mdx index 927c8d313486..faf6ff4bec50 100644 --- a/docs/01-app/02-guides/draft-mode.mdx +++ b/docs/01-app/02-guides/draft-mode.mdx @@ -1,7 +1,7 @@ --- title: How to preview content with Draft Mode in Next.js nav_title: Draft Mode -description: Next.js has draft mode to toggle between static and dynamic pages. You can learn how it works with App Router here. +description: Bypass Next.js caching for a request so editors can preview unpublished content from a headless CMS. related: title: Next Steps description: See the API reference for more information on how to use Draft Mode. @@ -9,27 +9,46 @@ related: - app/api-reference/functions/draft-mode --- -**Draft Mode** allows you to preview draft content from your headless CMS in your Next.js application. This is useful for static pages that are generated at build time as it allows you to switch to [dynamic rendering](/docs/app/glossary#dynamic-rendering) and see the draft changes without having to rebuild your entire site. +**Draft Mode** lets editors see how draft or in-progress content will render on your site, without waiting for revalidation. While an editor is in Draft Mode, cached or pre-rendered content is bypassed, and fetched from upstream sources directly. Other visitors continue to see the cached or pre-rendered version of the page. -This page walks through how to enable and use Draft Mode. +Your data-fetching code does not need to change if your CMS serves draft and published content from the same URL. Otherwise, see [When your CMS uses a separate draft endpoint](#when-your-cms-uses-a-separate-draft-endpoint). -## Step 1: Create a Route Handler +## What Draft Mode does -Create a [Route Handler](/docs/app/api-reference/file-conventions/route). It can have any name, for example, `app/api/draft/route.ts`. +When Draft Mode is enabled for a request: -```ts filename="app/api/draft/route.ts" switcher -export async function GET(request: Request) { - return new Response('') -} -``` +- `fetch()` calls skip the Next.js fetch cache and hit the network directly. +- Components and functions inside [`'use cache'`](/docs/app/api-reference/directives/use-cache) re-execute on every request, and their results are not saved to the cache. +- [`unstable_cache`](/docs/app/api-reference/functions/unstable_cache) reads and writes are bypassed in the same way. +- The page is excluded from the ISR response cache and is served with `Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate`. -```js filename="app/api/draft/route.js" switcher -export async function GET() { - return new Response('') -} -``` +The effect applies whether the page is statically generated, served from cache, or revalidated through ISR. + +## What this guide covers + +This guide assumes: + +- Your headless CMS supports configurable preview URLs (most do). +- The CMS opens a URL like `/api/draft?secret=XXX&slug=/posts/foo` in a new tab when an editor clicks "Preview". The secret is a shared token; the slug is the path to preview. +- Your Next.js app validates the secret, enables Draft Mode, and redirects to the slug. -Then, import the [`draftMode`](/docs/app/api-reference/functions/draft-mode) function and call the `enable()` method. +With that contract in mind, the rest of this guide walks through: + +1. Creating a Route Handler that enables Draft Mode by setting a cookie. +2. Securing that handler with the shared secret and slug from the CMS. +3. Rendering pages that read the latest draft. +4. Showing a preview banner with an exit form. + +Then, depending on your setup: + +- [Draft Mode with Cache Components](#draft-mode-with-cache-components) for surfacing the preview state from a `'use cache'` boundary. +- [When your CMS uses a separate draft endpoint](#when-your-cms-uses-a-separate-draft-endpoint) for branching the fetch URL on `isEnabled`. + +> **Good to know:** `GET` is meant to be a safe, read-only method. Operations that affect future requests, like enabling Draft Mode via a cookie, should use `POST`. The entry handler uses `GET` because we're assuming a CMS preview integration: the CMS opens the URL in a new browser tab, which is a `GET` request. The exit flow in Step 4 uses `POST` (via [Server Action](/docs/app/getting-started/mutating-data) or `POST` Route Handler). + +## Step 1: Create a Route Handler + +Create a [Route Handler](/docs/app/api-reference/file-conventions/route) that sets the Draft Mode cookie. It can have any name, for example, `app/api/draft/route.ts`. ```ts filename="app/api/draft/route.ts" switcher import { draftMode } from 'next/headers' @@ -51,17 +70,19 @@ export async function GET(request) { } ``` -This will set a **cookie** to enable draft mode. Subsequent requests containing this cookie will trigger draft mode and change the behavior of statically generated pages. +`draft.enable()` sets a cookie named `__prerender_bypass`. Subsequent requests that carry this cookie skip every cache layer listed above. + +You can test this manually by visiting `/api/draft` and looking at your browser's developer tools. Notice the `Set-Cookie` response header. -You can test this manually by visiting `/api/draft` and looking at your browser’s developer tools. Notice the `Set-Cookie` response header with a cookie named `__prerender_bypass`. +As written, the handler is public: anyone who hits `/api/draft` enables Draft Mode for themselves. Step 2 closes that with a shared secret so only your CMS can call it. -## Step 2: Access the Route Handler from your Headless CMS +## Step 2: Access the Route Handler from your headless CMS -> These steps assume that the headless CMS you’re using supports setting **custom draft URLs**. If it doesn’t, you can still use this method to secure your draft URLs, but you’ll need to construct and access the draft URL manually. The specific steps will vary depending on which headless CMS you’re using. +> These steps assume that the headless CMS you're using supports setting **custom draft URLs**. If it doesn't, you can still use this method to secure your draft URLs, but you'll need to construct and access the draft URL manually. The specific steps will vary depending on which headless CMS you're using. To securely access the Route Handler from your headless CMS: -1. Create a **secret token string** using a token generator of your choice. This secret will only be known by your Next.js app and your headless CMS. +1. Create a **secret token string** using a token generator of your choice. This secret is only known to your Next.js app and your headless CMS. 2. If your headless CMS supports setting custom draft URLs, specify a draft URL (this assumes that your Route Handler is located at `app/api/draft/route.ts`). For example: ```bash filename="Terminal" @@ -72,41 +93,35 @@ https:///api/draft?secret=&slug= > - `` should be replaced with the secret token you generated. > - `` should be the path for the page that you want to view. If you want to view `/posts/one`, then you should use `&slug=/posts/one`. > -> Your headless CMS might allow you to include a variable in the draft URL so that `` can be set dynamically based on the CMS’s data like so: `&slug=/posts/{entry.fields.slug}` +> Your headless CMS might allow you to include a variable in the draft URL so that `` can be set dynamically based on the CMS's data like so: `&slug=/posts/{entry.fields.slug}` -3. In your Route Handler, check that the secret matches and that the `slug` parameter exists (if not, the request should fail), call `draftMode.enable()` to set the cookie. Then, redirect the browser to the path specified by `slug`: +3. In your Route Handler, check that the secret matches and that the `slug` parameter exists (if not, the request should fail), call `draft.enable()` to set the cookie, then redirect the browser to the path specified by `slug`: ```ts filename="app/api/draft/route.ts" switcher import { draftMode } from 'next/headers' import { redirect } from 'next/navigation' export async function GET(request: Request) { - // Parse query string parameters const { searchParams } = new URL(request.url) const secret = searchParams.get('secret') const slug = searchParams.get('slug') - // Check the secret and next parameters // This secret should only be known to this Route Handler and the CMS if (secret !== 'MY_SECRET_TOKEN' || !slug) { return new Response('Invalid token', { status: 401 }) } - // Fetch the headless CMS to check if the provided `slug` exists - // getPostBySlug would implement the required fetching logic to the headless CMS + // Verify the slug exists in the CMS before enabling Draft Mode const post = await getPostBySlug(slug) - - // If the slug doesn't exist prevent draft mode from being enabled if (!post) { return new Response('Invalid slug', { status: 401 }) } - // Enable Draft Mode by setting the cookie const draft = await draftMode() draft.enable() - // Redirect to the path from the fetched post - // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities + // Redirect to the path from the fetched post, not from searchParams, + // to avoid open redirect vulnerabilities redirect(post.slug) } ``` @@ -116,132 +131,152 @@ import { draftMode } from 'next/headers' import { redirect } from 'next/navigation' export async function GET(request) { - // Parse query string parameters const { searchParams } = new URL(request.url) const secret = searchParams.get('secret') const slug = searchParams.get('slug') - // Check the secret and next parameters - // This secret should only be known to this Route Handler and the CMS if (secret !== 'MY_SECRET_TOKEN' || !slug) { return new Response('Invalid token', { status: 401 }) } - // Fetch the headless CMS to check if the provided `slug` exists - // getPostBySlug would implement the required fetching logic to the headless CMS const post = await getPostBySlug(slug) - - // If the slug doesn't exist prevent draft mode from being enabled if (!post) { return new Response('Invalid slug', { status: 401 }) } - // Enable Draft Mode by setting the cookie const draft = await draftMode() draft.enable() - // Redirect to the path from the fetched post - // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities redirect(post.slug) } ``` -If it succeeds, then the browser will be redirected to the path you want to view with the draft mode cookie. - -## Step 3: Preview the Draft Content - -The next step is to update your page to check the value of `draftMode().isEnabled`. +If it succeeds, the browser is redirected to the target path with the Draft Mode cookie set. -If you request a page which has the cookie set, then data will be fetched at **request time** (instead of at build time). +## Step 3: Preview the draft content -Furthermore, the value of `isEnabled` will be `true`. +Because Draft Mode bypasses the cache automatically, your page does not need to know whether Draft Mode is on to receive fresh content. Fetch as you normally would: -```tsx filename="app/page.tsx" switcher -// page that fetches data -import { draftMode } from 'next/headers' - -async function getData() { - const { isEnabled } = await draftMode() +```tsx filename="app/posts/[slug]/page.tsx" switcher +async function getPost(slug: string) { + const res = await fetch(`https://cms.example.com/posts/${slug}`) + return res.json() +} - const url = isEnabled - ? 'https://draft.example.com' - : 'https://production.example.com' +export default async function Page({ params }: PageProps<'/posts/[slug]'>) { + const { slug } = await params + const post = await getPost(slug) - const res = await fetch(url) + return ( +
+

{post.title}

+
{post.content}
+
+ ) +} +``` +```jsx filename="app/posts/[slug]/page.js" switcher +async function getPost(slug) { + const res = await fetch(`https://cms.example.com/posts/${slug}`) return res.json() } -export default async function Page() { - const { title, desc } = await getData() +export default async function Page({ params }) { + const { slug } = await params + const post = await getPost(slug) return (
-

{title}

-

{desc}

+

{post.title}

+
{post.content}
) } ``` -```jsx filename="app/page.js" switcher -// page that fetches data -import { draftMode } from 'next/headers' +When the Draft Mode cookie is present, the `fetch` above skips the Next.js fetch cache and hits your CMS for the current draft. When it is not, the same request can be served from cache as usual. -async function getData() { - const { isEnabled } = await draftMode() +If your CMS uses a different URL for drafts rather than serving them from the same endpoint, see [When your CMS uses a separate draft endpoint](#when-your-cms-uses-a-separate-draft-endpoint). - const url = isEnabled - ? 'https://draft.example.com' - : 'https://production.example.com' +## Step 4: Show a preview indicator - const res = await fetch(url) +`isEnabled` is most useful as a signal to the editor: a banner that confirms they are looking at draft content, plus a way to exit. Render an indicator from your root layout so it appears on every preview page. - return res.json() +```tsx filename="app/preview-banner.tsx" switcher +import { draftMode } from 'next/headers' +import { redirect } from 'next/navigation' + +async function exitPreview() { + 'use server' + const draft = await draftMode() + draft.disable() + redirect('/') } -export default async function Page() { - const { title, desc } = await getData() +export async function PreviewBanner() { + const { isEnabled } = await draftMode() + if (!isEnabled) return null return ( -
-

{title}

-

{desc}

-
+ ) } ``` -If you access the draft Route Handler (with `secret` and `slug`) from your headless CMS or manually using the URL, you should now be able to see the draft content. And, if you update your draft without publishing, you should be able to view the draft. +```jsx filename="app/preview-banner.js" switcher +import { draftMode } from 'next/headers' +import { redirect } from 'next/navigation' -## Draft Mode with Cache Components +async function exitPreview() { + 'use server' + const draft = await draftMode() + draft.disable() + redirect('/') +} -When using [caching directives](/docs/app/api-reference/directives/use-cache), Draft Mode forces all cached functions and components to produce fresh results on every request. You do not need to add any special handling to your caching code. +export async function PreviewBanner() { + const { isEnabled } = await draftMode() + if (!isEnabled) return null -When Draft Mode is enabled: + return ( + + ) +} +``` + +Exiting Draft Mode also works with a `GET` Route Handler, but a `POST` is semantically more correct, for example via a form submitted through a [Server Action](/docs/app/getting-started/mutating-data) or to a `POST` Route Handler. -- All functions and components under a caching directive scope re-execute on every request instead of serving from cache. -- Results are not saved to the cache, so draft requests do not pollute cached content. -- `fetch()` calls inside cached scopes use the original `fetch` implementation directly, without the Next.js fetch cache. -- Response headers are set to `private, no-cache, no-store, max-age=0, must-revalidate`. +If you do use a `GET` Route Handler, trigger it from a `
` rather than a [``](/docs/app/api-reference/components/link). Next.js prefetches `` components by default, which would clear the cookie before the editor clicks. Forms are not prefetched, regardless of method. -You can read if Draft Mode is enabled inside a caching directive scope, so you can conditionally fetch draft or published content: +## Draft Mode with Cache Components + +You can read `isEnabled` inside a [`'use cache'`](/docs/app/api-reference/directives/use-cache) scope to render a preview indicator from a cached component. The cache bypass still applies, so the component re-executes with fresh data on every draft request. -```tsx filename="app/post/[slug]/page.tsx" switcher +```tsx filename="app/posts/[slug]/page.tsx" switcher import { draftMode } from 'next/headers' async function Post({ slug }: { slug: string }) { 'use cache' + const post = await fetch(`https://cms.example.com/posts/${slug}`).then((r) => + r.json() + ) const { isEnabled } = await draftMode() - // Fetch draft or published content based on draft mode status - const post = isEnabled - ? await fetchDraftPost(slug) - : await fetchPublishedPost(slug) - return (
+ {isEnabled &&

Draft preview

}

{post.title}

{post.content}
@@ -249,21 +284,20 @@ async function Post({ slug }: { slug: string }) { } ``` -```jsx filename="app/post/[slug]/page.js" switcher +```jsx filename="app/posts/[slug]/page.js" switcher import { draftMode } from 'next/headers' async function Post({ slug }) { 'use cache' + const post = await fetch(`https://cms.example.com/posts/${slug}`).then((r) => + r.json() + ) const { isEnabled } = await draftMode() - // Fetch draft or published content based on draft mode status - const post = isEnabled - ? await fetchDraftPost(slug) - : await fetchPublishedPost(slug) - return (
+ {isEnabled &&

Draft preview

}

{post.title}

{post.content}
@@ -271,9 +305,38 @@ async function Post({ slug }) { } ``` -> **Good to know:** -> -> - You can await the `draftMode()` promise inside a caching directive scope to read `isEnabled`, but other runtime APIs like `cookies()` and `headers()` are not allowed and will throw an error, even when Draft Mode is active. (The [`"use cache: private"`](/docs/app/api-reference/directives/use-cache-private) directive does allow access to `cookies()` and `headers()`.) -> - You cannot call `draftMode().enable()` or `draftMode().disable()` inside a caching directive scope. Draft Mode can only be toggled in [Route Handlers](/docs/app/api-reference/file-conventions/route) or [Server Actions](/docs/app/getting-started/mutating-data). +> **Good to know:** `draftMode().enable()` and `draftMode().disable()` cannot be called inside a caching directive scope; toggle Draft Mode from a [Route Handler](/docs/app/api-reference/file-conventions/route) or [Server Action](/docs/app/getting-started/mutating-data) instead. + +## When your CMS uses a separate draft endpoint + +If your CMS exposes draft content at a different URL or requires different credentials, branch your fetch on `isEnabled`: + +```tsx filename="app/posts/[slug]/page.tsx" switcher +import { draftMode } from 'next/headers' + +async function getPost(slug: string) { + const { isEnabled } = await draftMode() + const baseUrl = isEnabled + ? 'https://cms.example.com/preview' + : 'https://cms.example.com/published' + + const res = await fetch(`${baseUrl}/posts/${slug}`) + return res.json() +} +``` + +```jsx filename="app/posts/[slug]/page.js" switcher +import { draftMode } from 'next/headers' + +async function getPost(slug) { + const { isEnabled } = await draftMode() + const baseUrl = isEnabled + ? 'https://cms.example.com/preview' + : 'https://cms.example.com/published' + + const res = await fetch(`${baseUrl}/posts/${slug}`) + return res.json() +} +``` -If your cached function needs values from `cookies()` or `headers()`, read them outside the caching directive scope and [pass them as arguments](/docs/app/getting-started/caching#passing-runtime-values-to-cached-functions). +The cache bypass still applies to both branches; the fork only chooses where to read from. diff --git a/docs/01-app/02-guides/instant-navigation.mdx b/docs/01-app/02-guides/instant-navigation.mdx index 4c84b2ade738..33e69b095583 100644 --- a/docs/01-app/02-guides/instant-navigation.mdx +++ b/docs/01-app/02-guides/instant-navigation.mdx @@ -84,29 +84,28 @@ Validation runs automatically on every page load using the real request from you The Next.js DevTools let you see what users see on page loads and client navigations before dynamic data streams in. Use it to verify your loading states look right, check that the right content appears immediately, and iterate on where to place `` boundaries. -Enable the toggle in your Next.js config: +The Navigation Inspector is available when Cache Components is enabled: -```ts filename="next.config.ts" highlight={5-7} +```ts filename="next.config.ts" highlight={4} import type { NextConfig } from 'next' const nextConfig: NextConfig = { cacheComponents: true, - experimental: { - instantNavigationDevToolsToggle: true, - }, } export default nextConfig ``` -Open the Next.js DevTools and select **Instant Navs**. Two options are available: +Open the Next.js DevTools and select **Navigation Inspector**, then click **Start Capturing**. While capturing is active: -- **Page load**: click **Reload** to refresh the page and freeze it at the initial static UI generated for this route, before any dynamic data streams in. -- **Client navigation**: once enabled, clicking any link in your app shows the prefetched UI for that page instead of the full result. +- Refresh the page to freeze the initial static UI generated for the route, before any dynamic data streams in. +- Click a link to freeze the prefetched UI for the destination route. -Try a **page load** on the product page. Two separate fallbacks appear: "Loading product..." and "Checking availability...". On the first visit the cache is cold and both fallbacks are visible. Navigate to the page again and the product name appears immediately from cache. +When the UI is frozen, click **Continue Rendering** to let the current navigation finish while keeping the Navigation Inspector in capturing mode. Click **Stop Capturing** when you are done inspecting loading states. -Now try a **client navigation** (click a link from `/store/shoes` to `/store/hats`). The product name and price appear immediately (cached). "Checking availability..." shows where inventory will stream in. +Try refreshing the product page. Two separate fallbacks appear: "Loading product..." and "Checking availability...". On the first visit the cache is cold and both fallbacks are visible. Navigate to the page again and the product name appears immediately from cache. + +Now click a link from `/store/shoes` to `/store/hats`. The product name and price appear immediately (cached). "Checking availability..." shows where inventory will stream in. > **Good to know:** Page loads and client navigations can produce different shells. Client-side hooks like `useSearchParams` suspend on page loads (search params are not known at build time) but resolve synchronously on client navigations (the router already has the params). diff --git a/docs/01-app/03-api-reference/01-directives/use-cache.mdx b/docs/01-app/03-api-reference/01-directives/use-cache.mdx index dc9a481d956a..febac342a258 100644 --- a/docs/01-app/03-api-reference/01-directives/use-cache.mdx +++ b/docs/01-app/03-api-reference/01-directives/use-cache.mdx @@ -443,6 +443,8 @@ export async function getData() { } ``` +> **Good to know:** When a cached directive (`use cache`, [`use cache: private`](/docs/app/api-reference/directives/use-cache-private), or [`use cache: remote`](/docs/app/api-reference/directives/use-cache-remote)) is at the top of a file, you can import its exported functions into a Client Component and call them directly; they run on the server and return the result, similar to a [Server Function](/docs/app/glossary#server-function). Prefer calling cached functions on the server and passing results down as props. + ### Interleaving In React, composition with `children` or slots is a well-known pattern for building flexible components. When using `use cache`, you can continue to compose your UI in this way. Anything included as `children`, or other compositional slots, in the returned JSX will be passed through the cached component without affecting its cache entry. diff --git a/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx b/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx index eb9484cc424c..65d165998b62 100644 --- a/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx +++ b/docs/01-app/03-api-reference/03-file-conventions/02-route-segment-config/instant.mdx @@ -122,22 +122,20 @@ Setting `unstable_instant = false` on a segment opts it out of validation entire ## Inspecting loading states -Enable the DevTools toggle with the experimental flag: +The Navigation Inspector is available when Cache Components is enabled: ```js filename="next.config.js" module.exports = { - experimental: { - instantNavigationDevToolsToggle: true, - }, + cacheComponents: true, } ``` -Open the Next.js DevTools and select **Instant Navs**. Two options are available: +Open the Next.js DevTools and select **Navigation Inspector**, then click **Start Capturing**. While capturing is active: -- **Page load**: click **Reload** to refresh the page and freeze it at the initial static UI that was generated for this route, before any dynamic data streams in. -- **Client navigation**: once enabled, clicking any link in your app shows the prefetched UI for that page instead of the full result. +- Refresh the page to freeze the initial static UI generated for the route, before any dynamic data streams in. +- Click a link to freeze the prefetched UI for the destination route. -Use both to check that your loading states look right on first visit and on navigation. +When the UI is frozen, click **Continue Rendering** to let the current navigation finish while keeping the Navigation Inspector in capturing mode. Click **Stop Capturing** when you are done. Use both page refreshes and link clicks to check that your loading states look right on first visit and on navigation. ## Testing instant navigation @@ -151,7 +149,7 @@ import { instant } from '@next/playwright' ## Known issue: shared cookie across projects -The DevTools use a `next-instant-navigation-testing` cookie to hold back dynamic content and freeze the page at the instant UI. Because cookies are scoped to the domain and not the port, running multiple projects on the same domain (typically `localhost`) means the cookie is shared across them and can cause unexpected behavior. Clear the cookie or close the Instant Navs panel when switching between projects to avoid issues. +The DevTools use a `next-instant-navigation-testing` cookie to hold back dynamic content and freeze the page at the instant UI. Because cookies are scoped to the domain and not the port, running multiple projects on the same domain (typically `localhost`) means the cookie is shared across them and can cause unexpected behavior. Clear the cookie or close the Navigation Inspector panel when switching between projects to avoid issues. > **Good to know:** This will be fixed as part of stabilizing the feature. diff --git a/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx b/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx index 610944c6e4d5..174201d4cc3a 100644 --- a/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx +++ b/docs/01-app/03-api-reference/05-config/01-next-config-js/rewrites.mdx @@ -91,10 +91,11 @@ The order Next.js routes are checked is: 1. [headers](/docs/app/api-reference/config/next-config-js/headers) are checked/applied 2. [redirects](/docs/app/api-reference/config/next-config-js/redirects) are checked/applied 3. [proxy](/docs/app/api-reference/file-conventions/proxy) -4. `beforeFiles` rewrites are checked/applied +4. `beforeFiles` rewrites: for each entry, if `source`, `has`, and `missing` matches the request, it's rewritten to `destination`. 5. static files from the [public directory](/docs/app/api-reference/file-conventions/public-folder), `_next/static` files, and non-dynamic pages are checked/served -6. `afterFiles` rewrites are checked/applied, if one of these rewrites is matched we check dynamic routes/static files after each match -7. `fallback` rewrites are checked/applied, these are applied before rendering the 404 page and after dynamic routes/all static assets have been checked. If you use [fallback: true/'blocking'](/docs/pages/api-reference/functions/get-static-paths#fallback-true) in `getStaticPaths`, the fallback `rewrites` defined in your `next.config.js` will _not_ be run. +6. `afterFiles` rewrites are tried in order. If a `source`, `has`, and `missing` matches the request, it's rewritten to `destination`; the first rewrite that resolves to a static file, page, or dynamic route is served. +7. dynamic routes (e.g., `app/blog/[slug]/page.tsx`) are matched against the current path +8. `fallback` rewrites are checked/applied, these are applied before rendering the 404 page and after dynamic routes/all static assets have been checked. If you use [fallback: true/'blocking'](/docs/pages/api-reference/functions/get-static-paths#fallback-true) in `getStaticPaths`, those dynamic routes take priority over the fallback `rewrites` defined in your `next.config.js`. @@ -102,10 +103,11 @@ The order Next.js routes are checked is: 1. [headers](/docs/pages/api-reference/config/next-config-js/headers) are checked/applied 2. [redirects](/docs/pages/api-reference/config/next-config-js/redirects) are checked/applied -3. `beforeFiles` rewrites are checked/applied +3. `beforeFiles` rewrites: for each entry, if `source` matches the request, it's rewritten to `destination`. 4. static files from the [public directory](/docs/pages/api-reference/file-conventions/public-folder), `_next/static` files, and non-dynamic pages are checked/served -5. `afterFiles` rewrites are checked/applied, if one of these rewrites is matched we check dynamic routes/static files after each match -6. `fallback` rewrites are checked/applied, these are applied before rendering the 404 page and after dynamic routes/all static assets have been checked. If you use [fallback: true/'blocking'](/docs/pages/api-reference/functions/get-static-paths#fallback-true) in `getStaticPaths`, the fallback `rewrites` defined in your `next.config.js` will _not_ be run. +5. `afterFiles` rewrites are tried in order. If a `source` matches the request, it's rewritten to `destination`; the first rewrite that resolves to a static file, page, or dynamic route is served. +6. dynamic routes (e.g., `pages/blog/[slug].tsx`) are matched against the current path +7. `fallback` rewrites are checked/applied, these are applied before rendering the 404 page and after dynamic routes/all static assets have been checked. If you use [fallback: true/'blocking'](/docs/pages/api-reference/functions/get-static-paths#fallback-true) in `getStaticPaths`, the fallback `rewrites` defined in your `next.config.js` will _not_ be run. diff --git a/packages/next/src/build/define-env.ts b/packages/next/src/build/define-env.ts index 67fcbd73d19e..eb15db519e8b 100644 --- a/packages/next/src/build/define-env.ts +++ b/packages/next/src/build/define-env.ts @@ -160,6 +160,8 @@ export function getDefineEnv({ ? 'development' : 'production', 'process.env.__NEXT_DEV_SERVER': dev ? '1' : '', + 'process.env.__NEXT_DISABLE_DEV_OVERLAY_UX': + process.env.NEXT_PRIVATE_DISABLE_DEV_OVERLAY_UX === '1', 'process.env.NEXT_RUNTIME': isEdgeServer ? 'edge' : isNodeServer @@ -177,8 +179,7 @@ export function getDefineEnv({ 'process.env.__NEXT_EXPERIMENTAL_CACHED_NAVIGATIONS': Boolean( config.experimental.cachedNavigations ), - 'process.env.__NEXT_INSTANT_NAV_TOGGLE': - !!config.experimental.instantNavigationDevToolsToggle, + 'process.env.__NEXT_INSTANT_NAV_TOGGLE': isCacheComponentsEnabled, 'process.env.__NEXT_USE_CACHE': isUseCacheEnabled, 'process.env.__NEXT_USE_NODE_STREAMS': isEdgeServer ? false diff --git a/packages/next/src/client/components/router-reducer/router-reducer.ts b/packages/next/src/client/components/router-reducer/router-reducer.ts index 1733cc3029bd..0e7ea9f68b6f 100644 --- a/packages/next/src/client/components/router-reducer/router-reducer.ts +++ b/packages/next/src/client/components/router-reducer/router-reducer.ts @@ -15,7 +15,6 @@ import { navigateReducer } from './reducers/navigate-reducer' import { serverPatchReducer } from './reducers/server-patch-reducer' import { restoreReducer } from './reducers/restore-reducer' import { refreshReducer } from './reducers/refresh-reducer' -import { hmrRefreshReducer } from './reducers/hmr-refresh-reducer' import { serverActionReducer } from './reducers/server-action-reducer' /** @@ -39,7 +38,15 @@ function clientReducer( return refreshReducer(state, action) } case ACTION_HMR_REFRESH: { - return hmrRefreshReducer(state) + if (process.env.NODE_ENV === 'development') { + const { hmrRefreshReducer } = + require('./reducers/hmr-refresh-reducer') as typeof import('./reducers/hmr-refresh-reducer') + return hmrRefreshReducer(state) + } else { + throw new Error( + 'hmrRefresh can only be used in development mode. Please use refresh instead.' + ) + } } case ACTION_SERVER_ACTION: { return serverActionReducer(state, action) diff --git a/packages/next/src/next-devtools/dev-overlay-ux.ts b/packages/next/src/next-devtools/dev-overlay-ux.ts new file mode 100644 index 000000000000..5aa0cf322dfa --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay-ux.ts @@ -0,0 +1,5 @@ +import './dev-overlay/global.css' +import './dev-overlay/components/toast/style.css' + +export { FontStyles } from './dev-overlay/font/font-styles' +export { DevOverlay } from './dev-overlay/dev-overlay' diff --git a/packages/next/src/next-devtools/dev-overlay.browser.tsx b/packages/next/src/next-devtools/dev-overlay.browser.tsx index 20052aa52ab5..be79172542d7 100644 --- a/packages/next/src/next-devtools/dev-overlay.browser.tsx +++ b/packages/next/src/next-devtools/dev-overlay.browser.tsx @@ -37,10 +37,8 @@ import { } from 'react' import { createRoot } from 'react-dom/client' import type { CacheIndicatorState } from './dev-overlay/cache-indicator' -import { FontStyles } from './dev-overlay/font/font-styles' import type { HydrationErrorState } from './shared/hydration-error' import type { DebugInfo } from './shared/types' -import { DevOverlay } from './dev-overlay/dev-overlay' import type { DevIndicatorServerState } from '../server/dev/dev-indicator-server-state' import type { VersionInfo } from '../server/dev/parse-version-info' import { @@ -85,6 +83,12 @@ type Dispatch = ReturnType[1] let maybeDispatch: Dispatch | null = null const queue: Array<(dispatch: Dispatch) => void> = [] +function loadDevOverlayUX() { + const { DevOverlay, FontStyles } = + require('./dev-overlay-ux') as typeof import('./dev-overlay-ux') + return { DevOverlay, FontStyles } +} + // Global state store for accessing current overlay state from outside React context type OverlayStateWithRouter = OverlayState & { routerType: 'pages' | 'app' } @@ -302,6 +306,12 @@ function DevOverlayRoot({ } }, []) + if (process.env.__NEXT_DISABLE_DEV_OVERLAY_UX) { + return null + } + + const { DevOverlay, FontStyles } = loadDevOverlayUX() + return ( <> {/* Fonts can only be loaded outside the Shadow DOM. */} @@ -351,22 +361,25 @@ export function renderAppDevOverlay( } if (!isAppMounted) { - // React 19 will not throw away `