diff --git a/crates/next-core/src/next_config.rs b/crates/next-core/src/next_config.rs index dba9ce2f5558..c409d0331a6a 100644 --- a/crates/next-core/src/next_config.rs +++ b/crates/next-core/src/next_config.rs @@ -96,6 +96,7 @@ pub struct NextConfig { experimental: ExperimentalConfig, images: ImageConfig, page_extensions: Vec, + instrumentation_client_inject: Option>, react_compiler: Option, react_production_profiling: Option, react_strict_mode: Option, @@ -1219,7 +1220,6 @@ pub struct ExperimentalConfig { /// This field is kept for backwards compatibility during migration. cache_components: Option, use_cache: Option, - root_params: Option, runtime_server_deployment_id: Option, supports_immutable_assets: Option, @@ -1796,6 +1796,15 @@ impl NextConfig { Vc::cell(extensions) } + #[turbo_tasks::function] + pub fn instrumentation_client_inject(&self) -> Vc> { + Vc::cell( + self.instrumentation_client_inject + .clone() + .unwrap_or_default(), + ) + } + #[turbo_tasks::function] pub fn is_global_not_found_enabled(&self) -> Vc { Vc::cell(self.experimental.global_not_found.unwrap_or_default()) @@ -2169,16 +2178,6 @@ impl NextConfig { ) } - #[turbo_tasks::function] - pub fn enable_root_params(&self) -> Vc { - Vc::cell( - self.experimental - .root_params - // rootParams should be enabled implicitly in cacheComponents. - .unwrap_or(self.cache_components.unwrap_or(false)), - ) - } - #[turbo_tasks::function] pub fn is_using_adapter(&self) -> Vc { Vc::cell(self.adapter_path.is_some()) diff --git a/crates/next-core/src/next_import_map.rs b/crates/next-core/src/next_import_map.rs index 0579532e00e7..31da89706419 100644 --- a/crates/next-core/src/next_import_map.rs +++ b/crates/next-core/src/next_import_map.rs @@ -7,12 +7,13 @@ use next_taskless::{EDGE_NODE_EXTERNALS, NODE_EXTERNALS}; use rustc_hash::FxHashMap; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{FxIndexMap, ResolvedVc, Vc, fxindexmap}; -use turbo_tasks_fs::{FileSystem, FileSystemPath, to_sys_path}; +use turbo_tasks_fs::{FileContent, FileSystem, FileSystemPath, to_sys_path}; use turbopack_core::{ + asset::AssetContent, issue::{Issue, IssueExt, IssueSeverity, IssueStage, StyledString}, reference_type::{CommonJsReferenceSubType, ReferenceType}, resolve::{ - AliasPattern, ExternalTraced, ExternalType, ResolveAliasMap, SubpathValue, + AliasPattern, ExternalTraced, ExternalType, ResolveAliasMap, ResolveResult, SubpathValue, node::node_cjs_resolve_options, options::{ConditionValue, ImportMap, ImportMapping, ResolvedMap}, parse::Request, @@ -20,6 +21,7 @@ use turbopack_core::{ resolve, }, source::Source, + virtual_source::VirtualSource, }; use turbopack_node::execution_context::ExecutionContext; @@ -209,13 +211,7 @@ pub async fn get_next_client_import_map( rcstr!("next/dist/compiled/server-only") => rcstr!("next/dist/compiled/server-only/index"), rcstr!("next/dist/compiled/client-only") => rcstr!("next/dist/compiled/client-only/index"),}, ); - insert_next_root_params_mapping( - &mut import_map, - next_config.enable_root_params(), - Either::Right(ty.clone()), - None, - ) - .await?; + insert_next_root_params_mapping(&mut import_map, Either::Right(ty.clone()), None).await?; match ty { ClientContextType::Pages { .. } @@ -231,7 +227,7 @@ pub async fn get_next_client_import_map( ClientContextType::Other => {} } - insert_instrumentation_client_alias(&mut import_map, project_path).await?; + insert_instrumentation_client_alias(&mut import_map, project_path, next_config).await?; insert_server_only_error_alias(&mut import_map); @@ -745,13 +741,7 @@ async fn insert_next_server_special_aliases( } } - insert_next_root_params_mapping( - import_map, - next_config.enable_root_params(), - Either::Left(ty), - collected_root_params, - ) - .await?; + insert_next_root_params_mapping(import_map, Either::Left(ty), collected_root_params).await?; import_map.insert_exact_alias( rcstr!("@vercel/og"), @@ -1376,24 +1366,81 @@ fn insert_package_alias(import_map: &mut ImportMap, prefix: &str, package_root: ); } -/// Handles instrumentation-client.ts bundling logic +/// Handles instrumentation-client.ts bundling logic. +/// +/// Resolves the `private-next-instrumentation-client` alias to a virtual module +/// that first requires each entry of `instrumentationClientInject` for side +/// effects (in array order) and then re-exports the user's +/// `instrumentation-client.{pageExt}` file via the +/// `private-next-instrumentation-client-user` alias. async fn insert_instrumentation_client_alias( import_map: &mut ImportMap, project_path: FileSystemPath, + next_config: Vc, ) -> Result<()> { + let user_file_alternatives = vec![ + request_to_import_mapping(project_path.clone(), rcstr!("./src/instrumentation-client")), + request_to_import_mapping( + project_path.clone(), + rcstr!("./src/instrumentation-client.ts"), + ), + request_to_import_mapping(project_path.clone(), rcstr!("./instrumentation-client")), + request_to_import_mapping(project_path.clone(), rcstr!("./instrumentation-client.ts")), + ImportMapping::Ignore.resolved_cell(), + ]; + + let injects = next_config.instrumentation_client_inject().await?; + + if injects.is_empty() { + insert_alias_to_alternatives( + import_map, + rcstr!("private-next-instrumentation-client"), + user_file_alternatives, + ); + return Ok(()); + } + + // The user file is reached through a separate alias so the existing + // alternative resolution stays unchanged. insert_alias_to_alternatives( import_map, + rcstr!("private-next-instrumentation-client-user"), + user_file_alternatives, + ); + + let injects = injects + .iter() + .map(|s| s.as_str()) + .chain(std::iter::once("private-next-instrumentation-client-user")); + + let mut body = String::new(); + for (i, spec) in injects.clone().enumerate() { + body.push_str(&format!( + "var mod_{i} = require({});\n", + serde_json::to_string(spec)? + )); + } + body.push_str("module.exports = { onRouterTransitionStart(url, type) {\n"); + for (i, _) in injects.enumerate() { + body.push_str(&format!( + " mod_{i}?.onRouterTransitionStart?.(url, type);\n" + )); + } + body.push_str("}};\n"); + + let virtual_source = VirtualSource::new( + project_path.join("__next_instrumentation_client.js")?, + AssetContent::file(FileContent::Content(body.into()).cell()), + ) + .to_resolved() + .await?; + + import_map.insert_exact_alias( rcstr!("private-next-instrumentation-client"), - vec![ - request_to_import_mapping(project_path.clone(), rcstr!("./src/instrumentation-client")), - request_to_import_mapping( - project_path.clone(), - rcstr!("./src/instrumentation-client.ts"), - ), - request_to_import_mapping(project_path.clone(), rcstr!("./instrumentation-client")), - request_to_import_mapping(project_path.clone(), rcstr!("./instrumentation-client.ts")), - ImportMapping::Ignore.resolved_cell(), - ], + ImportMapping::Direct( + ResolveResult::source(ResolvedVc::upcast(virtual_source)).resolved_cell(), + ) + .resolved_cell(), ); Ok(()) diff --git a/crates/next-core/src/next_root_params/mod.rs b/crates/next-core/src/next_root_params/mod.rs index c9a3acdb6818..0e291da2deda 100644 --- a/crates/next-core/src/next_root_params/mod.rs +++ b/crates/next-core/src/next_root_params/mod.rs @@ -30,26 +30,20 @@ use crate::{ pub async fn insert_next_root_params_mapping( import_map: &mut ImportMap, - is_root_params_enabled: Vc, ty: Either, collected_root_params: Option>, ) -> Result<()> { import_map.insert_exact_alias( "next/root-params", - get_next_root_params_mapping( - is_root_params_enabled, - EitherTaskInput(ty), - collected_root_params, - ) - .to_resolved() - .await?, + get_next_root_params_mapping(EitherTaskInput(ty), collected_root_params) + .to_resolved() + .await?, ); Ok(()) } #[turbo_tasks::function] async fn get_next_root_params_mapping( - is_root_params_enabled: Vc, ty: EitherTaskInput, collected_root_params: Option>, ) -> Result> { @@ -61,7 +55,7 @@ async fn get_next_root_params_mapping( // `collected_root_params` changes, the resolve options will remain the same, and // only the mapping result will be invalidated. let mapping = ImportMapping::Dynamic(ResolvedVc::upcast( - NextRootParamsMapper::new(is_root_params_enabled, ty, collected_root_params) + NextRootParamsMapper::new(ty, collected_root_params) .to_resolved() .await?, )); @@ -70,7 +64,6 @@ async fn get_next_root_params_mapping( #[turbo_tasks::value] struct NextRootParamsMapper { - is_root_params_enabled: ResolvedVc, #[bincode(with = "turbo_bincode::either")] context_type: Either, collected_root_params: Option>, @@ -80,12 +73,10 @@ struct NextRootParamsMapper { impl NextRootParamsMapper { #[turbo_tasks::function] pub fn new( - is_root_params_enabled: ResolvedVc, context_type: EitherTaskInput, collected_root_params: Option>, ) -> Vc { NextRootParamsMapper { - is_root_params_enabled, context_type: context_type.0, collected_root_params, } @@ -95,61 +86,47 @@ impl NextRootParamsMapper { #[turbo_tasks::function] async fn import_map_result(self: Vc) -> Result> { let this = self.await?; - Ok({ - if !(*this.is_root_params_enabled.await?) { + Ok(match &this.context_type { + Either::Left(server_ty) => match &server_ty { + ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } => { + let collected_root_params = *this.collected_root_params.ok_or_else(|| { + anyhow!( + "Invariant: Root params should have been collected for context {:?}. \ + This is a bug in Next.js.", + server_ty.clone() + ) + })?; + Self::valid_import_map_result(collected_root_params) + } + ServerContextType::PagesApi { .. } + | ServerContextType::Instrumentation { .. } + | ServerContextType::Middleware { .. } => { + // There's no sensible way to use root params outside of the app + // directory. TODO: make sure this error is consistent with webpack + Self::invalid_import_map_result( + "'next/root-params' can only be used inside the App Directory.".into(), + ) + } + _ => { + // In general, the compiler should prevent importing 'next/root-params' + // from client modules, but it doesn't catch everything. If an import + // slips through our validation, make it error. + Self::invalid_import_map_result( + "'next/root-params' cannot be imported from a Client Component module. It \ + should only be used from a Server Component." + .into(), + ) + } + }, + Either::Right(_) => { + // In general, the compiler should prevent importing 'next/root-params' from + // client modules, but it doesn't catch everything. If an import slips + // through our validation, make it error. Self::invalid_import_map_result( - "'next/root-params' can only be imported when `experimental.rootParams` is \ - enabled." + "'next/root-params' cannot be imported from a Client Component module. It \ + should only be used from a Server Component." .into(), ) - } else { - match &this.context_type { - Either::Left(server_ty) => match &server_ty { - ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } => { - let collected_root_params = - *this.collected_root_params.ok_or_else(|| { - anyhow!( - "Invariant: Root params should have been collected for \ - context {:?}. This is a bug in Next.js.", - server_ty.clone() - ) - })?; - Self::valid_import_map_result(collected_root_params) - } - ServerContextType::PagesApi { .. } - | ServerContextType::Instrumentation { .. } - | ServerContextType::Middleware { .. } => { - // There's no sensible way to use root params outside of the app - // directory. TODO: make sure this error is - // consistent with webpack - Self::invalid_import_map_result( - "'next/root-params' can only be used inside the App Directory." - .into(), - ) - } - _ => { - // In general, the compiler should prevent importing 'next/root-params' - // from client modules, but it doesn't catch - // everything. If an import slips through - // our validation, make it error. - Self::invalid_import_map_result( - "'next/root-params' cannot be imported from a Client Component \ - module. It should only be used from a Server Component." - .into(), - ) - } - }, - Either::Right(_) => { - // In general, the compiler should prevent importing 'next/root-params' from - // client modules, but it doesn't catch everything. If an - // import slips through our validation, make it error. - Self::invalid_import_map_result( - "'next/root-params' cannot be imported from a Client Component \ - module. It should only be used from a Server Component." - .into(), - ) - } - } } }) } diff --git a/packages/next/src/build/create-compiler-aliases.ts b/packages/next/src/build/create-compiler-aliases.ts index a258469fc5f7..c9ef02997af2 100644 --- a/packages/next/src/build/create-compiler-aliases.ts +++ b/packages/next/src/build/create-compiler-aliases.ts @@ -25,6 +25,17 @@ interface CompilerAliases { const isReact19 = typeof React.use === 'function' +/** + * Absolute path to the placeholder file that `private-next-instrumentation-client` + * resolves to. Its contents are replaced at build time by + * `next-instrumentation-client-loader` via a `module.rules` entry in + * `webpack-config.ts`. + */ +const INSTRUMENTATION_CLIENT_STUB_PATH = path.join( + NEXT_PROJECT_ROOT, + 'dist/build/webpack/loaders/instrumentation-client-stub.js' +) + export function createWebpackAliases({ distDir, isClient, @@ -138,7 +149,17 @@ export function createWebpackAliases({ [ROOT_DIR_ALIAS]: dir, ...(isClient ? { - 'private-next-instrumentation-client': [ + // `private-next-instrumentation-client` resolves to a placeholder + // file whose contents are replaced at build time by + // `next-instrumentation-client-loader` (registered via a + // `module.rules` entry in webpack-config.ts). The emitted module + // requires each `instrumentationClientInject` entry for side effects, + // then re-exports the user's `instrumentation-client.{pageExt}` file + // (resolved through the `private-next-instrumentation-client-user` + // alias below). + 'private-next-instrumentation-client': + INSTRUMENTATION_CLIENT_STUB_PATH, + 'private-next-instrumentation-client-user': [ path.join(dir, 'src', 'instrumentation-client'), path.join(dir, 'instrumentation-client'), 'private-next-empty-module', diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index bba1cd4a6529..ed0149b61654 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -1430,8 +1430,7 @@ export default async function build( await writeRootParamsTypes( routeTypesManifest, - path.join(distDir, 'types', 'root-params.d.ts'), - config + path.join(distDir, 'types', 'root-params.d.ts') ) }) diff --git a/packages/next/src/build/type-check.ts b/packages/next/src/build/type-check.ts index 3b2fe44a8722..861f31d3b36e 100644 --- a/packages/next/src/build/type-check.ts +++ b/packages/next/src/build/type-check.ts @@ -31,8 +31,7 @@ function verifyAndRunTypeScript( hasPagesDir: boolean, appDir: string | undefined, pagesDir: string | undefined, - debugBuildPaths: { app: string[]; pages: string[] } | undefined, - rootParams: boolean + debugBuildPaths: { app: string[]; pages: string[] } | undefined ) { let impl: typeof import('../lib/verify-typescript-setup').verifyAndRunTypeScript let typeCheckWorker: @@ -75,7 +74,6 @@ function verifyAndRunTypeScript( appDir, pagesDir, debugBuildPaths, - rootParams, }) .then((result) => { typeCheckWorker?.end() @@ -144,8 +142,7 @@ export async function startTypeChecking({ !!pagesDir, appDir, pagesDir, - debugBuildPaths, - !!config.experimental.rootParams || !!config.cacheComponents + debugBuildPaths ).then((resolved) => { const checkEnd = process.hrtime(typeCheckAndLintStart) return [resolved, checkEnd] as const diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 87ca8879d491..59be2705f54e 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1360,6 +1360,7 @@ export default async function getBaseWebpackConfig( 'error-loader', 'next-swc-loader', 'next-client-pages-loader', + 'next-instrumentation-client-loader', 'next-image-loader', 'next-metadata-image-loader', 'next-style-loader', @@ -1576,11 +1577,6 @@ export default async function getBaseWebpackConfig( : []), ...getNextRootParamsRules({ - isRootParamsEnabled: - config.experimental.rootParams ?? - // `cacheComponents` implies `experimental.rootParams`. - config.cacheComponents ?? - false, isClient, appDir, pageExtensions, @@ -1933,6 +1929,20 @@ export default async function getBaseWebpackConfig( test: /[\\/]next[\\/]dist[\\/](esm[\\/])?build[\\/]webpack[\\/]loaders[\\/]next-flight-loader[\\/]action-client-wrapper\.js/, sideEffects: false, }, + // The placeholder file aliased from `private-next-instrumentation-client`. + // The loader replaces its contents with a synthetic module that + // requires each `instrumentationClientInject` entry, then re-exports + // the user's `instrumentation-client.{pageExt}` (composing + // `onRouterTransitionStart` hooks across all of them). + { + test: /[\\/]next[\\/]dist[\\/](esm[\\/])?build[\\/]webpack[\\/]loaders[\\/]instrumentation-client-stub\.js$/, + use: { + loader: 'next-instrumentation-client-loader', + options: { + injects: JSON.stringify(config.instrumentationClientInject), + }, + }, + }, { // This loader rule should be before other rules, as it can output code // that still contains `"use client"` or `"use server"` statements that @@ -2866,12 +2876,10 @@ export default async function getBaseWebpackConfig( } function getNextRootParamsRules({ - isRootParamsEnabled, isClient, appDir, pageExtensions, }: { - isRootParamsEnabled: boolean isClient: boolean appDir: string | undefined pageExtensions: string[] @@ -2889,15 +2897,6 @@ function getNextRootParamsRules({ } satisfies webpack.RuleSetRule } - // Hard-error if the flag is not enabled, regardless of if we're on the server or on the client. - if (!isRootParamsEnabled) { - return [ - createInvalidImportRule( - "'next/root-params' can only be imported when `experimental.rootParams` is enabled." - ), - ] - } - // If there's no app-dir (and thus no layouts), there's no sensible way to use 'next/root-params', // because we wouldn't generate any getters. if (!appDir) { diff --git a/packages/next/src/build/webpack/loaders/instrumentation-client-stub.js b/packages/next/src/build/webpack/loaders/instrumentation-client-stub.js new file mode 100644 index 000000000000..e61ff82110d4 --- /dev/null +++ b/packages/next/src/build/webpack/loaders/instrumentation-client-stub.js @@ -0,0 +1,5 @@ +// Placeholder module. The `private-next-instrumentation-client` webpack alias +// resolves to this file so the resolver has a real path to find; its contents +// are then replaced by `next-instrumentation-client-loader` via a `module.rules` +// entry in webpack-config.ts. +module.exports = {} diff --git a/packages/next/src/build/webpack/loaders/next-instrumentation-client-loader.ts b/packages/next/src/build/webpack/loaders/next-instrumentation-client-loader.ts new file mode 100644 index 000000000000..2c9efe1ab16d --- /dev/null +++ b/packages/next/src/build/webpack/loaders/next-instrumentation-client-loader.ts @@ -0,0 +1,73 @@ +import { promisify } from 'util' +import type { webpack } from 'next/dist/compiled/webpack/webpack' + +/** + * Loader options for `next-instrumentation-client-loader`. The list of inject + * specifiers is JSON-stringified so it can travel through the loader query + * string. + */ +export type InstrumentationClientLoaderOptions = { + /** JSON-stringified `string[]` of module specifiers. */ + injects: string +} + +const NextInstrumentationClientLoader: webpack.LoaderDefinitionFunction = + function () { + const callback = this.async() + const { injects: injectsStringified } = this.getOptions() + const injects = JSON.parse(injectsStringified || '[]') as string[] + + // No injects: the alias is a transparent passthrough to the user's + // `instrumentation-client.{pageExt}` (or the empty module fallback). + if (injects.length === 0) { + callback( + null, + `module.exports = require('private-next-instrumentation-client-user');\n` + ) + return + } + + // Resolve each inject specifier against the project root so the emitted + // `require()` calls don't get resolved relative to the stub's location + // inside `node_modules/next/`. Bare specifiers (npm package names) are + // resolved against the project's `node_modules`. + const resolve = promisify(this.resolve) + const rootContext = this.rootContext + + Promise.all(injects.map((spec) => resolve(rootContext, spec))) + .then((resolvedInjects) => { + const allModules = [ + ...resolvedInjects, + 'private-next-instrumentation-client-user', + ] + + const lines: string[] = [] + allModules.forEach((spec, i) => { + lines.push(`var mod_${i} = require(${JSON.stringify(spec)});`) + }) + + // Compose a single `onRouterTransitionStart` that fans out to every + // module's hook (when exported), in array order, with the user file's + // hook running last. + const hookCalls = allModules + .map( + // Webpack doesn't transpile this, so use a manual version of optional chaining. + (_, i) => + ` mod_${i} && mod_${i}.onRouterTransitionStart && mod_${i}.onRouterTransitionStart(url, type);` + ) + .join('\n') + + lines.push( + `module.exports = {`, + ` onRouterTransitionStart: function (url, type) {`, + hookCalls, + ` },`, + `};` + ) + + callback(null, lines.join('\n') + '\n') + }) + .catch((err) => callback(err)) + } + +export default NextInstrumentationClientLoader diff --git a/packages/next/src/cli/next-test.ts b/packages/next/src/cli/next-test.ts index 166678d9aa2d..f48d76b88e23 100644 --- a/packages/next/src/cli/next-test.ts +++ b/packages/next/src/cli/next-test.ts @@ -149,8 +149,6 @@ async function runPlaywright( hasPagesDir: !!pagesDir, appDir: appDir || undefined, pagesDir: pagesDir || undefined, - rootParams: - !!nextConfig.experimental.rootParams || !!nextConfig.cacheComponents, }) const isUsingTypeScript = !!typeScriptVersion diff --git a/packages/next/src/cli/next-typegen.ts b/packages/next/src/cli/next-typegen.ts index 27fbdf8d7c16..976990959a92 100644 --- a/packages/next/src/cli/next-typegen.ts +++ b/packages/next/src/cli/next-typegen.ts @@ -55,8 +55,6 @@ const nextTypegen = async ( hasPagesDir: !!pagesDir, appDir: appDir || undefined, pagesDir: pagesDir || undefined, - rootParams: - !!nextConfig.experimental.rootParams || !!nextConfig.cacheComponents, }) console.log('Generating route types...') @@ -117,8 +115,7 @@ const nextTypegen = async ( await writeRootParamsTypes( routeTypesManifest, - join(distDir, 'types', 'root-params.d.ts'), - nextConfig + join(distDir, 'types', 'root-params.d.ts') ) console.log('✓ Types generated successfully') diff --git a/packages/next/src/client/.vercel.approvers b/packages/next/src/client/.vercel.approvers deleted file mode 100644 index 278e04f7809c..000000000000 --- a/packages/next/src/client/.vercel.approvers +++ /dev/null @@ -1,3 +0,0 @@ -use-intersection.tsx @timneutkens:notify -use-intersection.tsx @ijjk:notify -use-intersection.tsx @shuding:notify diff --git a/packages/next/src/client/components/segment-cache/cache.ts b/packages/next/src/client/components/segment-cache/cache.ts index d0ecf46e1cdb..3430c3ac7ffc 100644 --- a/packages/next/src/client/components/segment-cache/cache.ts +++ b/packages/next/src/client/components/segment-cache/cache.ts @@ -2594,6 +2594,24 @@ function fulfillEntrySpawnedByRuntimePrefetch( PendingSegmentCacheEntry > | null ) { + // Decide whether to re-key the entry under a more generic vary path based on + // which params the segment actually depends on. + // + // Skip re-keying for Full prefetches: as of today, `varyParams` tracking only + // works within the static stage portion of a response. A Full prefetch + // response covers all stages, and we can't track params during the dynamic + // stage without dead-locking the Flight stream, so the server-reported set is + // incomplete and can't be trusted for the full response. Re-keying with an + // untrustworthy set could replace concrete params with Fallback and let + // unrelated URLs read each other's content from the cache. + // + // When non-null, this is the param set to re-key by; when null, the entry + // stays keyed by the request's concrete vary path. + const fulfilledVaryParams = + process.env.__NEXT_VARY_PARAMS && fetchStrategy !== FetchStrategy.Full + ? segmentVaryParams + : null + // We should only write into cache entries that are owned by us. Or create // a new one and write into that. We must never write over an entry that was // created by a different task, because that causes data races. @@ -2608,11 +2626,10 @@ function fulfillEntrySpawnedByRuntimePrefetch( staleAt, isPartial ) - // Re-key the entry based on which params the segment actually depends on. - if (process.env.__NEXT_VARY_PARAMS && segmentVaryParams !== null) { + if (fulfilledVaryParams !== null) { const fulfilledVaryPath = getFulfilledSegmentVaryPath( tree.varyPath, - segmentVaryParams + fulfilledVaryParams ) const isRevalidation = false setInCacheMap( @@ -2638,11 +2655,10 @@ function fulfillEntrySpawnedByRuntimePrefetch( staleAt, isPartial ) - // Re-key the entry based on which params the segment actually depends on. - if (process.env.__NEXT_VARY_PARAMS && segmentVaryParams !== null) { + if (fulfilledVaryParams !== null) { const fulfilledVaryPath = getFulfilledSegmentVaryPath( tree.varyPath, - segmentVaryParams + fulfilledVaryParams ) const isRevalidation = false setInCacheMap( @@ -2664,11 +2680,9 @@ function fulfillEntrySpawnedByRuntimePrefetch( staleAt, isPartial ) - // Use the fulfilled vary path if available, otherwise fall back to - // the request vary path. const varyPath = - process.env.__NEXT_VARY_PARAMS && segmentVaryParams !== null - ? getFulfilledSegmentVaryPath(tree.varyPath, segmentVaryParams) + fulfilledVaryParams !== null + ? getFulfilledSegmentVaryPath(tree.varyPath, fulfilledVaryParams) : getSegmentVaryPathForRequest(fetchStrategy, tree) upsertSegmentEntry(now, varyPath, newEntry) } diff --git a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts index 4832a3acefeb..22112f835159 100644 --- a/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts +++ b/packages/next/src/lib/typescript/writeAppTypeDeclarations.ts @@ -10,7 +10,6 @@ export async function writeAppTypeDeclarations({ hasAppDir, strictRouteTypes, typedRoutes, - rootParams, }: { baseDir: string distDir: string @@ -19,7 +18,6 @@ export async function writeAppTypeDeclarations({ hasAppDir: boolean strictRouteTypes: boolean typedRoutes: boolean - rootParams: boolean }): Promise { // Reference `next` types const appTypeDeclarations = path.join(baseDir, 'next-env.d.ts') @@ -71,13 +69,11 @@ export async function writeAppTypeDeclarations({ // Use ESM import instead of triple-slash reference for better ESLint compatibility lines.push(`import "./${routeTypesPath}";`) - if (rootParams) { - const rootParamsTypesPath = path.posix.join( - distDir.replaceAll(path.win32.sep, path.posix.sep), - 'types/root-params.d.ts' - ) - lines.push(`import "./${rootParamsTypesPath}";`) - } + const rootParamsTypesPath = path.posix.join( + distDir.replaceAll(path.win32.sep, path.posix.sep), + 'types/root-params.d.ts' + ) + lines.push(`import "./${rootParamsTypesPath}";`) if (strictRouteTypes) { const cacheLifePath = path.posix.join( diff --git a/packages/next/src/lib/verify-typescript-setup.ts b/packages/next/src/lib/verify-typescript-setup.ts index 3983696d6ba2..52c63d869348 100644 --- a/packages/next/src/lib/verify-typescript-setup.ts +++ b/packages/next/src/lib/verify-typescript-setup.ts @@ -67,7 +67,6 @@ export async function verifyAndRunTypeScript({ appDir, pagesDir, debugBuildPaths, - rootParams, }: { dir: string distDir: string @@ -82,7 +81,6 @@ export async function verifyAndRunTypeScript({ appDir?: string pagesDir?: string debugBuildPaths?: { app?: string[]; pages?: string[] } - rootParams?: boolean }): Promise<{ result?: TypeCheckResult; version: string | null }> { const tsConfigFileName = tsconfigPath || 'tsconfig.json' const resolvedTsConfigPath = path.join(dir, tsConfigFileName) @@ -133,7 +131,6 @@ export async function verifyAndRunTypeScript({ hasAppDir, strictRouteTypes, typedRoutes, - rootParams: !!rootParams, }) return { version: null } @@ -217,7 +214,6 @@ export async function verifyAndRunTypeScript({ hasAppDir, strictRouteTypes, typedRoutes, - rootParams: !!rootParams, }) let result diff --git a/packages/next/src/server/.vercel.approvers b/packages/next/src/server/.vercel.approvers deleted file mode 100644 index f22d7dcea72b..000000000000 --- a/packages/next/src/server/.vercel.approvers +++ /dev/null @@ -1,8 +0,0 @@ -config.ts @timneutkens:notify -config.ts @ijjk:notify -config.ts @shuding:notify -config.ts @huozhi:notify -serve-static.ts @timneutkens:notify -serve-static.ts @ijjk:notify -serve-static.ts @shuding:notify -serve-static.ts @huozhi:notify diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 72cf0849feae..dcda307a385c 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -762,6 +762,7 @@ export const configSchema: zod.ZodType = z.lazy(() => .record(z.string(), z.array(z.string())) .optional(), pageExtensions: z.array(z.string()).min(1).optional(), + instrumentationClientInject: z.array(z.string()).optional(), poweredByHeader: z.boolean().optional(), productionBrowserSourceMaps: z.boolean().optional(), reactCompiler: z.union([ diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 72c7610e105e..a56de95104e2 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -1196,11 +1196,6 @@ export interface ExperimentalConfig { showSourceLocation?: boolean } - /** - * Enable accessing root params via the `next/root-params` module. - */ - rootParams?: boolean - /** * Body size limit for request bodies with middleware configured. * Defaults to 10MB. Can be specified as a number (bytes) or string (e.g. '5mb'). @@ -1527,6 +1522,13 @@ export interface NextConfig { /** @see [Including non-page files in the pages directory](https://nextjs.org/docs/app/api-reference/config/next-config-js/pageExtensions) */ pageExtensions?: string[] + /** + * Module specifiers that are required for side effects on the client before + * hydration, in array order, ahead of the user's `instrumentation-client.{ts,js}`. + * Each entry may be a bare npm package name or a path relative to the project root. + */ + instrumentationClientInject?: string[] + /** @see [Compression documentation](https://nextjs.org/docs/app/api-reference/config/next-config-js/compress) */ compress?: boolean @@ -1874,6 +1876,7 @@ export const defaultConfig = Object.freeze({ generateBuildId: () => null, generateEtags: true, pageExtensions: ['tsx', 'ts', 'jsx', 'js'], + instrumentationClientInject: [], poweredByHeader: true, compress: true, images: imageConfigDefault, diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index 2c4827f7abdd..4125151a83d5 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -180,6 +180,13 @@ function checkDeprecations( silent ) + warnOptionHasBeenDeprecated( + userConfig, + 'experimental.rootParams', + `\`experimental.rootParams\` is no longer needed, because \`next/root-params\` is available by default. You can remove it from ${configFileName}.`, + silent + ) + warnOptionHasBeenDeprecated( userConfig, 'eslint', diff --git a/packages/next/src/server/lib/router-utils/root-params-type-utils.ts b/packages/next/src/server/lib/router-utils/root-params-type-utils.ts index 8729d6cd17c8..953a21fd4cdf 100644 --- a/packages/next/src/server/lib/router-utils/root-params-type-utils.ts +++ b/packages/next/src/server/lib/router-utils/root-params-type-utils.ts @@ -1,7 +1,6 @@ import fs from 'fs' import path from 'path' import type { RouteTypesManifest } from './route-types-utils' -import type { NextConfigComplete } from '../../config-shared' export type RootParamValueType = 'string' | 'string[]' | 'undefined' @@ -45,24 +44,15 @@ function getRootParamReturnType(valueTypes: RootParamInfo): string { } /** - * Writes root-params type definitions to a file if the feature is enabled - * and root params were collected from layouts. + * Writes root-params type definitions to a file if root params were collected + * from layouts. */ export async function writeRootParamsTypes( manifest: RouteTypesManifest, - filePath: string, - config: NextConfigComplete + filePath: string ) { const rootParams = manifest.rootParams - const featureEnabled = - !!config.experimental.rootParams || !!config.cacheComponents - - if (!featureEnabled) { - await fs.promises.rm(filePath, { force: true }) - return - } - await fs.promises.mkdir(path.dirname(filePath), { recursive: true }) if (!rootParams.size) { diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 504ce9c4953c..d91df07f43cb 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -165,9 +165,6 @@ async function verifyTypeScript(opts: SetupOpts) { hasPagesDir: !!opts.pagesDir, appDir: opts.appDir, pagesDir: opts.pagesDir, - rootParams: - !!opts.nextConfig.experimental.rootParams || - !!opts.nextConfig.cacheComponents, }) if (verifyResult.version) { @@ -1200,8 +1197,7 @@ async function startWatcher( await writeRootParamsTypes( routeTypesManifest, - path.join(distTypesDir, 'root-params.d.ts'), - opts.nextConfig + path.join(distTypesDir, 'root-params.d.ts') ) } diff --git a/packages/next/src/server/lib/trace/tracer.ts b/packages/next/src/server/lib/trace/tracer.ts index b7ffe39cf32b..8866835fd73e 100644 --- a/packages/next/src/server/lib/trace/tracer.ts +++ b/packages/next/src/server/lib/trace/tracer.ts @@ -250,6 +250,14 @@ class NextTracerImpl implements NextTracer { ): T { const activeContext = context.active() + if ( + !NEXT_OTEL_PERFORMANCE_PREFIX && + !this.isTracingEnabled() && + !trace.getSpanContext(activeContext) + ) { + return fn() + } + if (force) { const remoteContext = propagation.extract(ROOT_CONTEXT, carrier, getter) diff --git a/packages/next/src/server/next.ts b/packages/next/src/server/next.ts index c9e2a0af88f8..adf3cc25b4d1 100644 --- a/packages/next/src/server/next.ts +++ b/packages/next/src/server/next.ts @@ -149,10 +149,13 @@ export class NextServer implements NextWrapperServer { res: ServerResponse, parsedUrl?: NextUrlWithParsedQuery ) => { - return getTracer().trace(NextServerSpan.getRequestHandler, async () => { - const requestHandler = await this.getServerRequestHandler() - return requestHandler(req, res, parsedUrl) - }) + const tracer = getTracer() + return tracer.withPropagatedContext(req.headers, () => + tracer.trace(NextServerSpan.getRequestHandler, async () => { + const requestHandler = await this.getServerRequestHandler() + return requestHandler(req, res, parsedUrl) + }) + ) } } @@ -166,13 +169,13 @@ export class NextServer implements NextWrapperServer { res: ServerResponse, parsedUrl?: NextUrlWithParsedQuery ) => { - return getTracer().trace( - NextServerSpan.getRequestHandlerWithMetadata, - async () => { + const tracer = getTracer() + return tracer.withPropagatedContext(req.headers, () => + tracer.trace(NextServerSpan.getRequestHandlerWithMetadata, async () => { const server = await this.getServer() const handler = server.getRequestHandlerWithMetadata(meta) return handler(req, res, parsedUrl) - } + }) ) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index edaec9faea43..d953ad71ae8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2004,6 +2004,9 @@ importers: inquirer: specifier: ^9.2.7 version: 9.2.23 + jstat: + specifier: ^1.9.6 + version: 1.9.6 minimist: specifier: ^1.2.8 version: 1.2.8 @@ -12360,6 +12363,9 @@ packages: resolution: {integrity: sha512-4Dj8Rf+fQ+/Pn7C5qeEX02op1WfOss3PKTE9Nsop3Dx+6UPxlm1dr/og7o2cRa5hNN07CACr4NFzRLtj/rjWog==} engines: {'0': node >=0.6.0} + jstat@1.9.6: + resolution: {integrity: sha512-rPBkJbK2TnA8pzs93QcDDPlKcrtZWuuCo2dVR0TFLOJSxhqfWOVCSp8aV3/oSbn+4uY4yw1URtLpHQedtmXfug==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -16519,6 +16525,7 @@ packages: sliced@1.0.1: resolution: {integrity: sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA==} + deprecated: Unsupported slide@1.1.6: resolution: {integrity: sha512-NwrtjCg+lZoqhFU8fOwl4ay2ei8PaqCBOUV3/ektPY9trO1yQ1oXEfmHAhKArUVUr/hOHvy5f6AdP17dCM0zMw==} @@ -17963,11 +17970,11 @@ packages: uuid@2.0.3: resolution: {integrity: sha512-FULf7fayPdpASncVy4DLh3xydlXEJJpvIELjYjNeQWYUZ9pclcpvCZSr2gkmN2FrrGcI7G/cJsIEwk5/8vfXpg==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). uuid@3.3.3: resolution: {integrity: sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==} - deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true uuid@8.3.2: @@ -23774,7 +23781,7 @@ snapshots: dependencies: '@typescript-eslint/tsconfig-utils': 8.46.0(typescript@6.0.2) '@typescript-eslint/types': 8.46.0 - debug: 4.4.0 + debug: 4.4.3 typescript: 6.0.2 transitivePeerDependencies: - supports-color @@ -23798,7 +23805,7 @@ snapshots: '@typescript-eslint/types': 8.46.0 '@typescript-eslint/typescript-estree': 8.46.0(typescript@6.0.2) '@typescript-eslint/utils': 8.46.0(eslint@9.37.0(jiti@2.6.1))(typescript@6.0.2) - debug: 4.4.0 + debug: 4.4.3 eslint: 9.37.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@6.0.2) typescript: 6.0.2 @@ -27337,7 +27344,7 @@ snapshots: esbuild-register@3.6.0(esbuild@0.25.9): dependencies: - debug: 4.4.0 + debug: 4.4.3 esbuild: 0.25.9 transitivePeerDependencies: - supports-color @@ -30863,6 +30870,8 @@ snapshots: json-schema: 0.2.3 verror: 1.10.0 + jstat@1.9.6: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 diff --git a/test/development/acceptance-app/rsc-build-errors.test.ts b/test/development/acceptance-app/rsc-build-errors.test.ts index fc816123aba2..5a7545837dd7 100644 --- a/test/development/acceptance-app/rsc-build-errors.test.ts +++ b/test/development/acceptance-app/rsc-build-errors.test.ts @@ -371,9 +371,7 @@ describe('Error overlay - RSC build errors', () => { }) describe('next/root-params', () => { - const isCacheComponentsEnabled = - process.env.__NEXT_CACHE_COMPONENTS === 'true' - it("importing 'next/root-params' when experimental.rootParams is not enabled", async () => { + it("importing a non-existent getter from 'next/root-params'", async () => { await using sandbox = await createSandbox( next, undefined, @@ -381,16 +379,13 @@ describe('Error overlay - RSC build errors', () => { ) const { session } = sandbox await session.waitForRedbox() - if (!isCacheComponentsEnabled) { + if (isTurbopack) { expect(await session.getRedboxSource()).toInclude( - `'next/root-params' can only be imported when \`experimental.rootParams\` is enabled.` + `Export whatever doesn't exist in target module` ) } else { - // in cacheComponents we auto-enable 'next/root-params', so we should get an error about using a non-existent getter instead. - expect(await session.getRedboxSource()).toInclude( - isTurbopack - ? `Export whatever doesn't exist in target module` - : `Attempted import error: 'whatever' is not exported from 'next/root-params' (imported as 'whatever').` + expect(await session.getRedboxDescription()).toInclude( + `whatever) is not a function` ) } }) @@ -398,17 +393,7 @@ describe('Error overlay - RSC build errors', () => { it("importing 'next/root-params' in a client component", async () => { await using sandbox = await createSandbox( next, - // if cacheComponents is not enabled, the import is guarded behind an experimental flag - isCacheComponentsEnabled - ? new Map() - : new Map([ - [ - 'next.config.js', - outdent` - module.exports = { experimental: { rootParams: true } } - `, - ], - ]), + undefined, `/server-with-errors/next-root-params/in-client` ) const { session } = sandbox @@ -421,17 +406,7 @@ describe('Error overlay - RSC build errors', () => { it("importing 'next/root-params' in a client component in a way that bypasses import analysis", async () => { await using sandbox = await createSandbox( next, - // if cacheComponents is not enabled, the import is guarded behind an experimental flag - isCacheComponentsEnabled - ? new Map() - : new Map([ - [ - 'next.config.js', - outdent` - module.exports = { experimental: { rootParams: true } } - `, - ], - ]), + undefined, `/server-with-errors/next-root-params/in-client-await-import` ) const { session } = sandbox diff --git a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts index b46cf8d215cc..36e95ed4856d 100644 --- a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts +++ b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts @@ -306,13 +306,6 @@ describe('Error Overlay for server components compiler errors in pages', () => { } `, ], - [ - // the import is guarded behind an experimental flag - 'next.config.js', - outdent` - module.exports = { experimental: { rootParams: true } } - `, - ], ]) await using sandbox = await createSandbox(next, files) const { session } = sandbox diff --git a/test/development/typescript-app-type-declarations/typescript-app-type-declarations.test.ts b/test/development/typescript-app-type-declarations/typescript-app-type-declarations.test.ts index 09e96a0dc2c2..4c3c0f2e8e3f 100644 --- a/test/development/typescript-app-type-declarations/typescript-app-type-declarations.test.ts +++ b/test/development/typescript-app-type-declarations/typescript-app-type-declarations.test.ts @@ -15,6 +15,7 @@ const nextEnvDts = strictRouteTypes ? `/// /// import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; import "./.next/dev/types/cache-life.d.ts"; import "./.next/dev/types/validator.ts"; @@ -24,6 +25,7 @@ import "./.next/dev/types/validator.ts"; : `/// /// import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/generate-static-params-error/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/generate-static-params-error/next.config.ts index 6993a945b3c6..e4f5738a310b 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/generate-static-params-error/next.config.ts +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/generate-static-params-error/next.config.ts @@ -1,9 +1,5 @@ import type { NextConfig } from 'next' -const nextConfig: NextConfig = { - experimental: { - rootParams: true, - }, -} +const nextConfig: NextConfig = {} export default nextConfig diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/generate-static-params/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/generate-static-params/next.config.ts index 6f9923b8467f..fa33c7c54f24 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/generate-static-params/next.config.ts +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/generate-static-params/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { - cacheComponents: true, // implies `rootParams: true`. + cacheComponents: true, } export default nextConfig diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/multiple-roots/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/multiple-roots/next.config.ts index 6993a945b3c6..e4f5738a310b 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/multiple-roots/next.config.ts +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/multiple-roots/next.config.ts @@ -1,9 +1,5 @@ import type { NextConfig } from 'next' -const nextConfig: NextConfig = { - experimental: { - rootParams: true, - }, -} +const nextConfig: NextConfig = {} export default nextConfig diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/simple/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/simple/next.config.ts index 6993a945b3c6..e4f5738a310b 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/simple/next.config.ts +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/simple/next.config.ts @@ -1,9 +1,5 @@ import type { NextConfig } from 'next' -const nextConfig: NextConfig = { - experimental: { - rootParams: true, - }, -} +const nextConfig: NextConfig = {} export default nextConfig diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-build/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-build/next.config.ts index f9ec19251f03..a4ba114fc0f0 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-build/next.config.ts +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-build/next.config.ts @@ -3,7 +3,6 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { experimental: { useCache: true, - rootParams: true, }, } diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-dedup/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-dedup/next.config.ts index f9ec19251f03..a4ba114fc0f0 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-dedup/next.config.ts +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-dedup/next.config.ts @@ -3,7 +3,6 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { experimental: { useCache: true, - rootParams: true, }, } diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/next.config.ts index f9ec19251f03..a4ba114fc0f0 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/next.config.ts +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-private/next.config.ts @@ -3,7 +3,6 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { experimental: { useCache: true, - rootParams: true, }, } diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-runtime/next.config.ts b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-runtime/next.config.ts index f9ec19251f03..a4ba114fc0f0 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-runtime/next.config.ts +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/use-cache-runtime/next.config.ts @@ -3,7 +3,6 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { experimental: { useCache: true, - rootParams: true, }, } diff --git a/test/e2e/app-dir/ppr-root-param-rsc-fallback/next.config.js b/test/e2e/app-dir/ppr-root-param-rsc-fallback/next.config.js index 74b8350cb508..e64bae22d658 100644 --- a/test/e2e/app-dir/ppr-root-param-rsc-fallback/next.config.js +++ b/test/e2e/app-dir/ppr-root-param-rsc-fallback/next.config.js @@ -3,9 +3,6 @@ */ const nextConfig = { cacheComponents: true, - experimental: { - rootParams: true, - }, } module.exports = nextConfig diff --git a/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/[slug]/layout.tsx b/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/[slug]/layout.tsx deleted file mode 100644 index 836805e6038e..000000000000 --- a/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/[slug]/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import Link from 'next/link' -import { ReactNode } from 'react' - -export default function ParamsLayout({ children }: { children: ReactNode }) { - return ( - <> -
    -
  • - - /with-fallback-params/foo - -
  • -
  • - - /with-fallback-params/bar - -
  • -
- {children} - - ) -} diff --git a/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/layout.tsx b/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/layout.tsx new file mode 100644 index 000000000000..fcc69c74f16d --- /dev/null +++ b/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/layout.tsx @@ -0,0 +1,51 @@ +'use client' + +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { ReactNode } from 'react' +import { LinkAccordion } from '../../components/link-accordion' + +export default function ParamsLayout({ children }: { children: ReactNode }) { + const { bfcacheId } = useRouter() + + return ( + <> +
    +
  • + + /with-fallback-params + +
  • +
  • + + /with-fallback-params/foo + +
  • +
  • + + /with-fallback-params/bar + +
  • +
  • + + /with-fallback-params/foo (prefetch=true) + +
  • +
  • + + /with-fallback-params/bar (prefetch=true) + +
  • +
+ {children} + + ) +} diff --git a/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/page.tsx b/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/page.tsx new file mode 100644 index 000000000000..2dcc5a7d10d4 --- /dev/null +++ b/test/e2e/app-dir/segment-cache/cached-navigations/app/with-fallback-params/page.tsx @@ -0,0 +1,3 @@ +export default function FallbackParamsHub() { + return

Fallback Params Hub

+} diff --git a/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts b/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts index 95c7e234bceb..fdfb8197e8e1 100644 --- a/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts +++ b/test/e2e/app-dir/segment-cache/cached-navigations/cached-navigations.test.ts @@ -935,4 +935,58 @@ describe('cached navigations', () => { 'Dynamic content' ) }) + + it('does not leak resolved param-specific content across params when using prefetch={true}', async () => { + let page: Playwright.Page + const browser = await next.browser('/with-fallback-params', { + beforePageLoad(p: Playwright.Page) { + page = p + }, + }) + const act = createRouterAct(page) + + // 1. Unveil the foo link. With prefetch={true} this triggers a Full + // dynamic prefetch that returns the fully rendered page. + await act(async () => { + await browser + .elementByCss('input[data-link-accordion="/with-fallback-params/foo"]') + .click() + }) + + // 2. Navigate to /with-fallback-params/foo using the prefetched data. + await act(async () => { + await browser.elementByCss('a[href="/with-fallback-params/foo"]').click() + }, 'no-requests') + await retry(async () => { + expect(await browser.elementById('params-boundary').text()).toBe( + 'Param: foo' + ) + }) + + // 3. Return to the hub via the layout's back link. + await browser.elementByCss('a[href="/with-fallback-params"]')?.click() + await retry(async () => { + expect(await browser.elementByCss('h1').text()).toBe( + 'Fallback Params Hub' + ) + }) + + // 4. Unveil the bar link. + await act(async () => { + await browser + .elementByCss('input[data-link-accordion="/with-fallback-params/bar"]') + .click() + }) + + // 5. Navigate to /with-fallback-params/bar; should render bar's content + // (sourced from the bar prefetch in step 4), not foo's. + await act(async () => { + await browser.elementByCss('a[href="/with-fallback-params/bar"]').click() + }, 'no-requests') + await retry(async () => { + const barContent = await browser.elementById('params-boundary').text() + expect(barContent).toBe('Param: bar') + expect(barContent).not.toContain('foo') + }) + }) }) diff --git a/test/e2e/app-dir/segment-cache/cached-navigations/components/link-accordion.tsx b/test/e2e/app-dir/segment-cache/cached-navigations/components/link-accordion.tsx new file mode 100644 index 000000000000..fd8f6781732e --- /dev/null +++ b/test/e2e/app-dir/segment-cache/cached-navigations/components/link-accordion.tsx @@ -0,0 +1,33 @@ +'use client' + +import Link, { LinkProps } from 'next/link' +import { useState } from 'react' + +export function LinkAccordion({ + href, + children, + prefetch, +}: { + href: string + children: React.ReactNode + prefetch?: LinkProps['prefetch'] +}) { + const [isVisible, setIsVisible] = useState(false) + return ( + <> + setIsVisible(!isVisible)} + data-link-accordion={href} + /> + {isVisible ? ( + + {children} + + ) : ( + `${children} (link is hidden)` + )} + + ) +} diff --git a/test/e2e/instrumentation-client-hook/inject/app/layout.tsx b/test/e2e/instrumentation-client-hook/inject/app/layout.tsx new file mode 100644 index 000000000000..568b8766b221 --- /dev/null +++ b/test/e2e/instrumentation-client-hook/inject/app/layout.tsx @@ -0,0 +1,19 @@ +import Link from 'next/link' + +export default function RootLayout({ children }) { + return ( + + +
    +
  • + Home +
  • +
  • + Some Page +
  • +
+ {children} + + + ) +} diff --git a/test/e2e/instrumentation-client-hook/inject/app/page.tsx b/test/e2e/instrumentation-client-hook/inject/app/page.tsx new file mode 100644 index 000000000000..113324caa66e --- /dev/null +++ b/test/e2e/instrumentation-client-hook/inject/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Home

