diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index cc04c3be6ab44..c66d531da0278 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -14,13 +14,13 @@ import type { EdgeFunctionDefinition, MiddlewareManifest, } from './webpack/plugins/middleware-plugin' -import type { AppRouteUserlandModule } from '../server/future/route-modules/app-route/module' import type { StaticGenerationAsyncStorage } from '../client/components/static-generation-async-storage' import '../server/require-hook' import '../server/node-polyfill-fetch' import '../server/node-polyfill-crypto' import '../server/node-environment' + import chalk from 'next/dist/compiled/chalk' import getGzipSize from 'next/dist/compiled/gzip-size' import textTable from 'next/dist/compiled/text-table' @@ -66,6 +66,7 @@ import { nodeFs } from '../server/lib/node-fs-methods' import * as ciEnvironment from '../telemetry/ci-info' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { denormalizeAppPagePath } from '../shared/lib/page-path/denormalize-app-path' +import { AppRouteRouteModule } from '../server/future/route-modules/app-route/module' export type ROUTER_TYPE = 'pages' | 'app' @@ -1435,10 +1436,6 @@ export async function isPageStatic({ isClientComponent = isClientReference(componentsResult.ComponentMod) const tree = componentsResult.ComponentMod.tree - // This is present on the new route modules. - const userland: AppRouteUserlandModule | undefined = - componentsResult.routeModule?.userland - const staticGenerationAsyncStorage: StaticGenerationAsyncStorage = componentsResult.ComponentMod.staticGenerationAsyncStorage if (!staticGenerationAsyncStorage) { @@ -1454,19 +1451,23 @@ export async function isPageStatic({ ) } - const generateParams: GenerateParams = userland - ? [ - { - config: { - revalidate: userland.revalidate, - dynamic: userland.dynamic, - dynamicParams: userland.dynamicParams, + const { routeModule } = componentsResult + + const generateParams: GenerateParams = + routeModule && AppRouteRouteModule.is(routeModule) + ? [ + { + config: { + revalidate: routeModule.userland.revalidate, + dynamic: routeModule.userland.dynamic, + dynamicParams: routeModule.userland.dynamicParams, + }, + generateStaticParams: + routeModule.userland.generateStaticParams, + segmentPath: page, }, - generateStaticParams: userland.generateStaticParams, - segmentPath: page, - }, - ] - : await collectGenerateParams(tree) + ] + : await collectGenerateParams(tree) appConfig = generateParams.reduce( (builtConfig: AppConfig, curGenParams): AppConfig => { diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index dc4237ea70095..971e286374226 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1954,6 +1954,12 @@ export default async function getBaseWebpackConfig( ), layer: WEBPACK_LAYERS.metadataRoute, }, + { + // Ensure that the app page module is in the client layers, this + // enables React to work correctly for RSC. + layer: WEBPACK_LAYERS.client, + test: /next[\\/]dist[\\/](esm[\\/])?server[\\/]future[\\/]route-modules[\\/]app-page[\\/]module/, + }, { // All app dir layers need to use this configured resolution logic issuerLayer: { diff --git a/packages/next/src/build/webpack/loaders/next-app-loader.ts b/packages/next/src/build/webpack/loaders/next-app-loader.ts index 74377e14dd3f8..906fe745507b2 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader.ts @@ -1,6 +1,7 @@ import type webpack from 'webpack' import type { ValueOf } from '../../../shared/lib/constants' import type { ModuleReference, CollectedMetadata } from './metadata/types' +import type { AppPageRouteModuleOptions } from '../../../server/future/route-modules/app-page/module' import path from 'path' import { stringify } from 'querystring' @@ -661,9 +662,26 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { } } + const pathname = new AppPathnameNormalizer().normalize(page) + const bundlePath = new AppBundlePathNormalizer().normalize(page) + + const options: Omit = { + definition: { + kind: RouteKind.APP_PAGE, + page, + pathname, + bundlePath, + // The following aren't used in production. + filename: '', + appPaths: [], + }, + } + // Prefer to modify next/src/server/app-render/entry-base.ts since this is shared with Turbopack. // Any changes to this code should be reflected in Turbopack's app_source.rs and/or app-renderer.tsx as well. const result = ` + import RouteModule from 'next/dist/server/future/route-modules/app-page/module' + export ${treeCodeResult.treeCode} export ${treeCodeResult.pages} @@ -683,6 +701,15 @@ const nextAppLoader: AppLoader = async function nextAppLoader() { } export * from 'next/dist/server/app-render/entry-base' + + // Create and export the route module that will be consumed. + const options = ${JSON.stringify(options)} + export const routeModule = new RouteModule({ + ...options, + userland: { + loaderTree: tree, + }, + }) ` return result diff --git a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts index 9a6105b16b46d..f97f1577fbca7 100644 --- a/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts +++ b/packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts @@ -4,7 +4,7 @@ import type { DocumentType, AppType } from '../../../../shared/lib/utils' import type { BuildManifest } from '../../../../server/get-page-files' import type { ReactLoadableManifest } from '../../../../server/load-components' import type { ClientReferenceManifest } from '../../plugins/flight-manifest-plugin' -import type { NextFontManifestPlugin } from '../../plugins/next-font-manifest-plugin' +import type { NextFontManifest } from '../../plugins/next-font-manifest-plugin' import WebServer from '../../../../server/web-server' import { @@ -57,17 +57,15 @@ export function getRender({ appServerMod: any config: NextConfigComplete buildId: string - nextFontManifest: NextFontManifestPlugin + nextFontManifest: NextFontManifest incrementalCacheHandler?: any }) { const isAppPath = pagesType === 'app' const baseLoadComponentResult = { dev, buildManifest, - prerenderManifest, reactLoadableManifest, subresourceIntegrityManifest, - nextFontManifest, Document, App: appMod?.default as AppType, clientReferenceManifest, @@ -89,6 +87,7 @@ export function getRender({ disableOptimizedLoading: true, serverActionsManifest, serverActionsBodySizeLimit, + nextFontManifest, }, renderToHTML, incrementalCacheHandler, diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index 3479cf20b0a8e..d2eadc9c165de 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -28,6 +28,7 @@ import type { PrerenderManifest } from '../build' import type { ClientReferenceManifest } from '../build/webpack/plugins/flight-manifest-plugin' import type { NextFontManifest } from '../build/webpack/plugins/next-font-manifest-plugin' import type { PagesRouteModule } from './future/route-modules/pages/module' +import type { AppPageRouteModule } from './future/route-modules/app-page/module' import type { NodeNextRequest, NodeNextResponse } from './base-http/node' import type { AppRouteRouteMatch } from './future/route-matches/app-route-route-match' import type { RouteDefinition } from './future/route-definitions/route-definition' @@ -1783,6 +1784,25 @@ export default abstract class Server { // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952 renderOpts.nextFontManifest = this.nextFontManifest + // Call the built-in render method on the module. + result = await module.render( + (req as NodeNextRequest).originalRequest ?? (req as WebNextRequest), + (res as NodeNextResponse).originalResponse ?? + (res as WebNextResponse), + { page: pathname, params: match.params, query, renderOpts } + ) + } else if ( + match && + isRouteMatch(match, RouteKind.APP_PAGE) && + components.routeModule + ) { + const module = components.routeModule as AppPageRouteModule + + // Due to the way we pass data by mutating `renderOpts`, we can't extend the + // object here but only updating its `clientReferenceManifest` field. + // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952 + renderOpts.nextFontManifest = this.nextFontManifest + // Call the built-in render method on the module. result = await module.render( (req as NodeNextRequest).originalRequest ?? (req as WebNextRequest), diff --git a/packages/next/src/server/dev/static-paths-worker.ts b/packages/next/src/server/dev/static-paths-worker.ts index 80b6beed12560..68932df7a36b0 100644 --- a/packages/next/src/server/dev/static-paths-worker.ts +++ b/packages/next/src/server/dev/static-paths-worker.ts @@ -1,9 +1,9 @@ import type { NextConfigComplete } from '../config-shared' -import type { AppRouteUserlandModule } from '../future/route-modules/app-route/module' import '../require-hook' import '../node-polyfill-fetch' import '../node-environment' + import { buildAppStaticPaths, buildStaticPaths, @@ -15,6 +15,7 @@ import { setHttpClientAndAgentOptions } from '../setup-http-agent-env' import { IncrementalCache } from '../lib/incremental-cache' import * as serverHooks from '../../client/components/hooks-server-context' import { staticGenerationAsyncStorage } from '../../client/components/static-generation-async-storage' +import { AppRouteRouteModule } from '../future/route-modules/app-route/module' type RuntimeConfig = any @@ -75,21 +76,21 @@ export async function loadStaticPaths({ } if (isAppPath) { - const userland: AppRouteUserlandModule | undefined = - components.routeModule?.userland - const generateParams: GenerateParams = userland - ? [ - { - config: { - revalidate: userland.revalidate, - dynamic: userland.dynamic, - dynamicParams: userland.dynamicParams, + const { routeModule } = components + const generateParams: GenerateParams = + routeModule && AppRouteRouteModule.is(routeModule) + ? [ + { + config: { + revalidate: routeModule.userland.revalidate, + dynamic: routeModule.userland.dynamic, + dynamicParams: routeModule.userland.dynamicParams, + }, + generateStaticParams: routeModule.userland.generateStaticParams, + segmentPath: pathname, }, - generateStaticParams: userland.generateStaticParams, - segmentPath: pathname, - }, - ] - : await collectGenerateParams(components.ComponentMod.tree) + ] + : await collectGenerateParams(components.ComponentMod.tree) return await buildAppStaticPaths({ page: pathname, diff --git a/packages/next/src/server/future/route-modules/app-page/module.ts b/packages/next/src/server/future/route-modules/app-page/module.ts new file mode 100644 index 0000000000000..870465551d915 --- /dev/null +++ b/packages/next/src/server/future/route-modules/app-page/module.ts @@ -0,0 +1,56 @@ +import type { IncomingMessage, ServerResponse } from 'http' +import type { AppPageRouteDefinition } from '../../route-definitions/app-page-route-definition' +import type RenderResult from '../../../render-result' +import type { RenderOpts } from '../../../app-render/types' +import type { NextParsedUrlQuery } from '../../../request-meta' +import type { LoaderTree } from '../../../lib/app-dir-module' + +import { renderToHTMLOrFlight } from '../../../app-render/app-render' +import { + RouteModule, + type RouteModuleOptions, + type RouteModuleHandleContext, +} from '../route-module' + +type AppPageUserlandModule = { + /** + * The tree created in next-app-loader that holds component segments and modules + */ + loaderTree: LoaderTree +} + +interface AppPageRouteHandlerContext extends RouteModuleHandleContext { + page: string + query: NextParsedUrlQuery + renderOpts: RenderOpts +} + +export type AppPageRouteModuleOptions = RouteModuleOptions< + AppPageRouteDefinition, + AppPageUserlandModule +> + +export class AppPageRouteModule extends RouteModule< + AppPageRouteDefinition, + AppPageUserlandModule +> { + public handle(): Promise { + throw new Error('Method not implemented.') + } + + public render( + req: IncomingMessage, + res: ServerResponse, + context: AppPageRouteHandlerContext + ): Promise { + return renderToHTMLOrFlight( + req, + res, + context.page, + context.query, + context.renderOpts + ) + } +} + +export default AppPageRouteModule diff --git a/packages/next/src/server/future/route-modules/app-route/module.ts b/packages/next/src/server/future/route-modules/app-route/module.ts index bcc7e19a80956..5ffb9170cf373 100644 --- a/packages/next/src/server/future/route-modules/app-route/module.ts +++ b/packages/next/src/server/future/route-modules/app-route/module.ts @@ -32,6 +32,7 @@ import * as Log from '../../../../build/output/log' import { autoImplementMethods } from './helpers/auto-implement-methods' import { getNonStaticMethods } from './helpers/get-non-static-methods' import { appendMutableCookies } from '../../../web/spec-extension/adapters/request-cookies' +import { RouteKind } from '../../route-kind' // These are imported weirdly like this because of the way that the bundling // works. We need to import the built files from the dist directory, but we @@ -158,6 +159,10 @@ export class AppRouteRouteModule extends RouteModule< private readonly nonStaticMethods: ReadonlyArray | false private readonly dynamic: AppRouteUserlandModule['dynamic'] + public static is(route: RouteModule): route is AppRouteRouteModule { + return route.definition.kind === RouteKind.APP_ROUTE + } + constructor({ userland, definition, diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index b3280f6499c50..aa83b42983d86 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -162,11 +162,13 @@ createNextDescribe( } ` ) - const html = await next.render('/invalid/first') + + // The page may take a moment to compile, so try it a few times. + await check(async () => { + return next.render('/invalid/first') + }, /A required parameter \(slug\) was not provided as a string received object/) + await next.deleteFile('app/invalid/[slug]/page.js') - expect(html).toContain( - 'A required parameter (slug) was not provided as a string received object' - ) }) it('should correctly handle multi-level generateStaticParams when some levels are missing', async () => {