diff --git a/crates/next-api/src/app.rs b/crates/next-api/src/app.rs index aae094c8c27c6..45c4acdd052fa 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 5d9b5e6547000..9b427f436780b 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", @@ -84,7 +97,8 @@ pub async fn get_app_route_entry( "VAR_USERLAND" => INNER.into(), }, fxindexmap! { - "nextConfigOutput" => output_type + "nextConfigOutput" => output_type, + "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 06c5a1a261941..037fb256029c7 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/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index ff092f5edf68d..552c2453f02f8 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,10 @@ export async function createEntrypoints( page, name: serverBundlePath, pagePath: absolutePagePath, + rootParams: + (staticInfo as AppPageStaticInfo).rootParams?.map( + (p) => p.param + ) || [], appDir, appPaths: matchedAppPaths, pageExtensions, @@ -773,6 +786,10 @@ export async function createEntrypoints( name: serverBundlePath, page, pagePath: absolutePagePath, + rootParams: + (staticInfo as AppPageStaticInfo).rootParams?.map( + (p) => p.param + ) || [], 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 829fcf32de3cb..a6c23a7470324 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 229d897844f5a..51957327845ff 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 4062d2a9b344c..1217dac429fee 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 @@ -17,6 +17,7 @@ export async function createAppRouteCode({ name, page, pagePath, + rootParams, resolveAppRoute, pageExtensions, nextConfigOutput, @@ -25,6 +26,7 @@ export async function createAppRouteCode({ name: string page: string pagePath: string + rootParams: string[] resolveAppRoute: ( pathname: string ) => Promise | string | undefined @@ -78,6 +80,7 @@ export async function createAppRouteCode({ }, { nextConfigOutput: JSON.stringify(nextConfigOutput), + 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 3faeb7ce4068b..ffc95d6c16239 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 @@ -38,6 +38,7 @@ export type AppLoaderOptions = { name: string page: string pagePath: string + rootParams: string[] appDir: string appPaths: readonly string[] | null preferredRegion: string | string[] | undefined @@ -556,6 +557,7 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { appDir, appPaths, pagePath, + rootParams, pageExtensions, rootDir, tsconfigPath, @@ -753,6 +755,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 9b388cf2e07ba..7ac40b38585ad 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 676ed25e5fba5..54baf7e90729b 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 8a66527ad69fd..e24079f19cba9 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,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { entryData.absolutePagePath ).replace(/\\/g, '/') ), + rootParams: + (staticInfo as AppPageStaticInfo).rootParams?.map( + (p) => p.param + ) || [], appDir: this.appDir!, pageExtensions: this.config.pageExtensions, rootDir: this.dir, @@ -1092,6 +1097,10 @@ export default class HotReloaderWebpack implements NextJsHotReloaderInterface { page, appPaths: entryData.appPaths, pagePath, + 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 207f299b54852..2f7be7716bf4a 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 1183b8d444fc0..8f5627f145882 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. @@ -384,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, @@ -481,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, @@ -567,7 +569,7 @@ export class AppRouteRouteModule extends RouteModule< prerenderStore = { type: 'prerender-legacy', phase: 'action', - rootParams: {}, + rootParams: requestStore.rootParams, implicitTags, revalidate: defaultRevalidate, expire: INFINITE_CACHE, @@ -690,9 +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, + 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 735cfb804c018..ff1072c839ad4 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 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 464aceeb4f30e..5f43947dc161a 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() }) } 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 608fcd647f5cf..55c63799ca31d 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) }) })