+} diff --git a/test/e2e/instrumentation-client-hook/inject/app/some-page/page.tsx b/test/e2e/instrumentation-client-hook/inject/app/some-page/page.tsx new file mode 100644 index 000000000000..8c6bb6df3ca6 --- /dev/null +++ b/test/e2e/instrumentation-client-hook/inject/app/some-page/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Some page

+} diff --git a/test/e2e/instrumentation-client-hook/inject/inject-a.js b/test/e2e/instrumentation-client-hook/inject/inject-a.js new file mode 100644 index 000000000000..ec97b40782f5 --- /dev/null +++ b/test/e2e/instrumentation-client-hook/inject/inject-a.js @@ -0,0 +1,8 @@ +window.__INJECT_ORDER = window.__INJECT_ORDER || [] +window.__INJECT_ORDER.push('a') +window.__INJECT_A_EXECUTED_AT = performance.now() + +export function onRouterTransitionStart(href, navigateType) { + const pathname = new URL(href, window.location.href).pathname + console.log(`[Router Transition Start] [${navigateType}] ${pathname} a`) +} diff --git a/test/e2e/instrumentation-client-hook/inject/inject-b.js b/test/e2e/instrumentation-client-hook/inject/inject-b.js new file mode 100644 index 000000000000..e19537614c5e --- /dev/null +++ b/test/e2e/instrumentation-client-hook/inject/inject-b.js @@ -0,0 +1,8 @@ +window.__INJECT_ORDER = window.__INJECT_ORDER || [] +window.__INJECT_ORDER.push('b') +window.__INJECT_B_EXECUTED_AT = performance.now() + +export function onRouterTransitionStart(href, navigateType) { + const pathname = new URL(href, window.location.href).pathname + console.log(`[Router Transition Start] [${navigateType}] ${pathname} b`) +} diff --git a/test/e2e/instrumentation-client-hook/inject/instrumentation-client.ts b/test/e2e/instrumentation-client-hook/inject/instrumentation-client.ts new file mode 100644 index 000000000000..6a82ba52b6e6 --- /dev/null +++ b/test/e2e/instrumentation-client-hook/inject/instrumentation-client.ts @@ -0,0 +1,8 @@ +;(window as any).__INJECT_ORDER = (window as any).__INJECT_ORDER || [] +;(window as any).__INJECT_ORDER.push('user') +;(window as any).__INSTRUMENTATION_CLIENT_EXECUTED_AT = performance.now() + +export function onRouterTransitionStart(href: string, navigateType: string) { + const pathname = new URL(href, window.location.href).pathname + console.log(`[Router Transition Start] [${navigateType}] ${pathname} user`) +} diff --git a/test/e2e/instrumentation-client-hook/inject/next.config.js b/test/e2e/instrumentation-client-hook/inject/next.config.js new file mode 100644 index 000000000000..ee32e6ca3fed --- /dev/null +++ b/test/e2e/instrumentation-client-hook/inject/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +module.exports = { + instrumentationClientInject: ['./inject-a.js', './inject-b.js'], +} diff --git a/test/e2e/instrumentation-client-hook/instrumentation-client-hook.test.ts b/test/e2e/instrumentation-client-hook/instrumentation-client-hook.test.ts index b47db90edecb..2a03659e432c 100644 --- a/test/e2e/instrumentation-client-hook/instrumentation-client-hook.test.ts +++ b/test/e2e/instrumentation-client-hook/instrumentation-client-hook.test.ts @@ -1,4 +1,4 @@ -import { nextTestSetup } from 'e2e-utils' +import { isNextDev, nextTestSetup } from 'e2e-utils' import { retry } from 'next-test-utils' import path from 'path' @@ -49,21 +49,21 @@ describe('Instrumentation Client Hook', () => { }) }) + function filterNavigationStartLogs(logs: Array<{ message: string }>) { + const result = [] + for (const log of logs) { + if (log.message.startsWith('[Router Transition Start]')) { + result.push(log.message) + } + } + return result + } + describe('onRouterTransitionStart', () => { const { next } = nextTestSetup({ files: path.join(__dirname, 'app-router'), }) - function filterNavigationStartLogs(logs: Array<{ message: string }>) { - const result = [] - for (const log of logs) { - if (log.message.startsWith('[Router Transition Start]')) { - result.push(log.message) - } - } - return result - } - it('onRouterTransitionStart fires at the start of a navigation', async () => { const browser = await next.browser('/') @@ -102,12 +102,62 @@ describe('Instrumentation Client Hook', () => { }) }) - describe('HMR in development mode', () => { - const { next, isNextDev } = nextTestSetup({ - files: path.join(__dirname, 'app-router'), + describe('instrumentationClientInject', () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'inject'), }) - if (isNextDev) { + it('runs each injected entry before the user instrumentation-client and before hydration, in array order', async () => { + const browser = await next.browser('/') + + const order = await browser.eval(`window.__INJECT_ORDER`) + expect(order).toEqual(['a', 'b', 'user']) + + const injectA = await browser.eval(`window.__INJECT_A_EXECUTED_AT`) + const injectB = await browser.eval(`window.__INJECT_B_EXECUTED_AT`) + const userTime = await browser.eval( + `window.__INSTRUMENTATION_CLIENT_EXECUTED_AT` + ) + const hydrationTime = await browser.eval(`window.__NEXT_HYDRATED_AT`) + + expect(injectA).toBeDefined() + expect(injectB).toBeDefined() + expect(userTime).toBeDefined() + expect(hydrationTime).toBeDefined() + + expect(injectA).toBeLessThanOrEqual(injectB) + expect(injectB).toBeLessThanOrEqual(userTime) + expect(userTime).toBeLessThan(hydrationTime) + }) + + it('still surfaces onRouterTransitionStart from the user instrumentation-client when injects are configured', async () => { + const browser = await next.browser('/') + + const linkToSomePage = await browser.elementByCss('a[href="/some-page"]') + await linkToSomePage.click() + await browser.elementById('some-page') + + const linkToHome = await browser.elementByCss('a[href="/"]') + await linkToHome.click() + await browser.elementById('home') + + expect(filterNavigationStartLogs(await browser.log())).toEqual([ + '[Router Transition Start] [push] /some-page a', + '[Router Transition Start] [push] /some-page b', + '[Router Transition Start] [push] /some-page user', + '[Router Transition Start] [push] / a', + '[Router Transition Start] [push] / b', + '[Router Transition Start] [push] / user', + ]) + }) + }) + + if (isNextDev) { + describe('HMR in development mode', () => { + const { next } = nextTestSetup({ + files: path.join(__dirname, 'app-router'), + }) + it('should reload instrumentation-client when modified', async () => { const browser = await next.browser('/') const initialTime = await browser.eval( @@ -146,13 +196,6 @@ describe('Instrumentation Client Hook', () => { // Restore the original file await next.patchFile(instrumentationPath, originalContent) }) - } else { - // Add a dummy test when not in dev mode - it('skips tests in non-dev mode', () => { - console.log( - 'Skipping instrumentation-client-hook tests in non-dev mode' - ) - }) - } - }) + }) + } }) diff --git a/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts b/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts index fa07de919651..525eda27ea6b 100644 --- a/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts +++ b/test/e2e/opentelemetry/instrumentation/opentelemetry.test.ts @@ -1159,6 +1159,74 @@ describe('opentelemetry', () => { ) } }) +;(process.env.__NEXT_CACHE_COMPONENTS ? describe.skip : describe)( + 'opentelemetry NEXT_OTEL_VERBOSE=1', + () => { + const { next, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + dependencies: require('./package.json').dependencies, + env: { + TEST_OTEL_COLLECTOR_PORT: String(COLLECTOR_PORT), + NEXT_TELEMETRY_DISABLED: '1', + NEXT_OTEL_VERBOSE: '1', + }, + }) + + if (skipped) { + return + } + + let collector: Collector | undefined + + beforeEach(async () => { + collector = await connectCollector({ port: COLLECTOR_PORT }) + }) + + afterEach(async () => { + await collector?.shutdown() + collector = undefined + }) + + // Regression for https://github.com/vercel/otel/issues/107. + it('all spans (including verbose) inherit traceId from incoming traceparent header', async () => { + const pathname = '/app/param/rsc-fetch' + await next.fetch(pathname, { + headers: { + traceparent: `00-${EXTERNAL.traceId}-${EXTERNAL.spanId}-01`, + }, + }) + + let spans: SavedSpan[] = [] + await retry(async () => { + const all = collector?.getSpans() ?? [] + const root = all.find( + (s) => + s.attributes?.['next.span_type'] === 'BaseServer.handleRequest' && + s.attributes?.['http.target'] === pathname + ) + expect(root).toBeDefined() + expect(root!.traceId).toBe(EXTERNAL.traceId) + + spans = all.filter((s) => s.traceId === root!.traceId) + expect(spans.length).toBeGreaterThan(1) + + const verbose = spans.find( + (s) => + s.attributes?.['next.span_type'] === 'NextServer.getRequestHandler' + ) + expect(verbose).toBeDefined() + expect(verbose!.traceId).toBe(EXTERNAL.traceId) + const parentSpanId = verbose!.parentId + expect(parentSpanId).toBe(EXTERNAL.spanId) + }) + + for (const span of spans) { + expect(span.traceId).toBe(EXTERNAL.traceId) + } + }) + } +) describe('opentelemetry with disabled fetch tracing', () => { const { next, skipped } = nextTestSetup({ diff --git a/test/production/app-dir/generate-static-params-errors/next.config.ts b/test/production/app-dir/generate-static-params-errors/next.config.ts index 42500be934ae..e4f5738a310b 100644 --- a/test/production/app-dir/generate-static-params-errors/next.config.ts +++ b/test/production/app-dir/generate-static-params-errors/next.config.ts @@ -1,7 +1,5 @@ import type { NextConfig } from 'next' -const nextConfig: NextConfig = { - experimental: { rootParams: true }, -} +const nextConfig: NextConfig = {} export default nextConfig diff --git a/test/rspack-dev-tests-manifest.json b/test/rspack-dev-tests-manifest.json index 25c92af64619..d5427fce2ab8 100644 --- a/test/rspack-dev-tests-manifest.json +++ b/test/rspack-dev-tests-manifest.json @@ -225,7 +225,7 @@ "Error overlay - RSC build errors importing 'next/cache' APIs in a client component unstable_noStore is allowed", "Error overlay - RSC build errors next/root-params importing 'next/root-params' in a client component", "Error overlay - RSC build errors next/root-params importing 'next/root-params' in a client component in a way that bypasses import analysis", - "Error overlay - RSC build errors next/root-params importing 'next/root-params' when experimental.rootParams is not enabled", + "Error overlay - RSC build errors next/root-params importing a non-existent getter from 'next/root-params'", "Error overlay - RSC build errors should allow to use and handle rsc poisoning client-only", "Error overlay - RSC build errors should allow to use and handle rsc poisoning server-only", "Error overlay - RSC build errors should error for invalid undefined module retuning from next dynamic", diff --git a/test/unit/write-app-declarations.test.ts b/test/unit/write-app-declarations.test.ts index f82b8887a7c4..24e4f7246a67 100644 --- a/test/unit/write-app-declarations.test.ts +++ b/test/unit/write-app-declarations.test.ts @@ -24,6 +24,8 @@ describe('find config', () => { : '') + `import "./.next/types/routes.d.ts";` + eol + + `import "./.next/types/root-params.d.ts";` + + eol + eol + '// NOTE: This file should not be edited' + eol + @@ -40,7 +42,6 @@ describe('find config', () => { hasAppDir: false, strictRouteTypes: false, typedRoutes: true, - rootParams: false, }) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) }) @@ -55,6 +56,8 @@ describe('find config', () => { : '') + `import "./.next/types/routes.d.ts";` + eol + + `import "./.next/types/root-params.d.ts";` + + eol + eol + '// NOTE: This file should not be edited' + eol + @@ -71,7 +74,6 @@ describe('find config', () => { hasAppDir: false, strictRouteTypes: false, typedRoutes: true, - rootParams: false, }) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) }) @@ -86,6 +88,8 @@ describe('find config', () => { : '') + `import "./.next/types/routes.d.ts";` + eol + + `import "./.next/types/root-params.d.ts";` + + eol + eol + '// NOTE: This file should not be edited' + eol + @@ -100,7 +104,6 @@ describe('find config', () => { hasAppDir: false, strictRouteTypes: false, typedRoutes: true, - rootParams: false, }) expect(await fs.readFile(declarationFile, 'utf8')).toBe(content) }) @@ -114,7 +117,6 @@ describe('find config', () => { hasAppDir: true, strictRouteTypes: false, typedRoutes: true, - rootParams: false, }) await expect(fs.readFile(declarationFile, 'utf8')).resolves.not.toContain( @@ -129,7 +131,6 @@ describe('find config', () => { hasAppDir: true, strictRouteTypes: false, typedRoutes: true, - rootParams: false, }) await expect(fs.readFile(declarationFile, 'utf8')).resolves.toContain( diff --git a/turbopack/packages/devlow-bench/package.json b/turbopack/packages/devlow-bench/package.json index 2c2776792b31..5be176caba24 100644 --- a/turbopack/packages/devlow-bench/package.json +++ b/turbopack/packages/devlow-bench/package.json @@ -45,6 +45,7 @@ "dependencies": { "@datadog/datadog-api-client": "^1.13.0", "inquirer": "^9.2.7", + "jstat": "^1.9.6", "minimist": "^1.2.8", "picocolors": "1.0.1", "pidusage-tree": "^2.0.5", diff --git a/turbopack/packages/devlow-bench/src/cli.ts b/turbopack/packages/devlow-bench/src/cli.ts index cc3ca16e4cb7..53f1743df0f8 100644 --- a/turbopack/packages/devlow-bench/src/cli.ts +++ b/turbopack/packages/devlow-bench/src/cli.ts @@ -3,8 +3,60 @@ import { setCurrentScenarios } from './describe.js' import { join } from 'path' import { Scenario, ScenarioVariant, runScenarios } from './index.js' import compose from './interfaces/compose.js' +import { groupRows, printComparison } from './compare.js' +import { readSnapshot, resolveCompareTarget } from './snapshot.js' import { pathToFileURL } from 'url' + +const SUBCOMMANDS = new Set(['run', 'compare']) + ;(async () => { + // Subcommand dispatch. `devlow-bench run [opts] scenario.mjs` and + // `devlow-bench compare ` are the explicit forms. A bare + // `devlow-bench