From 292adce96606cc6765952382167ab7689c2c4b0f Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Fri, 13 Jun 2025 13:28:42 +0200 Subject: [PATCH 1/3] wip: root params in route handlers --- .../next-core/src/next_app/app_route_entry.rs | 3 +- .../build/analysis/get-page-static-info.ts | 4 ++ packages/next/src/build/entries.ts | 43 ++++++++++++------- .../app/collect-root-param-keys.ts | 2 +- .../next/src/build/templates/app-route.ts | 5 ++- .../next-app-loader/create-app-route-code.ts | 4 ++ .../webpack/loaders/next-app-loader/index.ts | 4 ++ .../loaders/next-root-params-loader.ts | 4 +- .../src/server/async-storage/request-store.ts | 3 +- .../src/server/dev/hot-reloader-webpack.ts | 3 ++ .../server/route-modules/app-route/module.ts | 5 +++ packages/next/src/server/web/adapter.ts | 1 + 12 files changed, 59 insertions(+), 22 deletions(-) diff --git a/crates/next-core/src/next_app/app_route_entry.rs b/crates/next-core/src/next_app/app_route_entry.rs index 5d9b5e6547000f..f6b5285c5f6764 100644 --- a/crates/next-core/src/next_app/app_route_entry.rs +++ b/crates/next-core/src/next_app/app_route_entry.rs @@ -84,7 +84,8 @@ pub async fn get_app_route_entry( "VAR_USERLAND" => INNER.into(), }, fxindexmap! { - "nextConfigOutput" => output_type + "nextConfigOutput" => output_type, + // "rootParamNames" => ... // TODO(root-params) }, fxindexmap! {}, ) diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index ff092f5edf68d7..552c2453f02f88 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -35,6 +35,7 @@ import { } from '../segment-config/middleware/middleware-config' import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path' +import type { ParamInfo } from '../webpack/loaders/next-root-params-loader' const PARSE_PATTERN = /(? { @@ -132,25 +134,26 @@ export async function getStaticInfoIncludingLayouts({ const segments = [pageStaticInfo] - // inherit from layout files only if it's a page route - if (isAppPageRoute(page)) { - const layoutFiles = [] - const potentialLayoutFiles = pageExtensions.map((ext) => 'layout.' + ext) - let dir = dirname(pageFilePath) - - // Uses startsWith to not include directories further up. - while (dir.startsWith(appDir)) { - for (const potentialLayoutFile of potentialLayoutFiles) { - const layoutFile = join(dir, potentialLayoutFile) - if (!fs.existsSync(layoutFile)) { - continue - } - layoutFiles.push(layoutFile) + const layoutFiles: string[] = [] + const potentialLayoutFiles = pageExtensions.map((ext) => 'layout.' + ext) + let dir = dirname(pageFilePath) + + // We need to find the root layout for both pages and route handlers. + // Uses startsWith to not include directories further up. + while (dir.startsWith(appDir)) { + for (const potentialLayoutFile of potentialLayoutFiles) { + const layoutFile = join(dir, potentialLayoutFile) + if (!fs.existsSync(layoutFile)) { + continue } - // Walk up the directory tree - dir = join(dir, '..') + layoutFiles.push(layoutFile) } + // Walk up the directory tree + dir = join(dir, '..') + } + // inherit from layout files only if it's a page route + if (isAppPageRoute(page)) { for (const layoutFile of layoutFiles) { const layoutStaticInfo = await getAppPageStaticInfo({ nextConfig, @@ -164,6 +167,11 @@ export async function getStaticInfoIncludingLayouts({ } } + const rootLayout = layoutFiles.at(-1) + const rootParams = rootLayout + ? getParamsFromLayoutFilePath({ appDir, layoutFilePath: rootLayout }) + : [] + const config = reduceAppConfig(segments) return { @@ -172,6 +180,7 @@ export async function getStaticInfoIncludingLayouts({ runtime: config.runtime, preferredRegion: config.preferredRegion, maxDuration: config.maxDuration, + rootParams, } } @@ -695,6 +704,7 @@ export async function createEntrypoints( page, name: serverBundlePath, pagePath: absolutePagePath, + rootParams: (staticInfo as AppPageStaticInfo).rootParams!, appDir, appPaths: matchedAppPaths, pageExtensions, @@ -773,6 +783,7 @@ export async function createEntrypoints( name: serverBundlePath, page, pagePath: absolutePagePath, + rootParams: (staticInfo as AppPageStaticInfo).rootParams!, appDir: appDir!, appPaths: matchedAppPaths, pageExtensions, diff --git a/packages/next/src/build/segment-config/app/collect-root-param-keys.ts b/packages/next/src/build/segment-config/app/collect-root-param-keys.ts index 829fcf32de3cb8..a6c23a7470324f 100644 --- a/packages/next/src/build/segment-config/app/collect-root-param-keys.ts +++ b/packages/next/src/build/segment-config/app/collect-root-param-keys.ts @@ -52,7 +52,7 @@ export function collectRootParamKeys({ AppPageModule | AppRouteModule >): readonly string[] { if (isAppRouteRouteModule(routeModule)) { - return [] + return routeModule.rootParamNames } if (isAppPageRouteModule(routeModule)) { diff --git a/packages/next/src/build/templates/app-route.ts b/packages/next/src/build/templates/app-route.ts index 229d897844f5ae..51957327845ff7 100644 --- a/packages/next/src/build/templates/app-route.ts +++ b/packages/next/src/build/templates/app-route.ts @@ -36,10 +36,12 @@ import * as userland from 'VAR_USERLAND' // instead of a replacement because this could also be `undefined` instead of // an empty string. declare const nextConfigOutput: AppRouteRouteModuleOptions['nextConfigOutput'] +declare const rootParamNames: AppRouteRouteModuleOptions['rootParamNames'] -// We inject the nextConfigOutput here so that we can use them in the route +// We inject nextConfigOutput and rootParamNames here so that we can use them in the route // module. // INJECT:nextConfigOutput +// INJECT:rootParamNames const routeModule = new AppRouteRouteModule({ definition: { @@ -52,6 +54,7 @@ const routeModule = new AppRouteRouteModule({ distDir: process.env.__NEXT_RELATIVE_DIST_DIR || '', relativeProjectDir: process.env.__NEXT_RELATIVE_PROJECT_DIR || '', resolvedPagePath: 'VAR_RESOLVED_PAGE_PATH', + rootParamNames, nextConfigOutput, userland, }) diff --git a/packages/next/src/build/webpack/loaders/next-app-loader/create-app-route-code.ts b/packages/next/src/build/webpack/loaders/next-app-loader/create-app-route-code.ts index 4062d2a9b344cb..06e97504e3aaab 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader/create-app-route-code.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader/create-app-route-code.ts @@ -11,12 +11,14 @@ import { AppPathnameNormalizer } from '../../../../server/normalizers/built/app/ import { loadEntrypoint } from '../../../load-entrypoint' import type { PageExtensions } from '../../../page-extensions-type' import { getFilenameAndExtension } from '../next-metadata-route-loader' +import type { ParamInfo } from '../next-root-params-loader' export async function createAppRouteCode({ appDir, name, page, pagePath, + rootParams, resolveAppRoute, pageExtensions, nextConfigOutput, @@ -25,6 +27,7 @@ export async function createAppRouteCode({ name: string page: string pagePath: string + rootParams: ParamInfo[] resolveAppRoute: ( pathname: string ) => Promise | string | undefined @@ -78,6 +81,7 @@ export async function createAppRouteCode({ }, { nextConfigOutput: JSON.stringify(nextConfigOutput), + rootParamNames: JSON.stringify(rootParams.map((p) => p.param)), } ) } diff --git a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts index 3faeb7ce4068b2..d5620fd1d6cd09 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts @@ -33,11 +33,13 @@ import type { PageExtensions } from '../../../page-extensions-type' import { PARALLEL_ROUTE_DEFAULT_PATH } from '../../../../client/components/builtin/default' import type { Compilation } from 'webpack' import { createAppRouteCode } from './create-app-route-code' +import type { ParamInfo } from '../next-root-params-loader' export type AppLoaderOptions = { name: string page: string pagePath: string + rootParams: ParamInfo[] appDir: string appPaths: readonly string[] | null preferredRegion: string | string[] | undefined @@ -556,6 +558,7 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { appDir, appPaths, pagePath, + rootParams, pageExtensions, rootDir, tsconfigPath, @@ -753,6 +756,7 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { page: loaderOptions.page, name, pagePath, + rootParams, resolveAppRoute, pageExtensions, nextConfigOutput, diff --git a/packages/next/src/build/webpack/loaders/next-root-params-loader.ts b/packages/next/src/build/webpack/loaders/next-root-params-loader.ts index 9b388cf2e07ba5..7ac40b38585ad6 100644 --- a/packages/next/src/build/webpack/loaders/next-root-params-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-root-params-loader.ts @@ -153,9 +153,9 @@ async function findRootLayouts({ return visit(appDir) } -type ParamInfo = { param: string; type: DynamicParamTypes } +export type ParamInfo = { param: string; type: DynamicParamTypes } -function getParamsFromLayoutFilePath({ +export function getParamsFromLayoutFilePath({ appDir, layoutFilePath, }: { diff --git a/packages/next/src/server/async-storage/request-store.ts b/packages/next/src/server/async-storage/request-store.ts index 676ed25e5fba5e..54baf7e90729bf 100644 --- a/packages/next/src/server/async-storage/request-store.ts +++ b/packages/next/src/server/async-storage/request-store.ts @@ -135,6 +135,7 @@ export function createRequestStoreForRender( export function createRequestStoreForAPI( req: RequestContext['req'], url: RequestContext['url'], + rootParams: Params, implicitTags: RequestContext['implicitTags'], onUpdateCookies: RenderOpts['onUpdateCookies'], previewProps: WrapperRenderOpts['previewProps'] @@ -145,7 +146,7 @@ export function createRequestStoreForAPI( req, undefined, url, - {}, + rootParams, implicitTags, onUpdateCookies, undefined, diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts index 8a66527ad69fd0..6d87afdcd2a17a 100644 --- a/packages/next/src/server/dev/hot-reloader-webpack.ts +++ b/packages/next/src/server/dev/hot-reloader-webpack.ts @@ -95,6 +95,7 @@ import { devToolsConfigMiddleware, getDevToolsConfig, } from '../../next-devtools/server/devtools-config-middleware' +import type { AppPageStaticInfo } from '../../build/analysis/get-page-static-info' const MILLISECONDS_IN_NANOSECOND = BigInt(1_000_000) @@ -969,6 +970,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { entryData.absolutePagePath ).replace(/\\/g, '/') ), + rootParams: (staticInfo as AppPageStaticInfo).rootParams!, appDir: this.appDir!, pageExtensions: this.config.pageExtensions, rootDir: this.dir, @@ -1092,6 +1094,7 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { page, appPaths: entryData.appPaths, pagePath, + rootParams: (staticInfo as AppPageStaticInfo).rootParams!, appDir: this.appDir!, pageExtensions: this.config.pageExtensions, rootDir: this.dir, diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 1183b8d444fc0b..1f898da975b71c 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -173,6 +173,7 @@ export interface AppRouteRouteModuleOptions extends RouteModuleOptions { readonly resolvedPagePath: string readonly nextConfigOutput: NextConfig['output'] + readonly rootParamNames: string[] } /** @@ -208,6 +209,7 @@ export class AppRouteRouteModule extends RouteModule< public readonly resolvedPagePath: string public readonly nextConfigOutput: NextConfig['output'] | undefined + public readonly rootParamNames: AppRouteRouteModuleOptions['rootParamNames'] private readonly methods: Record private readonly hasNonStaticMethods: boolean @@ -219,12 +221,14 @@ export class AppRouteRouteModule extends RouteModule< distDir, relativeProjectDir, resolvedPagePath, + rootParamNames, nextConfigOutput, }: AppRouteRouteModuleOptions) { super({ userland, definition, distDir, relativeProjectDir }) this.resolvedPagePath = resolvedPagePath this.nextConfigOutput = nextConfigOutput + this.rootParamNames = rootParamNames // Automatically implement some methods if they aren't implemented by the // userland module. @@ -693,6 +697,7 @@ export class AppRouteRouteModule extends RouteModule< const requestStore = createRequestStoreForAPI( req, req.nextUrl, + {}, // TODO: real rootParams implicitTags, undefined, context.prerenderManifest.preview diff --git a/packages/next/src/server/web/adapter.ts b/packages/next/src/server/web/adapter.ts index 735cfb804c018c..ff1072c839ad4e 100644 --- a/packages/next/src/server/web/adapter.ts +++ b/packages/next/src/server/web/adapter.ts @@ -274,6 +274,7 @@ export async function adapter( const requestStore = createRequestStoreForAPI( request, request.nextUrl, + {}, // TODO(root-params): compute and pass real rootParams implicitTags, onUpdateCookies, previewProps From 7a3c26f8451e5a6a18eb1c263f4a4a7649ffa5f1 Mon Sep 17 00:00:00 2001 From: Ben Gubler Date: Wed, 9 Jul 2025 12:12:24 -0700 Subject: [PATCH 2/3] feat: handle rootParams in routes (finish changes) (#81086) This PR enables the use of root params in route handlers by: 1. Modifying the app route module to extract root params from context.params based on rootParamNames 2. Passing these root params to the request store in route handlers 3. Removing the error that previously prevented using root params in route handlers 4. Simplifying the rootParams handling by directly using string arrays instead of ParamInfo objects 5. Replacing the custom encodeToBase64 utility with standard Buffer.from().toString('base64') --- crates/next-api/src/app.rs | 23 +++++++++++++++---- .../next-core/src/next_app/app_route_entry.rs | 15 +++++++++++- .../next-core/src/next_app/metadata/route.rs | 1 + packages/next/src/build/entries.ts | 10 ++++++-- .../next-app-loader/create-app-route-code.ts | 5 ++-- .../webpack/loaders/next-app-loader/index.ts | 3 +-- .../src/server/dev/hot-reloader-webpack.ts | 10 ++++++-- .../next/src/server/request/root-params.ts | 6 ----- .../server/route-modules/app-route/module.ts | 20 +++++++++++----- .../[lang]/[locale]/route-handler/route.tsx | 6 +---- 10 files changed, 68 insertions(+), 31 deletions(-) diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index aae094c8c27c69..45c4acdd052fa4 100644 --- a/crates/next-api/src/app.rs +++ b/crates/next-api/src/app.rs @@ -4,8 +4,8 @@ use next_core::{ app_segment_config::NextSegmentConfig, app_structure::{ AppPageLoaderTree, CollectedRootParams, Entrypoint as AppEntrypoint, - Entrypoints as AppEntrypoints, FileSystemPathVec, MetadataItem, collect_root_params, - get_entrypoints, + Entrypoints as AppEntrypoints, FileSystemPathVec, MetadataItem, RootParamVecOption, + collect_root_params, get_entrypoints, }, get_edge_resolve_options_context, get_next_package, next_app::{ @@ -992,7 +992,10 @@ pub fn app_entry_point_to_route( ) -> Vc { match entrypoint { AppEntrypoint::AppPage { - pages, loader_tree, .. + pages, + loader_tree, + root_params, + .. } => Route::AppPage( pages .into_iter() @@ -1006,6 +1009,7 @@ pub fn app_entry_point_to_route( }, app_project, page: page.clone(), + root_params, } .resolved_cell(), ), @@ -1017,6 +1021,7 @@ pub fn app_entry_point_to_route( }, app_project, page, + root_params, } .resolved_cell(), ), @@ -1027,6 +1032,7 @@ pub fn app_entry_point_to_route( page, path, root_layouts, + root_params, .. } => Route::AppRoute { original_name: page.to_string().into(), @@ -1035,17 +1041,24 @@ pub fn app_entry_point_to_route( ty: AppEndpointType::Route { path, root_layouts }, app_project, page, + root_params, } .resolved_cell(), ), }, - AppEntrypoint::AppMetadata { page, metadata, .. } => Route::AppRoute { + AppEntrypoint::AppMetadata { + page, + metadata, + root_params, + .. + } => Route::AppRoute { original_name: page.to_string().into(), endpoint: ResolvedVc::upcast( AppEndpoint { ty: AppEndpointType::Metadata { metadata }, app_project, page, + root_params, } .resolved_cell(), ), @@ -1083,6 +1096,7 @@ struct AppEndpoint { ty: AppEndpointType, app_project: ResolvedVc, page: AppPage, + root_params: ResolvedVc, } #[turbo_tasks::value_impl] @@ -1129,6 +1143,7 @@ impl AppEndpoint { self.app_project.project().project_path().owned().await?, config, next_config, + *self.root_params, )) } diff --git a/crates/next-core/src/next_app/app_route_entry.rs b/crates/next-core/src/next_app/app_route_entry.rs index f6b5285c5f6764..9b427f436780bb 100644 --- a/crates/next-core/src/next_app/app_route_entry.rs +++ b/crates/next-core/src/next_app/app_route_entry.rs @@ -35,6 +35,7 @@ pub async fn get_app_route_entry( project_root: FileSystemPath, original_segment_config: Option>, next_config: Vc, + root_params: Vc, ) -> Result> { let segment_from_source = parse_segment_config_from_source(source); let config = if let Some(original_segment_config) = original_segment_config { @@ -70,6 +71,18 @@ pub async fn get_app_route_entry( .map(RcStr::from) .unwrap_or_else(|| "\"\"".into()); + // Convert root params to JSON array of parameter names + let root_param_names = if let Some(root_params_vec) = &*root_params.await? { + serde_json::to_string( + &root_params_vec + .iter() + .map(|param| param.param.as_str()) + .collect::>(), + )? + } else { + "[]".to_string() + }; + // Load the file from the next.js codebase. let virtual_source = load_next_js_template( "app-route.js", @@ -85,7 +98,7 @@ pub async fn get_app_route_entry( }, fxindexmap! { "nextConfigOutput" => output_type, - // "rootParamNames" => ... // TODO(root-params) + "rootParamNames" => root_param_names.into(), }, fxindexmap! {}, ) diff --git a/crates/next-core/src/next_app/metadata/route.rs b/crates/next-core/src/next_app/metadata/route.rs index 06c5a1a2619415..037fb256029c77 100644 --- a/crates/next-core/src/next_app/metadata/route.rs +++ b/crates/next-core/src/next_app/metadata/route.rs @@ -103,6 +103,7 @@ pub async fn get_app_metadata_route_entry( project_root, Some(segment_config), next_config, + Vc::cell(None), // Metadata routes don't have root params )) } diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 0beae53153359c..e8e1ec44d3a33b 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -704,7 +704,10 @@ export async function createEntrypoints( page, name: serverBundlePath, pagePath: absolutePagePath, - rootParams: (staticInfo as AppPageStaticInfo).rootParams!, + rootParams: + (staticInfo as AppPageStaticInfo).rootParams?.map( + (p) => p.param + ) || [], appDir, appPaths: matchedAppPaths, pageExtensions, @@ -783,7 +786,10 @@ export async function createEntrypoints( name: serverBundlePath, page, pagePath: absolutePagePath, - rootParams: (staticInfo as AppPageStaticInfo).rootParams!, + rootParams: + (staticInfo as AppPageStaticInfo).rootParams?.map( + (p) => p.param + ) || [], appDir: appDir!, appPaths: matchedAppPaths, pageExtensions, diff --git a/packages/next/src/build/webpack/loaders/next-app-loader/create-app-route-code.ts b/packages/next/src/build/webpack/loaders/next-app-loader/create-app-route-code.ts index 06e97504e3aaab..1217dac429fee8 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader/create-app-route-code.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader/create-app-route-code.ts @@ -11,7 +11,6 @@ import { AppPathnameNormalizer } from '../../../../server/normalizers/built/app/ import { loadEntrypoint } from '../../../load-entrypoint' import type { PageExtensions } from '../../../page-extensions-type' import { getFilenameAndExtension } from '../next-metadata-route-loader' -import type { ParamInfo } from '../next-root-params-loader' export async function createAppRouteCode({ appDir, @@ -27,7 +26,7 @@ export async function createAppRouteCode({ name: string page: string pagePath: string - rootParams: ParamInfo[] + rootParams: string[] resolveAppRoute: ( pathname: string ) => Promise | string | undefined @@ -81,7 +80,7 @@ export async function createAppRouteCode({ }, { nextConfigOutput: JSON.stringify(nextConfigOutput), - rootParamNames: JSON.stringify(rootParams.map((p) => p.param)), + rootParamNames: JSON.stringify(rootParams), } ) } diff --git a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts index d5620fd1d6cd09..ffc95d6c162396 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader/index.ts @@ -33,13 +33,12 @@ import type { PageExtensions } from '../../../page-extensions-type' import { PARALLEL_ROUTE_DEFAULT_PATH } from '../../../../client/components/builtin/default' import type { Compilation } from 'webpack' import { createAppRouteCode } from './create-app-route-code' -import type { ParamInfo } from '../next-root-params-loader' export type AppLoaderOptions = { name: string page: string pagePath: string - rootParams: ParamInfo[] + rootParams: string[] appDir: string appPaths: readonly string[] | null preferredRegion: string | string[] | undefined diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts index 6d87afdcd2a17a..e24079f19cba9e 100644 --- a/packages/next/src/server/dev/hot-reloader-webpack.ts +++ b/packages/next/src/server/dev/hot-reloader-webpack.ts @@ -970,7 +970,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { entryData.absolutePagePath ).replace(/\\/g, '/') ), - rootParams: (staticInfo as AppPageStaticInfo).rootParams!, + rootParams: + (staticInfo as AppPageStaticInfo).rootParams?.map( + (p) => p.param + ) || [], appDir: this.appDir!, pageExtensions: this.config.pageExtensions, rootDir: this.dir, @@ -1094,7 +1097,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { page, appPaths: entryData.appPaths, pagePath, - rootParams: (staticInfo as AppPageStaticInfo).rootParams!, + rootParams: + (staticInfo as AppPageStaticInfo).rootParams?.map( + (p) => p.param + ) || [], appDir: this.appDir!, pageExtensions: this.config.pageExtensions, rootDir: this.dir, diff --git a/packages/next/src/server/request/root-params.ts b/packages/next/src/server/request/root-params.ts index 207f299b548526..2f7be7716bf4a2 100644 --- a/packages/next/src/server/request/root-params.ts +++ b/packages/next/src/server/request/root-params.ts @@ -203,12 +203,6 @@ export function getRootParam(paramName: string): Promise { const actionStore = actionAsyncStorage.getStore() if (actionStore) { - if (actionStore.isAppRoute) { - // TODO(root-params): add support for route handlers - throw new Error( - `Route ${workStore.route} used ${apiName} inside a Route Handler. Support for this API in Route Handlers is planned for a future version of Next.js.` - ) - } if (actionStore.isAction) { // Actions are not fundamentally tied to a route (even if they're always submitted from some page), // so root params would be inconsistent if an action is called from multiple roots. diff --git a/packages/next/src/server/route-modules/app-route/module.ts b/packages/next/src/server/route-modules/app-route/module.ts index 1f898da975b71c..8f5627f1458821 100644 --- a/packages/next/src/server/route-modules/app-route/module.ts +++ b/packages/next/src/server/route-modules/app-route/module.ts @@ -388,9 +388,7 @@ export class AppRouteRouteModule extends RouteModule< (prerenderStore = { type: 'prerender', phase: 'action', - // This replicates prior behavior where rootParams is empty in routes - // TODO we need to make this have the proper rootParams for this route - rootParams: {}, + rootParams: requestStore.rootParams, fallbackRouteParams: null, implicitTags, renderSignal: prospectiveController.signal, @@ -485,7 +483,7 @@ export class AppRouteRouteModule extends RouteModule< const finalRoutePrerenderStore: PrerenderStore = (prerenderStore = { type: 'prerender', phase: 'action', - rootParams: {}, + rootParams: requestStore.rootParams, fallbackRouteParams: null, implicitTags, renderSignal: finalController.signal, @@ -571,7 +569,7 @@ export class AppRouteRouteModule extends RouteModule< prerenderStore = { type: 'prerender-legacy', phase: 'action', - rootParams: {}, + rootParams: requestStore.rootParams, implicitTags, revalidate: defaultRevalidate, expire: INFINITE_CACHE, @@ -694,10 +692,20 @@ export class AppRouteRouteModule extends RouteModule< null ) + // Extract root params from context.params based on rootParamNames + const rootParams: Record = {} + if (context.params && this.rootParamNames.length > 0) { + for (const paramName of this.rootParamNames) { + if (paramName in context.params) { + rootParams[paramName] = context.params[paramName] + } + } + } + const requestStore = createRequestStoreForAPI( req, req.nextUrl, - {}, // TODO: real rootParams + rootParams, implicitTags, undefined, context.prerenderManifest.preview diff --git a/test/e2e/app-dir/app-root-params-getters/fixtures/simple/app/[lang]/[locale]/route-handler/route.tsx b/test/e2e/app-dir/app-root-params-getters/fixtures/simple/app/[lang]/[locale]/route-handler/route.tsx index 464aceeb4f30ea..5f43947dc161af 100644 --- a/test/e2e/app-dir/app-root-params-getters/fixtures/simple/app/[lang]/[locale]/route-handler/route.tsx +++ b/test/e2e/app-dir/app-root-params-getters/fixtures/simple/app/[lang]/[locale]/route-handler/route.tsx @@ -1,9 +1,5 @@ import { lang, locale } from 'next/root-params' export async function GET() { - return Response.json( - // TODO(root-params): We're missing some wiring to set `requestStore.rootParams`, - // so both of these will currently return undefined - { lang: await lang(), locale: await locale() } - ) + return Response.json({ lang: await lang(), locale: await locale() }) } From c0d7e49509e66fdf60a4c9bcae10e841c1e16b29 Mon Sep 17 00:00:00 2001 From: Janka Uryga Date: Wed, 9 Jul 2025 21:58:53 +0200 Subject: [PATCH 3/3] update test --- .../app-dir/app-root-params-getters/simple.test.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/e2e/app-dir/app-root-params-getters/simple.test.ts b/test/e2e/app-dir/app-root-params-getters/simple.test.ts index 608fcd647f5cf0..55c63799ca31d8 100644 --- a/test/e2e/app-dir/app-root-params-getters/simple.test.ts +++ b/test/e2e/app-dir/app-root-params-getters/simple.test.ts @@ -124,18 +124,13 @@ describe('app-root-param-getters - simple', () => { } }) - // TODO(root-params): add support for route handlers - it('should error when used in a route handler (until we implement it)', async () => { + it('should allow reading params in a route handler', async () => { const params = { lang: 'en', locale: 'us' } const response = await next.fetch( `/${params.lang}/${params.locale}/route-handler` ) - expect(response.status).toBe(500) - if (!isNextDeploy) { - expect(next.cliOutput).toInclude( - "Route /[lang]/[locale]/route-handler used `import('next/root-params').lang()` inside a Route Handler. Support for this API in Route Handlers is planned for a future version of Next.js." - ) - } + expect(response.status).toBe(200) + expect(await response.json()).toEqual(params) }) })