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:
parent
f57eecde5e
commit
205d3845d1
13 changed files with 606 additions and 168 deletions
|
@ -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) =
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ use crate::next_config::{NextConfig, OutputType};
|
|||
#[derive(Debug, Clone, Copy, PartialEq, Eq, TaskInput)]
|
||||
pub enum PathType {
|
||||
Page,
|
||||
PagesAPI,
|
||||
Data,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<string>
|
||||
route?: RouteMeta
|
||||
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 {
|
||||
nextEdgeMiddleware?: EdgeMiddlewareMeta
|
||||
nextEdgeApiFunction?: EdgeMiddlewareMeta
|
||||
nextEdgeSSR?: EdgeSSRMeta
|
||||
nextWasmMiddlewareBinding?: AssetBinding
|
||||
nextAssetMiddlewareBinding?: AssetBinding
|
||||
usingIndirectEval?: boolean | Set<string>
|
||||
route?: RouteMeta
|
||||
importLocByPath?: Map<string, any>
|
||||
rootDir?: string
|
||||
rsc?: RSCMeta
|
||||
}
|
||||
return webpackModule.buildInfo as ModuleBuildInfo
|
||||
}
|
||||
|
||||
export interface RSCMeta {
|
||||
|
|
|
@ -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<PagesRouteModuleOptions, 'userland' | 'components'> = {
|
||||
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<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,
|
||||
})
|
||||
`
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -67,94 +279,24 @@ export function getRouteLoaderEntry(options: RouteLoaderOptionsInput): string {
|
|||
*/
|
||||
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')
|
||||
}
|
||||
|
||||
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<PagesRouteModuleOptions, 'userland' | 'components'> = {
|
||||
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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue