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.
This commit is contained in:
Wyatt Johnson 2023-07-20 23:51:37 -06:00 committed by GitHub
parent f57eecde5e
commit 205d3845d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 606 additions and 168 deletions

View file

@ -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<PagesDirectoryStructure>,
next_router_root: Vc<FileSystemPath>,
entries: &mut Vec<Vc<PageEntry>>,
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<Box<dyn Source>>,
next_router_root: Vc<FileSystemPath>,
next_router_path: Vc<FileSystemPath>,
next_original_path: Vc<FileSystemPath>,
path_type: PathType,
) -> Result<Vc<PageEntry>> {
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) =

View file

@ -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'

View file

@ -34,6 +34,7 @@ use crate::next_config::{NextConfig, OutputType};
#[derive(Debug, Clone, Copy, PartialEq, Eq, TaskInput)]
pub enum PathType {
Page,
PagesAPI,
Data,
}

View file

@ -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,

View file

@ -5,12 +5,7 @@ import type {
} from '../../analysis/get-page-static-info'
import { webpack } from 'next/dist/compiled/webpack/webpack'
/**
* 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 {
export type ModuleBuildInfo = {
nextEdgeMiddleware?: EdgeMiddlewareMeta
nextEdgeApiFunction?: EdgeMiddlewareMeta
nextEdgeSSR?: EdgeSSRMeta
@ -21,7 +16,14 @@ export function getModuleBuildInfo(webpackModule: webpack.Module) {
importLocByPath?: Map<string, any>
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 ModuleBuildInfo
}
export interface RSCMeta {

View file

@ -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,7 +101,10 @@ type RouteLoaderOptions = {
* @returns the encoded loader entry
*/
export function getRouteLoaderEntry(options: RouteLoaderOptionsInput): string {
const query: RouteLoaderOptions = {
switch (options.kind) {
case RouteKind.PAGES: {
const query: RouteLoaderPagesOptions = {
kind: options.kind,
page: options.page,
preferredRegion: options.preferredRegion,
absolutePagePath: options.absolutePagePath,
@ -59,34 +116,40 @@ export function getRouteLoaderEntry(options: RouteLoaderOptionsInput): string {
}
return `next-route-loader?${stringify(query)}!`
}
/**
* Handles the `next-route-loader` options.
* @returns the loader definition function
*/
const loader: webpack.LoaderDefinitionFunction<RouteLoaderOptions> =
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')
}
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.
const buildInfo = getModuleBuildInfo(this._module)
buildInfo.route = {
page,
absolutePagePath,
@ -111,8 +174,8 @@ const loader: webpack.LoaderDefinitionFunction<RouteLoaderOptions> =
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 Document from ${JSON.stringify(absoluteDocumentPath)}
import App from ${JSON.stringify(absoluteAppPath)}
// Import the userland code.
import * as userland from ${JSON.stringify(absolutePagePath)}
@ -144,17 +207,96 @@ const loader: webpack.LoaderDefinitionFunction<RouteLoaderOptions> =
// Create and export the route module that will be consumed.
const options = ${JSON.stringify(options)}
const routeModule = new RouteModule({
export const routeModule = new RouteModule({
...options,
components: {
App: moduleApp.default,
Document: moduleDocument.default,
App,
Document,
},
userland,
})
export { routeModule }
`
}
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<PagesAPIRouteModuleOptions, 'userland' | 'components'> = {
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,
})
`
}
/**
* Handles the `next-route-loader` options.
* @returns the loader definition function
*/
const loader: webpack.LoaderDefinitionFunction<RouteLoaderOptions> =
function () {
if (!this._module) {
throw new Error('Invariant: expected this to reference a module')
}
const buildInfo = getModuleBuildInfo(this._module)
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')
}
}
}
export default loader

View file

@ -191,17 +191,17 @@ export async function parseBody(
}
}
type RevalidateFn = (config: {
urlPath: string
revalidateHeaders: { [key: string]: string | string[] }
opts: { unstable_onlyGenerated?: boolean }
}) => Promise<void>
type ApiContext = __ApiPreviewProps & {
trustHostHeader?: boolean
allowedRevalidateHeaderKeys?: string[]
hostname?: string
revalidate?: (config: {
urlPath: string
revalidateHeaders: { [key: string]: string | string[] }
opts: { unstable_onlyGenerated?: boolean }
}) => Promise<any>
// (_req: IncomingMessage, _res: ServerResponse) => Promise<any>
revalidate?: RevalidateFn
}
function getMaxContentLength(responseLimit?: ResponseLimit) {

View file

@ -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<ServerOptions extends Options = Options> {
req: BaseNextRequest,
res: BaseNextResponse,
query: ParsedUrlQuery,
params: Params | undefined,
page: string,
builtPagePath: string
match: PagesAPIRouteMatch
): Promise<boolean>
protected abstract renderHTML(
@ -1897,13 +1896,13 @@ export default abstract class Server<ServerOptions extends Options = Options> {
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<ServerOptions extends Options = Options> {
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

View file

@ -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 = {

View file

@ -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<any>, b: Set<any>) {
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,

View file

@ -12,12 +12,11 @@ export class RouteModuleLoader {
id: string,
loader: ModuleLoader = new NodeModuleLoader()
): Promise<M> {
if (process.env.NEXT_RUNTIME !== 'edge') {
const { routeModule }: AppLoaderModule<M> = await loader.load(id)
return routeModule
const module: AppLoaderModule<M> = 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.`)
}
}

View file

@ -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<void>
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<void>
/**
* 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<Response> {
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<void> {
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

View file

@ -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<boolean> {
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<PagesAPIRouteModule>(
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<string, any>)
.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<boolean> {
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(