From 205d3845d1684bb9c8202106b91ad2e55483a39f Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 20 Jul 2023 23:51:37 -0600 Subject: [PATCH] Move Pages API rendering into bundle (#52149) Moves the rendering for Pages API routes into the bundle. This also implements the `routeModule` interface for both Pages and Pages API routes in the Turbopack output. This also fixes a bug where the order of the imports for `Document` and `App` were reversed in the Turbopack build. --- .../next-build/src/next_pages/page_entries.rs | 146 +++++++- .../js/src/entry/server-renderer.tsx | 2 +- .../next-swc/crates/next-core/src/util.rs | 1 + packages/next/src/build/entries.ts | 18 +- .../webpack/loaders/get-module-build-info.ts | 26 +- .../loaders/next-route-loader/index.ts | 342 +++++++++++++----- packages/next/src/server/api-utils/node.ts | 14 +- packages/next/src/server/base-server.ts | 13 +- packages/next/src/server/config-shared.ts | 5 + packages/next/src/server/dev/hot-reloader.ts | 11 +- .../module-loader/route-module-loader.ts | 9 +- .../future/route-modules/pages-api/module.ts | 137 +++++++ packages/next/src/server/next-server.ts | 50 ++- 13 files changed, 606 insertions(+), 168 deletions(-) create mode 100644 packages/next/src/server/future/route-modules/pages-api/module.ts diff --git a/packages/next-swc/crates/next-build/src/next_pages/page_entries.rs b/packages/next-swc/crates/next-build/src/next_pages/page_entries.rs index a82dfab2ae..ea45d929a2 100644 --- a/packages/next-swc/crates/next-build/src/next_pages/page_entries.rs +++ b/packages/next-swc/crates/next-build/src/next_pages/page_entries.rs @@ -1,4 +1,8 @@ +use std::io::Write; + use anyhow::{bail, Result}; +use indexmap::indexmap; +use indoc::writedoc; use next_core::{ create_page_loader_entry_module, get_asset_path_from_pathname, mode::NextMode, @@ -20,10 +24,15 @@ use next_core::{ }; use turbo_tasks::Vc; use turbopack_binding::{ - turbo::{tasks::Value, tasks_env::ProcessEnv, tasks_fs::FileSystemPath}, + turbo::{ + tasks::Value, + tasks_env::ProcessEnv, + tasks_fs::{rope::RopeBuilder, File, FileSystemPath}, + }, turbopack::{ build::BuildChunkingContext, core::{ + asset::AssetContent, chunk::{ChunkableModule, ChunkingContext, EvaluatableAssets}, compile_time_info::CompileTimeInfo, context::AssetContext, @@ -31,6 +40,7 @@ use turbopack_binding::{ output::OutputAsset, reference_type::{EntryReferenceSubType, ReferenceType}, source::Source, + virtual_source::VirtualSource, }, ecmascript::{ chunk::{EcmascriptChunkPlaceable, EcmascriptChunkingContext}, @@ -191,6 +201,8 @@ async fn get_page_entries_for_root_directory( Vc::upcast(FileSource::new(app.project_path)), next_router_root, app.next_router_path, + app.original_path, + PathType::Page, )); // This only makes sense on the server. @@ -201,6 +213,8 @@ async fn get_page_entries_for_root_directory( Vc::upcast(FileSource::new(document.project_path)), next_router_root, document.next_router_path, + document.original_path, + PathType::Page, )); // This only makes sense on both the client and the server, but they should map @@ -212,6 +226,8 @@ async fn get_page_entries_for_root_directory( Vc::upcast(FileSource::new(error.project_path)), next_router_root, error.next_router_path, + error.original_path, + PathType::Page, )); if let Some(api) = api { @@ -221,6 +237,7 @@ async fn get_page_entries_for_root_directory( api, next_router_root, &mut entries, + PathType::PagesAPI, ) .await?; } @@ -232,6 +249,7 @@ async fn get_page_entries_for_root_directory( pages, next_router_root, &mut entries, + PathType::Page, ) .await?; } @@ -246,6 +264,7 @@ async fn get_page_entries_for_directory( pages_structure: Vc, next_router_root: Vc, entries: &mut Vec>, + path_type: PathType, ) -> Result<()> { let PagesDirectoryStructure { ref items, @@ -257,7 +276,7 @@ async fn get_page_entries_for_directory( let PagesStructureItem { project_path, next_router_path, - original_path: _, + original_path, } = *item.await?; entries.push(get_page_entry_for_file( ssr_module_context, @@ -265,6 +284,8 @@ async fn get_page_entries_for_directory( Vc::upcast(FileSource::new(project_path)), next_router_root, next_router_path, + original_path, + path_type, )); } @@ -275,6 +296,7 @@ async fn get_page_entries_for_directory( *child, next_router_root, entries, + path_type, ) .await?; } @@ -300,13 +322,129 @@ async fn get_page_entry_for_file( source: Vc>, next_router_root: Vc, next_router_path: Vc, + next_original_path: Vc, + path_type: PathType, ) -> Result> { - let reference_type = Value::new(ReferenceType::Entry(EntryReferenceSubType::Page)); + let reference_type = Value::new(ReferenceType::Entry(match path_type { + PathType::Page => EntryReferenceSubType::Page, + PathType::PagesAPI => EntryReferenceSubType::PagesApi, + _ => bail!("Invalid path type"), + })); - let pathname = pathname_for_path(next_router_root, next_router_path, PathType::Page); + let pathname = pathname_for_path(next_router_root, next_router_path, path_type); + + let definition_page = format!("/{}", next_original_path.await?); + let definition_pathname = pathname.await?; let ssr_module = ssr_module_context.process(source, reference_type.clone()); + let mut result = RopeBuilder::default(); + + match path_type { + PathType::Page => { + // Sourced from https://github.com/vercel/next.js/blob/2848ce51d1552633119c89ab49ff7fe2e4e91c91/packages/next/src/build/webpack/loaders/next-route-loader/index.ts + writedoc!( + result, + r#" + import RouteModule from "next/dist/server/future/route-modules/pages/module" + import {{ hoist }} from "next/dist/build/webpack/loaders/next-route-loader/helpers" + + import Document from "@vercel/turbopack-next/pages/_document" + import App from "@vercel/turbopack-next/pages/_app" + + import * as userland from "INNER" + + export default hoist(userland, "default") + + export const getStaticProps = hoist(userland, "getStaticProps") + export const getStaticPaths = hoist(userland, "getStaticPaths") + export const getServerSideProps = hoist(userland, "getServerSideProps") + export const config = hoist(userland, "config") + export const reportWebVitals = hoist(userland, "reportWebVitals") + + export const unstable_getStaticProps = hoist(userland, "unstable_getStaticProps") + export const unstable_getStaticPaths = hoist(userland, "unstable_getStaticPaths") + export const unstable_getStaticParams = hoist(userland, "unstable_getStaticParams") + export const unstable_getServerProps = hoist(userland, "unstable_getServerProps") + export const unstable_getServerSideProps = hoist(userland, "unstable_getServerSideProps") + + export const routeModule = new RouteModule({{ + definition: {{ + kind: "PAGES", + page: "{definition_page}", + pathname: "{definition_pathname}", + // The following aren't used in production, but are + // required for the RouteModule constructor. + bundlePath: "", + filename: "", + }}, + components: {{ + App, + Document, + }}, + userland, + }}) + "# + )?; + + // When we're building the instrumentation page (only when the + // instrumentation file conflicts with a page also labeled + // /instrumentation) hoist the `register` method. + if definition_page == "/instrumentation" || definition_page == "/src/instrumentation" { + writeln!( + result, + r#"export const register = hoist(userland, "register")"# + )?; + } + } + PathType::PagesAPI => { + // Sourced from https://github.com/vercel/next.js/blob/2848ce51d1552633119c89ab49ff7fe2e4e91c91/packages/next/src/build/webpack/loaders/next-route-loader/index.ts + writedoc!( + result, + r#" + import RouteModule from "next/dist/server/future/route-modules/pages-api/module" + import {{ hoist }} from "next/dist/build/webpack/loaders/next-route-loader/helpers" + + import * as userland from "INNER" + + export default hoist(userland, "default") + export const config = hoist(userland, "config") + + export const routeModule = new RouteModule({{ + definition: {{ + kind: "PAGES_API", + page: "{definition_page}", + pathname: "{definition_pathname}", + // The following aren't used in production, but are + // required for the RouteModule constructor. + bundlePath: "", + filename: "", + }}, + userland, + }}) + "# + )?; + } + _ => bail!("Invalid path type"), + }; + + let file = File::from(result.build()); + + let asset = VirtualSource::new( + source.ident().path().join(match path_type { + PathType::Page => "pages-entry.tsx".to_string(), + PathType::PagesAPI => "pages-api-entry.tsx".to_string(), + _ => bail!("Invalid path type"), + }), + AssetContent::file(file.into()), + ); + let ssr_module = ssr_module_context.process( + Vc::upcast(asset), + Value::new(ReferenceType::Internal(Vc::cell(indexmap! { + "INNER".to_string() => ssr_module, + }))), + ); + let client_module = create_page_loader_entry_module(client_module_context, source, pathname); let Some(client_module) = diff --git a/packages/next-swc/crates/next-core/js/src/entry/server-renderer.tsx b/packages/next-swc/crates/next-core/js/src/entry/server-renderer.tsx index 76e4a664a1..1a1c48c482 100644 --- a/packages/next-swc/crates/next-core/js/src/entry/server-renderer.tsx +++ b/packages/next-swc/crates/next-core/js/src/entry/server-renderer.tsx @@ -2,8 +2,8 @@ // the other imports import startHandler from '../internal/page-server-handler' -import App from '@vercel/turbopack-next/pages/_app' import Document from '@vercel/turbopack-next/pages/_document' +import App from '@vercel/turbopack-next/pages/_app' import chunkGroup from 'INNER_CLIENT_CHUNK_GROUP' diff --git a/packages/next-swc/crates/next-core/src/util.rs b/packages/next-swc/crates/next-core/src/util.rs index d9afa592f1..b62becd7e2 100644 --- a/packages/next-swc/crates/next-core/src/util.rs +++ b/packages/next-swc/crates/next-core/src/util.rs @@ -34,6 +34,7 @@ use crate::next_config::{NextConfig, OutputType}; #[derive(Debug, Clone, Copy, PartialEq, Eq, TaskInput)] pub enum PathType { Page, + PagesAPI, Data, } diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 39e1da4e6e..e090732a2a 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -55,6 +55,8 @@ import { fileExists } from '../lib/file-exists' import { getRouteLoaderEntry } from './webpack/loaders/next-route-loader' import { isInternalComponent } from '../lib/is-internal-component' import { isStaticMetadataRouteFile } from '../lib/metadata/is-metadata-route' +import { RouteKind } from '../server/future/route-kind' +import { encodeToBase64 } from './webpack/loaders/utils' export async function getStaticInfoIncludingLayouts({ isInsideAppDir, @@ -589,9 +591,7 @@ export async function createEntrypoints( assetPrefix: config.assetPrefix, nextConfigOutput: config.output, preferredRegion: staticInfo.preferredRegion, - middlewareConfig: Buffer.from( - JSON.stringify(staticInfo.middleware || {}) - ).toString('base64'), + middlewareConfig: encodeToBase64(staticInfo.middleware || {}), }) } else if (isInstrumentationHookFile(page) && pagesType === 'root') { server[serverBundlePath.replace('src/', '')] = { @@ -599,13 +599,23 @@ export async function createEntrypoints( // the '../' is needed to make sure the file is not chunked filename: `../${INSTRUMENTATION_HOOK_FILENAME}.js`, } + } else if (isAPIRoute(page)) { + server[serverBundlePath] = [ + getRouteLoaderEntry({ + kind: RouteKind.PAGES_API, + page, + absolutePagePath, + preferredRegion: staticInfo.preferredRegion, + middlewareConfig: staticInfo.middleware || {}, + }), + ] } else if ( - !isAPIRoute(page) && !isMiddlewareFile(page) && !isInternalComponent(absolutePagePath) ) { server[serverBundlePath] = [ getRouteLoaderEntry({ + kind: RouteKind.PAGES, page, pages, absolutePagePath, diff --git a/packages/next/src/build/webpack/loaders/get-module-build-info.ts b/packages/next/src/build/webpack/loaders/get-module-build-info.ts index 319c1e1e27..f0ca52bf2f 100644 --- a/packages/next/src/build/webpack/loaders/get-module-build-info.ts +++ b/packages/next/src/build/webpack/loaders/get-module-build-info.ts @@ -5,23 +5,25 @@ import type { } from '../../analysis/get-page-static-info' import { webpack } from 'next/dist/compiled/webpack/webpack' +export type ModuleBuildInfo = { + nextEdgeMiddleware?: EdgeMiddlewareMeta + nextEdgeApiFunction?: EdgeMiddlewareMeta + nextEdgeSSR?: EdgeSSRMeta + nextWasmMiddlewareBinding?: AssetBinding + nextAssetMiddlewareBinding?: AssetBinding + usingIndirectEval?: boolean | Set + route?: RouteMeta + importLocByPath?: Map + rootDir?: string + rsc?: RSCMeta +} + /** * A getter for module build info that casts to the type it should have. * We also expose here types to make easier to use it. */ export function getModuleBuildInfo(webpackModule: webpack.Module) { - return webpackModule.buildInfo as { - nextEdgeMiddleware?: EdgeMiddlewareMeta - nextEdgeApiFunction?: EdgeMiddlewareMeta - nextEdgeSSR?: EdgeSSRMeta - nextWasmMiddlewareBinding?: AssetBinding - nextAssetMiddlewareBinding?: AssetBinding - usingIndirectEval?: boolean | Set - route?: RouteMeta - importLocByPath?: Map - rootDir?: string - rsc?: RSCMeta - } + return webpackModule.buildInfo as ModuleBuildInfo } export interface RSCMeta { diff --git a/packages/next/src/build/webpack/loaders/next-route-loader/index.ts b/packages/next/src/build/webpack/loaders/next-route-loader/index.ts index 71e880c394..85fc7f4b56 100644 --- a/packages/next/src/build/webpack/loaders/next-route-loader/index.ts +++ b/packages/next/src/build/webpack/loaders/next-route-loader/index.ts @@ -1,15 +1,25 @@ import type { webpack } from 'next/dist/compiled/webpack/webpack' import { stringify } from 'querystring' -import { getModuleBuildInfo } from '../get-module-build-info' +import { ModuleBuildInfo, getModuleBuildInfo } from '../get-module-build-info' import { PagesRouteModuleOptions } from '../../../../server/future/route-modules/pages/module' import { RouteKind } from '../../../../server/future/route-kind' import { normalizePagePath } from '../../../../shared/lib/page-path/normalize-page-path' import { MiddlewareConfig } from '../../../analysis/get-page-static-info' import { decodeFromBase64, encodeToBase64 } from '../utils' import { isInstrumentationHookFile } from '../../../worker' +import { PagesAPIRouteModuleOptions } from '../../../../server/future/route-modules/pages-api/module' -type RouteLoaderOptionsInput = { +type RouteLoaderOptionsPagesAPIInput = { + kind: RouteKind.PAGES_API + page: string + preferredRegion: string | string[] | undefined + absolutePagePath: string + middlewareConfig: MiddlewareConfig +} + +type RouteLoaderOptionsPagesInput = { + kind: RouteKind.PAGES page: string pages: { [page: string]: string } preferredRegion: string | string[] | undefined @@ -17,10 +27,13 @@ type RouteLoaderOptionsInput = { middlewareConfig: MiddlewareConfig } -/** - * The options for the route loader. - */ -type RouteLoaderOptions = { +type RouteLoaderOptionsInput = + | RouteLoaderOptionsPagesInput + | RouteLoaderOptionsPagesAPIInput + +type RouteLoaderPagesAPIOptions = { + kind: RouteKind.PAGES_API + /** * The page name for this particular route. */ @@ -35,11 +48,52 @@ type RouteLoaderOptions = { * The absolute path to the userland page file. */ absolutePagePath: string - absoluteAppPath: string - absoluteDocumentPath: string + + /** + * The middleware config for this route. + */ middlewareConfigBase64: string } +type RouteLoaderPagesOptions = { + kind: RouteKind.PAGES + + /** + * The page name for this particular route. + */ + page: string + + /** + * The preferred region for this route. + */ + preferredRegion: string | string[] | undefined + + /** + * The absolute path to the userland page file. + */ + absolutePagePath: string + + /** + * The absolute paths to the app path file. + */ + absoluteAppPath: string + + /** + * The absolute paths to the document path file. + */ + absoluteDocumentPath: string + + /** + * The middleware config for this route. + */ + middlewareConfigBase64: string +} + +/** + * The options for the route loader. + */ +type RouteLoaderOptions = RouteLoaderPagesOptions | RouteLoaderPagesAPIOptions + /** * Returns the loader entry for a given page. * @@ -47,18 +101,176 @@ type RouteLoaderOptions = { * @returns the encoded loader entry */ export function getRouteLoaderEntry(options: RouteLoaderOptionsInput): string { - const query: RouteLoaderOptions = { - page: options.page, - preferredRegion: options.preferredRegion, - absolutePagePath: options.absolutePagePath, - // These are the path references to the internal components that may be - // overridden by userland components. - absoluteAppPath: options.pages['/_app'], - absoluteDocumentPath: options.pages['/_document'], - middlewareConfigBase64: encodeToBase64(options.middlewareConfig), + switch (options.kind) { + case RouteKind.PAGES: { + const query: RouteLoaderPagesOptions = { + kind: options.kind, + page: options.page, + preferredRegion: options.preferredRegion, + absolutePagePath: options.absolutePagePath, + // These are the path references to the internal components that may be + // overridden by userland components. + absoluteAppPath: options.pages['/_app'], + absoluteDocumentPath: options.pages['/_document'], + middlewareConfigBase64: encodeToBase64(options.middlewareConfig), + } + + return `next-route-loader?${stringify(query)}!` + } + case RouteKind.PAGES_API: { + const query: RouteLoaderPagesAPIOptions = { + kind: options.kind, + page: options.page, + preferredRegion: options.preferredRegion, + absolutePagePath: options.absolutePagePath, + middlewareConfigBase64: encodeToBase64(options.middlewareConfig), + } + + return `next-route-loader?${stringify(query)}!` + } + default: { + throw new Error('Invariant: Unexpected route kind') + } + } +} + +const loadPages = ( + { + page, + absolutePagePath, + absoluteDocumentPath, + absoluteAppPath, + preferredRegion, + middlewareConfigBase64, + }: RouteLoaderPagesOptions, + buildInfo: ModuleBuildInfo +) => { + const middlewareConfig: MiddlewareConfig = decodeFromBase64( + middlewareConfigBase64 + ) + + // Attach build info to the module. + buildInfo.route = { + page, + absolutePagePath, + preferredRegion, + middlewareConfig, } - return `next-route-loader?${stringify(query)}!` + const options: Omit = { + definition: { + kind: RouteKind.PAGES, + page: normalizePagePath(page), + pathname: page, + // The following aren't used in production. + bundlePath: '', + filename: '', + }, + } + + return ` + // Next.js Route Loader + import RouteModule from "next/dist/server/future/route-modules/pages/module" + import { hoist } from "next/dist/build/webpack/loaders/next-route-loader/helpers" + + // Import the app and document modules. + import Document from ${JSON.stringify(absoluteDocumentPath)} + import App from ${JSON.stringify(absoluteAppPath)} + + // Import the userland code. + import * as userland from ${JSON.stringify(absolutePagePath)} + + // Re-export the component (should be the default export). + export default hoist(userland, "default") + + // Re-export methods. + export const getStaticProps = hoist(userland, "getStaticProps") + export const getStaticPaths = hoist(userland, "getStaticPaths") + export const getServerSideProps = hoist(userland, "getServerSideProps") + export const config = hoist(userland, "config") + export const reportWebVitals = hoist(userland, "reportWebVitals") + ${ + // When we're building the instrumentation page (only when the + // instrumentation file conflicts with a page also labeled + // /instrumentation) hoist the `register` method. + isInstrumentationHookFile(page) + ? 'export const register = hoist(userland, "register")' + : '' + } + + // Re-export legacy methods. + export const unstable_getStaticProps = hoist(userland, "unstable_getStaticProps") + export const unstable_getStaticPaths = hoist(userland, "unstable_getStaticPaths") + export const unstable_getStaticParams = hoist(userland, "unstable_getStaticParams") + export const unstable_getServerProps = hoist(userland, "unstable_getServerProps") + export const unstable_getServerSideProps = hoist(userland, "unstable_getServerSideProps") + + // Create and export the route module that will be consumed. + const options = ${JSON.stringify(options)} + export const routeModule = new RouteModule({ + ...options, + components: { + App, + Document, + }, + userland, + }) + ` +} + +const loadPagesAPI = ( + { + page, + absolutePagePath, + preferredRegion, + middlewareConfigBase64, + }: RouteLoaderPagesAPIOptions, + buildInfo: ModuleBuildInfo +) => { + const middlewareConfig: MiddlewareConfig = decodeFromBase64( + middlewareConfigBase64 + ) + + // Attach build info to the module. + buildInfo.route = { + page, + absolutePagePath, + preferredRegion, + middlewareConfig, + } + + const options: Omit = { + definition: { + kind: RouteKind.PAGES_API, + page: normalizePagePath(page), + pathname: page, + // The following aren't used in production. + bundlePath: '', + filename: '', + }, + } + + return ` + // Next.js Route Loader + import RouteModule from "next/dist/server/future/route-modules/pages-api/module" + import { hoist } from "next/dist/build/webpack/loaders/next-route-loader/helpers" + + // Import the userland code. + import * as userland from ${JSON.stringify(absolutePagePath)} + + // Re-export the handler (should be the default export). + export default hoist(userland, "default") + + // Re-export config. + export const config = hoist(userland, "config") + + // Create and export the route module that will be consumed. + const options = ${JSON.stringify(options)} + export const routeModule = new RouteModule({ + ...options, + userland, + }) + ` } /** @@ -67,94 +279,24 @@ export function getRouteLoaderEntry(options: RouteLoaderOptionsInput): string { */ const loader: webpack.LoaderDefinitionFunction = function () { - const { - page, - preferredRegion, - absolutePagePath, - absoluteAppPath, - absoluteDocumentPath, - middlewareConfigBase64, - } = this.getOptions() - - // Ensure we only run this loader for as a module. if (!this._module) { throw new Error('Invariant: expected this to reference a module') } - const middlewareConfig: MiddlewareConfig = decodeFromBase64( - middlewareConfigBase64 - ) - - // Attach build info to the module. const buildInfo = getModuleBuildInfo(this._module) - buildInfo.route = { - page, - absolutePagePath, - preferredRegion, - middlewareConfig, + const opts = this.getOptions() + + switch (opts.kind) { + case RouteKind.PAGES: { + return loadPages(opts, buildInfo) + } + case RouteKind.PAGES_API: { + return loadPagesAPI(opts, buildInfo) + } + default: { + throw new Error('Invariant: Unexpected route kind') + } } - - const options: Omit = { - definition: { - kind: RouteKind.PAGES, - page: normalizePagePath(page), - pathname: page, - // The following aren't used in production. - bundlePath: '', - filename: '', - }, - } - - return ` - // Next.js Route Loader - import RouteModule from "next/dist/server/future/route-modules/pages/module" - import { hoist } from "next/dist/build/webpack/loaders/next-route-loader/helpers" - - // Import the app and document modules. - import * as moduleDocument from ${JSON.stringify(absoluteDocumentPath)} - import * as moduleApp from ${JSON.stringify(absoluteAppPath)} - - // Import the userland code. - import * as userland from ${JSON.stringify(absolutePagePath)} - - // Re-export the component (should be the default export). - export default hoist(userland, "default") - - // Re-export methods. - export const getStaticProps = hoist(userland, "getStaticProps") - export const getStaticPaths = hoist(userland, "getStaticPaths") - export const getServerSideProps = hoist(userland, "getServerSideProps") - export const config = hoist(userland, "config") - export const reportWebVitals = hoist(userland, "reportWebVitals") - ${ - // When we're building the instrumentation page (only when the - // instrumentation file conflicts with a page also labeled - // /instrumentation) hoist the `register` method. - isInstrumentationHookFile(page) - ? 'export const register = hoist(userland, "register")' - : '' - } - - // Re-export legacy methods. - export const unstable_getStaticProps = hoist(userland, "unstable_getStaticProps") - export const unstable_getStaticPaths = hoist(userland, "unstable_getStaticPaths") - export const unstable_getStaticParams = hoist(userland, "unstable_getStaticParams") - export const unstable_getServerProps = hoist(userland, "unstable_getServerProps") - export const unstable_getServerSideProps = hoist(userland, "unstable_getServerSideProps") - - // Create and export the route module that will be consumed. - const options = ${JSON.stringify(options)} - const routeModule = new RouteModule({ - ...options, - components: { - App: moduleApp.default, - Document: moduleDocument.default, - }, - userland, - }) - - export { routeModule } - ` } export default loader diff --git a/packages/next/src/server/api-utils/node.ts b/packages/next/src/server/api-utils/node.ts index 86dbc11809..3cac3a8f66 100644 --- a/packages/next/src/server/api-utils/node.ts +++ b/packages/next/src/server/api-utils/node.ts @@ -191,17 +191,17 @@ export async function parseBody( } } +type RevalidateFn = (config: { + urlPath: string + revalidateHeaders: { [key: string]: string | string[] } + opts: { unstable_onlyGenerated?: boolean } +}) => Promise + type ApiContext = __ApiPreviewProps & { trustHostHeader?: boolean allowedRevalidateHeaderKeys?: string[] hostname?: string - revalidate?: (config: { - urlPath: string - revalidateHeaders: { [key: string]: string | string[] } - opts: { unstable_onlyGenerated?: boolean } - }) => Promise - - // (_req: IncomingMessage, _res: ServerResponse) => Promise + revalidate?: RevalidateFn } function getMaxContentLength(responseLimit?: ResponseLimit) { diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index b63dcb14c0..f33cf72d9b 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -31,6 +31,7 @@ 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' import type { WebNextRequest, WebNextResponse } from './base-http/web' +import type { PagesAPIRouteMatch } from './future/route-matches/pages-api-route-match' import { format as formatUrl, parse as parseUrl } from 'url' import { getRedirectStatus } from '../lib/redirect-status' @@ -314,9 +315,7 @@ export default abstract class Server { req: BaseNextRequest, res: BaseNextResponse, query: ParsedUrlQuery, - params: Params | undefined, - page: string, - builtPagePath: string + match: PagesAPIRouteMatch ): Promise protected abstract renderHTML( @@ -1897,13 +1896,13 @@ export default abstract class Server { components.routeModule ) { const module = components.routeModule as PagesRouteModule - renderOpts.clientReferenceManifest = components.clientReferenceManifest // Due to the way we pass data by mutating `renderOpts`, we can't extend - // the object here but only updating its `nextFontManifest` - // field. + // the object here but only updating its `clientReferenceManifest` and + // `nextFontManifest` properties. // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952 renderOpts.nextFontManifest = this.nextFontManifest + renderOpts.clientReferenceManifest = components.clientReferenceManifest // Call the built-in render method on the module. result = await module.render( @@ -1920,7 +1919,7 @@ export default abstract class Server { 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. + // object here but only updating its `nextFontManifest` field. // https://github.com/vercel/next.js/blob/df7cbd904c3bd85f399d1ce90680c0ecf92d2752/packages/next/server/render.tsx#L947-L952 renderOpts.nextFontManifest = this.nextFontManifest diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index 4d96f8ba4e..4b683dc320 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -297,6 +297,11 @@ export interface ExperimentalConfig { * Enables source maps generation for the server production bundle. */ serverSourceMaps?: boolean + + /** + * @internal Used by the Next.js internals only. + */ + trustHostHeader?: boolean } export type ExportPathMap = { diff --git a/packages/next/src/server/dev/hot-reloader.ts b/packages/next/src/server/dev/hot-reloader.ts index f6a0f496e6..43e7007166 100644 --- a/packages/next/src/server/dev/hot-reloader.ts +++ b/packages/next/src/server/dev/hot-reloader.ts @@ -62,6 +62,7 @@ import { parseVersionInfo, VersionInfo } from './parse-version-info' import { isAPIRoute } from '../../lib/is-api-route' import { getRouteLoaderEntry } from '../../build/webpack/loaders/next-route-loader' import { isInternalComponent } from '../../lib/is-internal-component' +import { RouteKind } from '../future/route-kind' function diff(a: Set, b: Set) { return new Set([...a].filter((v) => !b.has(v))) @@ -906,12 +907,20 @@ export default class HotReloader { JSON.stringify(staticInfo.middleware || {}) ).toString('base64'), }) + } else if (isAPIRoute(page)) { + value = getRouteLoaderEntry({ + kind: RouteKind.PAGES_API, + page, + absolutePagePath: relativeRequest, + preferredRegion: staticInfo.preferredRegion, + middlewareConfig: staticInfo.middleware || {}, + }) } else if ( - !isAPIRoute(page) && !isMiddlewareFile(page) && !isInternalComponent(relativeRequest) ) { value = getRouteLoaderEntry({ + kind: RouteKind.PAGES, page, pages: this.pagesMapping, absolutePagePath: relativeRequest, diff --git a/packages/next/src/server/future/helpers/module-loader/route-module-loader.ts b/packages/next/src/server/future/helpers/module-loader/route-module-loader.ts index 4f68de2d50..98fd2dbc81 100644 --- a/packages/next/src/server/future/helpers/module-loader/route-module-loader.ts +++ b/packages/next/src/server/future/helpers/module-loader/route-module-loader.ts @@ -12,12 +12,11 @@ export class RouteModuleLoader { id: string, loader: ModuleLoader = new NodeModuleLoader() ): Promise { - if (process.env.NEXT_RUNTIME !== 'edge') { - const { routeModule }: AppLoaderModule = await loader.load(id) - - return routeModule + const module: AppLoaderModule = await loader.load(id) + if ('routeModule' in module) { + return module.routeModule } - throw new Error('RouteModuleLoader is not supported in edge runtime.') + throw new Error(`Module "${id}" does not export a routeModule.`) } } diff --git a/packages/next/src/server/future/route-modules/pages-api/module.ts b/packages/next/src/server/future/route-modules/pages-api/module.ts new file mode 100644 index 0000000000..6147dd4dbd --- /dev/null +++ b/packages/next/src/server/future/route-modules/pages-api/module.ts @@ -0,0 +1,137 @@ +import type { IncomingMessage, ServerResponse } from 'http' +import type { PagesAPIRouteDefinition } from '../../route-definitions/pages-api-route-definition' +import type { PageConfig } from '../../../../../types' +import type { ParsedUrlQuery } from 'querystring' + +import { + RouteModule, + RouteModuleOptions, + type RouteModuleHandleContext, +} from '../route-module' +import { apiResolver } from '../../../api-utils/node' +import { __ApiPreviewProps } from '../../../api-utils' + +type PagesAPIHandleFn = ( + req: IncomingMessage, + res: ServerResponse +) => Promise + +type PagesAPIUserlandModule = { + /** + * The exported handler method. + */ + readonly default: PagesAPIHandleFn + + /** + * The exported page config. + */ + readonly config?: PageConfig +} + +type PagesAPIRouteHandlerContext = RouteModuleHandleContext & { + /** + * The incoming server request in non-edge runtime. + */ + req?: IncomingMessage + + /** + * The outgoing server response in non-edge runtime. + */ + res?: ServerResponse + + /** + * The revalidate method used by the `revalidate` API. + * + * @param config the configuration for the revalidation + */ + revalidate: (config: { + urlPath: string + revalidateHeaders: { [key: string]: string | string[] } + opts: { unstable_onlyGenerated?: boolean } + }) => Promise + + /** + * The hostname for the request. + */ + hostname?: string + + /** + * Keys allowed in the revalidate call. + */ + allowedRevalidateHeaderKeys?: string[] + + /** + * Whether to trust the host header. + */ + trustHostHeader?: boolean + + /** + * The query for the request. + */ + query: ParsedUrlQuery + + /** + * The preview props used by the `preview` API. + */ + previewProps: __ApiPreviewProps + + /** + * True if the server is in development mode. + */ + dev: boolean + + /** + * True if the server is in minimal mode. + */ + minimalMode: boolean + + /** + * The page that's being rendered. + */ + page: string +} + +export type PagesAPIRouteModuleOptions = RouteModuleOptions< + PagesAPIRouteDefinition, + PagesAPIUserlandModule +> + +export class PagesAPIRouteModule extends RouteModule< + PagesAPIRouteDefinition, + PagesAPIUserlandModule +> { + public handle(): Promise { + throw new Error('Method not implemented.') + } + + /** + * + * @param req the incoming server request + * @param res the outgoing server response + * @param context the context for the render + */ + public async render( + req: IncomingMessage, + res: ServerResponse, + context: PagesAPIRouteHandlerContext + ): Promise { + await apiResolver( + req, + res, + context.query, + this.userland, + { + ...context.previewProps, + revalidate: context.revalidate, + trustHostHeader: context.trustHostHeader, + allowedRevalidateHeaderKeys: context.allowedRevalidateHeaderKeys, + hostname: context.hostname, + }, + context.minimalMode, + context.dev, + context.page + ) + } +} + +export default PagesAPIRouteModule diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 9d6bddb231..57431a8634 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -31,6 +31,7 @@ import { renderToHTML, type RenderOpts } from './render' import fs from 'fs' import { join, resolve, isAbsolute } from 'path' import { IncomingMessage, ServerResponse } from 'http' +import type { PagesAPIRouteModule } from './future/route-modules/pages-api/module' import { addRequestMeta, getRequestMeta } from './request-meta' import { PAGES_MANIFEST, @@ -52,7 +53,6 @@ import { NodeNextRequest, NodeNextResponse } from './base-http/node' import { sendRenderResult } from './send-payload' import { getExtension, serveStatic } from './serve-static' import { ParsedUrlQuery } from 'querystring' -import { apiResolver } from './api-utils/node' import { ParsedUrl, parseUrl } from '../shared/lib/router/utils/parse-url' import * as Log from '../build/output/log' @@ -97,6 +97,7 @@ import { createRequestResponseMocks } from './lib/mock-request' import chalk from 'next/dist/compiled/chalk' import { NEXT_RSC_UNION_QUERY } from '../client/components/app-router-headers' import { signalFromNodeRequest } from './web/spec-extension/adapters/next-request' +import { RouteModuleLoader } from './future/helpers/module-loader/route-module-loader' import { loadManifest } from './load-manifest' export * from './base-server' @@ -382,20 +383,18 @@ export default class NextNodeServer extends BaseServer { req: BaseNextRequest | NodeNextRequest, res: BaseNextResponse | NodeNextResponse, query: ParsedUrlQuery, - params: Params | undefined, - page: string, - builtPagePath: string + match: PagesAPIRouteMatch ): Promise { const edgeFunctionsPages = this.getEdgeFunctionsPages() for (const edgeFunctionsPage of edgeFunctionsPages) { - if (edgeFunctionsPage === page) { + if (edgeFunctionsPage === match.definition.pathname) { const handledAsEdgeFunction = await this.runEdgeFunction({ req, res, query, - params, - page, + params: match.params, + page: match.definition.pathname, appPaths: null, }) @@ -405,32 +404,35 @@ export default class NextNodeServer extends BaseServer { } } - const pageModule = await require(builtPagePath) - query = { ...query, ...params } + // The module supports minimal mode, load the minimal module. + const module = await RouteModuleLoader.load( + match.definition.filename + ) + + query = { ...query, ...match.params } delete query.__nextLocale delete query.__nextDefaultLocale delete query.__nextInferredLocaleFromDefault - await apiResolver( + await module.render( (req as NodeNextRequest).originalRequest, (res as NodeNextResponse).originalResponse, - query, - pageModule, { - ...this.renderOpts.previewProps, + previewProps: this.renderOpts.previewProps, revalidate: this.revalidate.bind(this), - // internal config so is not typed - trustHostHeader: (this.nextConfig.experimental as Record) - .trustHostHeader, + trustHostHeader: this.nextConfig.experimental.trustHostHeader, allowedRevalidateHeaderKeys: this.nextConfig.experimental.allowedRevalidateHeaderKeys, hostname: this.hostname, - }, - this.minimalMode, - this.renderOpts.dev, - page + minimalMode: this.minimalMode, + dev: this.renderOpts.dev === true, + query, + params: match.params, + page: match.definition.pathname, + } ) + return true } @@ -1022,12 +1024,7 @@ export default class NextNodeServer extends BaseServer { query: ParsedUrlQuery, match: PagesAPIRouteMatch ): Promise { - const { - definition: { pathname, filename }, - params, - } = match - - return this.runApi(req, res, query, params, pathname, filename) + return this.runApi(req, res, query, match) } protected getCacheFilesystem(): CacheFs { @@ -1214,7 +1211,6 @@ export default class NextNodeServer extends BaseServer { ) { throw new Error(`Invalid response ${mocked.res.statusCode}`) } - return {} } public async render(