Reverts vercel/next.js#53016
This commit is contained in:
parent
552bca46eb
commit
1398de9977
68 changed files with 5853 additions and 4890 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'
|
||||
|
||||
|
|
|
@ -29,6 +29,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,
|
||||
|
|
|
@ -17,7 +17,6 @@ import os from 'os'
|
|||
import { Worker } from '../lib/worker'
|
||||
import { defaultConfig } from '../server/config-shared'
|
||||
import devalue from 'next/dist/compiled/devalue'
|
||||
import { escapeStringRegexp } from '../shared/lib/escape-regexp'
|
||||
import findUp from 'next/dist/compiled/find-up'
|
||||
import { nanoid } from 'next/dist/compiled/nanoid/index.cjs'
|
||||
import { pathToRegexp } from 'next/dist/compiled/path-to-regexp'
|
||||
|
@ -141,7 +140,12 @@ import { createClientRouterFilter } from '../lib/create-client-router-filter'
|
|||
import { createValidFileMatcher } from '../server/lib/find-page-file'
|
||||
import { startTypeChecking } from './type-check'
|
||||
import { generateInterceptionRoutesRewrites } from '../lib/generate-interception-routes-rewrites'
|
||||
import { baseOverrides, experimentalOverrides } from '../server/require-hook'
|
||||
import { buildDataRoute } from '../server/lib/router-utils/build-data-route'
|
||||
import {
|
||||
baseOverrides,
|
||||
defaultOverrides,
|
||||
experimentalOverrides,
|
||||
} from '../server/require-hook'
|
||||
|
||||
export type SsgRoute = {
|
||||
initialRevalidateSeconds: number | false
|
||||
|
@ -166,6 +170,66 @@ export type PrerenderManifest = {
|
|||
preview: __ApiPreviewProps
|
||||
}
|
||||
|
||||
type CustomRoute = {
|
||||
regex: string
|
||||
statusCode?: number | undefined
|
||||
permanent?: undefined
|
||||
source: string
|
||||
locale?: false | undefined
|
||||
basePath?: false | undefined
|
||||
destination?: string | undefined
|
||||
}
|
||||
|
||||
export type RoutesManifest = {
|
||||
version: number
|
||||
pages404: boolean
|
||||
basePath: string
|
||||
redirects: Array<CustomRoute>
|
||||
rewrites?:
|
||||
| Array<CustomRoute>
|
||||
| {
|
||||
beforeFiles: Array<CustomRoute>
|
||||
afterFiles: Array<CustomRoute>
|
||||
fallback: Array<CustomRoute>
|
||||
}
|
||||
headers: Array<CustomRoute>
|
||||
staticRoutes: Array<{
|
||||
page: string
|
||||
regex: string
|
||||
namedRegex?: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
}>
|
||||
dynamicRoutes: Array<{
|
||||
page: string
|
||||
regex: string
|
||||
namedRegex?: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
}>
|
||||
dataRoutes: Array<{
|
||||
page: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
dataRouteRegex: string
|
||||
namedDataRouteRegex?: string
|
||||
}>
|
||||
i18n?: {
|
||||
domains?: Array<{
|
||||
http?: true
|
||||
domain: string
|
||||
locales?: string[]
|
||||
defaultLocale: string
|
||||
}>
|
||||
locales: string[]
|
||||
defaultLocale: string
|
||||
localeDetection?: false
|
||||
}
|
||||
rsc: {
|
||||
header: typeof RSC
|
||||
varyHeader: typeof RSC_VARY_HEADER
|
||||
}
|
||||
skipMiddlewareUrlNormalize?: boolean
|
||||
caseSensitive?: boolean
|
||||
}
|
||||
|
||||
async function generateClientSsgManifest(
|
||||
prerenderManifest: PrerenderManifest,
|
||||
{
|
||||
|
@ -684,89 +748,45 @@ export default async function build(
|
|||
}
|
||||
|
||||
const routesManifestPath = path.join(distDir, ROUTES_MANIFEST)
|
||||
const routesManifest: {
|
||||
version: number
|
||||
pages404: boolean
|
||||
basePath: string
|
||||
redirects: Array<ReturnType<typeof buildCustomRoute>>
|
||||
rewrites?:
|
||||
| Array<ReturnType<typeof buildCustomRoute>>
|
||||
| {
|
||||
beforeFiles: Array<ReturnType<typeof buildCustomRoute>>
|
||||
afterFiles: Array<ReturnType<typeof buildCustomRoute>>
|
||||
fallback: Array<ReturnType<typeof buildCustomRoute>>
|
||||
const routesManifest: RoutesManifest = nextBuildSpan
|
||||
.traceChild('generate-routes-manifest')
|
||||
.traceFn(() => {
|
||||
const sortedRoutes = getSortedRoutes([
|
||||
...pageKeys.pages,
|
||||
...(pageKeys.app ?? []),
|
||||
])
|
||||
const dynamicRoutes: Array<ReturnType<typeof pageToRoute>> = []
|
||||
const staticRoutes: typeof dynamicRoutes = []
|
||||
|
||||
for (const route of sortedRoutes) {
|
||||
if (isDynamicRoute(route)) {
|
||||
dynamicRoutes.push(pageToRoute(route))
|
||||
} else if (!isReservedPage(route)) {
|
||||
staticRoutes.push(pageToRoute(route))
|
||||
}
|
||||
headers: Array<ReturnType<typeof buildCustomRoute>>
|
||||
staticRoutes: Array<{
|
||||
page: string
|
||||
regex: string
|
||||
namedRegex?: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
}>
|
||||
dynamicRoutes: Array<{
|
||||
page: string
|
||||
regex: string
|
||||
namedRegex?: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
}>
|
||||
dataRoutes: Array<{
|
||||
page: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
dataRouteRegex: string
|
||||
namedDataRouteRegex?: string
|
||||
}>
|
||||
i18n?: {
|
||||
domains?: Array<{
|
||||
http?: true
|
||||
domain: string
|
||||
locales?: string[]
|
||||
defaultLocale: string
|
||||
}>
|
||||
locales: string[]
|
||||
defaultLocale: string
|
||||
localeDetection?: false
|
||||
}
|
||||
rsc: {
|
||||
header: typeof RSC
|
||||
varyHeader: typeof RSC_VARY_HEADER
|
||||
}
|
||||
skipMiddlewareUrlNormalize?: boolean
|
||||
caseSensitive?: boolean
|
||||
} = nextBuildSpan.traceChild('generate-routes-manifest').traceFn(() => {
|
||||
const sortedRoutes = getSortedRoutes([
|
||||
...pageKeys.pages,
|
||||
...(pageKeys.app ?? []),
|
||||
])
|
||||
const dynamicRoutes: Array<ReturnType<typeof pageToRoute>> = []
|
||||
const staticRoutes: typeof dynamicRoutes = []
|
||||
|
||||
for (const route of sortedRoutes) {
|
||||
if (isDynamicRoute(route)) {
|
||||
dynamicRoutes.push(pageToRoute(route))
|
||||
} else if (!isReservedPage(route)) {
|
||||
staticRoutes.push(pageToRoute(route))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: 3,
|
||||
pages404: true,
|
||||
caseSensitive: !!config.experimental.caseSensitiveRoutes,
|
||||
basePath: config.basePath,
|
||||
redirects: redirects.map((r: any) => buildCustomRoute(r, 'redirect')),
|
||||
headers: headers.map((r: any) => buildCustomRoute(r, 'header')),
|
||||
dynamicRoutes,
|
||||
staticRoutes,
|
||||
dataRoutes: [],
|
||||
i18n: config.i18n || undefined,
|
||||
rsc: {
|
||||
header: RSC,
|
||||
varyHeader: RSC_VARY_HEADER,
|
||||
contentTypeHeader: RSC_CONTENT_TYPE_HEADER,
|
||||
},
|
||||
skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize,
|
||||
}
|
||||
})
|
||||
return {
|
||||
version: 3,
|
||||
pages404: true,
|
||||
caseSensitive: !!config.experimental.caseSensitiveRoutes,
|
||||
basePath: config.basePath,
|
||||
redirects: redirects.map((r: any) =>
|
||||
buildCustomRoute(r, 'redirect')
|
||||
),
|
||||
headers: headers.map((r: any) => buildCustomRoute(r, 'header')),
|
||||
dynamicRoutes,
|
||||
staticRoutes,
|
||||
dataRoutes: [],
|
||||
i18n: config.i18n || undefined,
|
||||
rsc: {
|
||||
header: RSC,
|
||||
varyHeader: RSC_VARY_HEADER,
|
||||
contentTypeHeader: RSC_CONTENT_TYPE_HEADER,
|
||||
},
|
||||
skipMiddlewareUrlNormalize: config.skipMiddlewareUrlNormalize,
|
||||
}
|
||||
})
|
||||
|
||||
if (rewrites.beforeFiles.length === 0 && rewrites.fallback.length === 0) {
|
||||
routesManifest.rewrites = rewrites.afterFiles.map((r: any) =>
|
||||
|
@ -903,6 +923,7 @@ export default async function build(
|
|||
]
|
||||
: []),
|
||||
path.join(SERVER_DIRECTORY, APP_PATHS_MANIFEST),
|
||||
path.join(APP_PATH_ROUTES_MANIFEST),
|
||||
APP_BUILD_MANIFEST,
|
||||
path.join(
|
||||
SERVER_DIRECTORY,
|
||||
|
@ -1962,6 +1983,11 @@ export default async function build(
|
|||
...Object.values(experimentalOverrides).map((override) =>
|
||||
require.resolve(override)
|
||||
),
|
||||
...(config.experimental.turbotrace
|
||||
? []
|
||||
: Object.values(defaultOverrides).map((value) =>
|
||||
require.resolve(value)
|
||||
)),
|
||||
]
|
||||
|
||||
// ensure we trace any dependencies needed for custom
|
||||
|
@ -1979,9 +2005,7 @@ export default async function build(
|
|||
const vanillaServerEntries = [
|
||||
...sharedEntriesSet,
|
||||
isStandalone
|
||||
? require.resolve(
|
||||
'next/dist/server/lib/render-server-standalone'
|
||||
)
|
||||
? require.resolve('next/dist/server/lib/start-server')
|
||||
: null,
|
||||
require.resolve('next/dist/server/next-server'),
|
||||
].filter(Boolean) as string[]
|
||||
|
@ -2158,49 +2182,7 @@ export default async function build(
|
|||
...serverPropsPages,
|
||||
...ssgPages,
|
||||
]).map((page) => {
|
||||
const pagePath = normalizePagePath(page)
|
||||
const dataRoute = path.posix.join(
|
||||
'/_next/data',
|
||||
buildId,
|
||||
`${pagePath}.json`
|
||||
)
|
||||
|
||||
let dataRouteRegex: string
|
||||
let namedDataRouteRegex: string | undefined
|
||||
let routeKeys: { [named: string]: string } | undefined
|
||||
|
||||
if (isDynamicRoute(page)) {
|
||||
const routeRegex = getNamedRouteRegex(
|
||||
dataRoute.replace(/\.json$/, ''),
|
||||
true
|
||||
)
|
||||
|
||||
dataRouteRegex = normalizeRouteRegex(
|
||||
routeRegex.re.source.replace(/\(\?:\\\/\)\?\$$/, `\\.json$`)
|
||||
)
|
||||
namedDataRouteRegex = routeRegex.namedRegex!.replace(
|
||||
/\(\?:\/\)\?\$$/,
|
||||
`\\.json$`
|
||||
)
|
||||
routeKeys = routeRegex.routeKeys
|
||||
} else {
|
||||
dataRouteRegex = normalizeRouteRegex(
|
||||
new RegExp(
|
||||
`^${path.posix.join(
|
||||
'/_next/data',
|
||||
escapeStringRegexp(buildId),
|
||||
`${pagePath}.json`
|
||||
)}$`
|
||||
).source
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
routeKeys,
|
||||
dataRouteRegex,
|
||||
namedDataRouteRegex,
|
||||
}
|
||||
return buildDataRoute(page, buildId)
|
||||
})
|
||||
|
||||
await fs.writeFile(
|
||||
|
|
|
@ -1928,12 +1928,12 @@ export async function copyTracedFiles(
|
|||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
const __dirname = fileURLToPath(new URL('.', import.meta.url))
|
||||
import { createServerHandler } from 'next/dist/server/lib/render-server-standalone.js'
|
||||
import { startServer } from 'next/dist/server/lib/start-server.js'
|
||||
`
|
||||
: `
|
||||
const http = require('http')
|
||||
const path = require('path')
|
||||
const { createServerHandler } = require('next/dist/server/lib/render-server-standalone')`
|
||||
const { startServer } = require('next/dist/server/lib/start-server')`
|
||||
}
|
||||
|
||||
const dir = path.join(__dirname)
|
||||
|
@ -1950,11 +1950,7 @@ if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
|
|||
|
||||
const currentPort = parseInt(process.env.PORT, 10) || 3000
|
||||
const hostname = process.env.HOSTNAME || 'localhost'
|
||||
const keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);
|
||||
const isValidKeepAliveTimeout =
|
||||
!Number.isNaN(keepAliveTimeout) &&
|
||||
Number.isFinite(keepAliveTimeout) &&
|
||||
keepAliveTimeout >= 0;
|
||||
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10);
|
||||
const nextConfig = ${JSON.stringify({
|
||||
...serverConfig,
|
||||
distDir: `./${path.relative(dir, distDir)}`,
|
||||
|
@ -1962,41 +1958,30 @@ const nextConfig = ${JSON.stringify({
|
|||
|
||||
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
|
||||
|
||||
createServerHandler({
|
||||
port: currentPort,
|
||||
hostname,
|
||||
if (
|
||||
Number.isNaN(keepAliveTimeout) ||
|
||||
!Number.isFinite(keepAliveTimeout) ||
|
||||
keepAliveTimeout < 0
|
||||
) {
|
||||
keepAliveTimeout = undefined
|
||||
}
|
||||
|
||||
startServer({
|
||||
dir,
|
||||
conf: nextConfig,
|
||||
keepAliveTimeout: isValidKeepAliveTimeout ? keepAliveTimeout : undefined,
|
||||
}).then((nextHandler) => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
await nextHandler(req, res)
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
}
|
||||
})
|
||||
|
||||
if (isValidKeepAliveTimeout) {
|
||||
server.keepAliveTimeout = keepAliveTimeout
|
||||
}
|
||||
|
||||
server.listen(currentPort, async (err) => {
|
||||
if (err) {
|
||||
console.error("Failed to start server", err)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'Listening on port',
|
||||
currentPort,
|
||||
'url: http://' + hostname + ':' + currentPort
|
||||
)
|
||||
});
|
||||
|
||||
}).catch(err => {
|
||||
isDev: false,
|
||||
config: nextConfig,
|
||||
hostname: hostname === 'localhost' ? '0.0.0.0' : hostname,
|
||||
port: currentPort,
|
||||
allowRetry: false,
|
||||
keepAliveTimeout,
|
||||
useWorkers: !!nextConfig.experimental?.appDir,
|
||||
}).then(() => {
|
||||
console.log(
|
||||
'Listening on port',
|
||||
currentPort,
|
||||
'url: http://' + hostname + ':' + currentPort
|
||||
)
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -35,10 +35,27 @@ function generateClientManifest(
|
|||
'NextJsBuildManifest-generateClientManifest'
|
||||
)
|
||||
|
||||
const normalizeRewrite = (item: {
|
||||
source: string
|
||||
destination: string
|
||||
has?: any
|
||||
}) => {
|
||||
return {
|
||||
has: item.has,
|
||||
source: item.source,
|
||||
destination: item.destination,
|
||||
}
|
||||
}
|
||||
|
||||
return genClientManifestSpan?.traceFn(() => {
|
||||
const clientManifest: ClientBuildManifest = {
|
||||
// TODO: update manifest type to include rewrites
|
||||
__rewrites: rewrites as any,
|
||||
__rewrites: {
|
||||
afterFiles: rewrites.afterFiles?.map((item) => normalizeRewrite(item)),
|
||||
beforeFiles: rewrites.beforeFiles?.map((item) =>
|
||||
normalizeRewrite(item)
|
||||
),
|
||||
fallback: rewrites.fallback?.map((item) => normalizeRewrite(item)),
|
||||
} as any,
|
||||
}
|
||||
const appDependencies = new Set(assetMap.pages['/_app'])
|
||||
const sortedPageKeys = getSortedRoutes(Object.keys(assetMap.pages))
|
||||
|
|
|
@ -20,9 +20,15 @@ const originModules = [
|
|||
|
||||
const RUNTIME_NAMES = ['webpack-runtime', 'webpack-api-runtime']
|
||||
|
||||
const nextDeleteCacheRpc = async (filePaths: string[]) => {
|
||||
if ((global as any)._nextDeleteCache) {
|
||||
return (global as any)._nextDeleteCache(filePaths)
|
||||
}
|
||||
}
|
||||
|
||||
export function deleteAppClientCache() {
|
||||
if ((global as any)._nextDeleteAppClientCache) {
|
||||
;(global as any)._nextDeleteAppClientCache()
|
||||
return (global as any)._nextDeleteAppClientCache()
|
||||
}
|
||||
// ensure we reset the cache for rsc components
|
||||
// loaded via react-server-dom-webpack
|
||||
|
@ -41,10 +47,6 @@ export function deleteAppClientCache() {
|
|||
}
|
||||
|
||||
export function deleteCache(filePath: string) {
|
||||
if ((global as any)._nextDeleteCache) {
|
||||
;(global as any)._nextDeleteCache(filePath)
|
||||
}
|
||||
|
||||
// try to clear it from the fs cache
|
||||
clearManifestCache(filePath)
|
||||
|
||||
|
@ -86,7 +88,7 @@ export class NextJsRequireCacheHotReloader implements WebpackPluginInstance {
|
|||
|
||||
apply(compiler: Compiler) {
|
||||
compiler.hooks.assetEmitted.tap(PLUGIN_NAME, (_file, { targetPath }) => {
|
||||
deleteCache(targetPath)
|
||||
nextDeleteCacheRpc([targetPath])
|
||||
|
||||
// Clear module context in other processes
|
||||
if ((global as any)._nextClearModuleContext) {
|
||||
|
@ -96,35 +98,33 @@ export class NextJsRequireCacheHotReloader implements WebpackPluginInstance {
|
|||
clearModuleContext(targetPath)
|
||||
})
|
||||
|
||||
compiler.hooks.afterEmit.tap(PLUGIN_NAME, (compilation) => {
|
||||
RUNTIME_NAMES.forEach((name) => {
|
||||
compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async (compilation) => {
|
||||
const cacheEntriesToDelete = []
|
||||
|
||||
for (const name of RUNTIME_NAMES) {
|
||||
const runtimeChunkPath = path.join(
|
||||
compilation.outputOptions.path!,
|
||||
`${name}.js`
|
||||
)
|
||||
deleteCache(runtimeChunkPath)
|
||||
})
|
||||
let hasAppPath = false
|
||||
cacheEntriesToDelete.push(runtimeChunkPath)
|
||||
}
|
||||
|
||||
// we need to make sure to clear all server entries from cache
|
||||
// since they can have a stale webpack-runtime cache
|
||||
// which needs to always be in-sync
|
||||
const entries = [...compilation.entries.keys()].filter((entry) => {
|
||||
const isAppPath = entry.toString().startsWith('app/')
|
||||
hasAppPath = hasAppPath || isAppPath
|
||||
return entry.toString().startsWith('pages/') || isAppPath
|
||||
})
|
||||
|
||||
if (hasAppPath) {
|
||||
}
|
||||
|
||||
entries.forEach((page) => {
|
||||
for (const page of entries) {
|
||||
const outputPath = path.join(
|
||||
compilation.outputOptions.path!,
|
||||
page + '.js'
|
||||
)
|
||||
deleteCache(outputPath)
|
||||
})
|
||||
cacheEntriesToDelete.push(outputPath)
|
||||
}
|
||||
await nextDeleteCacheRpc(cacheEntriesToDelete)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,11 +8,7 @@ import isError from '../lib/is-error'
|
|||
import { getProjectDir } from '../lib/get-project-dir'
|
||||
import { CONFIG_FILES, PHASE_DEVELOPMENT_SERVER } from '../shared/lib/constants'
|
||||
import path from 'path'
|
||||
import {
|
||||
defaultConfig,
|
||||
NextConfig,
|
||||
NextConfigComplete,
|
||||
} from '../server/config-shared'
|
||||
import { defaultConfig, NextConfigComplete } from '../server/config-shared'
|
||||
import { traceGlobals } from '../trace/shared'
|
||||
import { Telemetry } from '../telemetry/storage'
|
||||
import loadConfig from '../server/config'
|
||||
|
@ -26,6 +22,7 @@ import { getPossibleInstrumentationHookFilenames } from '../build/worker'
|
|||
import { resetEnv } from '@next/env'
|
||||
|
||||
let dir: string
|
||||
let config: NextConfigComplete
|
||||
let isTurboSession = false
|
||||
let sessionStopHandled = false
|
||||
let sessionStarted = Date.now()
|
||||
|
@ -38,13 +35,15 @@ const handleSessionStop = async () => {
|
|||
const { eventCliSession } =
|
||||
require('../telemetry/events/session-stopped') as typeof import('../telemetry/events/session-stopped')
|
||||
|
||||
const config = await loadConfig(
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
dir,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
config =
|
||||
config ||
|
||||
(await loadConfig(
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
dir,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
))
|
||||
|
||||
let telemetry =
|
||||
(traceGlobals.get('telemetry') as InstanceType<
|
||||
|
@ -211,12 +210,14 @@ const nextDev: CliCommand = async (argv) => {
|
|||
// We do not set a default host value here to prevent breaking
|
||||
// some set-ups that rely on listening on other interfaces
|
||||
const host = args['--hostname']
|
||||
config = await loadConfig(PHASE_DEVELOPMENT_SERVER, dir)
|
||||
|
||||
const devServerOptions: StartServerOptions = {
|
||||
dir,
|
||||
port,
|
||||
allowRetry,
|
||||
isDev: true,
|
||||
nextConfig: config,
|
||||
hostname: host,
|
||||
// This is required especially for app dir.
|
||||
useWorkers: true,
|
||||
|
@ -237,14 +238,6 @@ const nextDev: CliCommand = async (argv) => {
|
|||
resetEnv()
|
||||
let bindings = await loadBindings()
|
||||
|
||||
const config = await loadConfig(
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
dir,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
// Just testing code here:
|
||||
|
||||
const project = await bindings.turbo.createProject({
|
||||
|
@ -409,7 +402,6 @@ const nextDev: CliCommand = async (argv) => {
|
|||
try {
|
||||
let shouldFilter = false
|
||||
let devServerTeardown: (() => Promise<void>) | undefined
|
||||
let config: NextConfig | undefined
|
||||
|
||||
watchConfigFiles(devServerOptions.dir, (filename) => {
|
||||
Log.warn(
|
||||
|
@ -505,16 +497,6 @@ const nextDev: CliCommand = async (argv) => {
|
|||
// fallback to noop, if not provided
|
||||
resolveCleanup(async () => {})
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
config = await loadConfig(
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
dir,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await setupFork()
|
||||
|
|
|
@ -84,6 +84,7 @@ const nextStart: CliCommand = async (argv) => {
|
|||
|
||||
await startServer({
|
||||
dir,
|
||||
nextConfig: config,
|
||||
isDev: false,
|
||||
hostname: host,
|
||||
port,
|
||||
|
|
|
@ -36,6 +36,7 @@ const supportedTurbopackNextConfigOptions = [
|
|||
'experimental.isrFlushToDisk',
|
||||
'experimental.workerThreads',
|
||||
'experimenatl.pageEnv',
|
||||
'experimental.caseSensitiveRoutes',
|
||||
]
|
||||
|
||||
// The following will need to be supported by `next build --turbo`
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { IncomingMessage } from 'http'
|
||||
import type { BaseNextRequest } from '../base-http'
|
||||
import type { CookieSerializeOptions } from 'next/dist/compiled/cookie'
|
||||
import type { NextApiRequest, NextApiResponse } from '../../shared/lib/utils'
|
||||
import type { NextApiResponse } from '../../shared/lib/utils'
|
||||
|
||||
import { HeadersAdapter } from '../web/spec-extension/adapters/headers'
|
||||
import {
|
||||
|
@ -186,7 +186,7 @@ export function sendError(
|
|||
}
|
||||
|
||||
interface LazyProps {
|
||||
req: NextApiRequest
|
||||
req: IncomingMessage
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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) {
|
||||
|
@ -475,16 +475,7 @@ async function revalidate(
|
|||
headers: {},
|
||||
}
|
||||
)
|
||||
|
||||
const chunks = []
|
||||
|
||||
for await (const chunk of res) {
|
||||
if (chunk) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
}
|
||||
const body = Buffer.concat(chunks).toString()
|
||||
const result = JSON.parse(body)
|
||||
const result = await res.json()
|
||||
|
||||
if (result.err) {
|
||||
throw new Error(result.err.message)
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import type { __ApiPreviewProps } from './api-utils'
|
||||
import type { CustomRoutes } from '../lib/load-custom-routes'
|
||||
import type { DomainLocale } from './config'
|
||||
import type { RouterOptions } from './router'
|
||||
import type { FontManifest, FontConfig } from './font-utils'
|
||||
import type { LoadComponentsReturnType } from './load-components'
|
||||
import type { RouteMatchFn } from '../shared/lib/router/utils/route-matcher'
|
||||
|
@ -33,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'
|
||||
|
@ -51,7 +50,6 @@ import {
|
|||
checkIsOnDemandRevalidate,
|
||||
} from './api-utils'
|
||||
import { setConfig } from '../shared/lib/runtime-config'
|
||||
import Router from './router'
|
||||
|
||||
import { setRevalidateHeaders } from './send-payload/revalidate-headers'
|
||||
import { execOnce } from '../shared/lib/utils'
|
||||
|
@ -117,6 +115,7 @@ import {
|
|||
parsedUrlQueryToParams,
|
||||
type RouteMatch,
|
||||
} from './future/route-matches/route-match'
|
||||
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
|
||||
|
||||
export type FindComponentsResult = {
|
||||
components: LoadComponentsReturnType
|
||||
|
@ -268,9 +267,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
}
|
||||
protected serverOptions: ServerOptions
|
||||
private responseCache: ResponseCacheBase
|
||||
protected router: Router
|
||||
protected appPathRoutes?: Record<string, string[]>
|
||||
protected customRoutes: CustomRoutes
|
||||
protected clientReferenceManifest?: ClientReferenceManifest
|
||||
protected nextFontManifest?: NextFontManifest
|
||||
public readonly hostname?: string
|
||||
public readonly port?: number
|
||||
|
@ -282,7 +280,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
protected abstract getAppPathsManifest(): PagesManifest | undefined
|
||||
protected abstract getBuildId(): string
|
||||
|
||||
protected abstract getFilesystemPaths(): Set<string>
|
||||
protected abstract findPageComponents(params: {
|
||||
pathname: string
|
||||
query: NextParsedUrlQuery
|
||||
|
@ -300,11 +297,8 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
parsedUrl: NextUrlWithParsedQuery
|
||||
): void
|
||||
protected abstract getFallback(page: string): Promise<string>
|
||||
protected abstract getCustomRoutes(): CustomRoutes
|
||||
protected abstract hasPage(pathname: string): Promise<boolean>
|
||||
|
||||
protected abstract generateRoutes(dev?: boolean): RouterOptions
|
||||
|
||||
protected abstract sendRenderResult(
|
||||
req: BaseNextRequest,
|
||||
res: BaseNextResponse,
|
||||
|
@ -321,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(
|
||||
|
@ -334,11 +326,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
renderOpts: RenderOpts
|
||||
): Promise<RenderResult>
|
||||
|
||||
protected abstract handleCompression(
|
||||
req: BaseNextRequest,
|
||||
res: BaseNextResponse
|
||||
): void
|
||||
|
||||
protected abstract getIncrementalCache(options: {
|
||||
requestHeaders: Record<string, undefined | string | string[]>
|
||||
requestProtocol: 'http' | 'https'
|
||||
|
@ -358,7 +345,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
protected readonly handlers: RouteHandlerManager
|
||||
protected readonly i18nProvider?: I18NProvider
|
||||
protected readonly localeNormalizer?: LocaleRouteNormalizer
|
||||
protected readonly isRouterWorker?: boolean
|
||||
protected readonly isRenderWorker?: boolean
|
||||
|
||||
public constructor(options: ServerOptions) {
|
||||
|
@ -373,7 +359,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
port,
|
||||
} = options
|
||||
this.serverOptions = options
|
||||
this.isRouterWorker = options._routerWorker
|
||||
this.isRenderWorker = options._renderWorker
|
||||
|
||||
this.dir =
|
||||
|
@ -473,6 +458,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
|
||||
this.pagesManifest = this.getPagesManifest()
|
||||
this.appPathsManifest = this.getAppPathsManifest()
|
||||
this.appPathRoutes = this.getAppPathRoutes()
|
||||
|
||||
// Configure the routes.
|
||||
const { matchers, handlers } = this.getRoutes()
|
||||
|
@ -483,14 +469,42 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
// because we use the `waitTillReady` promise below in `handleRequest` to
|
||||
// wait. Also we can't `await` in the constructor.
|
||||
matchers.reload()
|
||||
|
||||
this.customRoutes = this.getCustomRoutes()
|
||||
this.router = new Router(this.generateRoutes(dev))
|
||||
this.setAssetPrefix(assetPrefix)
|
||||
|
||||
this.responseCache = this.getResponseCache({ dev })
|
||||
}
|
||||
|
||||
protected async normalizeNextData(
|
||||
_req: BaseNextRequest,
|
||||
_res: BaseNextResponse,
|
||||
_parsedUrl: NextUrlWithParsedQuery
|
||||
): Promise<{ finished: boolean }> {
|
||||
return { finished: false }
|
||||
}
|
||||
|
||||
protected async handleNextImageRequest(
|
||||
_req: BaseNextRequest,
|
||||
_res: BaseNextResponse,
|
||||
_parsedUrl: NextUrlWithParsedQuery
|
||||
): Promise<{ finished: boolean }> {
|
||||
return { finished: false }
|
||||
}
|
||||
|
||||
protected async handleCatchallRenderRequest(
|
||||
_req: BaseNextRequest,
|
||||
_res: BaseNextResponse,
|
||||
_parsedUrl: NextUrlWithParsedQuery
|
||||
): Promise<{ finished: boolean }> {
|
||||
return { finished: false }
|
||||
}
|
||||
|
||||
protected async handleCatchallMiddlewareRequest(
|
||||
_req: BaseNextRequest,
|
||||
_res: BaseNextResponse,
|
||||
_parsedUrl: NextUrlWithParsedQuery
|
||||
): Promise<{ finished: boolean }> {
|
||||
return { finished: false }
|
||||
}
|
||||
|
||||
protected getRoutes(): {
|
||||
matchers: RouteMatcherManager
|
||||
handlers: RouteHandlerManager
|
||||
|
@ -564,8 +578,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
'http.method': method,
|
||||
'http.target': req.url,
|
||||
},
|
||||
// We will fire this from the renderer worker
|
||||
hideSpan: this.isRouterWorker,
|
||||
},
|
||||
async (span) =>
|
||||
this.handleRequestImpl(req, res, parsedUrl).finally(() => {
|
||||
|
@ -708,6 +720,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
addRequestMeta(req, '_nextHadBasePath', true)
|
||||
}
|
||||
|
||||
// TODO: merge handling with x-invoke-path
|
||||
if (
|
||||
this.minimalMode &&
|
||||
typeof req.headers['x-matched-path'] === 'string'
|
||||
|
@ -787,7 +800,11 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
page: srcPathname,
|
||||
i18n: this.nextConfig.i18n,
|
||||
basePath: this.nextConfig.basePath,
|
||||
rewrites: this.customRoutes.rewrites,
|
||||
rewrites: this.getRoutesManifest()?.rewrites || {
|
||||
beforeFiles: [],
|
||||
afterFiles: [],
|
||||
fallback: [],
|
||||
},
|
||||
caseSensitive: !!this.nextConfig.experimental.caseSensitiveRoutes,
|
||||
})
|
||||
|
||||
|
@ -926,29 +943,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
}
|
||||
}
|
||||
|
||||
addRequestMeta(req, '__nextHadTrailingSlash', pathnameInfo.trailingSlash)
|
||||
addRequestMeta(req, '__nextIsLocaleDomain', Boolean(domainLocale))
|
||||
|
||||
if (pathnameInfo.locale) {
|
||||
req.url = formatUrl(url)
|
||||
addRequestMeta(req, '__nextStrippedLocale', true)
|
||||
}
|
||||
|
||||
// If we aren't in minimal mode or there is no locale in the query
|
||||
// string, add the locale to the query string.
|
||||
if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
|
||||
// If the locale is in the pathname, add it to the query string.
|
||||
if (pathnameInfo.locale) {
|
||||
parsedUrl.query.__nextLocale = pathnameInfo.locale
|
||||
}
|
||||
// If the default locale is available, add it to the query string and
|
||||
// mark it as inferred rather than implicit.
|
||||
else if (defaultLocale) {
|
||||
parsedUrl.query.__nextLocale = defaultLocale
|
||||
parsedUrl.query.__nextInferredLocaleFromDefault = '1'
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
// Edge runtime always has minimal mode enabled.
|
||||
process.env.NEXT_RUNTIME !== 'edge' &&
|
||||
|
@ -979,6 +973,174 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
}
|
||||
}
|
||||
|
||||
addRequestMeta(req, '__nextIsLocaleDomain', Boolean(domainLocale))
|
||||
|
||||
if (pathnameInfo.locale) {
|
||||
req.url = formatUrl(url)
|
||||
addRequestMeta(req, '__nextStrippedLocale', true)
|
||||
}
|
||||
|
||||
// If we aren't in minimal mode or there is no locale in the query
|
||||
// string, add the locale to the query string.
|
||||
if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
|
||||
// If the locale is in the pathname, add it to the query string.
|
||||
if (pathnameInfo.locale) {
|
||||
parsedUrl.query.__nextLocale = pathnameInfo.locale
|
||||
}
|
||||
// If the default locale is available, add it to the query string and
|
||||
// mark it as inferred rather than implicit.
|
||||
else if (defaultLocale) {
|
||||
parsedUrl.query.__nextLocale = defaultLocale
|
||||
parsedUrl.query.__nextInferredLocaleFromDefault = '1'
|
||||
}
|
||||
}
|
||||
|
||||
// set incremental cache to request meta so it can
|
||||
// be passed down for edge functions and the fetch disk
|
||||
// cache can be leveraged locally
|
||||
if (
|
||||
!(this.serverOptions as any).webServerConfig &&
|
||||
!getRequestMeta(req, '_nextIncrementalCache')
|
||||
) {
|
||||
let protocol: 'http:' | 'https:' = 'https:'
|
||||
|
||||
try {
|
||||
const parsedFullUrl = new URL(
|
||||
getRequestMeta(req, '__NEXT_INIT_URL') || '/',
|
||||
'http://n'
|
||||
)
|
||||
protocol = parsedFullUrl.protocol as 'https:' | 'http:'
|
||||
} catch (_) {}
|
||||
|
||||
const incrementalCache = this.getIncrementalCache({
|
||||
requestHeaders: Object.assign({}, req.headers),
|
||||
requestProtocol: protocol.substring(0, protocol.length - 1) as
|
||||
| 'http'
|
||||
| 'https',
|
||||
})
|
||||
addRequestMeta(req, '_nextIncrementalCache', incrementalCache)
|
||||
;(globalThis as any).__incrementalCache = incrementalCache
|
||||
}
|
||||
|
||||
// when x-invoke-path is specified we can short short circuit resolving
|
||||
// we only honor this header if we are inside of a render worker to
|
||||
// prevent external users coercing the routing path
|
||||
const matchedPath = req.headers['x-invoke-path'] as string
|
||||
|
||||
if (
|
||||
!(req.headers['x-matched-path'] && this.minimalMode) &&
|
||||
process.env.NEXT_RUNTIME !== 'edge' &&
|
||||
process.env.__NEXT_PRIVATE_RENDER_WORKER &&
|
||||
matchedPath
|
||||
) {
|
||||
if (req.headers['x-invoke-status']) {
|
||||
const invokeQuery = req.headers['x-invoke-query']
|
||||
|
||||
if (typeof invokeQuery === 'string') {
|
||||
Object.assign(
|
||||
parsedUrl.query,
|
||||
JSON.parse(decodeURIComponent(invokeQuery))
|
||||
)
|
||||
}
|
||||
|
||||
res.statusCode = Number(req.headers['x-invoke-status'])
|
||||
let err = null
|
||||
|
||||
if (typeof req.headers['x-invoke-error'] === 'string') {
|
||||
const invokeError = JSON.parse(
|
||||
req.headers['x-invoke-error'] || '{}'
|
||||
)
|
||||
err = new Error(invokeError.message)
|
||||
}
|
||||
|
||||
return this.renderError(err, req, res, '/_error', parsedUrl.query)
|
||||
}
|
||||
|
||||
const parsedMatchedPath = new URL(matchedPath || '/', 'http://n')
|
||||
const invokePathnameInfo = getNextPathnameInfo(
|
||||
parsedMatchedPath.pathname,
|
||||
{
|
||||
nextConfig: this.nextConfig,
|
||||
parseData: false,
|
||||
}
|
||||
)
|
||||
|
||||
if (invokePathnameInfo.locale) {
|
||||
parsedUrl.query.__nextLocale = invokePathnameInfo.locale
|
||||
}
|
||||
|
||||
if (parsedUrl.pathname !== parsedMatchedPath.pathname) {
|
||||
parsedUrl.pathname = parsedMatchedPath.pathname
|
||||
addRequestMeta(req, '_nextRewroteUrl', invokePathnameInfo.pathname)
|
||||
addRequestMeta(req, '_nextDidRewrite', true)
|
||||
}
|
||||
const normalizeResult = normalizeLocalePath(
|
||||
removePathPrefix(parsedUrl.pathname, this.nextConfig.basePath || ''),
|
||||
this.nextConfig.i18n?.locales || []
|
||||
)
|
||||
|
||||
if (normalizeResult.detectedLocale) {
|
||||
parsedUrl.query.__nextLocale = normalizeResult.detectedLocale
|
||||
}
|
||||
parsedUrl.pathname = normalizeResult.pathname
|
||||
|
||||
for (const key of Object.keys(parsedUrl.query)) {
|
||||
if (!key.startsWith('__next') && !key.startsWith('_next')) {
|
||||
delete parsedUrl.query[key]
|
||||
}
|
||||
}
|
||||
const invokeQuery = req.headers['x-invoke-query']
|
||||
|
||||
if (typeof invokeQuery === 'string') {
|
||||
Object.assign(
|
||||
parsedUrl.query,
|
||||
JSON.parse(decodeURIComponent(invokeQuery))
|
||||
)
|
||||
}
|
||||
|
||||
if (parsedUrl.pathname.startsWith('/_next/image')) {
|
||||
const imageResult = await this.handleNextImageRequest(
|
||||
req,
|
||||
res,
|
||||
parsedUrl
|
||||
)
|
||||
|
||||
if (imageResult.finished) {
|
||||
return
|
||||
}
|
||||
}
|
||||
const nextDataResult = await this.normalizeNextData(req, res, parsedUrl)
|
||||
|
||||
if (nextDataResult.finished) {
|
||||
return
|
||||
}
|
||||
await this.handleCatchallRenderRequest(req, res, parsedUrl)
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
process.env.NEXT_RUNTIME !== 'edge' &&
|
||||
process.env.__NEXT_PRIVATE_RENDER_WORKER &&
|
||||
req.headers['x-middleware-invoke']
|
||||
) {
|
||||
const nextDataResult = await this.normalizeNextData(req, res, parsedUrl)
|
||||
|
||||
if (nextDataResult.finished) {
|
||||
return
|
||||
}
|
||||
const result = await this.handleCatchallMiddlewareRequest(
|
||||
req,
|
||||
res,
|
||||
parsedUrl
|
||||
)
|
||||
if (!result.finished) {
|
||||
res.setHeader('x-middleware-next', '1')
|
||||
res.body('')
|
||||
res.send()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
res.statusCode = 200
|
||||
return await this.run(req, res, parsedUrl)
|
||||
} catch (err: any) {
|
||||
|
@ -1036,15 +1198,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
// Backwards compatibility
|
||||
protected async close(): Promise<void> {}
|
||||
|
||||
protected async _beforeCatchAllRender(
|
||||
_req: BaseNextRequest,
|
||||
_res: BaseNextResponse,
|
||||
_params: Params,
|
||||
_parsedUrl: UrlWithParsedQuery
|
||||
): Promise<boolean> {
|
||||
return false
|
||||
}
|
||||
|
||||
protected getAppPathRoutes(): Record<string, string[]> {
|
||||
const appPathRoutes: Record<string, string[]> = {}
|
||||
|
||||
|
@ -1073,49 +1226,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
res: BaseNextResponse,
|
||||
parsedUrl: UrlWithParsedQuery
|
||||
): Promise<void> {
|
||||
this.handleCompression(req, res)
|
||||
|
||||
// set incremental cache to request meta so it can
|
||||
// be passed down for edge functions and the fetch disk
|
||||
// cache can be leveraged locally
|
||||
if (
|
||||
!(this.serverOptions as any).webServerConfig &&
|
||||
!getRequestMeta(req, '_nextIncrementalCache')
|
||||
) {
|
||||
let protocol: 'http:' | 'https:' = 'https:'
|
||||
|
||||
try {
|
||||
const parsedFullUrl = new URL(
|
||||
getRequestMeta(req, '__NEXT_INIT_URL') || '/',
|
||||
'http://n'
|
||||
)
|
||||
protocol = parsedFullUrl.protocol as 'https:' | 'http:'
|
||||
} catch (_) {}
|
||||
|
||||
const incrementalCache = this.getIncrementalCache({
|
||||
requestHeaders: Object.assign({}, req.headers),
|
||||
requestProtocol: protocol.substring(0, protocol.length - 1) as
|
||||
| 'http'
|
||||
| 'https',
|
||||
})
|
||||
addRequestMeta(req, '_nextIncrementalCache', incrementalCache)
|
||||
;(globalThis as any).__incrementalCache = incrementalCache
|
||||
}
|
||||
|
||||
try {
|
||||
const matched = await this.router.execute(req, res, parsedUrl)
|
||||
if (matched) {
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof DecodeError || err instanceof NormalizeError) {
|
||||
res.statusCode = 400
|
||||
return this.renderError(null, req, res, '/_error', {})
|
||||
}
|
||||
throw err
|
||||
}
|
||||
|
||||
await this.render404(req, res, parsedUrl)
|
||||
await this.handleCatchallRenderRequest(req, res, parsedUrl)
|
||||
}
|
||||
|
||||
private async pipe(
|
||||
|
@ -1231,11 +1342,6 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
return this.handleRequest(req, res, parsedUrl)
|
||||
}
|
||||
|
||||
// Custom server users can run `app.render()` which needs compression.
|
||||
if (this.renderOpts.customServer) {
|
||||
this.handleCompression(req, res)
|
||||
}
|
||||
|
||||
if (isBlockedPage(pathname)) {
|
||||
return this.render404(req, res, parsedUrl)
|
||||
}
|
||||
|
@ -1790,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(
|
||||
|
@ -1813,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
|
||||
|
||||
|
@ -2263,6 +2369,28 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
)
|
||||
}
|
||||
|
||||
protected getMiddleware(): MiddlewareRoutingItem | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
protected getRoutesManifest():
|
||||
| {
|
||||
dynamicRoutes: {
|
||||
page: string
|
||||
regex: string
|
||||
namedRegex?: string
|
||||
routeKeys?: { [key: string]: string }
|
||||
}
|
||||
rewrites: {
|
||||
beforeFiles: any[]
|
||||
afterFiles: any[]
|
||||
fallback: any[]
|
||||
}
|
||||
}
|
||||
| undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
private async renderToResponseImpl(
|
||||
ctx: RequestContext
|
||||
): Promise<ResponsePayload | null> {
|
||||
|
@ -2278,6 +2406,19 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
|
||||
try {
|
||||
for await (const match of this.matchers.matchAll(pathname, options)) {
|
||||
// when a specific invoke-output is meant to be matched
|
||||
// ensure a prior dynamic route/page doesn't take priority
|
||||
const invokeOutput = ctx.req.headers['x-invoke-output']
|
||||
if (
|
||||
!this.minimalMode &&
|
||||
this.isRenderWorker &&
|
||||
typeof invokeOutput === 'string' &&
|
||||
isDynamicRoute(invokeOutput || '') &&
|
||||
invokeOutput !== match.definition.pathname
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const result = await this.renderPageComponent(
|
||||
{
|
||||
...ctx,
|
||||
|
@ -2366,7 +2507,7 @@ export default abstract class Server<ServerOptions extends Options = Options> {
|
|||
}
|
||||
|
||||
if (
|
||||
this.router.hasMiddleware &&
|
||||
this.getMiddleware() &&
|
||||
!!ctx.req.headers['x-nextjs-data'] &&
|
||||
(!res.statusCode || res.statusCode === 200 || res.statusCode === 404)
|
||||
) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
let installed: boolean = false
|
||||
|
||||
export function loadWebpackHook({ init }: { init: boolean }) {
|
||||
if (init) {
|
||||
const { init: initWebpack } = require('next/dist/compiled/webpack/webpack')
|
||||
if (installed) {
|
||||
return
|
||||
}
|
||||
installed = true
|
||||
initWebpack()
|
||||
export function loadWebpackHook() {
|
||||
const { init: initWebpack } = require('next/dist/compiled/webpack/webpack')
|
||||
if (installed) {
|
||||
return
|
||||
}
|
||||
installed = true
|
||||
initWebpack()
|
||||
|
||||
// hook the Node.js require so that webpack requires are
|
||||
// routed to the bundled and now initialized webpack version
|
||||
|
|
|
@ -711,10 +711,32 @@ export default async function loadConfig(
|
|||
rawConfig?: boolean,
|
||||
silent?: boolean
|
||||
): Promise<NextConfigComplete> {
|
||||
if (!process.env.__NEXT_PRIVATE_RENDER_WORKER) {
|
||||
try {
|
||||
loadWebpackHook()
|
||||
} catch (err) {
|
||||
// this can fail in standalone mode as the files
|
||||
// aren't traced/included
|
||||
if (!process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) {
|
||||
return JSON.parse(process.env.__NEXT_PRIVATE_STANDALONE_CONFIG)
|
||||
}
|
||||
|
||||
// For the render worker, we directly return the serialized config from the
|
||||
// parent worker (router worker) to avoid loading it again.
|
||||
// This is because loading the config might be expensive especiall when people
|
||||
// have Webpack plugins added.
|
||||
// Because of this change, unserializable fields like `.webpack` won't be
|
||||
// existing here but the render worker shouldn't use these as well.
|
||||
if (process.env.__NEXT_PRIVATE_RENDER_WORKER_CONFIG) {
|
||||
return JSON.parse(process.env.__NEXT_PRIVATE_RENDER_WORKER_CONFIG)
|
||||
}
|
||||
|
||||
const curLog = silent
|
||||
? {
|
||||
warn: () => {},
|
||||
|
@ -725,21 +747,6 @@ export default async function loadConfig(
|
|||
|
||||
loadEnvConfig(dir, phase === PHASE_DEVELOPMENT_SERVER, curLog)
|
||||
|
||||
loadWebpackHook({
|
||||
// For render workers, there's no need to init webpack eagerly
|
||||
init: !process.env.__NEXT_PRIVATE_RENDER_WORKER,
|
||||
})
|
||||
|
||||
// For the render worker, we directly return the serialized config from the
|
||||
// parent worker (router worker) to avoid loading it again.
|
||||
// This is because loading the config might be expensive especiall when people
|
||||
// have Webpack plugins added.
|
||||
// Because of this change, unserializable fields like `.webpack` won't be
|
||||
// existing here but the render worker shouldn't use these as well.
|
||||
if (process.env.__NEXT_PRIVATE_RENDER_WORKER_CONFIG) {
|
||||
return JSON.parse(process.env.__NEXT_PRIVATE_RENDER_WORKER_CONFIG)
|
||||
}
|
||||
|
||||
let configFileName = 'next.config.js'
|
||||
|
||||
if (customConfig) {
|
||||
|
|
|
@ -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)))
|
||||
|
@ -279,7 +280,7 @@ export default class HotReloader {
|
|||
parsedPageBundleUrl: UrlObject
|
||||
): Promise<{ finished?: true }> => {
|
||||
const { pathname } = parsedPageBundleUrl
|
||||
const params = matchNextPageBundleRequest<{ path: string[] }>(pathname)
|
||||
const params = matchNextPageBundleRequest(pathname)
|
||||
if (!params) {
|
||||
return {}
|
||||
}
|
||||
|
@ -288,7 +289,7 @@ export default class HotReloader {
|
|||
|
||||
try {
|
||||
decodedPagePath = `/${params.path
|
||||
.map((param) => decodeURIComponent(param))
|
||||
.map((param: string) => decodeURIComponent(param))
|
||||
.join('/')}`
|
||||
} catch (_) {
|
||||
throw new DecodeError('failed to decode param')
|
||||
|
@ -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,
|
||||
|
@ -1411,10 +1420,12 @@ export default class HotReloader {
|
|||
clientOnly,
|
||||
appPaths,
|
||||
match,
|
||||
isApp,
|
||||
}: {
|
||||
page: string
|
||||
clientOnly: boolean
|
||||
appPaths?: string[] | null
|
||||
isApp?: boolean
|
||||
match?: RouteMatch
|
||||
}): Promise<void> {
|
||||
// Make sure we don't re-build or dispose prebuilt pages
|
||||
|
@ -1432,6 +1443,7 @@ export default class HotReloader {
|
|||
clientOnly,
|
||||
appPaths,
|
||||
match,
|
||||
isApp,
|
||||
}) as any
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -689,11 +689,13 @@ export function onDemandEntryHandler({
|
|||
clientOnly,
|
||||
appPaths = null,
|
||||
match,
|
||||
isApp,
|
||||
}: {
|
||||
page: string
|
||||
clientOnly: boolean
|
||||
appPaths?: ReadonlyArray<string> | null
|
||||
match?: RouteMatch
|
||||
isApp?: boolean
|
||||
}): Promise<void> {
|
||||
const stalledTime = 60
|
||||
const stalledEnsureTimeout = setTimeout(() => {
|
||||
|
@ -722,6 +724,12 @@ export function onDemandEntryHandler({
|
|||
const isInsideAppDir =
|
||||
!!appDir && pagePathData.absolutePagePath.startsWith(appDir)
|
||||
|
||||
if (typeof isApp === 'boolean' && !(isApp === isInsideAppDir)) {
|
||||
throw new Error(
|
||||
'Ensure bailed, found path does not match ensure type (pages/app)'
|
||||
)
|
||||
}
|
||||
|
||||
const pageBundleType = getPageBundleType(pagePathData.bundlePath)
|
||||
const addEntry = (
|
||||
compilerType: CompilerNameValues
|
||||
|
@ -894,11 +902,13 @@ export function onDemandEntryHandler({
|
|||
clientOnly,
|
||||
appPaths = null,
|
||||
match,
|
||||
isApp,
|
||||
}: {
|
||||
page: string
|
||||
clientOnly: boolean
|
||||
appPaths?: ReadonlyArray<string> | null
|
||||
match?: RouteMatch
|
||||
isApp?: boolean
|
||||
}) {
|
||||
if (curEnsurePage.has(page)) {
|
||||
return curEnsurePage.get(page)
|
||||
|
@ -908,6 +918,7 @@ export function onDemandEntryHandler({
|
|||
clientOnly,
|
||||
appPaths,
|
||||
match,
|
||||
isApp,
|
||||
}).finally(() => {
|
||||
curEnsurePage.delete(page)
|
||||
})
|
||||
|
|
|
@ -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
|
|
@ -1,127 +0,0 @@
|
|||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
|
||||
import httpProxy from 'next/dist/compiled/http-proxy'
|
||||
import { Worker } from 'next/dist/compiled/jest-worker'
|
||||
import { normalizeRepeatedSlashes } from '../../shared/lib/utils'
|
||||
|
||||
export const createServerHandler = async ({
|
||||
port,
|
||||
hostname,
|
||||
dir,
|
||||
dev = false,
|
||||
minimalMode,
|
||||
keepAliveTimeout,
|
||||
}: {
|
||||
port: number
|
||||
hostname: string
|
||||
dir: string
|
||||
dev?: boolean
|
||||
minimalMode: boolean
|
||||
keepAliveTimeout?: number
|
||||
}) => {
|
||||
const routerWorker = new Worker(require.resolve('./render-server'), {
|
||||
numWorkers: 1,
|
||||
maxRetries: 10,
|
||||
forkOptions: {
|
||||
env: {
|
||||
FORCE_COLOR: '1',
|
||||
...process.env,
|
||||
},
|
||||
},
|
||||
exposedMethods: ['initialize'],
|
||||
}) as any as InstanceType<typeof Worker> & {
|
||||
initialize: typeof import('./render-server').initialize
|
||||
}
|
||||
|
||||
let didInitialize = false
|
||||
|
||||
for (const _worker of ((routerWorker as any)._workerPool?._workers || []) as {
|
||||
_child: ChildProcess
|
||||
}[]) {
|
||||
// eslint-disable-next-line no-loop-func
|
||||
_worker._child.on('exit', (code, signal) => {
|
||||
// catch failed initializing without retry
|
||||
if ((code || signal) && !didInitialize) {
|
||||
routerWorker?.end()
|
||||
process.exit(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const workerStdout = routerWorker.getStdout()
|
||||
const workerStderr = routerWorker.getStderr()
|
||||
|
||||
workerStdout.on('data', (data) => {
|
||||
process.stdout.write(data)
|
||||
})
|
||||
workerStderr.on('data', (data) => {
|
||||
process.stderr.write(data)
|
||||
})
|
||||
|
||||
const { port: routerPort } = await routerWorker.initialize({
|
||||
dir,
|
||||
port,
|
||||
dev,
|
||||
hostname,
|
||||
minimalMode,
|
||||
workerType: 'router',
|
||||
isNodeDebugging: false,
|
||||
keepAliveTimeout,
|
||||
})
|
||||
didInitialize = true
|
||||
|
||||
const getProxyServer = (pathname: string) => {
|
||||
const targetUrl = `http://${
|
||||
hostname === 'localhost' ? '127.0.0.1' : hostname
|
||||
}:${routerPort}${pathname}`
|
||||
const proxyServer = httpProxy.createProxy({
|
||||
target: targetUrl,
|
||||
changeOrigin: false,
|
||||
ignorePath: true,
|
||||
xfwd: true,
|
||||
ws: true,
|
||||
followRedirects: false,
|
||||
})
|
||||
return proxyServer
|
||||
}
|
||||
|
||||
// proxy to router worker
|
||||
return async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const urlParts = (req.url || '').split('?')
|
||||
const urlNoQuery = urlParts[0]
|
||||
|
||||
// this normalizes repeated slashes in the path e.g. hello//world ->
|
||||
// hello/world or backslashes to forward slashes, this does not
|
||||
// handle trailing slash as that is handled the same as a next.config.js
|
||||
// redirect
|
||||
if (urlNoQuery?.match(/(\\|\/\/)/)) {
|
||||
const cleanUrl = normalizeRepeatedSlashes(req.url!)
|
||||
res.statusCode = 308
|
||||
res.setHeader('Location', cleanUrl)
|
||||
res.end(cleanUrl)
|
||||
return
|
||||
}
|
||||
const proxyServer = getProxyServer(req.url || '/')
|
||||
|
||||
// http-proxy does not properly detect a client disconnect in newer
|
||||
// versions of Node.js. This is caused because it only listens for the
|
||||
// `aborted` event on the our request object, but it also fully reads and
|
||||
// closes the request object. Node **will not** fire `aborted` when the
|
||||
// request is already closed. Listening for `close` on our response object
|
||||
// will detect the disconnect, and we can abort the proxy's connection.
|
||||
proxyServer.on('proxyReq', (proxyReq) => {
|
||||
res.on('close', () => proxyReq.destroy())
|
||||
})
|
||||
proxyServer.on('proxyRes', (proxyRes) => {
|
||||
res.on('close', () => proxyRes.destroy())
|
||||
})
|
||||
|
||||
proxyServer.web(req, res)
|
||||
proxyServer.on('error', (err) => {
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
console.error(err)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,22 +1,11 @@
|
|||
import type { RequestHandler } from '../next'
|
||||
|
||||
import './cpu-profile'
|
||||
import v8 from 'v8'
|
||||
import http from 'http'
|
||||
import { isIPv6 } from 'net'
|
||||
|
||||
// This is required before other imports to ensure the require hook is setup.
|
||||
import '../require-hook'
|
||||
|
||||
// this must come first as it includes require hooks
|
||||
import { initializeServerWorker } from './setup-server-worker'
|
||||
import next from '../next'
|
||||
import { warn } from '../../build/output/log'
|
||||
import { getFreePort } from '../lib/worker-utils'
|
||||
|
||||
export const WORKER_SELF_EXIT_CODE = 77
|
||||
|
||||
const MAXIMUM_HEAP_SIZE_ALLOWED =
|
||||
(v8.getHeapStatistics().heap_size_limit / 1024 / 1024) * 0.9
|
||||
|
||||
let result:
|
||||
| undefined
|
||||
| {
|
||||
|
@ -24,6 +13,8 @@ let result:
|
|||
hostname: string
|
||||
}
|
||||
|
||||
let app: ReturnType<typeof next> | undefined
|
||||
|
||||
let sandboxContext: undefined | typeof import('../web/sandbox/context')
|
||||
let requireCacheHotReloader:
|
||||
| undefined
|
||||
|
@ -35,15 +26,46 @@ if (process.env.NODE_ENV !== 'production') {
|
|||
}
|
||||
|
||||
export function clearModuleContext(target: string) {
|
||||
sandboxContext?.clearModuleContext(target)
|
||||
return sandboxContext?.clearModuleContext(target)
|
||||
}
|
||||
|
||||
export function deleteAppClientCache() {
|
||||
requireCacheHotReloader?.deleteAppClientCache()
|
||||
return requireCacheHotReloader?.deleteAppClientCache()
|
||||
}
|
||||
|
||||
export function deleteCache(filePath: string) {
|
||||
requireCacheHotReloader?.deleteCache(filePath)
|
||||
export function deleteCache(filePaths: string[]) {
|
||||
for (const filePath of filePaths) {
|
||||
requireCacheHotReloader?.deleteCache(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
export async function propagateServerField(field: string, value: any) {
|
||||
if (!app) {
|
||||
throw new Error('Invariant cant propagate server field, no app initialized')
|
||||
}
|
||||
let appField = (app as any).server
|
||||
|
||||
if (field.includes('.')) {
|
||||
const parts = field.split('.')
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (appField) {
|
||||
appField = appField[parts[i]]
|
||||
}
|
||||
}
|
||||
field = parts[parts.length - 1]
|
||||
}
|
||||
|
||||
if (appField) {
|
||||
if (typeof appField[field] === 'function') {
|
||||
appField[field].apply(
|
||||
(app as any).server,
|
||||
Array.isArray(value) ? value : []
|
||||
)
|
||||
} else {
|
||||
appField[field] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function initialize(opts: {
|
||||
|
@ -55,6 +77,7 @@ export async function initialize(opts: {
|
|||
workerType: 'router' | 'render'
|
||||
isNodeDebugging: boolean
|
||||
keepAliveTimeout?: number
|
||||
serverFields?: any
|
||||
}): Promise<NonNullable<typeof result>> {
|
||||
// if we already setup the server return as we only need to do
|
||||
// this on first worker boot
|
||||
|
@ -62,97 +85,40 @@ export async function initialize(opts: {
|
|||
return result
|
||||
}
|
||||
|
||||
const isRouterWorker = opts.workerType === 'router'
|
||||
const isRenderWorker = opts.workerType === 'render'
|
||||
if (isRouterWorker) {
|
||||
process.title = 'next-router-worker'
|
||||
} else if (isRenderWorker) {
|
||||
const type = process.env.__NEXT_PRIVATE_RENDER_WORKER!
|
||||
process.title = 'next-render-worker-' + type
|
||||
}
|
||||
const type = process.env.__NEXT_PRIVATE_RENDER_WORKER!
|
||||
process.title = 'next-render-worker-' + type
|
||||
|
||||
let requestHandler: RequestHandler
|
||||
let upgradeHandler: any
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
return requestHandler(req, res)
|
||||
.catch((err) => {
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
console.error(err)
|
||||
})
|
||||
.finally(() => {
|
||||
if (
|
||||
process.memoryUsage().heapUsed / 1024 / 1024 >
|
||||
MAXIMUM_HEAP_SIZE_ALLOWED
|
||||
) {
|
||||
warn(
|
||||
'The server is running out of memory, restarting to free up memory.'
|
||||
)
|
||||
server.close()
|
||||
process.exit(WORKER_SELF_EXIT_CODE)
|
||||
}
|
||||
})
|
||||
const { port, server, hostname } = await initializeServerWorker(
|
||||
(...args) => {
|
||||
return requestHandler(...args)
|
||||
},
|
||||
(...args) => {
|
||||
return upgradeHandler(...args)
|
||||
},
|
||||
opts
|
||||
)
|
||||
|
||||
app = next({
|
||||
...opts,
|
||||
_routerWorker: opts.workerType === 'router',
|
||||
_renderWorker: opts.workerType === 'render',
|
||||
hostname: hostname === '0.0.0.0' ? 'localhost' : hostname,
|
||||
customServer: false,
|
||||
httpServer: server,
|
||||
port: opts.port,
|
||||
isNodeDebugging: opts.isNodeDebugging,
|
||||
})
|
||||
|
||||
if (opts.keepAliveTimeout) {
|
||||
server.keepAliveTimeout = opts.keepAliveTimeout
|
||||
requestHandler = app.getRequestHandler()
|
||||
upgradeHandler = app.getUpgradeHandler()
|
||||
await app.prepare(opts.serverFields)
|
||||
|
||||
result = {
|
||||
port,
|
||||
hostname: hostname === '0.0.0.0' ? '127.0.0.1' : hostname,
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
server.on('error', (err: NodeJS.ErrnoException) => {
|
||||
console.error(`Invariant: failed to start render worker`, err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
let upgradeHandler: any
|
||||
|
||||
if (!opts.dev) {
|
||||
server.on('upgrade', (req, socket, upgrade) => {
|
||||
upgradeHandler(req, socket, upgrade)
|
||||
})
|
||||
}
|
||||
|
||||
server.on('listening', async () => {
|
||||
try {
|
||||
const addr = server.address()
|
||||
const port = addr && typeof addr === 'object' ? addr.port : 0
|
||||
|
||||
if (!port) {
|
||||
console.error(`Invariant failed to detect render worker port`, addr)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let hostname =
|
||||
!opts.hostname || opts.hostname === '0.0.0.0'
|
||||
? 'localhost'
|
||||
: opts.hostname
|
||||
|
||||
if (isIPv6(hostname)) {
|
||||
hostname = hostname === '::' ? '[::1]' : `[${hostname}]`
|
||||
}
|
||||
result = {
|
||||
port,
|
||||
hostname,
|
||||
}
|
||||
const app = next({
|
||||
...opts,
|
||||
_routerWorker: isRouterWorker,
|
||||
_renderWorker: isRenderWorker,
|
||||
hostname,
|
||||
customServer: false,
|
||||
httpServer: server,
|
||||
port: opts.port,
|
||||
isNodeDebugging: opts.isNodeDebugging,
|
||||
})
|
||||
|
||||
requestHandler = app.getRequestHandler()
|
||||
upgradeHandler = app.getUpgradeHandler()
|
||||
await app.prepare()
|
||||
resolve(result)
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
server.listen(await getFreePort(), '0.0.0.0')
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -1,36 +1,27 @@
|
|||
import type { NextConfigComplete } from '../config-shared'
|
||||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import { join } from 'path'
|
||||
|
||||
import {
|
||||
StackFrame,
|
||||
parse as parseStackTrace,
|
||||
} from 'next/dist/compiled/stacktrace-parser'
|
||||
import '../require-hook'
|
||||
import '../node-polyfill-fetch'
|
||||
|
||||
import type { NextConfig } from '../config'
|
||||
import type { RouteDefinition } from '../future/route-definitions/route-definition'
|
||||
import { RouteKind } from '../future/route-kind'
|
||||
import { DefaultRouteMatcherManager } from '../future/route-matcher-managers/default-route-matcher-manager'
|
||||
import type { RouteMatch } from '../future/route-matches/route-match'
|
||||
import type { PageChecker, Route } from '../router'
|
||||
import { getMiddlewareMatchers } from '../../build/analysis/get-page-static-info'
|
||||
import url from 'url'
|
||||
import path from 'path'
|
||||
import http from 'http'
|
||||
import { findPageFile } from './find-page-file'
|
||||
import { getRequestMeta } from '../request-meta'
|
||||
import setupDebug from 'next/dist/compiled/debug'
|
||||
import { getCloneableBody } from '../body-streams'
|
||||
import { findPagesDir } from '../../lib/find-pages-dir'
|
||||
import { setupFsCheck } from './router-utils/filesystem'
|
||||
import { proxyRequest } from './router-utils/proxy-request'
|
||||
import { getResolveRoutes } from './router-utils/resolve-routes'
|
||||
import { PERMANENT_REDIRECT_STATUS } from '../../shared/lib/constants'
|
||||
import { splitCookiesString, toNodeOutgoingHttpHeaders } from '../web/utils'
|
||||
import { signalFromNodeRequest } from '../web/spec-extension/adapters/next-request'
|
||||
import { getMiddlewareRouteMatcher } from '../../shared/lib/router/utils/middleware-route-matcher'
|
||||
import {
|
||||
CLIENT_STATIC_FILES_PATH,
|
||||
DEV_MIDDLEWARE_MANIFEST,
|
||||
} from '../../shared/lib/constants'
|
||||
import type { BaseNextRequest } from '../base-http'
|
||||
import { pipeReadable } from './server-ipc/invoke-request'
|
||||
|
||||
export type MiddlewareConfig = {
|
||||
matcher: string[]
|
||||
files: string[]
|
||||
}
|
||||
|
||||
export type ServerAddress = {
|
||||
hostname?: string | null
|
||||
port?: number | null
|
||||
}
|
||||
|
||||
export type RouteResult =
|
||||
type RouteResult =
|
||||
| {
|
||||
type: 'rewrite'
|
||||
url: string
|
||||
|
@ -42,236 +33,255 @@ export type RouteResult =
|
|||
error: {
|
||||
name: string
|
||||
message: string
|
||||
stack: StackFrame[]
|
||||
stack: any[]
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'none'
|
||||
}
|
||||
|
||||
class DevRouteMatcherManager extends DefaultRouteMatcherManager {
|
||||
private hasPage: PageChecker
|
||||
|
||||
constructor(hasPage: PageChecker) {
|
||||
super()
|
||||
this.hasPage = hasPage
|
||||
}
|
||||
|
||||
async match(
|
||||
pathname: string
|
||||
): Promise<RouteMatch<RouteDefinition<RouteKind>> | null> {
|
||||
if (await this.hasPage(pathname)) {
|
||||
return {
|
||||
definition: {
|
||||
kind: RouteKind.PAGES,
|
||||
page: '',
|
||||
pathname,
|
||||
filename: '',
|
||||
bundlePath: '',
|
||||
},
|
||||
params: {},
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async test(pathname: string) {
|
||||
return (await this.match(pathname)) !== null
|
||||
}
|
||||
type MiddlewareConfig = {
|
||||
matcher: string[] | null
|
||||
files: string[]
|
||||
}
|
||||
|
||||
type ServerAddress = {
|
||||
hostname?: string
|
||||
port?: number
|
||||
}
|
||||
|
||||
const debug = setupDebug('next:router-server')
|
||||
|
||||
export async function makeResolver(
|
||||
dir: string,
|
||||
nextConfig: NextConfig,
|
||||
nextConfig: NextConfigComplete,
|
||||
middleware: MiddlewareConfig,
|
||||
serverAddr: Partial<ServerAddress>
|
||||
) {
|
||||
const url = require('url') as typeof import('url')
|
||||
const { default: Router } = require('../router') as typeof import('../router')
|
||||
const { getPathMatch } =
|
||||
require('../../shared/lib/router/utils/path-match') as typeof import('../../shared/lib/router/utils/path-match')
|
||||
const { default: DevServer } =
|
||||
require('../dev/next-dev-server') as typeof import('../dev/next-dev-server')
|
||||
|
||||
const { NodeNextRequest, NodeNextResponse } =
|
||||
require('../base-http/node') as typeof import('../base-http/node')
|
||||
|
||||
const { default: loadCustomRoutes } =
|
||||
require('../../lib/load-custom-routes') as typeof import('../../lib/load-custom-routes')
|
||||
|
||||
const routeResults = new WeakMap<any, RouteResult>()
|
||||
|
||||
class TurbopackDevServerProxy extends DevServer {
|
||||
// make sure static files are served by turbopack
|
||||
serveStatic(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// make turbopack handle errors
|
||||
async renderError(err: Error | null, req: BaseNextRequest): Promise<void> {
|
||||
if (err != null) {
|
||||
routeResults.set(req, {
|
||||
type: 'error',
|
||||
error: {
|
||||
name: err.name,
|
||||
message: err.message,
|
||||
stack: parseStackTrace(err.stack!),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
// make turbopack handle 404s
|
||||
render404(): Promise<void> {
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
const devServer = new TurbopackDevServerProxy({
|
||||
const fsChecker = await setupFsCheck({
|
||||
dir,
|
||||
conf: nextConfig,
|
||||
hostname: serverAddr.hostname || 'localhost',
|
||||
port: serverAddr.port || 3000,
|
||||
dev: true,
|
||||
minimalMode: false,
|
||||
config: nextConfig,
|
||||
})
|
||||
|
||||
await devServer.matchers.reload()
|
||||
|
||||
// @ts-expect-error private
|
||||
devServer.setDevReady!()
|
||||
|
||||
// @ts-expect-error protected
|
||||
devServer.customRoutes = await loadCustomRoutes(nextConfig)
|
||||
|
||||
if (middleware.files?.length) {
|
||||
const matchers = middleware.matcher
|
||||
? getMiddlewareMatchers(middleware.matcher, nextConfig)
|
||||
: [{ regexp: '.*', originalSource: '/:path*' }]
|
||||
// @ts-expect-error
|
||||
devServer.middleware = {
|
||||
page: '/',
|
||||
match: getMiddlewareRouteMatcher(matchers),
|
||||
matchers,
|
||||
}
|
||||
|
||||
type GetEdgeFunctionInfo =
|
||||
(typeof DevServer)['prototype']['getEdgeFunctionInfo']
|
||||
const getEdgeFunctionInfo = (
|
||||
original: GetEdgeFunctionInfo
|
||||
): GetEdgeFunctionInfo => {
|
||||
return (params: { page: string; middleware: boolean }) => {
|
||||
if (params.middleware) {
|
||||
return {
|
||||
name: 'middleware',
|
||||
paths: middleware.files.map((file) => join(process.cwd(), file)),
|
||||
wasm: [],
|
||||
assets: [],
|
||||
}
|
||||
}
|
||||
return original(params)
|
||||
}
|
||||
}
|
||||
// @ts-expect-error protected
|
||||
devServer.getEdgeFunctionInfo = getEdgeFunctionInfo(
|
||||
// @ts-expect-error protected
|
||||
devServer.getEdgeFunctionInfo.bind(devServer)
|
||||
)
|
||||
// @ts-expect-error protected
|
||||
devServer.hasMiddleware = () => true
|
||||
}
|
||||
|
||||
const routes = devServer.generateRoutes(true)
|
||||
// @ts-expect-error protected
|
||||
const catchAllMiddleware = devServer.generateCatchAllMiddlewareRoute(true)
|
||||
|
||||
routes.matchers = new DevRouteMatcherManager(
|
||||
// @ts-expect-error internal method
|
||||
devServer.hasPage.bind(devServer)
|
||||
const { appDir, pagesDir } = findPagesDir(
|
||||
dir,
|
||||
!!nextConfig.experimental.appDir
|
||||
)
|
||||
|
||||
// @ts-expect-error protected
|
||||
const buildId = devServer.buildId
|
||||
fsChecker.ensureCallback(async (item) => {
|
||||
let result: string | null = null
|
||||
|
||||
const router = new Router({
|
||||
...routes,
|
||||
catchAllMiddleware,
|
||||
catchAllRoute: {
|
||||
match: getPathMatch('/:path*'),
|
||||
name: 'catchall route',
|
||||
fn: async (req, res, _params, parsedUrl) => {
|
||||
// clean up internal query values
|
||||
for (const key of Object.keys(parsedUrl.query || {})) {
|
||||
if (key.startsWith('_next')) {
|
||||
delete parsedUrl.query[key]
|
||||
if (item.type === 'appFile') {
|
||||
if (!appDir) {
|
||||
throw new Error('no app dir present')
|
||||
}
|
||||
result = await findPageFile(
|
||||
appDir,
|
||||
item.itemPath,
|
||||
nextConfig.pageExtensions,
|
||||
true
|
||||
)
|
||||
} else if (item.type === 'pageFile') {
|
||||
if (!pagesDir) {
|
||||
throw new Error('no pages dir present')
|
||||
}
|
||||
result = await findPageFile(
|
||||
pagesDir,
|
||||
item.itemPath,
|
||||
nextConfig.pageExtensions,
|
||||
false
|
||||
)
|
||||
}
|
||||
if (!result) {
|
||||
throw new Error(`failed to find page file ${item.type} ${item.itemPath}`)
|
||||
}
|
||||
})
|
||||
|
||||
const distDir = path.join(dir, nextConfig.distDir)
|
||||
const middlewareInfo = middleware
|
||||
? {
|
||||
name: 'middleware',
|
||||
paths: middleware.files.map((file) => path.join(process.cwd(), file)),
|
||||
wasm: [],
|
||||
assets: [],
|
||||
}
|
||||
: {}
|
||||
|
||||
const middlewareServerPort = await new Promise((resolve) => {
|
||||
const srv = http.createServer(async (req, res) => {
|
||||
const cloneableBody = getCloneableBody(req)
|
||||
try {
|
||||
const { run } =
|
||||
require('../web/sandbox') as typeof import('../web/sandbox')
|
||||
|
||||
const result = await run({
|
||||
distDir,
|
||||
name: middlewareInfo.name || '/',
|
||||
paths: middlewareInfo.paths || [],
|
||||
edgeFunctionEntry: middlewareInfo,
|
||||
request: {
|
||||
headers: req.headers,
|
||||
method: req.method || 'GET',
|
||||
nextConfig: {
|
||||
i18n: nextConfig.i18n,
|
||||
basePath: nextConfig.basePath,
|
||||
trailingSlash: nextConfig.trailingSlash,
|
||||
},
|
||||
url: `http://${serverAddr.hostname || 'localhost'}:${
|
||||
serverAddr.port || 3000
|
||||
}${req.url}`,
|
||||
body: cloneableBody,
|
||||
signal: signalFromNodeRequest(req),
|
||||
},
|
||||
useCache: true,
|
||||
onWarning: console.warn,
|
||||
})
|
||||
|
||||
for (let [key, value] of result.response.headers) {
|
||||
if (key.toLowerCase() !== 'set-cookie') continue
|
||||
|
||||
// Clear existing header.
|
||||
result.response.headers.delete(key)
|
||||
|
||||
// Append each cookie individually.
|
||||
const cookies = splitCookiesString(value)
|
||||
for (const cookie of cookies) {
|
||||
result.response.headers.append(key, cookie)
|
||||
}
|
||||
}
|
||||
|
||||
routeResults.set(req, {
|
||||
type: 'rewrite',
|
||||
url: url.format({
|
||||
pathname: parsedUrl.pathname,
|
||||
query: parsedUrl.query,
|
||||
hash: parsedUrl.hash,
|
||||
}),
|
||||
statusCode: 200,
|
||||
headers: res.getHeaders(),
|
||||
})
|
||||
for (const [key, value] of Object.entries(
|
||||
toNodeOutgoingHttpHeaders(result.response.headers)
|
||||
)) {
|
||||
if (key !== 'content-encoding' && value !== undefined) {
|
||||
res.setHeader(key, value as string | string[])
|
||||
}
|
||||
}
|
||||
res.statusCode = result.response.status
|
||||
|
||||
return { finished: true }
|
||||
},
|
||||
} as Route,
|
||||
for await (const chunk of result.response.body || ([] as any)) {
|
||||
if (res.closed) break
|
||||
res.write(chunk)
|
||||
}
|
||||
res.end()
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
}
|
||||
})
|
||||
srv.on('listening', () => {
|
||||
resolve((srv.address() as any).port)
|
||||
})
|
||||
srv.listen(0)
|
||||
})
|
||||
|
||||
// @ts-expect-error internal field
|
||||
router.compiledRoutes = router.compiledRoutes.filter((route: Route) => {
|
||||
return (
|
||||
route.type === 'rewrite' ||
|
||||
route.type === 'redirect' ||
|
||||
route.type === 'header' ||
|
||||
route.name === 'catchall route' ||
|
||||
route.name === 'middleware catchall' ||
|
||||
route.name ===
|
||||
`_next/${CLIENT_STATIC_FILES_PATH}/${buildId}/${DEV_MIDDLEWARE_MANIFEST}` ||
|
||||
route.name?.includes('check')
|
||||
if (middleware?.files.length) {
|
||||
fsChecker.middlewareMatcher = getMiddlewareRouteMatcher(
|
||||
middleware.matcher?.map((item) => ({
|
||||
regexp: item,
|
||||
originalSource: item,
|
||||
})) || [{ regexp: '.*', originalSource: '/:path*' }]
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const resolveRoutes = getResolveRoutes(
|
||||
fsChecker,
|
||||
nextConfig,
|
||||
{
|
||||
dir,
|
||||
port: serverAddr.port || 3000,
|
||||
hostname: serverAddr.hostname,
|
||||
isNodeDebugging: false,
|
||||
dev: true,
|
||||
workerType: 'render',
|
||||
},
|
||||
{
|
||||
pages: {
|
||||
async initialize() {
|
||||
return {
|
||||
port: middlewareServerPort,
|
||||
hostname: '127.0.0.1',
|
||||
}
|
||||
},
|
||||
async deleteCache() {},
|
||||
async clearModuleContext() {},
|
||||
async deleteAppClientCache() {},
|
||||
async propagateServerField() {},
|
||||
} as any,
|
||||
},
|
||||
{} as any
|
||||
)
|
||||
|
||||
return async function resolveRoute(
|
||||
_req: IncomingMessage,
|
||||
_res: ServerResponse
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse
|
||||
): Promise<RouteResult | void> {
|
||||
const req = new NodeNextRequest(_req)
|
||||
const res = new NodeNextResponse(_res)
|
||||
const parsedUrl = url.parse(req.url!, true)
|
||||
// @ts-expect-error protected
|
||||
devServer.attachRequestMeta(req, parsedUrl)
|
||||
;(req as any)._initUrl = req.url
|
||||
const routeResult = await resolveRoutes(req, new Set(), false)
|
||||
const {
|
||||
matchedOutput,
|
||||
bodyStream,
|
||||
statusCode,
|
||||
parsedUrl,
|
||||
resHeaders,
|
||||
finished,
|
||||
} = routeResult
|
||||
|
||||
await router.execute(req, res, parsedUrl)
|
||||
debug('requestHandler!', req.url, {
|
||||
matchedOutput,
|
||||
statusCode,
|
||||
resHeaders,
|
||||
bodyStream: !!bodyStream,
|
||||
parsedUrl: {
|
||||
pathname: parsedUrl.pathname,
|
||||
query: parsedUrl.query,
|
||||
},
|
||||
finished,
|
||||
})
|
||||
|
||||
// If the headers are sent, then this was handled by middleware and there's
|
||||
// nothing for us to do.
|
||||
if (res.originalResponse.headersSent) {
|
||||
for (const key of Object.keys(resHeaders || {})) {
|
||||
res.setHeader(key, resHeaders[key])
|
||||
}
|
||||
|
||||
if (!bodyStream && statusCode && statusCode > 300 && statusCode < 400) {
|
||||
const destination = url.format(parsedUrl)
|
||||
res.statusCode = statusCode
|
||||
res.setHeader('location', destination)
|
||||
|
||||
if (statusCode === PERMANENT_REDIRECT_STATUS) {
|
||||
res.setHeader('Refresh', `0;url=${destination}`)
|
||||
}
|
||||
res.end(destination)
|
||||
return
|
||||
}
|
||||
|
||||
// The response won't be used, but we need to close the request so that the
|
||||
// ClientResponse's promise will resolve. We signal that this response is
|
||||
// unneeded via the header.
|
||||
res.setHeader('x-nextjs-route-result', '1')
|
||||
res.send()
|
||||
|
||||
// If we have a routeResult, then we hit the catchAllRoute during execution
|
||||
// and this is a rewrite request.
|
||||
const routeResult = routeResults.get(req)
|
||||
if (routeResult) {
|
||||
routeResults.delete(req)
|
||||
return routeResult
|
||||
// handle middleware body response
|
||||
if (bodyStream) {
|
||||
res.statusCode = statusCode || 200
|
||||
return await pipeReadable(bodyStream, res)
|
||||
}
|
||||
|
||||
// Finally, if the catchall didn't match, than this request is invalid
|
||||
// (maybe they're missing the basePath?)
|
||||
return { type: 'none' }
|
||||
if (finished && parsedUrl.protocol) {
|
||||
await proxyRequest(
|
||||
req,
|
||||
res,
|
||||
parsedUrl,
|
||||
undefined,
|
||||
getRequestMeta(req, '__NEXT_CLONABLE_BODY')?.cloneBodyStream(),
|
||||
nextConfig.experimental.proxyTimeout
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
res.setHeader('x-nextjs-route-result', '1')
|
||||
res.end()
|
||||
|
||||
return {
|
||||
type: 'rewrite',
|
||||
statusCode: 200,
|
||||
headers: resHeaders,
|
||||
url: url.format(parsedUrl),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
722
packages/next/src/server/lib/router-server.ts
Normal file
722
packages/next/src/server/lib/router-server.ts
Normal file
|
@ -0,0 +1,722 @@
|
|||
import type { IncomingMessage } from 'http'
|
||||
|
||||
// this must come first as it includes require hooks
|
||||
import { initializeServerWorker } from './setup-server-worker'
|
||||
|
||||
import url from 'url'
|
||||
import path from 'path'
|
||||
import loadConfig from '../config'
|
||||
import { serveStatic } from '../serve-static'
|
||||
import setupDebug from 'next/dist/compiled/debug'
|
||||
import { splitCookiesString, toNodeOutgoingHttpHeaders } from '../web/utils'
|
||||
import { Telemetry } from '../../telemetry/storage'
|
||||
import { DecodeError } from '../../shared/lib/utils'
|
||||
import { filterReqHeaders } from './server-ipc/utils'
|
||||
import { findPagesDir } from '../../lib/find-pages-dir'
|
||||
import { setupFsCheck } from './router-utils/filesystem'
|
||||
import { proxyRequest } from './router-utils/proxy-request'
|
||||
import { invokeRequest, pipeReadable } from './server-ipc/invoke-request'
|
||||
import { createRequestResponseMocks } from './mock-request'
|
||||
import { createIpcServer, createWorker } from './server-ipc'
|
||||
import { UnwrapPromise } from '../../lib/coalesced-function'
|
||||
import { getResolveRoutes } from './router-utils/resolve-routes'
|
||||
import { NextUrlWithParsedQuery, getRequestMeta } from '../request-meta'
|
||||
import { pathHasPrefix } from '../../shared/lib/router/utils/path-has-prefix'
|
||||
import { removePathPrefix } from '../../shared/lib/router/utils/remove-path-prefix'
|
||||
|
||||
import {
|
||||
PHASE_PRODUCTION_SERVER,
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
PERMANENT_REDIRECT_STATUS,
|
||||
} from '../../shared/lib/constants'
|
||||
|
||||
let initializeResult:
|
||||
| undefined
|
||||
| {
|
||||
port: number
|
||||
hostname: string
|
||||
}
|
||||
|
||||
const debug = setupDebug('next:router-server:main')
|
||||
|
||||
export type RenderWorker = InstanceType<
|
||||
typeof import('next/dist/compiled/jest-worker').Worker
|
||||
> & {
|
||||
initialize: typeof import('./render-server').initialize
|
||||
deleteCache: typeof import('./render-server').deleteCache
|
||||
deleteAppClientCache: typeof import('./render-server').deleteAppClientCache
|
||||
clearModuleContext: typeof import('./render-server').clearModuleContext
|
||||
propagateServerField: typeof import('./render-server').propagateServerField
|
||||
}
|
||||
|
||||
export async function initialize(opts: {
|
||||
dir: string
|
||||
port: number
|
||||
dev: boolean
|
||||
minimalMode?: boolean
|
||||
hostname?: string
|
||||
workerType: 'router' | 'render'
|
||||
isNodeDebugging: boolean
|
||||
keepAliveTimeout?: number
|
||||
customServer?: boolean
|
||||
}): Promise<NonNullable<typeof initializeResult>> {
|
||||
if (initializeResult) {
|
||||
return initializeResult
|
||||
}
|
||||
process.title = 'next-router-worker'
|
||||
|
||||
if (!process.env.NODE_ENV) {
|
||||
// @ts-ignore not readonly
|
||||
process.env.NODE_ENV = opts.dev ? 'development' : 'production'
|
||||
}
|
||||
|
||||
const config = await loadConfig(
|
||||
opts.dev ? PHASE_DEVELOPMENT_SERVER : PHASE_PRODUCTION_SERVER,
|
||||
opts.dir,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
|
||||
const fsChecker = await setupFsCheck({
|
||||
dev: opts.dev,
|
||||
dir: opts.dir,
|
||||
config,
|
||||
minimalMode: opts.minimalMode,
|
||||
})
|
||||
|
||||
let devInstance:
|
||||
| UnwrapPromise<
|
||||
ReturnType<typeof import('./router-utils/setup-dev').setupDev>
|
||||
>
|
||||
| undefined
|
||||
|
||||
if (opts.dev) {
|
||||
const telemetry = new Telemetry({
|
||||
distDir: path.join(opts.dir, config.distDir),
|
||||
})
|
||||
const { pagesDir, appDir } = findPagesDir(
|
||||
opts.dir,
|
||||
!!config.experimental.appDir
|
||||
)
|
||||
|
||||
const { setupDev } = await require('./router-utils/setup-dev')
|
||||
devInstance = await setupDev({
|
||||
appDir,
|
||||
pagesDir,
|
||||
telemetry,
|
||||
fsChecker,
|
||||
dir: opts.dir,
|
||||
nextConfig: config,
|
||||
isCustomServer: opts.customServer,
|
||||
})
|
||||
}
|
||||
|
||||
const renderWorkerOpts: Parameters<RenderWorker['initialize']>[0] = {
|
||||
port: opts.port,
|
||||
dir: opts.dir,
|
||||
workerType: 'render',
|
||||
hostname: opts.hostname,
|
||||
minimalMode: opts.minimalMode,
|
||||
dev: !!opts.dev,
|
||||
isNodeDebugging: !!opts.isNodeDebugging,
|
||||
serverFields: devInstance?.serverFields || {},
|
||||
}
|
||||
const renderWorkers: {
|
||||
app?: RenderWorker
|
||||
pages?: RenderWorker
|
||||
} = {}
|
||||
|
||||
const { ipcPort, ipcValidationKey } = await createIpcServer({
|
||||
async ensurePage(
|
||||
match: Parameters<
|
||||
InstanceType<typeof import('../dev/hot-reloader').default>['ensurePage']
|
||||
>[0]
|
||||
) {
|
||||
// TODO: remove after ensure is pulled out of server
|
||||
return await devInstance?.hotReloader.ensurePage(match)
|
||||
},
|
||||
async logErrorWithOriginalStack(...args: any[]) {
|
||||
// @ts-ignore
|
||||
return await devInstance?.logErrorWithOriginalStack(...args)
|
||||
},
|
||||
async getFallbackErrorComponents() {
|
||||
await devInstance?.hotReloader?.buildFallbackError()
|
||||
// Build the error page to ensure the fallback is built too.
|
||||
// TODO: See if this can be moved into hotReloader or removed.
|
||||
await devInstance?.hotReloader.ensurePage({
|
||||
page: '/_error',
|
||||
clientOnly: false,
|
||||
})
|
||||
},
|
||||
async getCompilationError(page: string) {
|
||||
const errors = await devInstance?.hotReloader?.getCompilationErrors(page)
|
||||
if (!errors) return
|
||||
|
||||
// Return the very first error we found.
|
||||
return errors[0]
|
||||
},
|
||||
async revalidate({
|
||||
urlPath,
|
||||
revalidateHeaders,
|
||||
opts: revalidateOpts,
|
||||
}: {
|
||||
urlPath: string
|
||||
revalidateHeaders: IncomingMessage['headers']
|
||||
opts: any
|
||||
}) {
|
||||
const mocked = createRequestResponseMocks({
|
||||
url: urlPath,
|
||||
headers: revalidateHeaders,
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
await requestHandler(mocked.req, mocked.res)
|
||||
await mocked.res.hasStreamed
|
||||
|
||||
if (
|
||||
mocked.res.getHeader('x-nextjs-cache') !== 'REVALIDATED' &&
|
||||
!(
|
||||
mocked.res.statusCode === 404 && revalidateOpts.unstable_onlyGenerated
|
||||
)
|
||||
) {
|
||||
throw new Error(`Invalid response ${mocked.res.statusCode}`)
|
||||
}
|
||||
return {}
|
||||
},
|
||||
} as any)
|
||||
|
||||
if (!!config.experimental.appDir) {
|
||||
renderWorkers.app = await createWorker(
|
||||
ipcPort,
|
||||
ipcValidationKey,
|
||||
opts.isNodeDebugging,
|
||||
'app',
|
||||
config
|
||||
)
|
||||
}
|
||||
renderWorkers.pages = await createWorker(
|
||||
ipcPort,
|
||||
ipcValidationKey,
|
||||
opts.isNodeDebugging,
|
||||
'pages',
|
||||
config
|
||||
)
|
||||
|
||||
// pre-initialize workers
|
||||
await renderWorkers.app?.initialize(renderWorkerOpts)
|
||||
await renderWorkers.pages?.initialize(renderWorkerOpts)
|
||||
|
||||
if (devInstance) {
|
||||
Object.assign(devInstance.renderWorkers, renderWorkers)
|
||||
;(global as any)._nextDeleteCache = async (filePaths: string[]) => {
|
||||
try {
|
||||
await Promise.all([
|
||||
renderWorkers.pages?.deleteCache(filePaths),
|
||||
renderWorkers.app?.deleteCache(filePaths),
|
||||
])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
;(global as any)._nextDeleteAppClientCache = async () => {
|
||||
try {
|
||||
await Promise.all([
|
||||
renderWorkers.pages?.deleteAppClientCache(),
|
||||
renderWorkers.app?.deleteAppClientCache(),
|
||||
])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
;(global as any)._nextClearModuleContext = async (targetPath: string) => {
|
||||
try {
|
||||
await Promise.all([
|
||||
renderWorkers.pages?.clearModuleContext(targetPath),
|
||||
renderWorkers.app?.clearModuleContext(targetPath),
|
||||
])
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
debug('router-server process cleanup')
|
||||
for (const curWorker of [
|
||||
...((renderWorkers.app as any)?._workerPool?._workers || []),
|
||||
...((renderWorkers.pages as any)?._workerPool?._workers || []),
|
||||
] as {
|
||||
_child?: import('child_process').ChildProcess
|
||||
}[]) {
|
||||
curWorker._child?.kill('SIGKILL')
|
||||
}
|
||||
}
|
||||
process.on('exit', cleanup)
|
||||
process.on('SIGINT', cleanup)
|
||||
process.on('SIGTERM', cleanup)
|
||||
process.on('uncaughtException', cleanup)
|
||||
process.on('unhandledRejection', cleanup)
|
||||
|
||||
const resolveRoutes = getResolveRoutes(
|
||||
fsChecker,
|
||||
config,
|
||||
opts,
|
||||
renderWorkers,
|
||||
renderWorkerOpts,
|
||||
devInstance?.ensureMiddleware
|
||||
)
|
||||
|
||||
const requestHandler: Parameters<typeof initializeServerWorker>[0] = async (
|
||||
req,
|
||||
res
|
||||
) => {
|
||||
req.on('error', (_err) => {
|
||||
// TODO: log socket errors?
|
||||
})
|
||||
res.on('error', (_err) => {
|
||||
// TODO: log socket errors?
|
||||
})
|
||||
|
||||
const matchedDynamicRoutes = new Set<string>()
|
||||
|
||||
async function invokeRender(
|
||||
parsedUrl: NextUrlWithParsedQuery,
|
||||
type: keyof typeof renderWorkers,
|
||||
handleIndex: number,
|
||||
invokePath: string,
|
||||
additionalInvokeHeaders: Record<string, string> = {}
|
||||
) {
|
||||
// invokeRender expects /api routes to not be locale prefixed
|
||||
// so normalize here before continuing
|
||||
if (
|
||||
config.i18n &&
|
||||
removePathPrefix(invokePath, config.basePath).startsWith(
|
||||
`/${parsedUrl.query.__nextLocale}/api`
|
||||
)
|
||||
) {
|
||||
invokePath = fsChecker.handleLocale(
|
||||
removePathPrefix(invokePath, config.basePath)
|
||||
).pathname
|
||||
}
|
||||
|
||||
if (
|
||||
req.headers['x-nextjs-data'] &&
|
||||
fsChecker.getMiddlewareMatchers()?.length &&
|
||||
removePathPrefix(invokePath, config.basePath) === '/404'
|
||||
) {
|
||||
res.setHeader('x-nextjs-matched-path', parsedUrl.pathname || '')
|
||||
res.statusCode = 200
|
||||
res.setHeader('content-type', 'application/json')
|
||||
res.end('{}')
|
||||
return null
|
||||
}
|
||||
|
||||
const curWorker = renderWorkers[type]
|
||||
const workerResult = await curWorker?.initialize(renderWorkerOpts)
|
||||
|
||||
if (!workerResult) {
|
||||
throw new Error(`Failed to initialize render worker ${type}`)
|
||||
}
|
||||
|
||||
const renderUrl = `http://${workerResult.hostname}:${workerResult.port}${req.url}`
|
||||
|
||||
const invokeHeaders: typeof req.headers = {
|
||||
...req.headers,
|
||||
'x-middleware-invoke': '',
|
||||
'x-invoke-path': invokePath,
|
||||
'x-invoke-query': encodeURIComponent(JSON.stringify(parsedUrl.query)),
|
||||
...(additionalInvokeHeaders || {}),
|
||||
}
|
||||
|
||||
debug('invokeRender', renderUrl, invokeHeaders)
|
||||
|
||||
const invokeRes = await invokeRequest(
|
||||
renderUrl,
|
||||
{
|
||||
headers: invokeHeaders,
|
||||
method: req.method,
|
||||
},
|
||||
getRequestMeta(req, '__NEXT_CLONABLE_BODY')?.cloneBodyStream()
|
||||
)
|
||||
|
||||
debug('invokeRender res', invokeRes.status, invokeRes.headers)
|
||||
|
||||
// when we receive x-no-fallback we restart
|
||||
if (invokeRes.headers.get('x-no-fallback')) {
|
||||
// eslint-disable-next-line
|
||||
await handleRequest(handleIndex + 1)
|
||||
return
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(
|
||||
filterReqHeaders(toNodeOutgoingHttpHeaders(invokeRes.headers))
|
||||
)) {
|
||||
if (value !== undefined) {
|
||||
if (key === 'set-cookie') {
|
||||
const curValue = res.getHeader(key) as string
|
||||
const newValue: string[] = [] as string[]
|
||||
|
||||
for (const cookie of Array.isArray(curValue)
|
||||
? curValue
|
||||
: splitCookiesString(curValue || '')) {
|
||||
newValue.push(cookie)
|
||||
}
|
||||
for (const val of (Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? [value]
|
||||
: []) as string[]) {
|
||||
newValue.push(val)
|
||||
}
|
||||
res.setHeader(key, newValue)
|
||||
} else {
|
||||
res.setHeader(key, value as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
res.statusCode = invokeRes.status || 200
|
||||
res.statusMessage = invokeRes.statusText || ''
|
||||
|
||||
if (invokeRes.body) {
|
||||
await pipeReadable(invokeRes.body, res)
|
||||
} else {
|
||||
res.end()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const handleRequest = async (handleIndex: number) => {
|
||||
if (handleIndex > 5) {
|
||||
throw new Error(`Attempted to handle request too many times ${req.url}`)
|
||||
}
|
||||
|
||||
// handle hot-reloader first
|
||||
if (devInstance) {
|
||||
const origUrl = req.url || '/'
|
||||
|
||||
if (config.basePath && pathHasPrefix(origUrl, config.basePath)) {
|
||||
req.url = removePathPrefix(origUrl, config.basePath)
|
||||
}
|
||||
const parsedUrl = url.parse(req.url || '/')
|
||||
|
||||
const hotReloaderResult = await devInstance.hotReloader.run(
|
||||
req,
|
||||
res,
|
||||
parsedUrl
|
||||
)
|
||||
|
||||
if (hotReloaderResult.finished) {
|
||||
return hotReloaderResult
|
||||
}
|
||||
req.url = origUrl
|
||||
}
|
||||
|
||||
const {
|
||||
finished,
|
||||
parsedUrl,
|
||||
statusCode,
|
||||
resHeaders,
|
||||
bodyStream,
|
||||
matchedOutput,
|
||||
} = await resolveRoutes(req, matchedDynamicRoutes, false)
|
||||
|
||||
if (devInstance && matchedOutput?.type === 'devVirtualFsItem') {
|
||||
const origUrl = req.url || '/'
|
||||
|
||||
if (config.basePath && pathHasPrefix(origUrl, config.basePath)) {
|
||||
req.url = removePathPrefix(origUrl, config.basePath)
|
||||
}
|
||||
|
||||
if (resHeaders) {
|
||||
for (const key of Object.keys(resHeaders)) {
|
||||
res.setHeader(key, resHeaders[key])
|
||||
}
|
||||
}
|
||||
const result = await devInstance.requestHandler(req, res)
|
||||
|
||||
if (result.finished) {
|
||||
return
|
||||
}
|
||||
// TODO: throw invariant if we resolved to this but it wasn't handled?
|
||||
req.url = origUrl
|
||||
}
|
||||
|
||||
debug('requestHandler!', req.url, {
|
||||
matchedOutput,
|
||||
statusCode,
|
||||
resHeaders,
|
||||
bodyStream: !!bodyStream,
|
||||
parsedUrl: {
|
||||
pathname: parsedUrl.pathname,
|
||||
query: parsedUrl.query,
|
||||
},
|
||||
finished,
|
||||
})
|
||||
|
||||
// apply any response headers from routing
|
||||
for (const key of Object.keys(resHeaders || {})) {
|
||||
res.setHeader(key, resHeaders[key])
|
||||
}
|
||||
|
||||
// handle redirect
|
||||
if (!bodyStream && statusCode && statusCode > 300 && statusCode < 400) {
|
||||
const destination = url.format(parsedUrl)
|
||||
res.statusCode = statusCode
|
||||
res.setHeader('location', destination)
|
||||
|
||||
if (statusCode === PERMANENT_REDIRECT_STATUS) {
|
||||
res.setHeader('Refresh', `0;url=${destination}`)
|
||||
}
|
||||
return res.end(destination)
|
||||
}
|
||||
|
||||
// handle middleware body response
|
||||
if (bodyStream) {
|
||||
res.statusCode = statusCode || 200
|
||||
return await pipeReadable(bodyStream, res)
|
||||
}
|
||||
|
||||
if (finished && parsedUrl.protocol) {
|
||||
return await proxyRequest(
|
||||
req,
|
||||
res,
|
||||
parsedUrl,
|
||||
undefined,
|
||||
getRequestMeta(req, '__NEXT_CLONABLE_BODY')?.cloneBodyStream(),
|
||||
config.experimental.proxyTimeout
|
||||
)
|
||||
}
|
||||
|
||||
if (matchedOutput?.fsPath && matchedOutput.itemPath) {
|
||||
if (
|
||||
opts.dev &&
|
||||
(fsChecker.appFiles.has(matchedOutput.itemPath) ||
|
||||
fsChecker.pageFiles.has(matchedOutput.itemPath))
|
||||
) {
|
||||
await invokeRender(parsedUrl, 'pages', handleIndex, '/_error', {
|
||||
'x-invoke-status': '500',
|
||||
'x-invoke-error': JSON.stringify({
|
||||
message: `A conflicting public file and page file was found for path ${matchedOutput.itemPath} https://nextjs.org/docs/messages/conflicting-public-file-page`,
|
||||
}),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!res.getHeader('cache-control') &&
|
||||
matchedOutput.type === 'nextStaticFolder'
|
||||
) {
|
||||
if (opts.dev) {
|
||||
res.setHeader('Cache-Control', 'no-store, must-revalidate')
|
||||
} else {
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
'public, max-age=31536000, immutable'
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!(req.method === 'GET' || req.method === 'HEAD')) {
|
||||
res.setHeader('Allow', ['GET', 'HEAD'])
|
||||
return await invokeRender(
|
||||
url.parse('/405', true),
|
||||
'pages',
|
||||
handleIndex,
|
||||
'/405',
|
||||
{
|
||||
'x-invoke-status': '405',
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
return await serveStatic(req, res, matchedOutput.itemPath, {
|
||||
root: matchedOutput.itemsRoot,
|
||||
})
|
||||
} catch (err: any) {
|
||||
/**
|
||||
* Hardcoded every possible error status code that could be thrown by "serveStatic" method
|
||||
* This is done by searching "this.error" inside "send" module's source code:
|
||||
* https://github.com/pillarjs/send/blob/master/index.js
|
||||
* https://github.com/pillarjs/send/blob/develop/index.js
|
||||
*/
|
||||
const POSSIBLE_ERROR_CODE_FROM_SERVE_STATIC = new Set([
|
||||
// send module will throw 500 when header is already sent or fs.stat error happens
|
||||
// https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L392
|
||||
// Note: we will use Next.js built-in 500 page to handle 500 errors
|
||||
// 500,
|
||||
|
||||
// send module will throw 404 when file is missing
|
||||
// https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L421
|
||||
// Note: we will use Next.js built-in 404 page to handle 404 errors
|
||||
// 404,
|
||||
|
||||
// send module will throw 403 when redirecting to a directory without enabling directory listing
|
||||
// https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L484
|
||||
// Note: Next.js throws a different error (without status code) for directory listing
|
||||
// 403,
|
||||
|
||||
// send module will throw 400 when fails to normalize the path
|
||||
// https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L520
|
||||
400,
|
||||
|
||||
// send module will throw 412 with conditional GET request
|
||||
// https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L632
|
||||
412,
|
||||
|
||||
// send module will throw 416 when range is not satisfiable
|
||||
// https://github.com/pillarjs/send/blob/53f0ab476145670a9bdd3dc722ab2fdc8d358fc6/index.js#L669
|
||||
416,
|
||||
])
|
||||
|
||||
let validErrorStatus = POSSIBLE_ERROR_CODE_FROM_SERVE_STATIC.has(
|
||||
err.statusCode
|
||||
)
|
||||
|
||||
// normalize non-allowed status codes
|
||||
if (!validErrorStatus) {
|
||||
;(err as any).statusCode = 400
|
||||
}
|
||||
|
||||
if (typeof err.statusCode === 'number') {
|
||||
const invokePath = `/${err.statusCode}`
|
||||
const invokeStatus = `${err.statusCode}`
|
||||
return await invokeRender(
|
||||
url.parse(invokePath, true),
|
||||
'pages',
|
||||
handleIndex,
|
||||
invokePath,
|
||||
{
|
||||
'x-invoke-status': invokeStatus,
|
||||
}
|
||||
)
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedOutput) {
|
||||
return await invokeRender(
|
||||
parsedUrl,
|
||||
matchedOutput.type === 'appFile' ? 'app' : 'pages',
|
||||
handleIndex,
|
||||
parsedUrl.pathname || '/',
|
||||
{
|
||||
'x-invoke-output': matchedOutput.itemPath,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 404 case
|
||||
res.setHeader(
|
||||
'Cache-Control',
|
||||
'no-cache, no-store, max-age=0, must-revalidate'
|
||||
)
|
||||
|
||||
const appNotFound = opts.dev
|
||||
? devInstance?.serverFields.hasAppNotFound
|
||||
: await fsChecker.getItem('/_not-found')
|
||||
|
||||
if (appNotFound) {
|
||||
return await invokeRender(
|
||||
parsedUrl,
|
||||
'app',
|
||||
handleIndex,
|
||||
'/_not-found',
|
||||
{
|
||||
'x-invoke-status': '404',
|
||||
}
|
||||
)
|
||||
}
|
||||
await invokeRender(parsedUrl, 'pages', handleIndex, '/404', {
|
||||
'x-invoke-status': '404',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await handleRequest(0)
|
||||
} catch (err) {
|
||||
try {
|
||||
let invokePath = '/500'
|
||||
let invokeStatus = '500'
|
||||
|
||||
if (err instanceof DecodeError) {
|
||||
invokePath = '/400'
|
||||
invokeStatus = '400'
|
||||
} else {
|
||||
console.error(err)
|
||||
}
|
||||
return await invokeRender(
|
||||
url.parse(invokePath, true),
|
||||
'pages',
|
||||
0,
|
||||
invokePath,
|
||||
{
|
||||
'x-invoke-status': invokeStatus,
|
||||
}
|
||||
)
|
||||
} catch (err2) {
|
||||
console.error(err2)
|
||||
}
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
}
|
||||
}
|
||||
|
||||
const upgradeHandler: Parameters<typeof initializeServerWorker>[1] = async (
|
||||
req,
|
||||
socket,
|
||||
head
|
||||
) => {
|
||||
try {
|
||||
req.on('error', (_err) => {
|
||||
// TODO: log socket errors?
|
||||
// console.error(_err);
|
||||
})
|
||||
socket.on('error', (_err) => {
|
||||
// TODO: log socket errors?
|
||||
// console.error(_err);
|
||||
})
|
||||
|
||||
if (opts.dev && devInstance) {
|
||||
if (req.url?.includes(`/_next/webpack-hmr`)) {
|
||||
return devInstance.hotReloader.onHMR(req, socket, head)
|
||||
}
|
||||
}
|
||||
|
||||
const { matchedOutput, parsedUrl } = await resolveRoutes(
|
||||
req,
|
||||
new Set(),
|
||||
true
|
||||
)
|
||||
|
||||
// TODO: allow upgrade requests to pages/app paths?
|
||||
// this was not previously supported
|
||||
if (matchedOutput) {
|
||||
return socket.end()
|
||||
}
|
||||
|
||||
if (parsedUrl.protocol) {
|
||||
return await proxyRequest(req, socket as any, parsedUrl, head)
|
||||
}
|
||||
// no match close socket
|
||||
socket.end()
|
||||
} catch (err) {
|
||||
console.error('Error handling upgrade request', err)
|
||||
socket.end()
|
||||
}
|
||||
}
|
||||
|
||||
const { port, hostname } = await initializeServerWorker(
|
||||
requestHandler,
|
||||
upgradeHandler,
|
||||
opts
|
||||
)
|
||||
|
||||
initializeResult = {
|
||||
port,
|
||||
hostname: hostname === '0.0.0.0' ? '127.0.0.1' : hostname,
|
||||
}
|
||||
|
||||
return initializeResult
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import path from '../../../shared/lib/isomorphic/path'
|
||||
import { normalizePagePath } from '../../../shared/lib/page-path/normalize-page-path'
|
||||
import { isDynamicRoute } from '../../../shared/lib/router/utils/is-dynamic'
|
||||
import { getNamedRouteRegex } from '../../../shared/lib/router/utils/route-regex'
|
||||
import { normalizeRouteRegex } from '../../../lib/load-custom-routes'
|
||||
import { escapeStringRegexp } from '../../../shared/lib/escape-regexp'
|
||||
|
||||
export function buildDataRoute(page: string, buildId: string) {
|
||||
const pagePath = normalizePagePath(page)
|
||||
const dataRoute = path.posix.join('/_next/data', buildId, `${pagePath}.json`)
|
||||
|
||||
let dataRouteRegex: string
|
||||
let namedDataRouteRegex: string | undefined
|
||||
let routeKeys: { [named: string]: string } | undefined
|
||||
|
||||
if (isDynamicRoute(page)) {
|
||||
const routeRegex = getNamedRouteRegex(
|
||||
dataRoute.replace(/\.json$/, ''),
|
||||
true
|
||||
)
|
||||
|
||||
dataRouteRegex = normalizeRouteRegex(
|
||||
routeRegex.re.source.replace(/\(\?:\\\/\)\?\$$/, `\\.json$`)
|
||||
)
|
||||
namedDataRouteRegex = routeRegex.namedRegex!.replace(
|
||||
/\(\?:\/\)\?\$$/,
|
||||
`\\.json$`
|
||||
)
|
||||
routeKeys = routeRegex.routeKeys
|
||||
} else {
|
||||
dataRouteRegex = normalizeRouteRegex(
|
||||
new RegExp(
|
||||
`^${path.posix.join(
|
||||
'/_next/data',
|
||||
escapeStringRegexp(buildId),
|
||||
`${pagePath}.json`
|
||||
)}$`
|
||||
).source
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
page,
|
||||
routeKeys,
|
||||
dataRouteRegex,
|
||||
namedDataRouteRegex,
|
||||
}
|
||||
}
|
620
packages/next/src/server/lib/router-utils/filesystem.ts
Normal file
620
packages/next/src/server/lib/router-utils/filesystem.ts
Normal file
|
@ -0,0 +1,620 @@
|
|||
import type { PrerenderManifest, RoutesManifest } from '../../../build'
|
||||
import type { NextConfigComplete } from '../../config-shared'
|
||||
import type { MiddlewareManifest } from '../../../build/webpack/plugins/middleware-plugin'
|
||||
|
||||
import path from 'path'
|
||||
import fs from 'fs/promises'
|
||||
import * as Log from '../../../build/output/log'
|
||||
import setupDebug from 'next/dist/compiled/debug'
|
||||
import LRUCache from 'next/dist/compiled/lru-cache'
|
||||
import loadCustomRoutes from '../../../lib/load-custom-routes'
|
||||
import { modifyRouteRegex } from '../../../lib/redirect-status'
|
||||
import { UnwrapPromise } from '../../../lib/coalesced-function'
|
||||
import { FileType, fileExists } from '../../../lib/file-exists'
|
||||
import { recursiveReadDir } from '../../../lib/recursive-readdir'
|
||||
import { isDynamicRoute } from '../../../shared/lib/router/utils'
|
||||
import { escapeStringRegexp } from '../../../shared/lib/escape-regexp'
|
||||
import { getPathMatch } from '../../../shared/lib/router/utils/path-match'
|
||||
import { getRouteRegex } from '../../../shared/lib/router/utils/route-regex'
|
||||
import { getRouteMatcher } from '../../../shared/lib/router/utils/route-matcher'
|
||||
import { pathHasPrefix } from '../../../shared/lib/router/utils/path-has-prefix'
|
||||
import { normalizeLocalePath } from '../../../shared/lib/i18n/normalize-locale-path'
|
||||
import { removePathPrefix } from '../../../shared/lib/router/utils/remove-path-prefix'
|
||||
|
||||
import {
|
||||
MiddlewareRouteMatch,
|
||||
getMiddlewareRouteMatcher,
|
||||
} from '../../../shared/lib/router/utils/middleware-route-matcher'
|
||||
|
||||
import {
|
||||
APP_PATH_ROUTES_MANIFEST,
|
||||
BUILD_ID_FILE,
|
||||
MIDDLEWARE_MANIFEST,
|
||||
PAGES_MANIFEST,
|
||||
PRERENDER_MANIFEST,
|
||||
ROUTES_MANIFEST,
|
||||
} from '../../../shared/lib/constants'
|
||||
import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep'
|
||||
|
||||
export type FsOutput = {
|
||||
type:
|
||||
| 'appFile'
|
||||
| 'pageFile'
|
||||
| 'nextImage'
|
||||
| 'publicFolder'
|
||||
| 'nextStaticFolder'
|
||||
| 'legacyStaticFolder'
|
||||
| 'devVirtualFsItem'
|
||||
|
||||
itemPath: string
|
||||
fsPath?: string
|
||||
itemsRoot?: string
|
||||
locale?: string
|
||||
}
|
||||
|
||||
const debug = setupDebug('next:router-server:filesystem')
|
||||
|
||||
export const buildCustomRoute = <T>(
|
||||
type: 'redirect' | 'header' | 'rewrite' | 'before_files_rewrite',
|
||||
item: T & { source: string },
|
||||
basePath?: string,
|
||||
caseSensitive?: boolean
|
||||
): T & { match: ReturnType<typeof getPathMatch>; check?: boolean } => {
|
||||
const restrictedRedirectPaths = ['/_next'].map((p) =>
|
||||
basePath ? `${basePath}${p}` : p
|
||||
)
|
||||
const match = getPathMatch(item.source, {
|
||||
strict: true,
|
||||
removeUnnamedParams: true,
|
||||
regexModifier: !(item as any).internal
|
||||
? (regex: string) =>
|
||||
modifyRouteRegex(
|
||||
regex,
|
||||
type === 'redirect' ? restrictedRedirectPaths : undefined
|
||||
)
|
||||
: undefined,
|
||||
sensitive: caseSensitive,
|
||||
})
|
||||
return {
|
||||
...item,
|
||||
...(type === 'rewrite' ? { check: true } : {}),
|
||||
match,
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupFsCheck(opts: {
|
||||
dir: string
|
||||
dev: boolean
|
||||
minimalMode?: boolean
|
||||
config: NextConfigComplete
|
||||
addDevWatcherCallback?: (
|
||||
arg: (files: Map<string, { timestamp: number }>) => void
|
||||
) => void
|
||||
}) {
|
||||
const getItemsLru = new LRUCache<string, FsOutput | null>({
|
||||
max: 1024 * 1024,
|
||||
length(value, key) {
|
||||
if (!value) return key?.length || 0
|
||||
return (
|
||||
(key || '').length +
|
||||
(value.fsPath || '').length +
|
||||
value.itemPath.length +
|
||||
value.type.length
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// routes that have _next/data endpoints (SSG/SSP)
|
||||
const nextDataRoutes = new Set<string>()
|
||||
const publicFolderItems = new Set<string>()
|
||||
const nextStaticFolderItems = new Set<string>()
|
||||
const legacyStaticFolderItems = new Set<string>()
|
||||
|
||||
const appFiles = new Set<string>()
|
||||
const pageFiles = new Set<string>()
|
||||
let dynamicRoutes: (RoutesManifest['dynamicRoutes'][0] & {
|
||||
match: ReturnType<typeof getPathMatch>
|
||||
})[] = []
|
||||
|
||||
let middlewareMatcher:
|
||||
| ReturnType<typeof getMiddlewareRouteMatcher>
|
||||
| undefined = () => false
|
||||
|
||||
const distDir = path.join(opts.dir, opts.config.distDir)
|
||||
const publicFolderPath = path.join(opts.dir, 'public')
|
||||
const nextStaticFolderPath = path.join(distDir, 'static')
|
||||
const legacyStaticFolderPath = path.join(opts.dir, 'static')
|
||||
let customRoutes: UnwrapPromise<ReturnType<typeof loadCustomRoutes>> = {
|
||||
redirects: [],
|
||||
rewrites: {
|
||||
beforeFiles: [],
|
||||
afterFiles: [],
|
||||
fallback: [],
|
||||
},
|
||||
headers: [],
|
||||
}
|
||||
let buildId = 'development'
|
||||
let prerenderManifest: PrerenderManifest
|
||||
|
||||
if (!opts.dev) {
|
||||
const buildIdPath = path.join(opts.dir, opts.config.distDir, BUILD_ID_FILE)
|
||||
buildId = await fs.readFile(buildIdPath, 'utf8')
|
||||
|
||||
try {
|
||||
for (let file of await recursiveReadDir(publicFolderPath, () => true)) {
|
||||
file = normalizePathSep(file)
|
||||
// ensure filename is encoded
|
||||
publicFolderItems.add(encodeURI(file))
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (let file of await recursiveReadDir(
|
||||
legacyStaticFolderPath,
|
||||
() => true
|
||||
)) {
|
||||
file = normalizePathSep(file)
|
||||
// ensure filename is encoded
|
||||
legacyStaticFolderItems.add(encodeURI(file))
|
||||
}
|
||||
Log.warn(
|
||||
`The static directory has been deprecated in favor of the public directory. https://nextjs.org/docs/messages/static-dir-deprecated`
|
||||
)
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
for (let file of await recursiveReadDir(
|
||||
nextStaticFolderPath,
|
||||
() => true
|
||||
)) {
|
||||
file = normalizePathSep(file)
|
||||
// ensure filename is encoded
|
||||
nextStaticFolderItems.add(
|
||||
path.posix.join('/_next/static', encodeURI(file))
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
if (opts.config.output !== 'standalone') throw err
|
||||
}
|
||||
|
||||
const routesManifestPath = path.join(distDir, ROUTES_MANIFEST)
|
||||
const prerenderManifestPath = path.join(distDir, PRERENDER_MANIFEST)
|
||||
const middlewareManifestPath = path.join(
|
||||
distDir,
|
||||
'server',
|
||||
MIDDLEWARE_MANIFEST
|
||||
)
|
||||
const pagesManifestPath = path.join(distDir, 'server', PAGES_MANIFEST)
|
||||
const appRoutesManifestPath = path.join(distDir, APP_PATH_ROUTES_MANIFEST)
|
||||
|
||||
const routesManifest = JSON.parse(
|
||||
await fs.readFile(routesManifestPath, 'utf8')
|
||||
) as RoutesManifest
|
||||
|
||||
prerenderManifest = JSON.parse(
|
||||
await fs.readFile(prerenderManifestPath, 'utf8')
|
||||
) as PrerenderManifest
|
||||
|
||||
const middlewareManifest = JSON.parse(
|
||||
await fs.readFile(middlewareManifestPath, 'utf8').catch(() => '{}')
|
||||
) as MiddlewareManifest
|
||||
|
||||
const pagesManifest = JSON.parse(
|
||||
await fs.readFile(pagesManifestPath, 'utf8')
|
||||
)
|
||||
const appRoutesManifest = JSON.parse(
|
||||
await fs.readFile(appRoutesManifestPath, 'utf8').catch(() => '{}')
|
||||
)
|
||||
|
||||
for (const key of Object.keys(pagesManifest)) {
|
||||
// ensure the non-locale version is in the set
|
||||
if (opts.config.i18n) {
|
||||
pageFiles.add(
|
||||
normalizeLocalePath(key, opts.config.i18n.locales).pathname
|
||||
)
|
||||
} else {
|
||||
pageFiles.add(key)
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(appRoutesManifest)) {
|
||||
appFiles.add(appRoutesManifest[key])
|
||||
}
|
||||
|
||||
const escapedBuildId = escapeStringRegexp(buildId)
|
||||
|
||||
for (const route of routesManifest.dataRoutes) {
|
||||
if (isDynamicRoute(route.page)) {
|
||||
const routeRegex = getRouteRegex(route.page)
|
||||
dynamicRoutes.push({
|
||||
...route,
|
||||
regex: routeRegex.re.toString(),
|
||||
match: getRouteMatcher({
|
||||
// TODO: fix this in the manifest itself, must also be fixed in
|
||||
// upstream builder that relies on this
|
||||
re: opts.config.i18n
|
||||
? new RegExp(
|
||||
route.dataRouteRegex.replace(
|
||||
`/${escapedBuildId}/`,
|
||||
`/${escapedBuildId}/(?<nextLocale>.+?)/`
|
||||
)
|
||||
)
|
||||
: new RegExp(route.dataRouteRegex),
|
||||
groups: routeRegex.groups,
|
||||
}),
|
||||
})
|
||||
}
|
||||
nextDataRoutes.add(route.page)
|
||||
}
|
||||
|
||||
for (const route of routesManifest.dynamicRoutes) {
|
||||
dynamicRoutes.push({
|
||||
...route,
|
||||
match: getRouteMatcher(getRouteRegex(route.page)),
|
||||
})
|
||||
}
|
||||
|
||||
if (middlewareManifest.middleware?.['/']?.matchers) {
|
||||
middlewareMatcher = getMiddlewareRouteMatcher(
|
||||
middlewareManifest.middleware?.['/']?.matchers
|
||||
)
|
||||
}
|
||||
|
||||
customRoutes = {
|
||||
// @ts-expect-error additional fields in manifest type
|
||||
redirects: routesManifest.redirects,
|
||||
// @ts-expect-error additional fields in manifest type
|
||||
rewrites: Array.isArray(routesManifest.rewrites)
|
||||
? {
|
||||
beforeFiles: [],
|
||||
afterFiles: routesManifest.rewrites,
|
||||
fallback: [],
|
||||
}
|
||||
: routesManifest.rewrites,
|
||||
// @ts-expect-error additional fields in manifest type
|
||||
headers: routesManifest.headers,
|
||||
}
|
||||
} else {
|
||||
// dev handling
|
||||
customRoutes = await loadCustomRoutes(opts.config)
|
||||
|
||||
prerenderManifest = {
|
||||
version: 4,
|
||||
routes: {},
|
||||
dynamicRoutes: {},
|
||||
notFoundRoutes: [],
|
||||
preview: {
|
||||
previewModeId: require('crypto').randomBytes(16).toString('hex'),
|
||||
previewModeSigningKey: require('crypto')
|
||||
.randomBytes(32)
|
||||
.toString('hex'),
|
||||
previewModeEncryptionKey: require('crypto')
|
||||
.randomBytes(32)
|
||||
.toString('hex'),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const headers = customRoutes.headers.map((item) =>
|
||||
buildCustomRoute(
|
||||
'header',
|
||||
item,
|
||||
opts.config.basePath,
|
||||
opts.config.experimental.caseSensitiveRoutes
|
||||
)
|
||||
)
|
||||
const redirects = customRoutes.redirects.map((item) =>
|
||||
buildCustomRoute(
|
||||
'redirect',
|
||||
item,
|
||||
opts.config.basePath,
|
||||
opts.config.experimental.caseSensitiveRoutes
|
||||
)
|
||||
)
|
||||
const rewrites = {
|
||||
// TODO: add interception routes generateInterceptionRoutesRewrites()
|
||||
beforeFiles: customRoutes.rewrites.beforeFiles.map((item) =>
|
||||
buildCustomRoute('before_files_rewrite', item)
|
||||
),
|
||||
afterFiles: customRoutes.rewrites.afterFiles.map((item) =>
|
||||
buildCustomRoute(
|
||||
'rewrite',
|
||||
item,
|
||||
opts.config.basePath,
|
||||
opts.config.experimental.caseSensitiveRoutes
|
||||
)
|
||||
),
|
||||
fallback: customRoutes.rewrites.fallback.map((item) =>
|
||||
buildCustomRoute(
|
||||
'rewrite',
|
||||
item,
|
||||
opts.config.basePath,
|
||||
opts.config.experimental.caseSensitiveRoutes
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
const { i18n } = opts.config
|
||||
|
||||
const handleLocale = (pathname: string, locales?: string[]) => {
|
||||
let locale: string | undefined
|
||||
|
||||
if (i18n) {
|
||||
const i18nResult = normalizeLocalePath(pathname, locales || i18n.locales)
|
||||
|
||||
pathname = i18nResult.pathname
|
||||
locale = i18nResult.detectedLocale
|
||||
}
|
||||
return { locale, pathname }
|
||||
}
|
||||
|
||||
debug('nextDataRoutes', nextDataRoutes)
|
||||
debug('dynamicRoutes', dynamicRoutes)
|
||||
debug('pageFiles', pageFiles)
|
||||
debug('appFiles', appFiles)
|
||||
|
||||
let ensureFn: (item: FsOutput) => Promise<void> | undefined
|
||||
|
||||
return {
|
||||
headers,
|
||||
rewrites,
|
||||
redirects,
|
||||
|
||||
buildId,
|
||||
handleLocale,
|
||||
|
||||
appFiles,
|
||||
pageFiles,
|
||||
dynamicRoutes,
|
||||
nextDataRoutes,
|
||||
|
||||
interceptionRoutes: undefined as
|
||||
| undefined
|
||||
| ReturnType<typeof buildCustomRoute>[],
|
||||
|
||||
devVirtualFsItems: new Set<string>(),
|
||||
|
||||
prerenderManifest,
|
||||
middlewareMatcher: middlewareMatcher as MiddlewareRouteMatch | undefined,
|
||||
|
||||
ensureCallback(fn: typeof ensureFn) {
|
||||
ensureFn = fn
|
||||
},
|
||||
|
||||
async getItem(itemPath: string): Promise<FsOutput | null> {
|
||||
const originalItemPath = itemPath
|
||||
const itemKey = originalItemPath
|
||||
const lruResult = getItemsLru.get(itemKey)
|
||||
|
||||
if (lruResult) {
|
||||
return lruResult
|
||||
}
|
||||
|
||||
// handle minimal mode case with .rsc output path (this is
|
||||
// mostly for testings)
|
||||
if (opts.minimalMode && itemPath.endsWith('.rsc')) {
|
||||
itemPath = itemPath.substring(0, itemPath.length - '.rsc'.length)
|
||||
}
|
||||
|
||||
const { basePath } = opts.config
|
||||
|
||||
if (basePath && !pathHasPrefix(itemPath, basePath)) {
|
||||
return null
|
||||
}
|
||||
itemPath = removePathPrefix(itemPath, basePath) || '/'
|
||||
|
||||
if (itemPath !== '/' && itemPath.endsWith('/')) {
|
||||
itemPath = itemPath.substring(0, itemPath.length - 1)
|
||||
}
|
||||
|
||||
let decodedItemPath = itemPath
|
||||
|
||||
try {
|
||||
decodedItemPath = decodeURIComponent(itemPath)
|
||||
} catch (_) {}
|
||||
|
||||
if (itemPath === '/_next/image') {
|
||||
return {
|
||||
itemPath,
|
||||
type: 'nextImage',
|
||||
}
|
||||
}
|
||||
|
||||
const itemsToCheck: Array<[Set<string>, FsOutput['type']]> = [
|
||||
[this.devVirtualFsItems, 'devVirtualFsItem'],
|
||||
[nextStaticFolderItems, 'nextStaticFolder'],
|
||||
[legacyStaticFolderItems, 'legacyStaticFolder'],
|
||||
[publicFolderItems, 'publicFolder'],
|
||||
[appFiles, 'appFile'],
|
||||
[pageFiles, 'pageFile'],
|
||||
]
|
||||
|
||||
for (let [items, type] of itemsToCheck) {
|
||||
let locale: string | undefined
|
||||
let curItemPath = itemPath
|
||||
let curDecodedItemPath = decodedItemPath
|
||||
|
||||
const isDynamicOutput = type === 'pageFile' || type === 'appFile'
|
||||
|
||||
if (i18n) {
|
||||
const localeResult = handleLocale(
|
||||
itemPath,
|
||||
// legacy behavior allows visiting static assets under
|
||||
// default locale but no other locale
|
||||
isDynamicOutput ? undefined : [i18n?.defaultLocale]
|
||||
)
|
||||
|
||||
if (localeResult.pathname !== curItemPath) {
|
||||
curItemPath = localeResult.pathname
|
||||
locale = localeResult.locale
|
||||
|
||||
try {
|
||||
curDecodedItemPath = decodeURIComponent(curItemPath)
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'legacyStaticFolder') {
|
||||
if (!pathHasPrefix(curItemPath, '/static')) {
|
||||
continue
|
||||
}
|
||||
curItemPath = curItemPath.substring('/static'.length)
|
||||
|
||||
try {
|
||||
curDecodedItemPath = decodeURIComponent(curItemPath)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'nextStaticFolder' &&
|
||||
!pathHasPrefix(curItemPath, '/_next/static')
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const nextDataPrefix = `/_next/data/${buildId}/`
|
||||
|
||||
if (
|
||||
type === 'pageFile' &&
|
||||
curItemPath.startsWith(nextDataPrefix) &&
|
||||
curItemPath.endsWith('.json')
|
||||
) {
|
||||
items = nextDataRoutes
|
||||
// remove _next/data/<build-id> prefix
|
||||
curItemPath = curItemPath.substring(nextDataPrefix.length - 1)
|
||||
|
||||
// remove .json postfix
|
||||
curItemPath = curItemPath.substring(
|
||||
0,
|
||||
curItemPath.length - '.json'.length
|
||||
)
|
||||
const curLocaleResult = handleLocale(curItemPath)
|
||||
curItemPath =
|
||||
curLocaleResult.pathname === '/index'
|
||||
? '/'
|
||||
: curLocaleResult.pathname
|
||||
|
||||
locale = curLocaleResult.locale
|
||||
|
||||
try {
|
||||
curDecodedItemPath = decodeURIComponent(curItemPath)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// check decoded variant as well
|
||||
if (!items.has(curItemPath) && !opts.dev) {
|
||||
curItemPath = curDecodedItemPath
|
||||
}
|
||||
const matchedItem = items.has(curItemPath)
|
||||
|
||||
if (matchedItem || opts.dev) {
|
||||
let fsPath: string | undefined
|
||||
let itemsRoot: string | undefined
|
||||
|
||||
switch (type) {
|
||||
case 'nextStaticFolder': {
|
||||
itemsRoot = nextStaticFolderPath
|
||||
curItemPath = curItemPath.substring('/_next/static'.length)
|
||||
break
|
||||
}
|
||||
case 'legacyStaticFolder': {
|
||||
itemsRoot = legacyStaticFolderPath
|
||||
break
|
||||
}
|
||||
case 'publicFolder': {
|
||||
itemsRoot = publicFolderPath
|
||||
break
|
||||
}
|
||||
default: {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (itemsRoot && curItemPath) {
|
||||
fsPath = path.posix.join(itemsRoot, curItemPath)
|
||||
}
|
||||
|
||||
// dynamically check fs in development so we don't
|
||||
// have to wait on the watcher
|
||||
if (!matchedItem && opts.dev) {
|
||||
const isStaticAsset = (
|
||||
[
|
||||
'nextStaticFolder',
|
||||
'publicFolder',
|
||||
'legacyStaticFolder',
|
||||
] as (typeof type)[]
|
||||
).includes(type)
|
||||
|
||||
if (isStaticAsset && itemsRoot) {
|
||||
let found = fsPath && (await fileExists(fsPath, FileType.File))
|
||||
|
||||
if (!found) {
|
||||
try {
|
||||
// In dev, we ensure encoded paths match
|
||||
// decoded paths on the filesystem so check
|
||||
// that variation as well
|
||||
const tempItemPath = decodeURIComponent(curItemPath)
|
||||
fsPath = path.posix.join(itemsRoot, tempItemPath)
|
||||
found = await fileExists(fsPath, FileType.File)
|
||||
} catch (_) {}
|
||||
|
||||
if (!found) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
} else if (type === 'pageFile' || type === 'appFile') {
|
||||
if (
|
||||
ensureFn &&
|
||||
(await ensureFn({
|
||||
type,
|
||||
itemPath: curItemPath,
|
||||
})?.catch(() => 'ENSURE_FAILED')) === 'ENSURE_FAILED'
|
||||
) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// i18n locales aren't matched for app dir
|
||||
if (type === 'appFile' && locale && locale !== i18n?.defaultLocale) {
|
||||
continue
|
||||
}
|
||||
|
||||
const itemResult = {
|
||||
type,
|
||||
fsPath,
|
||||
locale,
|
||||
itemsRoot,
|
||||
itemPath: curItemPath,
|
||||
}
|
||||
|
||||
if (!opts.dev) {
|
||||
getItemsLru.set(itemKey, itemResult)
|
||||
}
|
||||
return itemResult
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.dev) {
|
||||
getItemsLru.set(itemKey, null)
|
||||
}
|
||||
return null
|
||||
},
|
||||
getDynamicRoutes() {
|
||||
// this should include data routes
|
||||
return this.dynamicRoutes
|
||||
},
|
||||
getMiddlewareMatchers() {
|
||||
return this.middlewareMatcher
|
||||
},
|
||||
}
|
||||
}
|
95
packages/next/src/server/lib/router-utils/proxy-request.ts
Normal file
95
packages/next/src/server/lib/router-utils/proxy-request.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import type { NextUrlWithParsedQuery } from '../../request-meta'
|
||||
|
||||
import url from 'url'
|
||||
import { stringifyQuery } from '../../server-route-utils'
|
||||
|
||||
export async function proxyRequest(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
parsedUrl: NextUrlWithParsedQuery,
|
||||
upgradeHead?: any,
|
||||
reqBody?: any,
|
||||
proxyTimeout?: number | null
|
||||
) {
|
||||
const { query } = parsedUrl
|
||||
delete (parsedUrl as any).query
|
||||
parsedUrl.search = stringifyQuery(req as any, query)
|
||||
|
||||
const target = url.format(parsedUrl)
|
||||
const HttpProxy =
|
||||
require('next/dist/compiled/http-proxy') as typeof import('next/dist/compiled/http-proxy')
|
||||
|
||||
const proxy = new HttpProxy({
|
||||
target,
|
||||
changeOrigin: true,
|
||||
ignorePath: true,
|
||||
xfwd: true,
|
||||
ws: true,
|
||||
// we limit proxy requests to 30s by default, in development
|
||||
// we don't time out WebSocket requests to allow proxying
|
||||
proxyTimeout: proxyTimeout === null ? undefined : proxyTimeout || 30_000,
|
||||
})
|
||||
|
||||
await new Promise((proxyResolve, proxyReject) => {
|
||||
let finished = false
|
||||
|
||||
proxy.on('proxyRes', (proxyRes, innerReq, innerRes) => {
|
||||
const cleanup = (err: any) => {
|
||||
// cleanup event listeners to allow clean garbage collection
|
||||
proxyRes.removeListener('error', cleanup)
|
||||
proxyRes.removeListener('close', cleanup)
|
||||
innerRes.removeListener('error', cleanup)
|
||||
innerRes.removeListener('close', cleanup)
|
||||
|
||||
// destroy all source streams to propagate the caught event backward
|
||||
innerReq.destroy(err)
|
||||
proxyRes.destroy(err)
|
||||
}
|
||||
|
||||
proxyRes.once('error', cleanup)
|
||||
proxyRes.once('close', cleanup)
|
||||
innerRes.once('error', cleanup)
|
||||
innerRes.once('close', cleanup)
|
||||
})
|
||||
|
||||
proxy.on('error', (err) => {
|
||||
console.error(`Failed to proxy ${target}`, err)
|
||||
if (!finished) {
|
||||
finished = true
|
||||
proxyReject(err)
|
||||
|
||||
if (!res.closed) {
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// if upgrade head is present treat as WebSocket request
|
||||
if (upgradeHead) {
|
||||
proxy.on('proxyReqWs', (proxyReq) => {
|
||||
proxyReq.on('close', () => {
|
||||
if (!finished) {
|
||||
finished = true
|
||||
proxyResolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
proxy.ws(req as any as IncomingMessage, res, upgradeHead)
|
||||
proxyResolve(true)
|
||||
} else {
|
||||
proxy.on('proxyReq', (proxyReq) => {
|
||||
proxyReq.on('close', () => {
|
||||
if (!finished) {
|
||||
finished = true
|
||||
proxyResolve(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
proxy.web(req, res, {
|
||||
buffer: reqBody,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
711
packages/next/src/server/lib/router-utils/resolve-routes.ts
Normal file
711
packages/next/src/server/lib/router-utils/resolve-routes.ts
Normal file
|
@ -0,0 +1,711 @@
|
|||
import type { TLSSocket } from 'tls'
|
||||
import type { FsOutput } from './filesystem'
|
||||
import type { IncomingMessage } from 'http'
|
||||
import type { NextConfigComplete } from '../../config-shared'
|
||||
|
||||
import url from 'url'
|
||||
import { Redirect } from '../../../../types'
|
||||
import { RenderWorker } from '../router-server'
|
||||
import setupDebug from 'next/dist/compiled/debug'
|
||||
import { getCloneableBody } from '../../body-streams'
|
||||
import { filterReqHeaders } from '../server-ipc/utils'
|
||||
import { Header } from '../../../lib/load-custom-routes'
|
||||
import { stringifyQuery } from '../../server-route-utils'
|
||||
import { toNodeOutgoingHttpHeaders } from '../../web/utils'
|
||||
import { invokeRequest } from '../server-ipc/invoke-request'
|
||||
import { getCookieParser, setLazyProp } from '../../api-utils'
|
||||
import { getHostname } from '../../../shared/lib/get-hostname'
|
||||
import { UnwrapPromise } from '../../../lib/coalesced-function'
|
||||
import { getRedirectStatus } from '../../../lib/redirect-status'
|
||||
import { normalizeRepeatedSlashes } from '../../../shared/lib/utils'
|
||||
import { getPathMatch } from '../../../shared/lib/router/utils/path-match'
|
||||
import { relativizeURL } from '../../../shared/lib/router/utils/relativize-url'
|
||||
import { addPathPrefix } from '../../../shared/lib/router/utils/add-path-prefix'
|
||||
import { pathHasPrefix } from '../../../shared/lib/router/utils/path-has-prefix'
|
||||
import { detectDomainLocale } from '../../../shared/lib/i18n/detect-domain-locale'
|
||||
import { normalizeLocalePath } from '../../../shared/lib/i18n/normalize-locale-path'
|
||||
import { removePathPrefix } from '../../../shared/lib/router/utils/remove-path-prefix'
|
||||
|
||||
import {
|
||||
NextUrlWithParsedQuery,
|
||||
addRequestMeta,
|
||||
getRequestMeta,
|
||||
} from '../../request-meta'
|
||||
import {
|
||||
compileNonPath,
|
||||
matchHas,
|
||||
prepareDestination,
|
||||
} from '../../../shared/lib/router/utils/prepare-destination'
|
||||
|
||||
const debug = setupDebug('next:router-server:resolve-routes')
|
||||
|
||||
export function getResolveRoutes(
|
||||
fsChecker: UnwrapPromise<
|
||||
ReturnType<typeof import('./filesystem').setupFsCheck>
|
||||
>,
|
||||
config: NextConfigComplete,
|
||||
opts: Parameters<typeof import('../router-server').initialize>[0],
|
||||
renderWorkers: {
|
||||
app?: RenderWorker
|
||||
pages?: RenderWorker
|
||||
},
|
||||
renderWorkerOpts: Parameters<RenderWorker['initialize']>[0],
|
||||
ensureMiddleware?: () => Promise<void>
|
||||
) {
|
||||
const routes: ({
|
||||
match: ReturnType<typeof getPathMatch>
|
||||
check?: boolean
|
||||
name?: string
|
||||
internal?: boolean
|
||||
} & Partial<Header> &
|
||||
Partial<Redirect>)[] = [
|
||||
// _next/data with middleware handling
|
||||
{ match: () => ({} as any), name: 'middleware_next_data' },
|
||||
|
||||
...(opts.minimalMode ? [] : fsChecker.headers),
|
||||
...(opts.minimalMode ? [] : fsChecker.redirects),
|
||||
|
||||
// check middleware (using matchers)
|
||||
{ match: () => ({} as any), name: 'middleware' },
|
||||
|
||||
...(opts.minimalMode ? [] : fsChecker.rewrites.beforeFiles),
|
||||
|
||||
// check middleware (using matchers)
|
||||
{ match: () => ({} as any), name: 'before_files_end' },
|
||||
|
||||
// we check exact matches on fs before continuing to
|
||||
// after files rewrites
|
||||
{ match: () => ({} as any), name: 'check_fs' },
|
||||
|
||||
...(opts.minimalMode ? [] : fsChecker.rewrites.afterFiles),
|
||||
|
||||
// we always do the check: true handling before continuing to
|
||||
// fallback rewrites
|
||||
{
|
||||
check: true,
|
||||
match: () => ({} as any),
|
||||
name: 'after files check: true',
|
||||
},
|
||||
|
||||
...(opts.minimalMode ? [] : fsChecker.rewrites.fallback),
|
||||
]
|
||||
|
||||
async function resolveRoutes(
|
||||
req: IncomingMessage,
|
||||
matchedDynamicRoutes: Set<string>,
|
||||
isUpgradeReq?: boolean
|
||||
): Promise<{
|
||||
finished: boolean
|
||||
statusCode?: number
|
||||
bodyStream?: ReadableStream | null
|
||||
resHeaders: Record<string, string | string[]>
|
||||
parsedUrl: NextUrlWithParsedQuery
|
||||
matchedOutput?: FsOutput | null
|
||||
}> {
|
||||
let finished = false
|
||||
let resHeaders: Record<string, string | string[]> = {}
|
||||
let matchedOutput: FsOutput | null = null
|
||||
let parsedUrl = url.parse(req.url || '', true) as NextUrlWithParsedQuery
|
||||
let didRewrite = false
|
||||
|
||||
const urlParts = (req.url || '').split('?')
|
||||
const urlNoQuery = urlParts[0]
|
||||
|
||||
// this normalizes repeated slashes in the path e.g. hello//world ->
|
||||
// hello/world or backslashes to forward slashes, this does not
|
||||
// handle trailing slash as that is handled the same as a next.config.js
|
||||
// redirect
|
||||
if (urlNoQuery?.match(/(\\|\/\/)/)) {
|
||||
parsedUrl = url.parse(normalizeRepeatedSlashes(req.url!), true)
|
||||
return {
|
||||
parsedUrl,
|
||||
resHeaders,
|
||||
finished: true,
|
||||
statusCode: 308,
|
||||
}
|
||||
}
|
||||
// TODO: inherit this from higher up
|
||||
const protocol =
|
||||
(req?.socket as TLSSocket)?.encrypted ||
|
||||
req.headers['x-forwarded-proto'] === 'https'
|
||||
? 'https'
|
||||
: 'http'
|
||||
|
||||
// When there are hostname and port we build an absolute URL
|
||||
const initUrl = (config.experimental as any).trustHostHeader
|
||||
? `https://${req.headers.host || 'localhost'}${req.url}`
|
||||
: opts.port
|
||||
? `${protocol}://${opts.hostname || 'localhost'}:${opts.port}${req.url}`
|
||||
: req.url || ''
|
||||
|
||||
addRequestMeta(req, '__NEXT_INIT_URL', initUrl)
|
||||
addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query })
|
||||
addRequestMeta(req, '_protocol', protocol)
|
||||
setLazyProp({ req }, 'cookies', () => getCookieParser(req.headers)())
|
||||
|
||||
if (!isUpgradeReq) {
|
||||
addRequestMeta(req, '__NEXT_CLONABLE_BODY', getCloneableBody(req))
|
||||
}
|
||||
|
||||
const maybeAddTrailingSlash = (pathname: string) => {
|
||||
if (
|
||||
config.trailingSlash &&
|
||||
!config.skipMiddlewareUrlNormalize &&
|
||||
!pathname.endsWith('/')
|
||||
) {
|
||||
return `${pathname}/`
|
||||
}
|
||||
return pathname
|
||||
}
|
||||
|
||||
let domainLocale: ReturnType<typeof detectDomainLocale> | undefined
|
||||
let defaultLocale: string | undefined
|
||||
let initialLocaleResult:
|
||||
| ReturnType<typeof normalizeLocalePath>
|
||||
| undefined = undefined
|
||||
|
||||
if (config.i18n) {
|
||||
const hadTrailingSlash = parsedUrl.pathname?.endsWith('/')
|
||||
const hadBasePath = pathHasPrefix(
|
||||
parsedUrl.pathname || '',
|
||||
config.basePath
|
||||
)
|
||||
initialLocaleResult = normalizeLocalePath(
|
||||
removePathPrefix(parsedUrl.pathname || '/', config.basePath),
|
||||
config.i18n.locales
|
||||
)
|
||||
|
||||
domainLocale = detectDomainLocale(
|
||||
config.i18n.domains,
|
||||
getHostname(parsedUrl, req.headers)
|
||||
)
|
||||
defaultLocale = domainLocale?.defaultLocale || config.i18n.defaultLocale
|
||||
|
||||
parsedUrl.query.__nextDefaultLocale = defaultLocale
|
||||
parsedUrl.query.__nextLocale =
|
||||
initialLocaleResult.detectedLocale || defaultLocale
|
||||
|
||||
// ensure locale is present for resolving routes
|
||||
if (
|
||||
!initialLocaleResult.detectedLocale &&
|
||||
!initialLocaleResult.pathname.startsWith('/_next/')
|
||||
) {
|
||||
parsedUrl.pathname = addPathPrefix(
|
||||
initialLocaleResult.pathname === '/'
|
||||
? `/${defaultLocale}`
|
||||
: addPathPrefix(
|
||||
initialLocaleResult.pathname || '',
|
||||
`/${defaultLocale}`
|
||||
),
|
||||
hadBasePath ? config.basePath : ''
|
||||
)
|
||||
|
||||
if (hadTrailingSlash) {
|
||||
parsedUrl.pathname = maybeAddTrailingSlash(parsedUrl.pathname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const checkLocaleApi = (pathname: string) => {
|
||||
if (
|
||||
config.i18n &&
|
||||
pathname === urlNoQuery &&
|
||||
initialLocaleResult?.detectedLocale &&
|
||||
pathHasPrefix(initialLocaleResult.pathname, '/api')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTrue() {
|
||||
if (checkLocaleApi(parsedUrl.pathname || '')) {
|
||||
return
|
||||
}
|
||||
const output = await fsChecker.getItem(parsedUrl.pathname || '')
|
||||
|
||||
if (output) {
|
||||
if (
|
||||
config.useFileSystemPublicRoutes ||
|
||||
didRewrite ||
|
||||
(output.type !== 'appFile' && output.type !== 'pageFile')
|
||||
) {
|
||||
return output
|
||||
}
|
||||
}
|
||||
const dynamicRoutes = fsChecker.getDynamicRoutes()
|
||||
let curPathname = parsedUrl.pathname
|
||||
|
||||
if (config.basePath) {
|
||||
if (!pathHasPrefix(curPathname || '', config.basePath)) {
|
||||
return
|
||||
}
|
||||
curPathname = curPathname?.substring(config.basePath.length) || '/'
|
||||
}
|
||||
const localeResult = fsChecker.handleLocale(curPathname || '')
|
||||
|
||||
for (const route of dynamicRoutes) {
|
||||
// when resolving fallback: false we attempt to
|
||||
// render worker may return a no-fallback response
|
||||
// which signals we need to continue resolving.
|
||||
// TODO: optimize this to collect static paths
|
||||
// to use at the routing layer
|
||||
if (matchedDynamicRoutes.has(route.page)) {
|
||||
continue
|
||||
}
|
||||
const params = route.match(localeResult.pathname)
|
||||
|
||||
if (params) {
|
||||
const pageOutput = await fsChecker.getItem(
|
||||
addPathPrefix(route.page, config.basePath || '')
|
||||
)
|
||||
|
||||
// i18n locales aren't matched for app dir
|
||||
if (
|
||||
pageOutput?.type === 'appFile' &&
|
||||
initialLocaleResult?.detectedLocale
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (pageOutput && curPathname?.startsWith('/_next/data')) {
|
||||
parsedUrl.query.__nextDataReq = '1'
|
||||
}
|
||||
matchedDynamicRoutes.add(route.page)
|
||||
|
||||
if (config.useFileSystemPublicRoutes || didRewrite) {
|
||||
return pageOutput
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRoute(
|
||||
route: (typeof routes)[0]
|
||||
): Promise<UnwrapPromise<ReturnType<typeof resolveRoutes>> | void> {
|
||||
let curPathname = parsedUrl.pathname || '/'
|
||||
|
||||
if (config.i18n && route.internal) {
|
||||
const hadTrailingSlash = curPathname.endsWith('/')
|
||||
|
||||
if (config.basePath) {
|
||||
curPathname = removePathPrefix(curPathname, config.basePath)
|
||||
}
|
||||
const hadBasePath = curPathname !== parsedUrl.pathname
|
||||
|
||||
const localeResult = normalizeLocalePath(
|
||||
curPathname,
|
||||
config.i18n.locales
|
||||
)
|
||||
const isDefaultLocale = localeResult.detectedLocale === defaultLocale
|
||||
|
||||
if (isDefaultLocale) {
|
||||
curPathname =
|
||||
localeResult.pathname === '/' && hadBasePath
|
||||
? config.basePath
|
||||
: addPathPrefix(
|
||||
localeResult.pathname,
|
||||
hadBasePath ? config.basePath : ''
|
||||
)
|
||||
} else if (hadBasePath) {
|
||||
curPathname =
|
||||
curPathname === '/'
|
||||
? config.basePath
|
||||
: addPathPrefix(curPathname, config.basePath)
|
||||
}
|
||||
|
||||
if ((isDefaultLocale || hadBasePath) && hadTrailingSlash) {
|
||||
curPathname = maybeAddTrailingSlash(curPathname)
|
||||
}
|
||||
}
|
||||
let params = route.match(curPathname)
|
||||
|
||||
if ((route.has || route.missing) && params) {
|
||||
const hasParams = matchHas(
|
||||
req,
|
||||
parsedUrl.query,
|
||||
route.has,
|
||||
route.missing
|
||||
)
|
||||
if (hasParams) {
|
||||
Object.assign(params, hasParams)
|
||||
} else {
|
||||
params = false
|
||||
}
|
||||
}
|
||||
|
||||
if (params) {
|
||||
if (fsChecker.interceptionRoutes && route.name === 'before_files_end') {
|
||||
for (const interceptionRoute of fsChecker.interceptionRoutes) {
|
||||
const result = await handleRoute(interceptionRoute)
|
||||
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (route.name === 'middleware_next_data') {
|
||||
if (fsChecker.getMiddlewareMatchers()?.length) {
|
||||
const nextDataPrefix = addPathPrefix(
|
||||
`/_next/data/${fsChecker.buildId}/`,
|
||||
config.basePath
|
||||
)
|
||||
|
||||
if (
|
||||
parsedUrl.pathname?.startsWith(nextDataPrefix) &&
|
||||
parsedUrl.pathname.endsWith('.json')
|
||||
) {
|
||||
parsedUrl.query.__nextDataReq = '1'
|
||||
parsedUrl.pathname = parsedUrl.pathname.substring(
|
||||
nextDataPrefix.length - 1
|
||||
)
|
||||
parsedUrl.pathname = parsedUrl.pathname.substring(
|
||||
0,
|
||||
parsedUrl.pathname.length - '.json'.length
|
||||
)
|
||||
parsedUrl.pathname = addPathPrefix(
|
||||
parsedUrl.pathname || '',
|
||||
config.basePath
|
||||
)
|
||||
parsedUrl.pathname =
|
||||
parsedUrl.pathname === '/index' ? '/' : parsedUrl.pathname
|
||||
|
||||
parsedUrl.pathname = maybeAddTrailingSlash(parsedUrl.pathname)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (route.name === 'check_fs') {
|
||||
if (checkLocaleApi(parsedUrl.pathname || '')) {
|
||||
return
|
||||
}
|
||||
const output = await fsChecker.getItem(parsedUrl.pathname || '')
|
||||
|
||||
if (
|
||||
output &&
|
||||
!(
|
||||
config.i18n &&
|
||||
initialLocaleResult?.detectedLocale &&
|
||||
pathHasPrefix(parsedUrl.pathname || '', '/api')
|
||||
)
|
||||
) {
|
||||
if (
|
||||
config.useFileSystemPublicRoutes ||
|
||||
didRewrite ||
|
||||
(output.type !== 'appFile' && output.type !== 'pageFile')
|
||||
) {
|
||||
matchedOutput = output
|
||||
|
||||
if (output.locale) {
|
||||
parsedUrl.query.__nextLocale = output.locale
|
||||
}
|
||||
return {
|
||||
parsedUrl,
|
||||
resHeaders,
|
||||
finished: true,
|
||||
matchedOutput,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!opts.minimalMode && route.name === 'middleware') {
|
||||
const match = fsChecker.getMiddlewareMatchers()
|
||||
if (
|
||||
// @ts-expect-error BaseNextRequest stuff
|
||||
match?.(parsedUrl.pathname, req, parsedUrl.query) &&
|
||||
(!ensureMiddleware ||
|
||||
(await ensureMiddleware?.()
|
||||
.then(() => true)
|
||||
.catch(() => false)))
|
||||
) {
|
||||
const workerResult = await (
|
||||
renderWorkers.app || renderWorkers.pages
|
||||
)?.initialize(renderWorkerOpts)
|
||||
|
||||
if (!workerResult) {
|
||||
throw new Error(`Failed to initialize render worker "middleware"`)
|
||||
}
|
||||
const stringifiedQuery = stringifyQuery(
|
||||
req as any,
|
||||
getRequestMeta(req, '__NEXT_INIT_QUERY') || {}
|
||||
)
|
||||
const parsedInitUrl = new URL(
|
||||
getRequestMeta(req, '__NEXT_INIT_URL') || '/',
|
||||
'http://n'
|
||||
)
|
||||
|
||||
const curUrl = config.skipMiddlewareUrlNormalize
|
||||
? `${parsedInitUrl.pathname}${parsedInitUrl.search}`
|
||||
: `${parsedUrl.pathname}${stringifiedQuery ? '?' : ''}${
|
||||
stringifiedQuery || ''
|
||||
}`
|
||||
|
||||
const renderUrl = `http://${workerResult.hostname}:${workerResult.port}${curUrl}`
|
||||
|
||||
const invokeHeaders: typeof req.headers = {
|
||||
...req.headers,
|
||||
'x-invoke-path': '',
|
||||
'x-invoke-query': '',
|
||||
'x-invoke-output': '',
|
||||
'x-middleware-invoke': '1',
|
||||
}
|
||||
|
||||
debug('invoking middleware', renderUrl, invokeHeaders)
|
||||
|
||||
const middlewareRes = await invokeRequest(
|
||||
renderUrl,
|
||||
{
|
||||
headers: invokeHeaders,
|
||||
method: req.method,
|
||||
},
|
||||
getRequestMeta(req, '__NEXT_CLONABLE_BODY')?.cloneBodyStream()
|
||||
)
|
||||
const middlewareHeaders = toNodeOutgoingHttpHeaders(
|
||||
middlewareRes.headers
|
||||
) as Record<string, string | string[] | undefined>
|
||||
|
||||
debug('middleware res', middlewareRes.status, middlewareHeaders)
|
||||
|
||||
if (middlewareHeaders['x-middleware-override-headers']) {
|
||||
const overriddenHeaders: Set<string> = new Set()
|
||||
let overrideHeaders: string | string[] =
|
||||
middlewareHeaders['x-middleware-override-headers']
|
||||
|
||||
if (typeof overrideHeaders === 'string') {
|
||||
overrideHeaders = overrideHeaders.split(',')
|
||||
}
|
||||
|
||||
for (const key of overrideHeaders) {
|
||||
overriddenHeaders.add(key.trim())
|
||||
}
|
||||
delete middlewareHeaders['x-middleware-override-headers']
|
||||
|
||||
// Delete headers.
|
||||
for (const key of Object.keys(req.headers)) {
|
||||
if (!overriddenHeaders.has(key)) {
|
||||
delete req.headers[key]
|
||||
}
|
||||
}
|
||||
|
||||
// Update or add headers.
|
||||
for (const key of overriddenHeaders.keys()) {
|
||||
const valueKey = 'x-middleware-request-' + key
|
||||
const newValue = middlewareHeaders[valueKey]
|
||||
const oldValue = req.headers[key]
|
||||
|
||||
if (oldValue !== newValue) {
|
||||
req.headers[key] = newValue === null ? undefined : newValue
|
||||
}
|
||||
delete middlewareHeaders[valueKey]
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!middlewareHeaders['x-middleware-rewrite'] &&
|
||||
!middlewareHeaders['x-middleware-next'] &&
|
||||
!middlewareHeaders['location']
|
||||
) {
|
||||
middlewareHeaders['x-middleware-refresh'] = '1'
|
||||
}
|
||||
delete middlewareHeaders['x-middleware-next']
|
||||
|
||||
for (const [key, value] of Object.entries({
|
||||
...filterReqHeaders(middlewareHeaders),
|
||||
})) {
|
||||
if (
|
||||
[
|
||||
'content-length',
|
||||
'x-middleware-rewrite',
|
||||
'x-middleware-redirect',
|
||||
'x-middleware-refresh',
|
||||
'x-middleware-invoke',
|
||||
'x-invoke-path',
|
||||
'x-invoke-query',
|
||||
].includes(key)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
if (value) {
|
||||
resHeaders[key] = value
|
||||
req.headers[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
if (middlewareHeaders['x-middleware-rewrite']) {
|
||||
const value = middlewareHeaders['x-middleware-rewrite'] as string
|
||||
const rel = relativizeURL(value, initUrl)
|
||||
resHeaders['x-middleware-rewrite'] = rel
|
||||
|
||||
const query = parsedUrl.query
|
||||
parsedUrl = url.parse(rel, true)
|
||||
|
||||
if (parsedUrl.protocol) {
|
||||
return {
|
||||
parsedUrl,
|
||||
resHeaders,
|
||||
finished: true,
|
||||
}
|
||||
}
|
||||
|
||||
// keep internal query state
|
||||
for (const key of Object.keys(query)) {
|
||||
if (key.startsWith('_next') || key.startsWith('__next')) {
|
||||
parsedUrl.query[key] = query[key]
|
||||
}
|
||||
}
|
||||
|
||||
if (config.i18n) {
|
||||
const curLocaleResult = normalizeLocalePath(
|
||||
parsedUrl.pathname || '',
|
||||
config.i18n.locales
|
||||
)
|
||||
|
||||
if (curLocaleResult.detectedLocale) {
|
||||
parsedUrl.query.__nextLocale = curLocaleResult.detectedLocale
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (middlewareHeaders['location']) {
|
||||
const value = middlewareHeaders['location'] as string
|
||||
const rel = relativizeURL(value, initUrl)
|
||||
resHeaders['location'] = rel
|
||||
parsedUrl = url.parse(rel, true)
|
||||
|
||||
return {
|
||||
parsedUrl,
|
||||
resHeaders,
|
||||
finished: true,
|
||||
statusCode: middlewareRes.status,
|
||||
}
|
||||
}
|
||||
|
||||
if (middlewareHeaders['x-middleware-refresh']) {
|
||||
return {
|
||||
parsedUrl,
|
||||
resHeaders,
|
||||
finished: true,
|
||||
bodyStream: middlewareRes.body,
|
||||
statusCode: middlewareRes.status,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle redirect
|
||||
if (
|
||||
('statusCode' in route || 'permanent' in route) &&
|
||||
route.destination
|
||||
) {
|
||||
const { parsedDestination } = prepareDestination({
|
||||
appendParamsToQuery: false,
|
||||
destination: route.destination,
|
||||
params: params,
|
||||
query: parsedUrl.query,
|
||||
})
|
||||
|
||||
const { query } = parsedDestination
|
||||
delete (parsedDestination as any).query
|
||||
|
||||
parsedDestination.search = stringifyQuery(req as any, query)
|
||||
|
||||
parsedDestination.pathname = normalizeRepeatedSlashes(
|
||||
parsedDestination.pathname
|
||||
)
|
||||
|
||||
return {
|
||||
finished: true,
|
||||
// @ts-expect-error custom ParsedUrl
|
||||
parsedUrl: parsedDestination,
|
||||
statusCode: getRedirectStatus(route),
|
||||
}
|
||||
}
|
||||
|
||||
// handle headers
|
||||
if (route.headers) {
|
||||
const hasParams = Object.keys(params).length > 0
|
||||
for (const header of route.headers) {
|
||||
let { key, value } = header
|
||||
if (hasParams) {
|
||||
key = compileNonPath(key, params)
|
||||
value = compileNonPath(value, params)
|
||||
}
|
||||
|
||||
if (key.toLowerCase() === 'set-cookie') {
|
||||
if (!Array.isArray(resHeaders[key])) {
|
||||
const val = resHeaders[key]
|
||||
resHeaders[key] = typeof val === 'string' ? [val] : []
|
||||
}
|
||||
;(resHeaders[key] as string[]).push(value)
|
||||
} else {
|
||||
resHeaders[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle rewrite
|
||||
if (route.destination) {
|
||||
const { parsedDestination } = prepareDestination({
|
||||
appendParamsToQuery: true,
|
||||
destination: route.destination,
|
||||
params: params,
|
||||
query: parsedUrl.query,
|
||||
})
|
||||
|
||||
if (parsedDestination.protocol) {
|
||||
return {
|
||||
// @ts-expect-error custom ParsedUrl
|
||||
parsedUrl: parsedDestination,
|
||||
finished: true,
|
||||
}
|
||||
}
|
||||
|
||||
if (config.i18n) {
|
||||
const curLocaleResult = normalizeLocalePath(
|
||||
removePathPrefix(parsedDestination.pathname, config.basePath),
|
||||
config.i18n.locales
|
||||
)
|
||||
|
||||
if (curLocaleResult.detectedLocale) {
|
||||
parsedUrl.query.__nextLocale = curLocaleResult.detectedLocale
|
||||
}
|
||||
}
|
||||
didRewrite = true
|
||||
parsedUrl.pathname = parsedDestination.pathname
|
||||
Object.assign(parsedUrl.query, parsedDestination.query)
|
||||
}
|
||||
|
||||
// handle check: true
|
||||
if (route.check) {
|
||||
const output = await checkTrue()
|
||||
|
||||
if (output) {
|
||||
return {
|
||||
parsedUrl,
|
||||
resHeaders,
|
||||
finished: true,
|
||||
matchedOutput: output,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
const result = await handleRoute(route)
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
finished,
|
||||
parsedUrl,
|
||||
resHeaders,
|
||||
matchedOutput,
|
||||
}
|
||||
}
|
||||
|
||||
return resolveRoutes
|
||||
}
|
902
packages/next/src/server/lib/router-utils/setup-dev.ts
Normal file
902
packages/next/src/server/lib/router-utils/setup-dev.ts
Normal file
|
@ -0,0 +1,902 @@
|
|||
import type { NextConfigComplete } from '../../config-shared'
|
||||
|
||||
import fs from 'fs'
|
||||
import url from 'url'
|
||||
import path from 'path'
|
||||
import qs from 'querystring'
|
||||
import Watchpack from 'watchpack'
|
||||
import { loadEnvConfig } from '@next/env'
|
||||
import isError from '../../../lib/is-error'
|
||||
import findUp from 'next/dist/compiled/find-up'
|
||||
import { buildCustomRoute } from './filesystem'
|
||||
import * as Log from '../../../build/output/log'
|
||||
import HotReloader from '../../dev/hot-reloader'
|
||||
import { traceGlobals } from '../../../trace/shared'
|
||||
import { Telemetry } from '../../../telemetry/storage'
|
||||
import { IncomingMessage, ServerResponse } from 'http'
|
||||
import loadJsConfig from '../../../build/load-jsconfig'
|
||||
import { createValidFileMatcher } from '../find-page-file'
|
||||
import { eventCliSession } from '../../../telemetry/events'
|
||||
import { getDefineEnv } from '../../../build/webpack-config'
|
||||
import { logAppDirError } from '../../dev/log-app-dir-error'
|
||||
import { UnwrapPromise } from '../../../lib/coalesced-function'
|
||||
import { getSortedRoutes } from '../../../shared/lib/router/utils'
|
||||
import { getStaticInfoIncludingLayouts } from '../../../build/entries'
|
||||
import { verifyTypeScriptSetup } from '../../../lib/verifyTypeScriptSetup'
|
||||
import { verifyPartytownSetup } from '../../../lib/verify-partytown-setup'
|
||||
import { getRouteRegex } from '../../../shared/lib/router/utils/route-regex'
|
||||
import { normalizeAppPath } from '../../../shared/lib/router/utils/app-paths'
|
||||
import { buildDataRoute } from './build-data-route'
|
||||
import { MiddlewareMatcher } from '../../../build/analysis/get-page-static-info'
|
||||
import { getRouteMatcher } from '../../../shared/lib/router/utils/route-matcher'
|
||||
import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep'
|
||||
import { createClientRouterFilter } from '../../../lib/create-client-router-filter'
|
||||
import { absolutePathToPage } from '../../../shared/lib/page-path/absolute-path-to-page'
|
||||
import { generateInterceptionRoutesRewrites } from '../../../lib/generate-interception-routes-rewrites'
|
||||
|
||||
import {
|
||||
CLIENT_STATIC_FILES_PATH,
|
||||
COMPILER_NAMES,
|
||||
DEV_CLIENT_PAGES_MANIFEST,
|
||||
DEV_MIDDLEWARE_MANIFEST,
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
} from '../../../shared/lib/constants'
|
||||
|
||||
import {
|
||||
MiddlewareRouteMatch,
|
||||
getMiddlewareRouteMatcher,
|
||||
} from '../../../shared/lib/router/utils/middleware-route-matcher'
|
||||
import { NextBuildContext } from '../../../build/build-context'
|
||||
|
||||
import {
|
||||
isMiddlewareFile,
|
||||
NestedMiddlewareError,
|
||||
isInstrumentationHookFile,
|
||||
getPossibleMiddlewareFilenames,
|
||||
getPossibleInstrumentationHookFilenames,
|
||||
} from '../../../build/worker'
|
||||
import {
|
||||
createOriginalStackFrame,
|
||||
getErrorSource,
|
||||
getSourceById,
|
||||
parseStack,
|
||||
} from 'next/dist/compiled/@next/react-dev-overlay/dist/middleware'
|
||||
|
||||
type SetupOpts = {
|
||||
dir: string
|
||||
appDir?: string
|
||||
pagesDir?: string
|
||||
telemetry: Telemetry
|
||||
isCustomServer?: boolean
|
||||
fsChecker: UnwrapPromise<
|
||||
ReturnType<typeof import('./filesystem').setupFsCheck>
|
||||
>
|
||||
nextConfig: NextConfigComplete
|
||||
}
|
||||
|
||||
async function verifyTypeScript(opts: SetupOpts) {
|
||||
let usingTypeScript = false
|
||||
const verifyResult = await verifyTypeScriptSetup({
|
||||
dir: opts.dir,
|
||||
distDir: opts.nextConfig.distDir,
|
||||
intentDirs: [opts.pagesDir, opts.appDir].filter(Boolean) as string[],
|
||||
typeCheckPreflight: false,
|
||||
tsconfigPath: opts.nextConfig.typescript.tsconfigPath,
|
||||
disableStaticImages: opts.nextConfig.images.disableStaticImages,
|
||||
hasAppDir: !!opts.appDir,
|
||||
hasPagesDir: !!opts.pagesDir,
|
||||
})
|
||||
|
||||
if (verifyResult.version) {
|
||||
usingTypeScript = true
|
||||
}
|
||||
return usingTypeScript
|
||||
}
|
||||
|
||||
async function startWatcher(opts: SetupOpts) {
|
||||
const { nextConfig, appDir, pagesDir, dir } = opts
|
||||
const { useFileSystemPublicRoutes } = nextConfig
|
||||
const usingTypeScript = await verifyTypeScript(opts)
|
||||
|
||||
const distDir = path.join(opts.dir, opts.nextConfig.distDir)
|
||||
|
||||
traceGlobals.set('distDir', distDir)
|
||||
traceGlobals.set('phase', PHASE_DEVELOPMENT_SERVER)
|
||||
|
||||
const validFileMatcher = createValidFileMatcher(
|
||||
nextConfig.pageExtensions,
|
||||
appDir
|
||||
)
|
||||
|
||||
const hotReloader = new HotReloader(opts.dir, {
|
||||
appDir,
|
||||
pagesDir,
|
||||
distDir: distDir,
|
||||
config: opts.nextConfig,
|
||||
buildId: 'development',
|
||||
telemetry: opts.telemetry,
|
||||
rewrites: opts.fsChecker.rewrites,
|
||||
previewProps: opts.fsChecker.prerenderManifest.preview,
|
||||
})
|
||||
const renderWorkers: {
|
||||
app?: import('../router-server').RenderWorker
|
||||
pages?: import('../router-server').RenderWorker
|
||||
} = {}
|
||||
|
||||
await hotReloader.start()
|
||||
|
||||
if (opts.nextConfig.experimental.nextScriptWorkers) {
|
||||
await verifyPartytownSetup(
|
||||
opts.dir,
|
||||
path.join(distDir, CLIENT_STATIC_FILES_PATH)
|
||||
)
|
||||
}
|
||||
|
||||
opts.fsChecker.ensureCallback(async function ensure(item) {
|
||||
if (item.type === 'appFile' || item.type === 'pageFile') {
|
||||
await hotReloader.ensurePage({
|
||||
clientOnly: false,
|
||||
page: item.itemPath,
|
||||
isApp: item.type === 'appFile',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
let resolved = false
|
||||
let prevSortedRoutes: string[] = []
|
||||
|
||||
const serverFields: {
|
||||
actualMiddlewareFile?: string | undefined
|
||||
actualInstrumentationHookFile?: string | undefined
|
||||
appPathRoutes?: Record<string, string | string[]>
|
||||
middleware?:
|
||||
| {
|
||||
page: string
|
||||
match: MiddlewareRouteMatch
|
||||
matchers?: MiddlewareMatcher[]
|
||||
}
|
||||
| undefined
|
||||
hasAppNotFound?: boolean
|
||||
interceptionRoutes?: ReturnType<
|
||||
typeof import('./filesystem').buildCustomRoute
|
||||
>[]
|
||||
} = {}
|
||||
|
||||
async function propagateToWorkers(field: string, args: any) {
|
||||
await renderWorkers.app?.propagateServerField(field, args)
|
||||
await renderWorkers.pages?.propagateServerField(field, args)
|
||||
}
|
||||
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
if (pagesDir) {
|
||||
// Watchpack doesn't emit an event for an empty directory
|
||||
fs.readdir(pagesDir, (_, files) => {
|
||||
if (files?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!resolved) {
|
||||
resolve()
|
||||
resolved = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const pages = pagesDir ? [pagesDir] : []
|
||||
const app = appDir ? [appDir] : []
|
||||
const directories = [...pages, ...app]
|
||||
|
||||
const rootDir = pagesDir || appDir
|
||||
const files = [
|
||||
...getPossibleMiddlewareFilenames(
|
||||
path.join(rootDir!, '..'),
|
||||
nextConfig.pageExtensions
|
||||
),
|
||||
...getPossibleInstrumentationHookFilenames(
|
||||
path.join(rootDir!, '..'),
|
||||
nextConfig.pageExtensions
|
||||
),
|
||||
]
|
||||
let nestedMiddleware: string[] = []
|
||||
|
||||
const envFiles = [
|
||||
'.env.development.local',
|
||||
'.env.local',
|
||||
'.env.development',
|
||||
'.env',
|
||||
].map((file) => path.join(dir, file))
|
||||
|
||||
files.push(...envFiles)
|
||||
|
||||
// tsconfig/jsconfig paths hot-reloading
|
||||
const tsconfigPaths = [
|
||||
path.join(dir, 'tsconfig.json'),
|
||||
path.join(dir, 'jsconfig.json'),
|
||||
]
|
||||
files.push(...tsconfigPaths)
|
||||
|
||||
const wp = new Watchpack({
|
||||
ignored: (pathname: string) => {
|
||||
return (
|
||||
!files.some((file) => file.startsWith(pathname)) &&
|
||||
!directories.some(
|
||||
(d) => pathname.startsWith(d) || d.startsWith(pathname)
|
||||
)
|
||||
)
|
||||
},
|
||||
})
|
||||
const fileWatchTimes = new Map()
|
||||
let enabledTypeScript = usingTypeScript
|
||||
let previousClientRouterFilters: any
|
||||
let previousConflictingPagePaths: Set<string> = new Set()
|
||||
|
||||
wp.on('aggregated', async () => {
|
||||
let middlewareMatchers: MiddlewareMatcher[] | undefined
|
||||
const routedPages: string[] = []
|
||||
const knownFiles = wp.getTimeInfoEntries()
|
||||
const appPaths: Record<string, string[]> = {}
|
||||
const pageNameSet = new Set<string>()
|
||||
const conflictingAppPagePaths = new Set<string>()
|
||||
const appPageFilePaths = new Map<string, string>()
|
||||
const pagesPageFilePaths = new Map<string, string>()
|
||||
|
||||
let envChange = false
|
||||
let tsconfigChange = false
|
||||
let conflictingPageChange = 0
|
||||
let hasRootAppNotFound = false
|
||||
|
||||
const { appFiles, pageFiles } = opts.fsChecker
|
||||
|
||||
appFiles.clear()
|
||||
pageFiles.clear()
|
||||
|
||||
for (const [fileName, meta] of knownFiles) {
|
||||
if (
|
||||
!files.includes(fileName) &&
|
||||
!directories.some((d) => fileName.startsWith(d))
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const watchTime = fileWatchTimes.get(fileName)
|
||||
const watchTimeChange = watchTime && watchTime !== meta?.timestamp
|
||||
fileWatchTimes.set(fileName, meta.timestamp)
|
||||
|
||||
if (envFiles.includes(fileName)) {
|
||||
if (watchTimeChange) {
|
||||
envChange = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (tsconfigPaths.includes(fileName)) {
|
||||
if (fileName.endsWith('tsconfig.json')) {
|
||||
enabledTypeScript = true
|
||||
}
|
||||
if (watchTimeChange) {
|
||||
tsconfigChange = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if (
|
||||
meta?.accuracy === undefined ||
|
||||
!validFileMatcher.isPageFile(fileName)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
const isAppPath = Boolean(
|
||||
appDir &&
|
||||
normalizePathSep(fileName).startsWith(
|
||||
normalizePathSep(appDir) + '/'
|
||||
)
|
||||
)
|
||||
const isPagePath = Boolean(
|
||||
pagesDir &&
|
||||
normalizePathSep(fileName).startsWith(
|
||||
normalizePathSep(pagesDir) + '/'
|
||||
)
|
||||
)
|
||||
|
||||
const rootFile = absolutePathToPage(fileName, {
|
||||
dir: dir,
|
||||
extensions: nextConfig.pageExtensions,
|
||||
keepIndex: false,
|
||||
pagesType: 'root',
|
||||
})
|
||||
|
||||
if (isMiddlewareFile(rootFile)) {
|
||||
const staticInfo = await getStaticInfoIncludingLayouts({
|
||||
pageFilePath: fileName,
|
||||
config: nextConfig,
|
||||
appDir: appDir,
|
||||
page: rootFile,
|
||||
isDev: true,
|
||||
isInsideAppDir: isAppPath,
|
||||
pageExtensions: nextConfig.pageExtensions,
|
||||
})
|
||||
if (nextConfig.output === 'export') {
|
||||
Log.error(
|
||||
'Middleware cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
|
||||
)
|
||||
continue
|
||||
}
|
||||
serverFields.actualMiddlewareFile = rootFile
|
||||
await propagateToWorkers(
|
||||
'actualMiddlewareFile',
|
||||
serverFields.actualMiddlewareFile
|
||||
)
|
||||
middlewareMatchers = staticInfo.middleware?.matchers || [
|
||||
{ regexp: '.*', originalSource: '/:path*' },
|
||||
]
|
||||
continue
|
||||
}
|
||||
if (
|
||||
isInstrumentationHookFile(rootFile) &&
|
||||
nextConfig.experimental.instrumentationHook
|
||||
) {
|
||||
NextBuildContext.hasInstrumentationHook = true
|
||||
serverFields.actualInstrumentationHookFile = rootFile
|
||||
await propagateToWorkers(
|
||||
'actualInstrumentationHookFile',
|
||||
serverFields.actualInstrumentationHookFile
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (fileName.endsWith('.ts') || fileName.endsWith('.tsx')) {
|
||||
enabledTypeScript = true
|
||||
}
|
||||
|
||||
if (!(isAppPath || isPagePath)) {
|
||||
continue
|
||||
}
|
||||
|
||||
let pageName = absolutePathToPage(fileName, {
|
||||
dir: isAppPath ? appDir! : pagesDir!,
|
||||
extensions: nextConfig.pageExtensions,
|
||||
keepIndex: isAppPath,
|
||||
pagesType: isAppPath ? 'app' : 'pages',
|
||||
})
|
||||
|
||||
if (
|
||||
!isAppPath &&
|
||||
pageName.startsWith('/api/') &&
|
||||
nextConfig.output === 'export'
|
||||
) {
|
||||
Log.error(
|
||||
'API Routes cannot be used with "output: export". See more info here: https://nextjs.org/docs/advanced-features/static-html-export'
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
if (isAppPath) {
|
||||
const isRootNotFound = validFileMatcher.isRootNotFound(fileName)
|
||||
|
||||
if (isRootNotFound) {
|
||||
hasRootAppNotFound = true
|
||||
continue
|
||||
}
|
||||
if (!isRootNotFound && !validFileMatcher.isAppRouterPage(fileName)) {
|
||||
continue
|
||||
}
|
||||
// Ignore files/directories starting with `_` in the app directory
|
||||
if (normalizePathSep(pageName).includes('/_')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const originalPageName = pageName
|
||||
pageName = normalizeAppPath(pageName).replace(/%5F/g, '_')
|
||||
if (!appPaths[pageName]) {
|
||||
appPaths[pageName] = []
|
||||
}
|
||||
appPaths[pageName].push(originalPageName)
|
||||
|
||||
if (useFileSystemPublicRoutes) {
|
||||
appFiles.add(pageName)
|
||||
}
|
||||
|
||||
if (routedPages.includes(pageName)) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if (useFileSystemPublicRoutes) {
|
||||
pageFiles.add(pageName)
|
||||
// always add to nextDataRoutes for now but in future only add
|
||||
// entries that actually use getStaticProps/getServerSideProps
|
||||
opts.fsChecker.nextDataRoutes.add(pageName)
|
||||
}
|
||||
}
|
||||
;(isAppPath ? appPageFilePaths : pagesPageFilePaths).set(
|
||||
pageName,
|
||||
fileName
|
||||
)
|
||||
|
||||
if (appDir && pageNameSet.has(pageName)) {
|
||||
conflictingAppPagePaths.add(pageName)
|
||||
} else {
|
||||
pageNameSet.add(pageName)
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is a middleware that is not declared in the root we will
|
||||
* warn without adding it so it doesn't make its way into the system.
|
||||
*/
|
||||
if (/[\\\\/]_middleware$/.test(pageName)) {
|
||||
nestedMiddleware.push(pageName)
|
||||
continue
|
||||
}
|
||||
|
||||
routedPages.push(pageName)
|
||||
}
|
||||
|
||||
const numConflicting = conflictingAppPagePaths.size
|
||||
conflictingPageChange = numConflicting - previousConflictingPagePaths.size
|
||||
|
||||
if (conflictingPageChange !== 0) {
|
||||
if (numConflicting > 0) {
|
||||
let errorMessage = `Conflicting app and page file${
|
||||
numConflicting === 1 ? ' was' : 's were'
|
||||
} found, please remove the conflicting files to continue:\n`
|
||||
|
||||
for (const p of conflictingAppPagePaths) {
|
||||
const appPath = path.relative(dir, appPageFilePaths.get(p)!)
|
||||
const pagesPath = path.relative(dir, pagesPageFilePaths.get(p)!)
|
||||
errorMessage += ` "${pagesPath}" - "${appPath}"\n`
|
||||
}
|
||||
hotReloader.setHmrServerError(new Error(errorMessage))
|
||||
} else if (numConflicting === 0) {
|
||||
hotReloader.clearHmrServerError()
|
||||
await propagateToWorkers('matchers.reload', undefined)
|
||||
}
|
||||
}
|
||||
|
||||
previousConflictingPagePaths = conflictingAppPagePaths
|
||||
|
||||
let clientRouterFilters: any
|
||||
if (nextConfig.experimental.clientRouterFilter) {
|
||||
clientRouterFilters = createClientRouterFilter(
|
||||
Object.keys(appPaths),
|
||||
nextConfig.experimental.clientRouterFilterRedirects
|
||||
? ((nextConfig as any)._originalRedirects || []).filter(
|
||||
(r: any) => !r.internal
|
||||
)
|
||||
: [],
|
||||
nextConfig.experimental.clientRouterFilterAllowedRate
|
||||
)
|
||||
|
||||
if (
|
||||
!previousClientRouterFilters ||
|
||||
JSON.stringify(previousClientRouterFilters) !==
|
||||
JSON.stringify(clientRouterFilters)
|
||||
) {
|
||||
envChange = true
|
||||
previousClientRouterFilters = clientRouterFilters
|
||||
}
|
||||
}
|
||||
|
||||
if (!usingTypeScript && enabledTypeScript) {
|
||||
// we tolerate the error here as this is best effort
|
||||
// and the manual install command will be shown
|
||||
await verifyTypeScript(opts)
|
||||
.then(() => {
|
||||
tsconfigChange = true
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (envChange || tsconfigChange) {
|
||||
if (envChange) {
|
||||
loadEnvConfig(dir, true, Log, true)
|
||||
await propagateToWorkers('loadEnvConfig', [
|
||||
{ dev: true, forceReload: true, silent: true },
|
||||
])
|
||||
}
|
||||
let tsconfigResult:
|
||||
| UnwrapPromise<ReturnType<typeof loadJsConfig>>
|
||||
| undefined
|
||||
|
||||
if (tsconfigChange) {
|
||||
try {
|
||||
tsconfigResult = await loadJsConfig(dir, nextConfig)
|
||||
} catch (_) {
|
||||
/* do we want to log if there are syntax errors in tsconfig while editing? */
|
||||
}
|
||||
}
|
||||
|
||||
hotReloader.activeConfigs?.forEach((config, idx) => {
|
||||
const isClient = idx === 0
|
||||
const isNodeServer = idx === 1
|
||||
const isEdgeServer = idx === 2
|
||||
const hasRewrites =
|
||||
opts.fsChecker.rewrites.afterFiles.length > 0 ||
|
||||
opts.fsChecker.rewrites.beforeFiles.length > 0 ||
|
||||
opts.fsChecker.rewrites.fallback.length > 0
|
||||
|
||||
if (tsconfigChange) {
|
||||
config.resolve?.plugins?.forEach((plugin: any) => {
|
||||
// look for the JsConfigPathsPlugin and update with
|
||||
// the latest paths/baseUrl config
|
||||
if (plugin && plugin.jsConfigPlugin && tsconfigResult) {
|
||||
const { resolvedBaseUrl, jsConfig } = tsconfigResult
|
||||
const currentResolvedBaseUrl = plugin.resolvedBaseUrl
|
||||
const resolvedUrlIndex = config.resolve?.modules?.findIndex(
|
||||
(item) => item === currentResolvedBaseUrl
|
||||
)
|
||||
|
||||
if (
|
||||
resolvedBaseUrl &&
|
||||
resolvedBaseUrl !== currentResolvedBaseUrl
|
||||
) {
|
||||
// remove old baseUrl and add new one
|
||||
if (resolvedUrlIndex && resolvedUrlIndex > -1) {
|
||||
config.resolve?.modules?.splice(resolvedUrlIndex, 1)
|
||||
}
|
||||
config.resolve?.modules?.push(resolvedBaseUrl)
|
||||
}
|
||||
|
||||
if (jsConfig?.compilerOptions?.paths && resolvedBaseUrl) {
|
||||
Object.keys(plugin.paths).forEach((key) => {
|
||||
delete plugin.paths[key]
|
||||
})
|
||||
Object.assign(plugin.paths, jsConfig.compilerOptions.paths)
|
||||
plugin.resolvedBaseUrl = resolvedBaseUrl
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (envChange) {
|
||||
config.plugins?.forEach((plugin: any) => {
|
||||
// we look for the DefinePlugin definitions so we can
|
||||
// update them on the active compilers
|
||||
if (
|
||||
plugin &&
|
||||
typeof plugin.definitions === 'object' &&
|
||||
plugin.definitions.__NEXT_DEFINE_ENV
|
||||
) {
|
||||
const newDefine = getDefineEnv({
|
||||
dev: true,
|
||||
config: nextConfig,
|
||||
distDir,
|
||||
isClient,
|
||||
hasRewrites,
|
||||
isNodeServer,
|
||||
isEdgeServer,
|
||||
clientRouterFilters,
|
||||
})
|
||||
|
||||
Object.keys(plugin.definitions).forEach((key) => {
|
||||
if (!(key in newDefine)) {
|
||||
delete plugin.definitions[key]
|
||||
}
|
||||
})
|
||||
Object.assign(plugin.definitions, newDefine)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
hotReloader.invalidate({
|
||||
reloadAfterInvalidation: envChange,
|
||||
})
|
||||
}
|
||||
|
||||
if (nestedMiddleware.length > 0) {
|
||||
Log.error(
|
||||
new NestedMiddlewareError(
|
||||
nestedMiddleware,
|
||||
dir,
|
||||
(pagesDir || appDir)!
|
||||
).message
|
||||
)
|
||||
nestedMiddleware = []
|
||||
}
|
||||
|
||||
// Make sure to sort parallel routes to make the result deterministic.
|
||||
serverFields.appPathRoutes = Object.fromEntries(
|
||||
Object.entries(appPaths).map(([k, v]) => [k, v.sort()])
|
||||
)
|
||||
await propagateToWorkers('appPathRoutes', serverFields.appPathRoutes)
|
||||
|
||||
// TODO: pass this to fsChecker/next-dev-server?
|
||||
serverFields.middleware = middlewareMatchers
|
||||
? {
|
||||
match: null as any,
|
||||
page: '/',
|
||||
matchers: middlewareMatchers,
|
||||
}
|
||||
: undefined
|
||||
|
||||
await propagateToWorkers('middleware', serverFields.middleware)
|
||||
serverFields.hasAppNotFound = hasRootAppNotFound
|
||||
|
||||
opts.fsChecker.middlewareMatcher = serverFields.middleware?.matchers
|
||||
? getMiddlewareRouteMatcher(serverFields.middleware?.matchers)
|
||||
: undefined
|
||||
|
||||
opts.fsChecker.interceptionRoutes =
|
||||
generateInterceptionRoutesRewrites(Object.keys(appPaths))?.map((item) =>
|
||||
buildCustomRoute(
|
||||
'before_files_rewrite',
|
||||
item,
|
||||
opts.nextConfig.basePath,
|
||||
opts.nextConfig.experimental.caseSensitiveRoutes
|
||||
)
|
||||
) || []
|
||||
|
||||
const exportPathMap =
|
||||
(await nextConfig.exportPathMap?.(
|
||||
{},
|
||||
{
|
||||
dev: true,
|
||||
dir: opts.dir,
|
||||
outDir: null,
|
||||
distDir: distDir,
|
||||
buildId: 'development',
|
||||
}
|
||||
)) || {}
|
||||
|
||||
for (const [key, value] of Object.entries(exportPathMap)) {
|
||||
opts.fsChecker.interceptionRoutes.push(
|
||||
buildCustomRoute(
|
||||
'before_files_rewrite',
|
||||
{
|
||||
source: key,
|
||||
destination: `${value.page}${
|
||||
value.query ? '?' : ''
|
||||
}${qs.stringify(value.query)}`,
|
||||
},
|
||||
opts.nextConfig.basePath,
|
||||
opts.nextConfig.experimental.caseSensitiveRoutes
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
// we serve a separate manifest with all pages for the client in
|
||||
// dev mode so that we can match a page after a rewrite on the client
|
||||
// before it has been built and is populated in the _buildManifest
|
||||
const sortedRoutes = getSortedRoutes(routedPages)
|
||||
|
||||
opts.fsChecker.dynamicRoutes = sortedRoutes
|
||||
.map((page) => {
|
||||
const regex = getRouteRegex(page)
|
||||
return {
|
||||
match: getRouteMatcher(regex),
|
||||
page,
|
||||
re: regex.re,
|
||||
groups: regex.groups,
|
||||
}
|
||||
})
|
||||
.filter(Boolean) as any
|
||||
|
||||
for (const page of sortedRoutes) {
|
||||
const route = buildDataRoute(page, 'development')
|
||||
const routeRegex = getRouteRegex(route.page)
|
||||
opts.fsChecker.dynamicRoutes.push({
|
||||
...route,
|
||||
regex: routeRegex.re.toString(),
|
||||
match: getRouteMatcher({
|
||||
// TODO: fix this in the manifest itself, must also be fixed in
|
||||
// upstream builder that relies on this
|
||||
re: opts.nextConfig.i18n
|
||||
? new RegExp(
|
||||
route.dataRouteRegex.replace(
|
||||
`/development/`,
|
||||
`/development/(?<nextLocale>.+?)/`
|
||||
)
|
||||
)
|
||||
: new RegExp(route.dataRouteRegex),
|
||||
groups: routeRegex.groups,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
if (!prevSortedRoutes?.every((val, idx) => val === sortedRoutes[idx])) {
|
||||
// emit the change so clients fetch the update
|
||||
hotReloader.send('devPagesManifestUpdate', {
|
||||
devPagesManifest: true,
|
||||
})
|
||||
}
|
||||
prevSortedRoutes = sortedRoutes
|
||||
|
||||
if (!resolved) {
|
||||
resolve()
|
||||
resolved = true
|
||||
}
|
||||
} catch (e) {
|
||||
if (!resolved) {
|
||||
reject(e)
|
||||
resolved = true
|
||||
} else {
|
||||
Log.warn('Failed to reload dynamic routes:', e)
|
||||
}
|
||||
} finally {
|
||||
// Reload the matchers. The filesystem would have been written to,
|
||||
// and the matchers need to re-scan it to update the router.
|
||||
await propagateToWorkers('middleware.reload', undefined)
|
||||
}
|
||||
})
|
||||
|
||||
wp.watch({ directories: [dir], startTime: 0 })
|
||||
})
|
||||
|
||||
const clientPagesManifestPath = `/_next/${CLIENT_STATIC_FILES_PATH}/development/${DEV_CLIENT_PAGES_MANIFEST}`
|
||||
opts.fsChecker.devVirtualFsItems.add(clientPagesManifestPath)
|
||||
|
||||
const devMiddlewareManifestPath = `/_next/${CLIENT_STATIC_FILES_PATH}/development/${DEV_MIDDLEWARE_MANIFEST}`
|
||||
opts.fsChecker.devVirtualFsItems.add(devMiddlewareManifestPath)
|
||||
|
||||
async function requestHandler(req: IncomingMessage, res: ServerResponse) {
|
||||
const parsedUrl = url.parse(req.url || '/')
|
||||
|
||||
if (parsedUrl.pathname === clientPagesManifestPath) {
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
res.end(
|
||||
JSON.stringify({
|
||||
pages: prevSortedRoutes.filter(
|
||||
(route) => !opts.fsChecker.appFiles.has(route)
|
||||
),
|
||||
})
|
||||
)
|
||||
return { finished: true }
|
||||
}
|
||||
|
||||
if (parsedUrl.pathname === devMiddlewareManifestPath) {
|
||||
res.statusCode = 200
|
||||
res.setHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
res.end(JSON.stringify(serverFields.middleware?.matchers || []))
|
||||
return { finished: true }
|
||||
}
|
||||
return { finished: false }
|
||||
}
|
||||
|
||||
async function logErrorWithOriginalStack(
|
||||
err: unknown,
|
||||
type?: 'unhandledRejection' | 'uncaughtException' | 'warning' | 'app-dir'
|
||||
) {
|
||||
let usedOriginalStack = false
|
||||
|
||||
if (isError(err) && err.stack) {
|
||||
try {
|
||||
const frames = parseStack(err.stack!)
|
||||
// Filter out internal edge related runtime stack
|
||||
const frame = frames.find(
|
||||
({ file }) =>
|
||||
!file?.startsWith('eval') &&
|
||||
!file?.includes('web/adapter') &&
|
||||
!file?.includes('web/globals') &&
|
||||
!file?.includes('sandbox/context') &&
|
||||
!file?.includes('<anonymous>')
|
||||
)
|
||||
|
||||
if (frame?.lineNumber && frame?.file) {
|
||||
const moduleId = frame.file!.replace(
|
||||
/^(webpack-internal:\/\/\/|file:\/\/)/,
|
||||
''
|
||||
)
|
||||
const modulePath = frame.file.replace(
|
||||
/^(webpack-internal:\/\/\/|file:\/\/)(\(.*\)\/)?/,
|
||||
''
|
||||
)
|
||||
|
||||
const src = getErrorSource(err as Error)
|
||||
const isEdgeCompiler = src === COMPILER_NAMES.edgeServer
|
||||
const compilation = (
|
||||
isEdgeCompiler
|
||||
? hotReloader.edgeServerStats?.compilation
|
||||
: hotReloader.serverStats?.compilation
|
||||
)!
|
||||
|
||||
const source = await getSourceById(
|
||||
!!frame.file?.startsWith(path.sep) ||
|
||||
!!frame.file?.startsWith('file:'),
|
||||
moduleId,
|
||||
compilation
|
||||
)
|
||||
|
||||
const originalFrame = await createOriginalStackFrame({
|
||||
line: frame.lineNumber,
|
||||
column: frame.column,
|
||||
source,
|
||||
frame,
|
||||
moduleId,
|
||||
modulePath,
|
||||
rootDirectory: opts.dir,
|
||||
errorMessage: err.message,
|
||||
serverCompilation: isEdgeCompiler
|
||||
? undefined
|
||||
: hotReloader.serverStats?.compilation,
|
||||
edgeCompilation: isEdgeCompiler
|
||||
? hotReloader.edgeServerStats?.compilation
|
||||
: undefined,
|
||||
}).catch(() => {})
|
||||
|
||||
if (originalFrame) {
|
||||
const { originalCodeFrame, originalStackFrame } = originalFrame
|
||||
const { file, lineNumber, column, methodName } = originalStackFrame
|
||||
|
||||
Log[type === 'warning' ? 'warn' : 'error'](
|
||||
`${file} (${lineNumber}:${column}) @ ${methodName}`
|
||||
)
|
||||
if (isEdgeCompiler) {
|
||||
err = err.message
|
||||
}
|
||||
if (type === 'warning') {
|
||||
Log.warn(err)
|
||||
} else if (type === 'app-dir') {
|
||||
logAppDirError(err)
|
||||
} else if (type) {
|
||||
Log.error(`${type}:`, err)
|
||||
} else {
|
||||
Log.error(err)
|
||||
}
|
||||
console[type === 'warning' ? 'warn' : 'error'](originalCodeFrame)
|
||||
usedOriginalStack = true
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// failed to load original stack using source maps
|
||||
// this un-actionable by users so we don't show the
|
||||
// internal error and only show the provided stack
|
||||
}
|
||||
}
|
||||
|
||||
if (!usedOriginalStack) {
|
||||
if (type === 'warning') {
|
||||
Log.warn(err)
|
||||
} else if (type === 'app-dir') {
|
||||
logAppDirError(err)
|
||||
} else if (type) {
|
||||
Log.error(`${type}:`, err)
|
||||
} else {
|
||||
Log.error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
serverFields,
|
||||
|
||||
hotReloader,
|
||||
renderWorkers,
|
||||
requestHandler,
|
||||
logErrorWithOriginalStack,
|
||||
|
||||
async ensureMiddleware() {
|
||||
if (!serverFields.actualMiddlewareFile) return
|
||||
return hotReloader.ensurePage({
|
||||
page: serverFields.actualMiddlewareFile,
|
||||
clientOnly: false,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupDev(opts: SetupOpts) {
|
||||
const isSrcDir = path
|
||||
.relative(opts.dir, opts.pagesDir || opts.appDir || '')
|
||||
.startsWith('src')
|
||||
|
||||
const result = await startWatcher(opts)
|
||||
|
||||
opts.telemetry.record(
|
||||
eventCliSession(
|
||||
path.join(opts.dir, opts.nextConfig.distDir),
|
||||
opts.nextConfig,
|
||||
{
|
||||
webpackVersion: 5,
|
||||
isSrcDir,
|
||||
turboFlag: false,
|
||||
cliCommand: 'dev',
|
||||
appDir: !!opts.appDir,
|
||||
pagesDir: !!opts.pagesDir,
|
||||
isCustomServer: !!opts.isCustomServer,
|
||||
hasNowJson: !!(await findUp('now.json', { cwd: opts.dir })),
|
||||
}
|
||||
)
|
||||
)
|
||||
return result
|
||||
}
|
|
@ -88,13 +88,14 @@ export const createWorker = async (
|
|||
nextConfig: NextConfigComplete
|
||||
) => {
|
||||
const { initialEnv } = require('@next/env') as typeof import('@next/env')
|
||||
const { Worker } = require('next/dist/compiled/jest-worker')
|
||||
const useServerActions = !!nextConfig.experimental.serverActions
|
||||
const { Worker } =
|
||||
require('next/dist/compiled/jest-worker') as typeof import('next/dist/compiled/jest-worker')
|
||||
|
||||
const worker = new Worker(require.resolve('../render-server'), {
|
||||
numWorkers: 1,
|
||||
// TODO: do we want to allow more than 10 OOM restarts?
|
||||
maxRetries: 10,
|
||||
// TODO: do we want to allow more than 8 OOM restarts?
|
||||
maxRetries: 8,
|
||||
forkOptions: {
|
||||
env: {
|
||||
FORCE_COLOR: '1',
|
||||
|
@ -129,11 +130,14 @@ export const createWorker = async (
|
|||
'deleteCache',
|
||||
'deleteAppClientCache',
|
||||
'clearModuleContext',
|
||||
'propagateServerField',
|
||||
],
|
||||
}) as any as InstanceType<typeof Worker> & {
|
||||
initialize: typeof import('../render-server').initialize
|
||||
deleteCache: typeof import('../render-server').deleteCache
|
||||
deleteAppClientCache: typeof import('../render-server').deleteAppClientCache
|
||||
clearModuleContext: typeof import('../render-server').clearModuleContext
|
||||
propagateServerField: typeof import('../render-server').propagateServerField
|
||||
}
|
||||
|
||||
worker.getStderr().pipe(process.stderr)
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import { IncomingMessage } from 'http'
|
||||
import '../../node-polyfill-fetch'
|
||||
|
||||
import type { IncomingMessage } from 'http'
|
||||
import type { Writable, Readable } from 'stream'
|
||||
import { filterReqHeaders } from './utils'
|
||||
|
||||
export const invokeRequest = async (
|
||||
|
@ -7,51 +10,62 @@ export const invokeRequest = async (
|
|||
headers: IncomingMessage['headers']
|
||||
method: IncomingMessage['method']
|
||||
},
|
||||
readableBody?: import('stream').Readable
|
||||
readableBody?: Readable | ReadableStream
|
||||
) => {
|
||||
// force to 127.0.0.1 as IPC always runs on this hostname
|
||||
// to avoid localhost issues
|
||||
const parsedTargetUrl = new URL(targetUrl)
|
||||
parsedTargetUrl.hostname = '127.0.0.1'
|
||||
|
||||
const invokeHeaders = filterReqHeaders({
|
||||
'cache-control': '',
|
||||
...requestInit.headers,
|
||||
}) as IncomingMessage['headers']
|
||||
|
||||
const invokeRes = await new Promise<IncomingMessage>(
|
||||
(resolveInvoke, rejectInvoke) => {
|
||||
const http = require('http') as typeof import('http')
|
||||
const invokeRes = await fetch(parsedTargetUrl.toString(), {
|
||||
headers: invokeHeaders as any as Headers,
|
||||
method: requestInit.method,
|
||||
redirect: 'manual',
|
||||
|
||||
try {
|
||||
// force to 127.0.0.1 as IPC always runs on this hostname
|
||||
// to avoid localhost issues
|
||||
const parsedTargetUrl = new URL(targetUrl)
|
||||
parsedTargetUrl.hostname = '127.0.0.1'
|
||||
|
||||
const invokeReq = http.request(
|
||||
parsedTargetUrl.toString(),
|
||||
{
|
||||
headers: invokeHeaders,
|
||||
method: requestInit.method,
|
||||
},
|
||||
(res) => {
|
||||
resolveInvoke(res)
|
||||
}
|
||||
)
|
||||
invokeReq.on('error', (err) => {
|
||||
rejectInvoke(err)
|
||||
})
|
||||
|
||||
if (requestInit.method !== 'GET' && requestInit.method !== 'HEAD') {
|
||||
if (readableBody) {
|
||||
readableBody.pipe(invokeReq)
|
||||
readableBody.on('close', () => {
|
||||
invokeReq.end()
|
||||
})
|
||||
}
|
||||
} else {
|
||||
invokeReq.end()
|
||||
...(requestInit.method !== 'GET' &&
|
||||
requestInit.method !== 'HEAD' &&
|
||||
readableBody
|
||||
? {
|
||||
body: readableBody as BodyInit,
|
||||
duplex: 'half',
|
||||
}
|
||||
} catch (err) {
|
||||
rejectInvoke(err)
|
||||
}
|
||||
}
|
||||
)
|
||||
: {}),
|
||||
|
||||
next: {
|
||||
// @ts-ignore
|
||||
internal: true,
|
||||
},
|
||||
})
|
||||
|
||||
return invokeRes
|
||||
}
|
||||
|
||||
export async function pipeReadable(
|
||||
readable: ReadableStream,
|
||||
writable: Writable
|
||||
) {
|
||||
const reader = readable.getReader()
|
||||
|
||||
async function doRead() {
|
||||
const item = await reader.read()
|
||||
|
||||
if (item?.value) {
|
||||
writable.write(Buffer.from(item?.value))
|
||||
|
||||
if ('flush' in writable) {
|
||||
;(writable as any).flush()
|
||||
}
|
||||
}
|
||||
|
||||
if (!item?.done) {
|
||||
return doRead()
|
||||
}
|
||||
}
|
||||
await doRead()
|
||||
writable.end()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export const forbiddenHeaders = [
|
||||
'accept-encoding',
|
||||
'keepalive',
|
||||
'keep-alive',
|
||||
'content-encoding',
|
||||
'transfer-encoding',
|
||||
// https://github.com/nodejs/undici/issues/1470
|
||||
|
@ -8,7 +9,7 @@ export const forbiddenHeaders = [
|
|||
]
|
||||
|
||||
export const filterReqHeaders = (
|
||||
headers: Record<string, undefined | string | string[]>
|
||||
headers: Record<string, undefined | string | number | string[]>
|
||||
) => {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (
|
||||
|
@ -18,5 +19,5 @@ export const filterReqHeaders = (
|
|||
delete headers[key]
|
||||
}
|
||||
}
|
||||
return headers
|
||||
return headers as Record<string, undefined | string | string[]>
|
||||
}
|
||||
|
|
105
packages/next/src/server/lib/setup-server-worker.ts
Normal file
105
packages/next/src/server/lib/setup-server-worker.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import './cpu-profile'
|
||||
import v8 from 'v8'
|
||||
import http, { IncomingMessage, ServerResponse } from 'http'
|
||||
|
||||
// This is required before other imports to ensure the require hook is setup.
|
||||
import '../require-hook'
|
||||
import '../node-polyfill-fetch'
|
||||
|
||||
import { warn } from '../../build/output/log'
|
||||
import { Duplex } from 'stream'
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error(err)
|
||||
})
|
||||
|
||||
export const WORKER_SELF_EXIT_CODE = 77
|
||||
|
||||
const MAXIMUM_HEAP_SIZE_ALLOWED =
|
||||
(v8.getHeapStatistics().heap_size_limit / 1024 / 1024) * 0.9
|
||||
|
||||
export async function initializeServerWorker(
|
||||
requestHandler: (req: IncomingMessage, res: ServerResponse) => Promise<any>,
|
||||
upgradeHandler: (req: IncomingMessage, socket: Duplex, head: Buffer) => any,
|
||||
opts: {
|
||||
dir: string
|
||||
port: number
|
||||
dev: boolean
|
||||
minimalMode?: boolean
|
||||
hostname?: string
|
||||
workerType: 'router' | 'render'
|
||||
isNodeDebugging: boolean
|
||||
keepAliveTimeout?: number
|
||||
}
|
||||
): Promise<{
|
||||
port: number
|
||||
hostname: string
|
||||
server: http.Server
|
||||
}> {
|
||||
const server = http.createServer((req, res) => {
|
||||
return requestHandler(req, res)
|
||||
.catch((err) => {
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
console.error(err)
|
||||
})
|
||||
.finally(() => {
|
||||
if (
|
||||
process.memoryUsage().heapUsed / 1024 / 1024 >
|
||||
MAXIMUM_HEAP_SIZE_ALLOWED
|
||||
) {
|
||||
warn(
|
||||
'The server is running out of memory, restarting to free up memory.'
|
||||
)
|
||||
server.close()
|
||||
process.exit(WORKER_SELF_EXIT_CODE)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (opts.keepAliveTimeout) {
|
||||
server.keepAliveTimeout = opts.keepAliveTimeout
|
||||
}
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
server.on('error', (err: NodeJS.ErrnoException) => {
|
||||
console.error(`Invariant: failed to start server worker`, err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
if (upgradeHandler) {
|
||||
server.on('upgrade', (req, socket, upgrade) => {
|
||||
upgradeHandler(req, socket, upgrade)
|
||||
})
|
||||
}
|
||||
const hostname =
|
||||
!opts.hostname || opts.hostname === 'localhost'
|
||||
? '0.0.0.0'
|
||||
: opts.hostname
|
||||
|
||||
server.on('listening', async () => {
|
||||
try {
|
||||
const addr = server.address()
|
||||
const port = addr && typeof addr === 'object' ? addr.port : 0
|
||||
|
||||
if (!port) {
|
||||
console.error(`Invariant failed to detect render worker port`, addr)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
resolve({
|
||||
server,
|
||||
port,
|
||||
hostname,
|
||||
})
|
||||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
})
|
||||
server.listen(0, hostname)
|
||||
})
|
||||
}
|
|
@ -1,51 +1,49 @@
|
|||
import type { Duplex } from 'stream'
|
||||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import type { ChildProcess } from 'child_process'
|
||||
import type { NextConfigComplete } from '../config-shared'
|
||||
|
||||
import http from 'http'
|
||||
import { isIPv6 } from 'net'
|
||||
import * as Log from '../../build/output/log'
|
||||
import { normalizeRepeatedSlashes } from '../../shared/lib/utils'
|
||||
import { initialEnv } from '@next/env'
|
||||
import * as Log from '../../build/output/log'
|
||||
import setupDebug from 'next/dist/compiled/debug'
|
||||
import { splitCookiesString, toNodeOutgoingHttpHeaders } from '../web/utils'
|
||||
import { getCloneableBody } from '../body-streams'
|
||||
import { filterReqHeaders } from './server-ipc/utils'
|
||||
import setupCompression from 'next/dist/compiled/compression'
|
||||
import { normalizeRepeatedSlashes } from '../../shared/lib/utils'
|
||||
import { invokeRequest, pipeReadable } from './server-ipc/invoke-request'
|
||||
import {
|
||||
genRouterWorkerExecArgv,
|
||||
getDebugPort,
|
||||
getNodeOptionsWithoutInspect,
|
||||
} from './utils'
|
||||
|
||||
const debug = setupDebug('next:start-server')
|
||||
|
||||
export interface StartServerOptions {
|
||||
dir: string
|
||||
prevDir?: string
|
||||
port: number
|
||||
logReady?: boolean
|
||||
isDev: boolean
|
||||
hostname: string
|
||||
useWorkers: boolean
|
||||
allowRetry?: boolean
|
||||
isTurbopack?: boolean
|
||||
customServer?: boolean
|
||||
isExperimentalTurbo?: boolean
|
||||
minimalMode?: boolean
|
||||
keepAliveTimeout?: number
|
||||
onStdout?: (data: any) => void
|
||||
onStderr?: (data: any) => void
|
||||
nextConfig: NextConfigComplete
|
||||
}
|
||||
|
||||
type TeardownServer = () => Promise<void>
|
||||
|
||||
export async function startServer({
|
||||
dir,
|
||||
prevDir,
|
||||
port,
|
||||
isDev,
|
||||
hostname,
|
||||
useWorkers,
|
||||
allowRetry,
|
||||
keepAliveTimeout,
|
||||
onStdout,
|
||||
onStderr,
|
||||
}: StartServerOptions): Promise<TeardownServer> {
|
||||
const sockets = new Set<ServerResponse | Duplex>()
|
||||
let worker: import('next/dist/compiled/jest-worker').Worker | undefined
|
||||
let handlersReady = () => {}
|
||||
let handlersError = () => {}
|
||||
|
||||
export const checkIsNodeDebugging = () => {
|
||||
let isNodeDebugging: 'brk' | boolean = !!(
|
||||
process.execArgv.some((localArg) => localArg.startsWith('--inspect')) ||
|
||||
process.env.NODE_OPTIONS?.match?.(/--inspect(=\S+)?( |$)/)
|
||||
|
@ -57,6 +55,61 @@ export async function startServer({
|
|||
) {
|
||||
isNodeDebugging = 'brk'
|
||||
}
|
||||
return isNodeDebugging
|
||||
}
|
||||
|
||||
export const createRouterWorker = async (
|
||||
routerServerPath: string,
|
||||
isNodeDebugging: boolean | 'brk',
|
||||
jestWorkerPath = require.resolve('next/dist/compiled/jest-worker')
|
||||
) => {
|
||||
const { Worker } =
|
||||
require(jestWorkerPath) as typeof import('next/dist/compiled/jest-worker')
|
||||
|
||||
return new Worker(routerServerPath, {
|
||||
numWorkers: 1,
|
||||
// TODO: do we want to allow more than 8 OOM restarts?
|
||||
maxRetries: 8,
|
||||
forkOptions: {
|
||||
execArgv: await genRouterWorkerExecArgv(
|
||||
isNodeDebugging === undefined ? false : isNodeDebugging
|
||||
),
|
||||
env: {
|
||||
FORCE_COLOR: '1',
|
||||
...((initialEnv || process.env) as typeof process.env),
|
||||
NODE_OPTIONS: getNodeOptionsWithoutInspect(),
|
||||
...(process.env.NEXT_CPU_PROF
|
||||
? { __NEXT_PRIVATE_CPU_PROFILE: `CPU.router` }
|
||||
: {}),
|
||||
WATCHPACK_WATCHER_LIMIT: '20',
|
||||
},
|
||||
},
|
||||
exposedMethods: ['initialize'],
|
||||
}) as any as InstanceType<typeof Worker> & {
|
||||
initialize: typeof import('./render-server').initialize
|
||||
}
|
||||
}
|
||||
|
||||
export async function startServer({
|
||||
dir,
|
||||
nextConfig,
|
||||
prevDir,
|
||||
port,
|
||||
isDev,
|
||||
hostname,
|
||||
useWorkers,
|
||||
minimalMode,
|
||||
allowRetry,
|
||||
keepAliveTimeout,
|
||||
onStdout,
|
||||
onStderr,
|
||||
logReady = true,
|
||||
}: StartServerOptions): Promise<TeardownServer> {
|
||||
const sockets = new Set<ServerResponse | Duplex>()
|
||||
let worker: import('next/dist/compiled/jest-worker').Worker | undefined
|
||||
let routerPort: number | undefined
|
||||
let handlersReady = () => {}
|
||||
let handlersError = () => {}
|
||||
|
||||
let handlersPromise: Promise<void> | undefined = new Promise<void>(
|
||||
(resolve, reject) => {
|
||||
|
@ -141,6 +194,7 @@ export async function startServer({
|
|||
})
|
||||
|
||||
let targetHost = hostname
|
||||
const isNodeDebugging = checkIsNodeDebugging()
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.on('listening', () => {
|
||||
|
@ -168,14 +222,18 @@ export async function startServer({
|
|||
)
|
||||
}
|
||||
|
||||
Log.ready(
|
||||
`started server on ${normalizedHostname}${
|
||||
(port + '').startsWith(':') ? '' : ':'
|
||||
}${port}, url: ${appUrl}`
|
||||
)
|
||||
if (logReady) {
|
||||
Log.ready(
|
||||
`started server on ${normalizedHostname}${
|
||||
(port + '').startsWith(':') ? '' : ':'
|
||||
}${port}, url: ${appUrl}`
|
||||
)
|
||||
// expose the main port to render workers
|
||||
process.env.PORT = port + ''
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
server.listen(port, hostname)
|
||||
server.listen(port, hostname === 'localhost' ? '0.0.0.0' : hostname)
|
||||
})
|
||||
|
||||
try {
|
||||
|
@ -183,40 +241,35 @@ export async function startServer({
|
|||
const httpProxy =
|
||||
require('next/dist/compiled/http-proxy') as typeof import('next/dist/compiled/http-proxy')
|
||||
|
||||
let renderServerPath = require.resolve('./render-server')
|
||||
let routerServerPath = require.resolve('./router-server')
|
||||
let jestWorkerPath = require.resolve('next/dist/compiled/jest-worker')
|
||||
|
||||
if (prevDir) {
|
||||
jestWorkerPath = jestWorkerPath.replace(prevDir, dir)
|
||||
renderServerPath = renderServerPath.replace(prevDir, dir)
|
||||
routerServerPath = routerServerPath.replace(prevDir, dir)
|
||||
}
|
||||
|
||||
const { Worker } =
|
||||
require(jestWorkerPath) as typeof import('next/dist/compiled/jest-worker')
|
||||
const routerWorker = await createRouterWorker(
|
||||
routerServerPath,
|
||||
isNodeDebugging,
|
||||
jestWorkerPath
|
||||
)
|
||||
const cleanup = () => {
|
||||
debug('start-server process cleanup')
|
||||
|
||||
const routerWorker = new Worker(renderServerPath, {
|
||||
numWorkers: 1,
|
||||
// TODO: do we want to allow more than 10 OOM restarts?
|
||||
maxRetries: 10,
|
||||
forkOptions: {
|
||||
execArgv: await genRouterWorkerExecArgv(
|
||||
isNodeDebugging === undefined ? false : isNodeDebugging
|
||||
),
|
||||
env: {
|
||||
FORCE_COLOR: '1',
|
||||
...((initialEnv || process.env) as typeof process.env),
|
||||
PORT: port + '',
|
||||
NODE_OPTIONS: getNodeOptionsWithoutInspect(),
|
||||
...(process.env.NEXT_CPU_PROF
|
||||
? { __NEXT_PRIVATE_CPU_PROFILE: `CPU.router` }
|
||||
: {}),
|
||||
WATCHPACK_WATCHER_LIMIT: '20',
|
||||
},
|
||||
},
|
||||
exposedMethods: ['initialize'],
|
||||
}) as any as InstanceType<typeof Worker> & {
|
||||
initialize: typeof import('./render-server').initialize
|
||||
for (const curWorker of ((routerWorker as any)._workerPool?._workers ||
|
||||
[]) as {
|
||||
_child?: ChildProcess
|
||||
}[]) {
|
||||
curWorker._child?.kill('SIGKILL')
|
||||
}
|
||||
}
|
||||
process.on('exit', cleanup)
|
||||
process.on('SIGINT', cleanup)
|
||||
process.on('SIGTERM', cleanup)
|
||||
process.on('uncaughtException', cleanup)
|
||||
process.on('unhandledRejection', cleanup)
|
||||
|
||||
let didInitialize = false
|
||||
|
||||
for (const _worker of ((routerWorker as any)._workerPool?._workers ||
|
||||
|
@ -251,17 +304,25 @@ export async function startServer({
|
|||
}
|
||||
})
|
||||
|
||||
const { port: routerPort } = await routerWorker.initialize({
|
||||
const initializeResult = await routerWorker.initialize({
|
||||
dir,
|
||||
port,
|
||||
hostname,
|
||||
dev: !!isDev,
|
||||
minimalMode,
|
||||
workerType: 'router',
|
||||
isNodeDebugging: !!isNodeDebugging,
|
||||
keepAliveTimeout,
|
||||
})
|
||||
routerPort = initializeResult.port
|
||||
didInitialize = true
|
||||
|
||||
let compress: ReturnType<typeof setupCompression> | undefined
|
||||
|
||||
if (nextConfig?.compress !== false) {
|
||||
compress = setupCompression()
|
||||
}
|
||||
|
||||
const getProxyServer = (pathname: string) => {
|
||||
const targetUrl = `http://${
|
||||
targetHost === 'localhost' ? '127.0.0.1' : targetHost
|
||||
|
@ -275,9 +336,29 @@ export async function startServer({
|
|||
followRedirects: false,
|
||||
})
|
||||
|
||||
// add error listener to prevent uncaught exceptions
|
||||
proxyServer.on('error', (_err) => {
|
||||
// TODO?: enable verbose error logs with --debug flag?
|
||||
})
|
||||
|
||||
proxyServer.on('proxyRes', (proxyRes, innerReq, innerRes) => {
|
||||
const cleanupProxy = (err: any) => {
|
||||
// cleanup event listeners to allow clean garbage collection
|
||||
proxyRes.removeListener('error', cleanupProxy)
|
||||
proxyRes.removeListener('close', cleanupProxy)
|
||||
innerRes.removeListener('error', cleanupProxy)
|
||||
innerRes.removeListener('close', cleanupProxy)
|
||||
|
||||
// destroy all source streams to propagate the caught event backward
|
||||
innerReq.destroy(err)
|
||||
proxyRes.destroy(err)
|
||||
}
|
||||
|
||||
proxyRes.once('error', cleanupProxy)
|
||||
proxyRes.once('close', cleanupProxy)
|
||||
innerRes.once('error', cleanupProxy)
|
||||
innerRes.once('close', cleanupProxy)
|
||||
})
|
||||
return proxyServer
|
||||
}
|
||||
|
||||
|
@ -297,26 +378,75 @@ export async function startServer({
|
|||
res.end(cleanUrl)
|
||||
return
|
||||
}
|
||||
const proxyServer = getProxyServer(req.url || '/')
|
||||
|
||||
// http-proxy does not properly detect a client disconnect in newer
|
||||
// versions of Node.js. This is caused because it only listens for the
|
||||
// `aborted` event on the our request object, but it also fully reads
|
||||
// and closes the request object. Node **will not** fire `aborted` when
|
||||
// the request is already closed. Listening for `close` on our response
|
||||
// object will detect the disconnect, and we can abort the proxy's
|
||||
// connection.
|
||||
proxyServer.on('proxyReq', (proxyReq) => {
|
||||
res.on('close', () => proxyReq.destroy())
|
||||
})
|
||||
proxyServer.on('proxyRes', (proxyRes) => {
|
||||
res.on('close', () => proxyRes.destroy())
|
||||
})
|
||||
if (typeof compress === 'function') {
|
||||
// @ts-expect-error not express req/res
|
||||
compress(req, res, () => {})
|
||||
}
|
||||
|
||||
proxyServer.web(req, res)
|
||||
const targetUrl = `http://${
|
||||
targetHost === 'localhost' ? '127.0.0.1' : targetHost
|
||||
}:${routerPort}${req.url || '/'}`
|
||||
|
||||
const invokeRes = await invokeRequest(
|
||||
targetUrl,
|
||||
{
|
||||
headers: req.headers,
|
||||
method: req.method,
|
||||
},
|
||||
getCloneableBody(req).cloneBodyStream()
|
||||
)
|
||||
|
||||
res.statusCode = invokeRes.status
|
||||
res.statusMessage = invokeRes.statusText
|
||||
|
||||
for (const [key, value] of Object.entries(
|
||||
filterReqHeaders(toNodeOutgoingHttpHeaders(invokeRes.headers))
|
||||
)) {
|
||||
if (value !== undefined) {
|
||||
if (key === 'set-cookie') {
|
||||
const curValue = res.getHeader(key) as string
|
||||
const newValue: string[] = [] as string[]
|
||||
|
||||
for (const cookie of Array.isArray(curValue)
|
||||
? curValue
|
||||
: splitCookiesString(curValue || '')) {
|
||||
newValue.push(cookie)
|
||||
}
|
||||
for (const val of (Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? [value]
|
||||
: []) as string[]) {
|
||||
newValue.push(val)
|
||||
}
|
||||
res.setHeader(key, newValue)
|
||||
} else {
|
||||
res.setHeader(key, value as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (invokeRes.body) {
|
||||
await pipeReadable(invokeRes.body, res)
|
||||
} else {
|
||||
res.end()
|
||||
}
|
||||
}
|
||||
upgradeHandler = async (req, socket, head) => {
|
||||
// add error listeners to prevent uncaught exceptions on socket errors
|
||||
req.on('error', (_err) => {
|
||||
// TODO: log socket errors?
|
||||
// console.log(_err)
|
||||
})
|
||||
socket.on('error', (_err) => {
|
||||
// TODO: log socket errors?
|
||||
// console.log(_err)
|
||||
})
|
||||
const proxyServer = getProxyServer(req.url || '/')
|
||||
proxyServer.on('proxyReqWs', (proxyReq) => {
|
||||
socket.on('close', () => proxyReq.destroy())
|
||||
})
|
||||
proxyServer.ws(req, socket, head)
|
||||
}
|
||||
handlersReady()
|
||||
|
@ -358,5 +488,6 @@ export async function startServer({
|
|||
await worker.end()
|
||||
}
|
||||
}
|
||||
teardown.port = routerPort
|
||||
return teardown
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,25 +3,29 @@ import type { NodeRequestHandler } from './next-server'
|
|||
import type { UrlWithParsedQuery } from 'url'
|
||||
import type { NextConfigComplete } from './config-shared'
|
||||
import type { IncomingMessage, ServerResponse } from 'http'
|
||||
import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta'
|
||||
import {
|
||||
addRequestMeta,
|
||||
type NextParsedUrlQuery,
|
||||
type NextUrlWithParsedQuery,
|
||||
} from './request-meta'
|
||||
|
||||
import './require-hook'
|
||||
import './node-polyfill-fetch'
|
||||
import './node-polyfill-crypto'
|
||||
|
||||
import url from 'url'
|
||||
import { default as Server } from './next-server'
|
||||
import * as log from '../build/output/log'
|
||||
import loadConfig from './config'
|
||||
import { join, resolve } from 'path'
|
||||
import { resolve } from 'path'
|
||||
import { NON_STANDARD_NODE_ENV } from '../lib/constants'
|
||||
import {
|
||||
PHASE_DEVELOPMENT_SERVER,
|
||||
SERVER_DIRECTORY,
|
||||
} from '../shared/lib/constants'
|
||||
import { PHASE_DEVELOPMENT_SERVER } from '../shared/lib/constants'
|
||||
import { PHASE_PRODUCTION_SERVER } from '../shared/lib/constants'
|
||||
import { getTracer } from './lib/trace/tracer'
|
||||
import { NextServerSpan } from './lib/trace/constants'
|
||||
import { formatUrl } from '../shared/lib/router/utils/format-url'
|
||||
import { findDir } from '../lib/find-pages-dir'
|
||||
import { proxyRequest } from './lib/router-utils/proxy-request'
|
||||
import { TLSSocket } from 'tls'
|
||||
|
||||
let ServerImpl: typeof Server
|
||||
|
||||
|
@ -136,16 +140,14 @@ export class NextServer {
|
|||
return server.render404(...args)
|
||||
}
|
||||
|
||||
async serveStatic(...args: Parameters<Server['serveStatic']>) {
|
||||
const server = await this.getServer()
|
||||
return server.serveStatic(...args)
|
||||
}
|
||||
|
||||
async prepare() {
|
||||
async prepare(serverFields?: any) {
|
||||
if (this.standaloneMode) return
|
||||
|
||||
const server = await this.getServer()
|
||||
|
||||
if (serverFields) {
|
||||
Object.assign(server, serverFields)
|
||||
}
|
||||
// We shouldn't prepare the server in production,
|
||||
// because this code won't be executed when deployed
|
||||
if (this.options.dev) {
|
||||
|
@ -271,15 +273,43 @@ function createServer(options: NextServerOptions): NextServer {
|
|||
// both types of renderers (pages, app) running in separated processes,
|
||||
// instead of having the Next server only.
|
||||
let shouldUseStandaloneMode = false
|
||||
|
||||
const dir = resolve(options.dir || '.')
|
||||
const server = new NextServer(options)
|
||||
|
||||
const { createServerHandler } =
|
||||
require('./lib/render-server-standalone') as typeof import('./lib/render-server-standalone')
|
||||
const { createRouterWorker, checkIsNodeDebugging } =
|
||||
require('./lib/start-server') as typeof import('./lib/start-server')
|
||||
|
||||
let handlerPromise: Promise<ReturnType<typeof createServerHandler>>
|
||||
let didWebSocketSetup = false
|
||||
let serverPort: number = 0
|
||||
|
||||
function setupWebSocketHandler(
|
||||
customServer?: import('http').Server,
|
||||
_req?: IncomingMessage
|
||||
) {
|
||||
if (!didWebSocketSetup) {
|
||||
didWebSocketSetup = true
|
||||
customServer = customServer || (_req?.socket as any)?.server
|
||||
|
||||
if (!customServer) {
|
||||
// this is very unlikely to happen but show an error in case
|
||||
// it does somehow
|
||||
console.error(
|
||||
`Invalid IncomingMessage received, make sure http.createServer is being used to handle requests.`
|
||||
)
|
||||
} else {
|
||||
customServer.on('upgrade', async (req, socket, head) => {
|
||||
if (shouldUseStandaloneMode) {
|
||||
await proxyRequest(
|
||||
req,
|
||||
socket as any,
|
||||
url.parse(`http://127.0.0.1:${serverPort}${req.url}`, true),
|
||||
head
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Proxy(
|
||||
{},
|
||||
{
|
||||
|
@ -287,47 +317,42 @@ function createServer(options: NextServerOptions): NextServer {
|
|||
switch (propKey) {
|
||||
case 'prepare':
|
||||
return async () => {
|
||||
// Instead of running Next Server's `prepare`, we'll run the loadConfig first to determine
|
||||
// if we should run the standalone server or not.
|
||||
const config = await server[SYMBOL_LOAD_CONFIG]()
|
||||
|
||||
// Check if the application has app dir or not. This depends on the mode (dev or prod).
|
||||
// For dev, `app` should be existing in the sources and for prod it should be existing
|
||||
// in the dist folder.
|
||||
const distDir =
|
||||
process.env.NEXT_RUNTIME === 'edge'
|
||||
? config.distDir
|
||||
: join(dir, config.distDir)
|
||||
const serverDistDir = join(distDir, SERVER_DIRECTORY)
|
||||
const hasAppDir = !!findDir(
|
||||
options.dev ? dir : serverDistDir,
|
||||
'app'
|
||||
shouldUseStandaloneMode = true
|
||||
server[SYMBOL_SET_STANDALONE_MODE]()
|
||||
const isNodeDebugging = checkIsNodeDebugging()
|
||||
const routerWorker = await createRouterWorker(
|
||||
require.resolve('./lib/router-server'),
|
||||
isNodeDebugging
|
||||
)
|
||||
|
||||
if (hasAppDir) {
|
||||
shouldUseStandaloneMode = true
|
||||
server[SYMBOL_SET_STANDALONE_MODE]()
|
||||
|
||||
handlerPromise =
|
||||
handlerPromise ||
|
||||
createServerHandler({
|
||||
port: options.port || 3000,
|
||||
dev: options.dev,
|
||||
dir,
|
||||
hostname: options.hostname || 'localhost',
|
||||
minimalMode: false,
|
||||
})
|
||||
} else {
|
||||
return server.prepare()
|
||||
}
|
||||
const initResult = await routerWorker.initialize({
|
||||
dir,
|
||||
port: options.port || 3000,
|
||||
hostname: options.hostname || 'localhost',
|
||||
isNodeDebugging: !!isNodeDebugging,
|
||||
workerType: 'router',
|
||||
dev: !!options.dev,
|
||||
minimalMode: options.minimalMode,
|
||||
})
|
||||
serverPort = initResult.port
|
||||
}
|
||||
case 'getRequestHandler': {
|
||||
return () => {
|
||||
let handler: RequestHandler
|
||||
return async (req: IncomingMessage, res: ServerResponse) => {
|
||||
if (shouldUseStandaloneMode) {
|
||||
const standaloneHandler = await handlerPromise
|
||||
return standaloneHandler(req, res)
|
||||
setupWebSocketHandler(options.httpServer, req)
|
||||
const parsedUrl = url.parse(
|
||||
`http://127.0.0.1:${serverPort}${req.url}`,
|
||||
true
|
||||
)
|
||||
if ((req?.socket as TLSSocket)?.encrypted) {
|
||||
req.headers['x-forwarded-proto'] = 'https'
|
||||
}
|
||||
addRequestMeta(req, '__NEXT_INIT_QUERY', parsedUrl.query)
|
||||
|
||||
await proxyRequest(req, res, parsedUrl, undefined, req)
|
||||
return
|
||||
}
|
||||
handler = handler || server.getRequestHandler()
|
||||
return handler(req, res)
|
||||
|
@ -343,13 +368,37 @@ function createServer(options: NextServerOptions): NextServer {
|
|||
parsedUrl?: NextUrlWithParsedQuery
|
||||
) => {
|
||||
if (shouldUseStandaloneMode) {
|
||||
const handler = await handlerPromise
|
||||
setupWebSocketHandler(options.httpServer, req)
|
||||
|
||||
if (!pathname.startsWith('/')) {
|
||||
console.error(`Cannot render page with path "${pathname}"`)
|
||||
pathname = `/${pathname}`
|
||||
}
|
||||
pathname = pathname === '/index' ? '/' : pathname
|
||||
|
||||
req.url = formatUrl({
|
||||
...parsedUrl,
|
||||
pathname,
|
||||
query,
|
||||
})
|
||||
return handler(req, res)
|
||||
|
||||
if ((req?.socket as TLSSocket)?.encrypted) {
|
||||
req.headers['x-forwarded-proto'] = 'https'
|
||||
}
|
||||
addRequestMeta(
|
||||
req,
|
||||
'__NEXT_INIT_QUERY',
|
||||
parsedUrl?.query || query || {}
|
||||
)
|
||||
|
||||
await proxyRequest(
|
||||
req,
|
||||
res,
|
||||
url.parse(`http://127.0.0.1:${serverPort}${req.url}`, true),
|
||||
undefined,
|
||||
req
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
return server.render(req, res, pathname, query, parsedUrl)
|
||||
|
@ -366,7 +415,6 @@ function createServer(options: NextServerOptions): NextServer {
|
|||
}
|
||||
) as any
|
||||
}
|
||||
|
||||
return new NextServer(options)
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,11 @@ const resolve = process.env.NEXT_MINIMAL
|
|||
const toResolveMap = (map: Record<string, string>): [string, string][] =>
|
||||
Object.entries(map).map(([key, value]) => [key, resolve(value)])
|
||||
|
||||
export const defaultOverrides = {
|
||||
'styled-jsx': dirname(resolve('styled-jsx/package.json')),
|
||||
'styled-jsx/style': resolve('styled-jsx/style'),
|
||||
}
|
||||
|
||||
export const baseOverrides = {
|
||||
react: 'next/dist/compiled/react',
|
||||
'react/package.json': 'next/dist/compiled/react/package.json',
|
||||
|
@ -73,12 +78,7 @@ export function addHookAliases(aliases: [string, string][] = []) {
|
|||
}
|
||||
|
||||
// Add default aliases
|
||||
addHookAliases([
|
||||
// Use `require.resolve` explicitly to make them statically analyzable
|
||||
// styled-jsx needs to be resolved as the external dependency.
|
||||
['styled-jsx', dirname(resolve('styled-jsx/package.json'))],
|
||||
['styled-jsx/style', resolve('styled-jsx/style')],
|
||||
])
|
||||
addHookAliases(toResolveMap(defaultOverrides))
|
||||
|
||||
// Override built-in React packages if necessary
|
||||
function overrideReact() {
|
||||
|
|
|
@ -1,564 +0,0 @@
|
|||
import type { NextConfig } from './config'
|
||||
import type { ParsedUrlQuery } from 'querystring'
|
||||
import type { BaseNextRequest, BaseNextResponse } from './base-http'
|
||||
import type {
|
||||
RouteMatchFn,
|
||||
Params,
|
||||
} from '../shared/lib/router/utils/route-matcher'
|
||||
import type { RouteHas } from '../lib/load-custom-routes'
|
||||
|
||||
import {
|
||||
addRequestMeta,
|
||||
getNextInternalQuery,
|
||||
NextUrlWithParsedQuery,
|
||||
} from './request-meta'
|
||||
import { isAPIRoute } from '../lib/is-api-route'
|
||||
import { getPathMatch } from '../shared/lib/router/utils/path-match'
|
||||
import { matchHas } from '../shared/lib/router/utils/prepare-destination'
|
||||
import { removePathPrefix } from '../shared/lib/router/utils/remove-path-prefix'
|
||||
import { getRequestMeta } from './request-meta'
|
||||
import { formatNextPathnameInfo } from '../shared/lib/router/utils/format-next-pathname-info'
|
||||
import { getNextPathnameInfo } from '../shared/lib/router/utils/get-next-pathname-info'
|
||||
import {
|
||||
MatchOptions,
|
||||
RouteMatcherManager,
|
||||
} from './future/route-matcher-managers/route-matcher-manager'
|
||||
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
|
||||
import type { I18NProvider } from './future/helpers/i18n-provider'
|
||||
import { getTracer } from './lib/trace/tracer'
|
||||
import { RouterSpan } from './lib/trace/constants'
|
||||
|
||||
type RouteResult = {
|
||||
finished: boolean
|
||||
pathname?: string
|
||||
query?: ParsedUrlQuery
|
||||
}
|
||||
|
||||
type RouteFn = (
|
||||
req: BaseNextRequest,
|
||||
res: BaseNextResponse,
|
||||
params: Params,
|
||||
parsedUrl: NextUrlWithParsedQuery,
|
||||
upgradeHead?: Buffer
|
||||
) => Promise<RouteResult> | RouteResult
|
||||
|
||||
export type Route = {
|
||||
match: RouteMatchFn
|
||||
has?: RouteHas[]
|
||||
missing?: RouteHas[]
|
||||
type: string
|
||||
check?: boolean
|
||||
statusCode?: number
|
||||
name: string
|
||||
matchesBasePath?: true
|
||||
matchesLocale?: true
|
||||
matchesLocaleAPIRoutes?: true
|
||||
matchesTrailingSlash?: true
|
||||
internal?: true
|
||||
fn: RouteFn
|
||||
}
|
||||
|
||||
export type RouterOptions = {
|
||||
headers: ReadonlyArray<Route>
|
||||
fsRoutes: ReadonlyArray<Route>
|
||||
rewrites: {
|
||||
beforeFiles: ReadonlyArray<Route>
|
||||
afterFiles: ReadonlyArray<Route>
|
||||
fallback: ReadonlyArray<Route>
|
||||
}
|
||||
redirects: ReadonlyArray<Route>
|
||||
catchAllRoute: Route
|
||||
catchAllMiddleware: ReadonlyArray<Route>
|
||||
matchers: RouteMatcherManager
|
||||
useFileSystemPublicRoutes: boolean
|
||||
nextConfig: NextConfig
|
||||
i18nProvider?: I18NProvider
|
||||
}
|
||||
|
||||
export type PageChecker = (pathname: string) => Promise<boolean>
|
||||
|
||||
export default class Router {
|
||||
public catchAllMiddleware: ReadonlyArray<Route>
|
||||
|
||||
private readonly headers: ReadonlyArray<Route>
|
||||
private readonly fsRoutes: Route[]
|
||||
private readonly redirects: ReadonlyArray<Route>
|
||||
private rewrites: {
|
||||
beforeFiles: ReadonlyArray<Route>
|
||||
afterFiles: ReadonlyArray<Route>
|
||||
fallback: ReadonlyArray<Route>
|
||||
}
|
||||
private readonly catchAllRoute: Route
|
||||
private readonly matchers: RouteMatcherManager
|
||||
private readonly useFileSystemPublicRoutes: boolean
|
||||
private readonly nextConfig: NextConfig
|
||||
private readonly i18nProvider?: I18NProvider
|
||||
private compiledRoutes: ReadonlyArray<Route>
|
||||
private needsRecompilation: boolean
|
||||
|
||||
constructor({
|
||||
headers = [],
|
||||
fsRoutes = [],
|
||||
rewrites = {
|
||||
beforeFiles: [],
|
||||
afterFiles: [],
|
||||
fallback: [],
|
||||
},
|
||||
redirects = [],
|
||||
catchAllRoute,
|
||||
catchAllMiddleware = [],
|
||||
matchers,
|
||||
useFileSystemPublicRoutes,
|
||||
nextConfig,
|
||||
i18nProvider,
|
||||
}: RouterOptions) {
|
||||
this.nextConfig = nextConfig
|
||||
this.headers = headers
|
||||
this.fsRoutes = [...fsRoutes]
|
||||
this.rewrites = rewrites
|
||||
this.redirects = redirects
|
||||
this.catchAllRoute = catchAllRoute
|
||||
this.catchAllMiddleware = catchAllMiddleware
|
||||
this.matchers = matchers
|
||||
this.useFileSystemPublicRoutes = useFileSystemPublicRoutes
|
||||
this.i18nProvider = i18nProvider
|
||||
|
||||
// Perform the initial route compilation.
|
||||
this.compiledRoutes = this.compileRoutes()
|
||||
this.needsRecompilation = false
|
||||
}
|
||||
|
||||
get basePath() {
|
||||
return this.nextConfig.basePath || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the router has catch-all middleware routes configured.
|
||||
*/
|
||||
get hasMiddleware(): boolean {
|
||||
return this.catchAllMiddleware.length > 0
|
||||
}
|
||||
|
||||
public setCatchallMiddleware(catchAllMiddleware: ReadonlyArray<Route>) {
|
||||
this.catchAllMiddleware = catchAllMiddleware
|
||||
this.needsRecompilation = true
|
||||
}
|
||||
|
||||
public setRewrites(rewrites: RouterOptions['rewrites']) {
|
||||
this.rewrites = rewrites
|
||||
this.needsRecompilation = true
|
||||
}
|
||||
|
||||
public addFsRoute(fsRoute: Route) {
|
||||
// We use unshift so that we're sure the routes is defined before Next's
|
||||
// default routes.
|
||||
this.fsRoutes.unshift(fsRoute)
|
||||
this.needsRecompilation = true
|
||||
}
|
||||
|
||||
private compileRoutes(): ReadonlyArray<Route> {
|
||||
/*
|
||||
Desired routes order
|
||||
- headers
|
||||
- redirects
|
||||
- Check filesystem (including pages), if nothing found continue
|
||||
- User rewrites (checking filesystem and pages each match)
|
||||
*/
|
||||
|
||||
const [middlewareCatchAllRoute] = this.catchAllMiddleware
|
||||
|
||||
return [
|
||||
...(middlewareCatchAllRoute
|
||||
? this.fsRoutes
|
||||
.filter((route) => route.name === '_next/data catchall')
|
||||
.map((route) => ({
|
||||
...route,
|
||||
name: '_next/data normalizing',
|
||||
check: false,
|
||||
}))
|
||||
: []),
|
||||
...this.headers,
|
||||
...this.redirects,
|
||||
...(this.useFileSystemPublicRoutes && middlewareCatchAllRoute
|
||||
? [middlewareCatchAllRoute]
|
||||
: []),
|
||||
...this.rewrites.beforeFiles,
|
||||
...this.fsRoutes,
|
||||
// We only check the catch-all route if public page routes hasn't been
|
||||
// disabled
|
||||
...(this.useFileSystemPublicRoutes
|
||||
? [
|
||||
{
|
||||
type: 'route',
|
||||
matchesLocale: true,
|
||||
name: 'page checker',
|
||||
match: getPathMatch('/:path*'),
|
||||
fn: async (req, res, params, parsedUrl, upgradeHead) => {
|
||||
// Next.js performs all route matching without the trailing slash.
|
||||
const pathname = removeTrailingSlash(parsedUrl.pathname || '/')
|
||||
|
||||
// Normalize and detect the locale on the pathname.
|
||||
const options: MatchOptions = {
|
||||
// We need to skip dynamic route matching because the next
|
||||
// step we're processing the afterFiles rewrites which must
|
||||
// not include dynamic matches.
|
||||
skipDynamic: true,
|
||||
i18n: this.i18nProvider?.analyze(pathname),
|
||||
}
|
||||
|
||||
// If the locale was inferred from the default, we should mark
|
||||
// it in the match options.
|
||||
if (
|
||||
options.i18n &&
|
||||
parsedUrl.query.__nextInferredLocaleFromDefault
|
||||
) {
|
||||
options.i18n.inferredFromDefault = true
|
||||
}
|
||||
|
||||
const match = await this.matchers.match(pathname, options)
|
||||
if (!match) return { finished: false }
|
||||
|
||||
// Add the match so we can get it later.
|
||||
addRequestMeta(req, '_nextMatch', match)
|
||||
|
||||
return this.catchAllRoute.fn(
|
||||
req,
|
||||
res,
|
||||
params,
|
||||
parsedUrl,
|
||||
upgradeHead
|
||||
)
|
||||
},
|
||||
} as Route,
|
||||
]
|
||||
: []),
|
||||
...this.rewrites.afterFiles,
|
||||
...(this.rewrites.fallback.length
|
||||
? [
|
||||
{
|
||||
type: 'route',
|
||||
name: 'dynamic route/page check',
|
||||
match: getPathMatch('/:path*'),
|
||||
fn: async (req, res, _params, parsedCheckerUrl, upgradeHead) => {
|
||||
return {
|
||||
finished: await this.checkFsRoutes(
|
||||
req,
|
||||
res,
|
||||
parsedCheckerUrl,
|
||||
upgradeHead
|
||||
),
|
||||
}
|
||||
},
|
||||
} as Route,
|
||||
...this.rewrites.fallback,
|
||||
]
|
||||
: []),
|
||||
|
||||
// We only check the catch-all route if public page routes hasn't been
|
||||
// disabled
|
||||
...(this.useFileSystemPublicRoutes ? [this.catchAllRoute] : []),
|
||||
].map((route) => {
|
||||
if (route.fn) {
|
||||
return {
|
||||
...route,
|
||||
fn: getTracer().wrap(
|
||||
RouterSpan.executeRoute,
|
||||
{
|
||||
attributes: {
|
||||
'next.route': route.name,
|
||||
},
|
||||
},
|
||||
route.fn
|
||||
),
|
||||
}
|
||||
}
|
||||
return route
|
||||
})
|
||||
}
|
||||
|
||||
private async checkFsRoutes(
|
||||
req: BaseNextRequest,
|
||||
res: BaseNextResponse,
|
||||
parsedUrl: NextUrlWithParsedQuery,
|
||||
upgradeHead?: Buffer
|
||||
) {
|
||||
const fsPathname = removePathPrefix(parsedUrl.pathname!, this.basePath)
|
||||
|
||||
for (const route of this.fsRoutes) {
|
||||
const params = route.match(fsPathname)
|
||||
if (!params) continue
|
||||
|
||||
const { finished } = await route.fn(req, res, params, {
|
||||
...parsedUrl,
|
||||
pathname: fsPathname,
|
||||
})
|
||||
if (finished) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize and detect the locale on the pathname.
|
||||
const options: MatchOptions = {
|
||||
i18n: this.i18nProvider?.analyze(fsPathname),
|
||||
}
|
||||
|
||||
const match = await this.matchers.test(fsPathname, options)
|
||||
if (!match) return false
|
||||
|
||||
// Matched a page or dynamic route so render it using catchAllRoute
|
||||
const params = this.catchAllRoute.match(parsedUrl.pathname)
|
||||
if (!params) {
|
||||
throw new Error(
|
||||
`Invariant: could not match params, this is an internal error please open an issue.`
|
||||
)
|
||||
}
|
||||
|
||||
const { finished } = await this.catchAllRoute.fn(
|
||||
req,
|
||||
res,
|
||||
params,
|
||||
{
|
||||
...parsedUrl,
|
||||
pathname: fsPathname,
|
||||
query: {
|
||||
...parsedUrl.query,
|
||||
_nextBubbleNoFallback: '1',
|
||||
},
|
||||
},
|
||||
upgradeHead
|
||||
)
|
||||
|
||||
return finished
|
||||
}
|
||||
|
||||
async execute(
|
||||
req: BaseNextRequest,
|
||||
res: BaseNextResponse,
|
||||
parsedUrl: NextUrlWithParsedQuery,
|
||||
upgradeHead?: Buffer
|
||||
): Promise<boolean> {
|
||||
// Only recompile if the routes need to be recompiled, this should only
|
||||
// happen in development.
|
||||
if (this.needsRecompilation) {
|
||||
this.compiledRoutes = this.compileRoutes()
|
||||
this.needsRecompilation = false
|
||||
}
|
||||
|
||||
// Create a deep copy of the parsed URL.
|
||||
const parsedUrlUpdated = {
|
||||
...parsedUrl,
|
||||
query: {
|
||||
...parsedUrl.query,
|
||||
},
|
||||
}
|
||||
|
||||
// when x-invoke-path is specified we can short short circuit resolving
|
||||
// we only honor this header if we are inside of a render worker to
|
||||
// prevent external users coercing the routing path
|
||||
const matchedPath = req.headers['x-invoke-path'] as string
|
||||
let curRoutes = this.compiledRoutes
|
||||
|
||||
if (
|
||||
process.env.NEXT_RUNTIME !== 'edge' &&
|
||||
process.env.__NEXT_PRIVATE_RENDER_WORKER &&
|
||||
matchedPath
|
||||
) {
|
||||
curRoutes = this.compiledRoutes.filter((r) => {
|
||||
return r.name === 'Catchall render' || r.name === '_next/data catchall'
|
||||
})
|
||||
|
||||
const parsedMatchedPath = new URL(matchedPath || '/', 'http://n')
|
||||
|
||||
const pathnameInfo = getNextPathnameInfo(parsedMatchedPath.pathname, {
|
||||
nextConfig: this.nextConfig,
|
||||
parseData: false,
|
||||
})
|
||||
|
||||
if (pathnameInfo.locale) {
|
||||
parsedUrlUpdated.query.__nextLocale = pathnameInfo.locale
|
||||
}
|
||||
|
||||
if (parsedUrlUpdated.pathname !== parsedMatchedPath.pathname) {
|
||||
parsedUrlUpdated.pathname = parsedMatchedPath.pathname
|
||||
addRequestMeta(req, '_nextRewroteUrl', pathnameInfo.pathname)
|
||||
addRequestMeta(req, '_nextDidRewrite', true)
|
||||
}
|
||||
|
||||
for (const key of Object.keys(parsedUrlUpdated.query)) {
|
||||
if (!key.startsWith('__next') && !key.startsWith('_next')) {
|
||||
delete parsedUrlUpdated.query[key]
|
||||
}
|
||||
}
|
||||
const invokeQuery = req.headers['x-invoke-query']
|
||||
|
||||
if (typeof invokeQuery === 'string') {
|
||||
Object.assign(
|
||||
parsedUrlUpdated.query,
|
||||
JSON.parse(decodeURIComponent(invokeQuery))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const route of curRoutes) {
|
||||
// only process rewrites for upgrade request
|
||||
if (upgradeHead && route.type !== 'rewrite') {
|
||||
continue
|
||||
}
|
||||
|
||||
const originalPathname = parsedUrlUpdated.pathname!
|
||||
const pathnameInfo = getNextPathnameInfo(originalPathname, {
|
||||
nextConfig: this.nextConfig,
|
||||
parseData: false,
|
||||
})
|
||||
|
||||
// If the request has a locale and the route is an api route that doesn't
|
||||
// support matching locales, skip the route.
|
||||
if (
|
||||
pathnameInfo.locale &&
|
||||
!route.matchesLocaleAPIRoutes &&
|
||||
isAPIRoute(pathnameInfo.pathname)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Restore the `basePath` if the request had a `basePath`.
|
||||
if (getRequestMeta(req, '_nextHadBasePath')) {
|
||||
pathnameInfo.basePath = this.basePath
|
||||
}
|
||||
|
||||
// Create a copy of the `basePath` so we can modify it for the next
|
||||
// request if the route doesn't match with the `basePath`.
|
||||
const basePath = pathnameInfo.basePath
|
||||
if (!route.matchesBasePath) {
|
||||
pathnameInfo.basePath = undefined
|
||||
}
|
||||
|
||||
// Add the locale to the information if the route supports matching
|
||||
// locales and the locale is not present in the info.
|
||||
const locale = parsedUrlUpdated.query.__nextLocale
|
||||
if (route.matchesLocale && locale && !pathnameInfo.locale) {
|
||||
pathnameInfo.locale = locale
|
||||
}
|
||||
|
||||
// If the route doesn't support matching locales and the locale is the
|
||||
// default locale then remove it from the info.
|
||||
if (
|
||||
!route.matchesLocale &&
|
||||
pathnameInfo.locale === this.nextConfig.i18n?.defaultLocale &&
|
||||
pathnameInfo.locale
|
||||
) {
|
||||
pathnameInfo.locale = undefined
|
||||
}
|
||||
|
||||
// If the route doesn't support trailing slashes and the request had a
|
||||
// trailing slash then remove it from the info.
|
||||
if (
|
||||
route.matchesTrailingSlash &&
|
||||
getRequestMeta(req, '__nextHadTrailingSlash')
|
||||
) {
|
||||
pathnameInfo.trailingSlash = true
|
||||
}
|
||||
|
||||
// Construct a new pathname based on the info.
|
||||
const matchPathname = formatNextPathnameInfo({
|
||||
ignorePrefix: true,
|
||||
...pathnameInfo,
|
||||
})
|
||||
|
||||
let params = route.match(matchPathname)
|
||||
if ((route.has || route.missing) && params) {
|
||||
const hasParams = matchHas(
|
||||
req,
|
||||
parsedUrlUpdated.query,
|
||||
route.has,
|
||||
route.missing
|
||||
)
|
||||
if (hasParams) {
|
||||
Object.assign(params, hasParams)
|
||||
} else {
|
||||
params = false
|
||||
}
|
||||
}
|
||||
|
||||
// If it is a matcher that doesn't match the basePath (like the public
|
||||
// directory) but Next.js is configured to use a basePath that was
|
||||
// never there, we consider this an invalid match and keep routing.
|
||||
if (
|
||||
params &&
|
||||
this.basePath &&
|
||||
!route.matchesBasePath &&
|
||||
!getRequestMeta(req, '_nextDidRewrite') &&
|
||||
!basePath
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (params) {
|
||||
const isNextDataNormalizing = route.name === '_next/data normalizing'
|
||||
|
||||
if (isNextDataNormalizing) {
|
||||
addRequestMeta(req, '_nextDataNormalizing', true)
|
||||
}
|
||||
parsedUrlUpdated.pathname = matchPathname
|
||||
const result = await route.fn(
|
||||
req,
|
||||
res,
|
||||
params,
|
||||
parsedUrlUpdated,
|
||||
upgradeHead
|
||||
)
|
||||
|
||||
if (isNextDataNormalizing) {
|
||||
addRequestMeta(req, '_nextDataNormalizing', false)
|
||||
}
|
||||
if (result.finished) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If the result includes a pathname then we need to update the
|
||||
// parsed url pathname to continue routing.
|
||||
if (result.pathname) {
|
||||
parsedUrlUpdated.pathname = result.pathname
|
||||
} else {
|
||||
// since the fs route didn't finish routing we need to re-add the
|
||||
// basePath to continue checking with the basePath present
|
||||
parsedUrlUpdated.pathname = originalPathname
|
||||
}
|
||||
|
||||
// Copy over only internal query parameters from the original query and
|
||||
// merge with the result query.
|
||||
if (result.query) {
|
||||
parsedUrlUpdated.query = {
|
||||
...getNextInternalQuery(parsedUrlUpdated.query),
|
||||
...result.query,
|
||||
}
|
||||
}
|
||||
|
||||
// check filesystem
|
||||
if (
|
||||
route.check &&
|
||||
(await this.checkFsRoutes(req, res, parsedUrlUpdated))
|
||||
) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All routes were tested, none were found.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let _makeResolver: any = () => {}
|
||||
|
||||
if (
|
||||
// ensure this isn't bundled for edge runtime
|
||||
process.env.NEXT_RUNTIME !== 'edge' &&
|
||||
!process.env.NEXT_MINIMAL &&
|
||||
// only load if we are inside of the turbopack handler
|
||||
process.argv.some((arg) => arg.endsWith('router.js'))
|
||||
) {
|
||||
_makeResolver = require('./lib/route-resolver').makeResolver
|
||||
}
|
||||
|
||||
export const makeResolver = _makeResolver
|
|
@ -10,10 +10,11 @@ send.mime.define({
|
|||
export function serveStatic(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
path: string
|
||||
path: string,
|
||||
opts?: Parameters<typeof send>[2]
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
send(req, path)
|
||||
send(req, path, opts)
|
||||
.on('directory', () => {
|
||||
// We don't allow directories to be read.
|
||||
const err: any = new Error('No directory access')
|
||||
|
|
|
@ -1,116 +1,8 @@
|
|||
/* eslint-disable no-redeclare */
|
||||
import type {
|
||||
Header,
|
||||
Redirect,
|
||||
Rewrite,
|
||||
RouteType,
|
||||
} from '../lib/load-custom-routes'
|
||||
import type { Route } from './router'
|
||||
import type { BaseNextRequest } from './base-http'
|
||||
import type { ParsedUrlQuery } from 'querystring'
|
||||
|
||||
import { getRedirectStatus, modifyRouteRegex } from '../lib/redirect-status'
|
||||
import { getPathMatch } from '../shared/lib/router/utils/path-match'
|
||||
import {
|
||||
compileNonPath,
|
||||
prepareDestination,
|
||||
} from '../shared/lib/router/utils/prepare-destination'
|
||||
import { getRequestMeta } from './request-meta'
|
||||
import { stringify as stringifyQs } from 'querystring'
|
||||
import { format as formatUrl } from 'url'
|
||||
import { normalizeRepeatedSlashes } from '../shared/lib/utils'
|
||||
|
||||
export function getCustomRoute(params: {
|
||||
rule: Header
|
||||
type: RouteType
|
||||
restrictedRedirectPaths: string[]
|
||||
caseSensitive: boolean
|
||||
}): Route & Header
|
||||
export function getCustomRoute(params: {
|
||||
rule: Rewrite
|
||||
type: RouteType
|
||||
restrictedRedirectPaths: string[]
|
||||
caseSensitive: boolean
|
||||
}): Route & Rewrite
|
||||
export function getCustomRoute(params: {
|
||||
rule: Redirect
|
||||
type: RouteType
|
||||
restrictedRedirectPaths: string[]
|
||||
caseSensitive: boolean
|
||||
}): Route & Redirect
|
||||
export function getCustomRoute(params: {
|
||||
rule: Rewrite | Redirect | Header
|
||||
type: RouteType
|
||||
restrictedRedirectPaths: string[]
|
||||
caseSensitive: boolean
|
||||
}): (Route & Rewrite) | (Route & Header) | (Route & Rewrite) {
|
||||
const { rule, type, restrictedRedirectPaths } = params
|
||||
const match = getPathMatch(rule.source, {
|
||||
strict: true,
|
||||
removeUnnamedParams: true,
|
||||
regexModifier: !(rule as any).internal
|
||||
? (regex: string) =>
|
||||
modifyRouteRegex(
|
||||
regex,
|
||||
type === 'redirect' ? restrictedRedirectPaths : undefined
|
||||
)
|
||||
: undefined,
|
||||
sensitive: params.caseSensitive,
|
||||
})
|
||||
|
||||
return {
|
||||
...rule,
|
||||
type,
|
||||
match,
|
||||
name: type,
|
||||
fn: async (_req, _res, _params, _parsedUrl) => ({ finished: false }),
|
||||
}
|
||||
}
|
||||
|
||||
export const createHeaderRoute = ({
|
||||
rule,
|
||||
restrictedRedirectPaths,
|
||||
caseSensitive,
|
||||
}: {
|
||||
rule: Header
|
||||
restrictedRedirectPaths: string[]
|
||||
caseSensitive: boolean
|
||||
}): Route => {
|
||||
const headerRoute = getCustomRoute({
|
||||
type: 'header',
|
||||
rule,
|
||||
restrictedRedirectPaths,
|
||||
caseSensitive,
|
||||
})
|
||||
return {
|
||||
match: headerRoute.match,
|
||||
matchesBasePath: true,
|
||||
matchesLocale: true,
|
||||
matchesLocaleAPIRoutes: true,
|
||||
matchesTrailingSlash: true,
|
||||
has: headerRoute.has,
|
||||
missing: headerRoute.missing,
|
||||
type: headerRoute.type,
|
||||
name: `${headerRoute.type} ${headerRoute.source} header route`,
|
||||
fn: async (_req, res, params, _parsedUrl) => {
|
||||
const hasParams = Object.keys(params).length > 0
|
||||
for (const header of headerRoute.headers) {
|
||||
let { key, value } = header
|
||||
if (hasParams) {
|
||||
key = compileNonPath(key, params)
|
||||
value = compileNonPath(value, params)
|
||||
}
|
||||
|
||||
if (key.toLowerCase() === 'set-cookie') {
|
||||
res.appendHeader(key, value)
|
||||
} else {
|
||||
res.setHeader(key, value)
|
||||
}
|
||||
}
|
||||
return { finished: false }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// since initial query values are decoded by querystring.parse
|
||||
// we need to re-encode them here but still allow passing through
|
||||
|
@ -138,61 +30,3 @@ export const stringifyQuery = (req: BaseNextRequest, query: ParsedUrlQuery) => {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const createRedirectRoute = ({
|
||||
rule,
|
||||
restrictedRedirectPaths,
|
||||
caseSensitive,
|
||||
}: {
|
||||
rule: Redirect
|
||||
restrictedRedirectPaths: string[]
|
||||
caseSensitive: boolean
|
||||
}): Route => {
|
||||
const redirectRoute = getCustomRoute({
|
||||
type: 'redirect',
|
||||
rule,
|
||||
restrictedRedirectPaths,
|
||||
caseSensitive,
|
||||
})
|
||||
return {
|
||||
internal: redirectRoute.internal,
|
||||
type: redirectRoute.type,
|
||||
match: redirectRoute.match,
|
||||
matchesBasePath: true,
|
||||
matchesLocale: redirectRoute.internal ? undefined : true,
|
||||
matchesLocaleAPIRoutes: true,
|
||||
matchesTrailingSlash: true,
|
||||
has: redirectRoute.has,
|
||||
missing: redirectRoute.missing,
|
||||
statusCode: redirectRoute.statusCode,
|
||||
name: `Redirect route ${redirectRoute.source}`,
|
||||
fn: async (req, res, params, parsedUrl) => {
|
||||
const { parsedDestination } = prepareDestination({
|
||||
appendParamsToQuery: false,
|
||||
destination: redirectRoute.destination,
|
||||
params: params,
|
||||
query: parsedUrl.query,
|
||||
})
|
||||
|
||||
const { query } = parsedDestination
|
||||
delete (parsedDestination as any).query
|
||||
|
||||
parsedDestination.search = stringifyQuery(req, query)
|
||||
|
||||
let updatedDestination = formatUrl(parsedDestination)
|
||||
|
||||
if (updatedDestination.startsWith('/')) {
|
||||
updatedDestination = normalizeRepeatedSlashes(updatedDestination)
|
||||
}
|
||||
|
||||
res
|
||||
.redirect(updatedDestination, getRedirectStatus(redirectRoute))
|
||||
.body(updatedDestination)
|
||||
.send()
|
||||
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta'
|
|||
import type { Params } from '../shared/lib/router/utils/route-matcher'
|
||||
import type { PayloadOptions } from './send-payload'
|
||||
import type { LoadComponentsReturnType } from './load-components'
|
||||
import type { Route, RouterOptions } from './router'
|
||||
import type { BaseNextRequest, BaseNextResponse } from './base-http'
|
||||
import type { UrlWithParsedQuery } from 'url'
|
||||
|
||||
|
@ -170,187 +169,145 @@ export default class NextWebServer extends BaseServer<WebServerOptions> {
|
|||
return this.serverOptions.webServerConfig.extendRenderOpts.nextFontManifest
|
||||
}
|
||||
|
||||
protected generateRoutes(): RouterOptions {
|
||||
const fsRoutes: Route[] = [
|
||||
{
|
||||
match: getPathMatch('/_next/data/:path*'),
|
||||
type: 'route',
|
||||
name: '_next/data catchall',
|
||||
check: true,
|
||||
fn: async (req, res, params, _parsedUrl) => {
|
||||
// Make sure to 404 for /_next/data/ itself and
|
||||
// we also want to 404 if the buildId isn't correct
|
||||
if (!params.path || params.path[0] !== this.buildId) {
|
||||
await this.render404(req, res, _parsedUrl)
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
}
|
||||
// remove buildId from URL
|
||||
params.path.shift()
|
||||
protected async normalizeNextData(
|
||||
req: BaseNextRequest,
|
||||
res: BaseNextResponse,
|
||||
parsedUrl: NextUrlWithParsedQuery
|
||||
): Promise<{ finished: boolean }> {
|
||||
const middleware = this.getMiddleware()
|
||||
const params = getPathMatch('/_next/data/:path*')(parsedUrl.pathname)
|
||||
|
||||
const lastParam = params.path[params.path.length - 1]
|
||||
// Make sure to 404 for /_next/data/ itself and
|
||||
// we also want to 404 if the buildId isn't correct
|
||||
if (!params || !params.path || params.path[0] !== this.buildId) {
|
||||
await this.render404(req, res, parsedUrl)
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
}
|
||||
// remove buildId from URL
|
||||
params.path.shift()
|
||||
|
||||
// show 404 if it doesn't end with .json
|
||||
if (typeof lastParam !== 'string' || !lastParam.endsWith('.json')) {
|
||||
await this.render404(req, res, _parsedUrl)
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
}
|
||||
const lastParam = params.path[params.path.length - 1]
|
||||
|
||||
// re-create page's pathname
|
||||
let pathname = `/${params.path.join('/')}`
|
||||
pathname = getRouteFromAssetPath(pathname, '.json')
|
||||
|
||||
// ensure trailing slash is normalized per config
|
||||
if (this.router.hasMiddleware) {
|
||||
if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) {
|
||||
pathname += '/'
|
||||
}
|
||||
if (
|
||||
!this.nextConfig.trailingSlash &&
|
||||
pathname.length > 1 &&
|
||||
pathname.endsWith('/')
|
||||
) {
|
||||
pathname = pathname.substring(0, pathname.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.nextConfig.i18n) {
|
||||
const { host } = req?.headers || {}
|
||||
// remove port from host and remove port if present
|
||||
const hostname = host?.split(':')[0].toLowerCase()
|
||||
const localePathResult = normalizeLocalePath(
|
||||
pathname,
|
||||
this.nextConfig.i18n.locales
|
||||
)
|
||||
const domainLocale = this.i18nProvider?.detectDomainLocale(hostname)
|
||||
|
||||
let detectedLocale = ''
|
||||
|
||||
if (localePathResult.detectedLocale) {
|
||||
pathname = localePathResult.pathname
|
||||
detectedLocale = localePathResult.detectedLocale
|
||||
}
|
||||
|
||||
_parsedUrl.query.__nextLocale = detectedLocale
|
||||
_parsedUrl.query.__nextDefaultLocale =
|
||||
domainLocale?.defaultLocale || this.nextConfig.i18n.defaultLocale
|
||||
|
||||
if (!detectedLocale && !this.router.hasMiddleware) {
|
||||
_parsedUrl.query.__nextLocale =
|
||||
_parsedUrl.query.__nextDefaultLocale
|
||||
await this.render404(req, res, _parsedUrl)
|
||||
return { finished: true }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pathname,
|
||||
query: { ..._parsedUrl.query, __nextDataReq: '1' },
|
||||
finished: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
match: getPathMatch('/_next/:path*'),
|
||||
type: 'route',
|
||||
name: '_next catchall',
|
||||
// This path is needed because `render()` does a check for `/_next` and the calls the routing again
|
||||
fn: async (req, res, _params, parsedUrl) => {
|
||||
await this.render404(req, res, parsedUrl)
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const catchAllRoute: Route = {
|
||||
match: getPathMatch('/:path*'),
|
||||
type: 'route',
|
||||
matchesLocale: true,
|
||||
name: 'Catchall render',
|
||||
fn: async (req, res, _params, parsedUrl) => {
|
||||
let { pathname, query } = parsedUrl
|
||||
if (!pathname) {
|
||||
throw new Error('pathname is undefined')
|
||||
}
|
||||
|
||||
// interpolate query information into page for dynamic route
|
||||
// so that rewritten paths are handled properly
|
||||
const normalizedPage = this.serverOptions.webServerConfig.normalizedPage
|
||||
|
||||
if (pathname !== normalizedPage) {
|
||||
pathname = normalizedPage
|
||||
|
||||
if (isDynamicRoute(pathname)) {
|
||||
const routeRegex = getNamedRouteRegex(pathname, false)
|
||||
pathname = interpolateDynamicPath(pathname, query, routeRegex)
|
||||
normalizeVercelUrl(
|
||||
req,
|
||||
true,
|
||||
Object.keys(routeRegex.routeKeys),
|
||||
true,
|
||||
routeRegex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// next.js core assumes page path without trailing slash
|
||||
pathname = removeTrailingSlash(pathname)
|
||||
|
||||
if (this.i18nProvider) {
|
||||
const { detectedLocale } = await this.i18nProvider.analyze(pathname)
|
||||
if (detectedLocale) {
|
||||
parsedUrl.query.__nextLocale = detectedLocale
|
||||
}
|
||||
}
|
||||
|
||||
const bubbleNoFallback = !!query._nextBubbleNoFallback
|
||||
|
||||
if (isAPIRoute(pathname)) {
|
||||
delete query._nextBubbleNoFallback
|
||||
}
|
||||
|
||||
try {
|
||||
await this.render(req, res, pathname, query, parsedUrl, true)
|
||||
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof NoFallbackError && bubbleNoFallback) {
|
||||
return {
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
throw err
|
||||
}
|
||||
},
|
||||
// show 404 if it doesn't end with .json
|
||||
if (typeof lastParam !== 'string' || !lastParam.endsWith('.json')) {
|
||||
await this.render404(req, res, parsedUrl)
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
}
|
||||
|
||||
const { useFileSystemPublicRoutes } = this.nextConfig
|
||||
// re-create page's pathname
|
||||
let pathname = `/${params.path.join('/')}`
|
||||
pathname = getRouteFromAssetPath(pathname, '.json')
|
||||
|
||||
if (useFileSystemPublicRoutes) {
|
||||
this.appPathRoutes = this.getAppPathRoutes()
|
||||
// ensure trailing slash is normalized per config
|
||||
if (middleware) {
|
||||
if (this.nextConfig.trailingSlash && !pathname.endsWith('/')) {
|
||||
pathname += '/'
|
||||
}
|
||||
if (
|
||||
!this.nextConfig.trailingSlash &&
|
||||
pathname.length > 1 &&
|
||||
pathname.endsWith('/')
|
||||
) {
|
||||
pathname = pathname.substring(0, pathname.length - 1)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
headers: [],
|
||||
fsRoutes,
|
||||
rewrites: {
|
||||
beforeFiles: [],
|
||||
afterFiles: [],
|
||||
fallback: [],
|
||||
},
|
||||
redirects: [],
|
||||
catchAllRoute,
|
||||
catchAllMiddleware: [],
|
||||
useFileSystemPublicRoutes,
|
||||
matchers: this.matchers,
|
||||
nextConfig: this.nextConfig,
|
||||
if (this.nextConfig.i18n) {
|
||||
const { host } = req?.headers || {}
|
||||
// remove port from host and remove port if present
|
||||
const hostname = host?.split(':')[0].toLowerCase()
|
||||
const localePathResult = normalizeLocalePath(
|
||||
pathname,
|
||||
this.nextConfig.i18n.locales
|
||||
)
|
||||
const domainLocale = this.i18nProvider?.detectDomainLocale(hostname)
|
||||
|
||||
let detectedLocale = ''
|
||||
|
||||
if (localePathResult.detectedLocale) {
|
||||
pathname = localePathResult.pathname
|
||||
detectedLocale = localePathResult.detectedLocale
|
||||
}
|
||||
|
||||
parsedUrl.query.__nextLocale = detectedLocale
|
||||
parsedUrl.query.__nextDefaultLocale =
|
||||
domainLocale?.defaultLocale || this.nextConfig.i18n.defaultLocale
|
||||
|
||||
if (!detectedLocale && !middleware) {
|
||||
parsedUrl.query.__nextLocale = parsedUrl.query.__nextDefaultLocale
|
||||
await this.render404(req, res, parsedUrl)
|
||||
return { finished: true }
|
||||
}
|
||||
}
|
||||
parsedUrl.pathname = pathname
|
||||
parsedUrl.query.__nextDataReq = '1'
|
||||
|
||||
return { finished: false }
|
||||
}
|
||||
|
||||
protected async handleCatchallRenderRequest(
|
||||
req: BaseNextRequest,
|
||||
res: BaseNextResponse,
|
||||
parsedUrl: NextUrlWithParsedQuery
|
||||
): Promise<{ finished: boolean }> {
|
||||
let { pathname, query } = parsedUrl
|
||||
if (!pathname) {
|
||||
throw new Error('pathname is undefined')
|
||||
}
|
||||
|
||||
// interpolate query information into page for dynamic route
|
||||
// so that rewritten paths are handled properly
|
||||
const normalizedPage = this.serverOptions.webServerConfig.normalizedPage
|
||||
|
||||
if (pathname !== normalizedPage) {
|
||||
pathname = normalizedPage
|
||||
|
||||
if (isDynamicRoute(pathname)) {
|
||||
const routeRegex = getNamedRouteRegex(pathname, false)
|
||||
pathname = interpolateDynamicPath(pathname, query, routeRegex)
|
||||
normalizeVercelUrl(
|
||||
req,
|
||||
true,
|
||||
Object.keys(routeRegex.routeKeys),
|
||||
true,
|
||||
routeRegex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// next.js core assumes page path without trailing slash
|
||||
pathname = removeTrailingSlash(pathname)
|
||||
|
||||
if (this.i18nProvider) {
|
||||
const { detectedLocale } = await this.i18nProvider.analyze(pathname)
|
||||
if (detectedLocale) {
|
||||
parsedUrl.query.__nextLocale = detectedLocale
|
||||
}
|
||||
}
|
||||
|
||||
const bubbleNoFallback = !!query._nextBubbleNoFallback
|
||||
|
||||
if (isAPIRoute(pathname)) {
|
||||
delete query._nextBubbleNoFallback
|
||||
}
|
||||
|
||||
try {
|
||||
await this.render(req, res, pathname, query, parsedUrl, true)
|
||||
|
||||
return {
|
||||
finished: true,
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof NoFallbackError && bubbleNoFallback) {
|
||||
return {
|
||||
finished: false,
|
||||
}
|
||||
}
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,10 +52,10 @@ export function getPathMatch(path: string, options?: Options) {
|
|||
* `false` but if it does it will return an object with the matched params
|
||||
* merged with the params provided in the second argument.
|
||||
*/
|
||||
return <T extends { [key: string]: any }>(
|
||||
return (
|
||||
pathname?: string | null,
|
||||
params?: any
|
||||
): false | T => {
|
||||
): false | { [key: string]: any } => {
|
||||
const res = pathname == null ? false : matcher(pathname)
|
||||
if (!res) {
|
||||
return false
|
||||
|
|
|
@ -38,17 +38,21 @@ createNextDescribe(
|
|||
}, 'success')
|
||||
})
|
||||
|
||||
it('should render the 404 page when the file is removed, and restore the page when re-added', async () => {
|
||||
const browser = await next.browser('/')
|
||||
await check(() => browser.elementByCss('h1').text(), 'My page')
|
||||
await next.renameFile('./app/page.js', './app/foo.js')
|
||||
await check(
|
||||
() => browser.elementByCss('h1').text(),
|
||||
'This Is The Not Found Page'
|
||||
)
|
||||
await next.renameFile('./app/foo.js', './app/page.js')
|
||||
await check(() => browser.elementByCss('h1').text(), 'My page')
|
||||
})
|
||||
// TODO: investigate isEdge case
|
||||
if (!isEdge) {
|
||||
it('should render the 404 page when the file is removed, and restore the page when re-added', async () => {
|
||||
const browser = await next.browser('/')
|
||||
await check(() => browser.elementByCss('h1').text(), 'My page')
|
||||
await next.renameFile('./app/page.js', './app/foo.js')
|
||||
await check(
|
||||
() => browser.elementByCss('h1').text(),
|
||||
'This Is The Not Found Page'
|
||||
)
|
||||
// TODO: investigate flakey behavior
|
||||
// await next.renameFile('./app/foo.js', './app/page.js')
|
||||
// await check(() => browser.elementByCss('h1').text(), 'My page')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNextDev && !isEdge) {
|
||||
|
|
|
@ -6,6 +6,9 @@ createNextDescribe(
|
|||
files: __dirname,
|
||||
skipDeployment: true,
|
||||
startCommand: 'node server.js',
|
||||
dependencies: {
|
||||
'get-port': '5.1.1',
|
||||
},
|
||||
},
|
||||
({ next }) => {
|
||||
it.each(['/', '/render'])('should render %s', async (page) => {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
const http = require('http')
|
||||
const { parse } = require('url')
|
||||
const next = require('next')
|
||||
const getPort = require('get-port')
|
||||
|
||||
async function main() {
|
||||
const dev = process.env.NEXT_TEST_MODE === 'dev'
|
||||
process.env.NODE_ENV = dev ? 'development' : 'production'
|
||||
|
||||
const port = parseInt(process.env.PORT, 10) || 3000
|
||||
|
||||
const port = await getPort()
|
||||
const app = next({ dev, port })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
|
@ -34,7 +34,7 @@ async function main() {
|
|||
process.exit(1)
|
||||
})
|
||||
|
||||
server.listen(port, () => {
|
||||
server.listen(port, '0.0.0.0', () => {
|
||||
console.log(
|
||||
`> started server on url: http://localhost:${port} as ${
|
||||
dev ? 'development' : process.env.NODE_ENV
|
||||
|
|
|
@ -178,35 +178,6 @@ describe('Middleware fetches with body', () => {
|
|||
})
|
||||
|
||||
describe('with custom bodyParser sizeLimit (5mb)', () => {
|
||||
it('should return 413 for body equal to 10mb', async () => {
|
||||
const bodySize = 10 * 1024 * 1024
|
||||
const body = 't'.repeat(bodySize)
|
||||
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/api/size_limit_5mb',
|
||||
{},
|
||||
{
|
||||
body,
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
expect(res.status).toBe(413)
|
||||
|
||||
if (!(global as any).isNextDeploy) {
|
||||
expect(res.statusText).toBe('Body exceeded 5mb limit')
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: investigate occasional EPIPE errors causing
|
||||
// a 500 status instead of a 413
|
||||
if (res.status !== 500) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('should return 413 for body greater than 5mb', async () => {
|
||||
const bodySize = 5 * 1024 * 1024 + 1
|
||||
const body = 'u'.repeat(bodySize)
|
||||
|
@ -308,4 +279,33 @@ describe('Middleware fetches with body', () => {
|
|||
).toBe(bodySize / 32 + 1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should return 413 for body equal to 10mb', async () => {
|
||||
const bodySize = 10 * 1024 * 1024
|
||||
const body = 't'.repeat(bodySize)
|
||||
|
||||
const res = await fetchViaHTTP(
|
||||
next.url,
|
||||
'/api/size_limit_5mb',
|
||||
{},
|
||||
{
|
||||
body,
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
expect(res.status).toBe(413)
|
||||
|
||||
if (!(global as any).isNextDeploy) {
|
||||
expect(res.statusText).toBe('Body exceeded 5mb limit')
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: investigate occasional EPIPE errors causing
|
||||
// a 500 status instead of a 413
|
||||
if (res.status !== 500) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -116,7 +116,18 @@ export async function middleware(request) {
|
|||
}
|
||||
|
||||
if (url.pathname === '/global') {
|
||||
return serializeData(JSON.stringify({ process: { env: process.env } }))
|
||||
return serializeData(
|
||||
JSON.stringify({
|
||||
process: {
|
||||
env: {
|
||||
ANOTHER_MIDDLEWARE_TEST: process.env.ANOTHER_MIDDLEWARE_TEST,
|
||||
STRING_ENV_VAR: process.env.STRING_ENV_VAR,
|
||||
MIDDLEWARE_TEST: process.env.MIDDLEWARE_TEST,
|
||||
NEXT_RUNTIME: process.env.NEXT_RUNTIME,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (url.pathname.endsWith('/globalthis')) {
|
||||
|
|
|
@ -15,7 +15,12 @@ createNextDescribe(
|
|||
const traces = await next.readFile(traceFile)
|
||||
return traces
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.filter((val) => {
|
||||
if (val.includes('127.0.0.1')) {
|
||||
return false
|
||||
}
|
||||
return !!val
|
||||
})
|
||||
.map((line) => JSON.parse(line))
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,9 @@ function runTests() {
|
|||
killApp(app)
|
||||
})
|
||||
|
||||
it('should not throw if request body is already parsed in custom middleware', async () => {
|
||||
// TODO: we can't allow req fields with the proxying required for separate
|
||||
// workers
|
||||
it.skip('should not throw if request body is already parsed in custom middleware', async () => {
|
||||
await startServer()
|
||||
const data = await makeRequest()
|
||||
expect(data).toEqual([{ title: 'Nextjs' }])
|
||||
|
|
|
@ -3,96 +3,86 @@
|
|||
import { join } from 'path'
|
||||
import cheerio from 'cheerio'
|
||||
import {
|
||||
stopApp,
|
||||
startApp,
|
||||
nextBuild,
|
||||
nextServer,
|
||||
fetchViaHTTP,
|
||||
findPort,
|
||||
launchApp,
|
||||
killApp,
|
||||
nextStart,
|
||||
} from 'next-test-utils'
|
||||
|
||||
const appDir = join(__dirname, '../')
|
||||
let appPort
|
||||
let server
|
||||
let app
|
||||
|
||||
const respectsSideEffects = async () => {
|
||||
const res = await fetchViaHTTP(appPort, '/')
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const expectSideEffectsOrder = ['_document', '_app', 'page']
|
||||
|
||||
const sideEffectCalls = $('.side-effect-calls')
|
||||
|
||||
Array.from(sideEffectCalls).forEach((sideEffectCall, index) => {
|
||||
expect($(sideEffectCall).text()).toEqual(expectSideEffectsOrder[index])
|
||||
})
|
||||
}
|
||||
|
||||
const respectsChunkAttachmentOrder = async () => {
|
||||
const res = await fetchViaHTTP(appPort, '/')
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const requiredByRegex = /^\/_next\/static\/chunks\/(requiredBy\w*).*\.js/
|
||||
const chunks = Array.from($('head').contents())
|
||||
.filter(
|
||||
(child) =>
|
||||
child.type === 'script' &&
|
||||
child.name === 'script' &&
|
||||
child.attribs.src.match(requiredByRegex)
|
||||
)
|
||||
.map((child) => child.attribs.src.match(requiredByRegex)[1])
|
||||
|
||||
const requiredByAppIndex = chunks.indexOf('requiredByApp')
|
||||
const requiredByPageIndex = chunks.indexOf('requiredByPage')
|
||||
|
||||
expect(requiredByAppIndex).toBeLessThan(requiredByPageIndex)
|
||||
}
|
||||
|
||||
describe('Root components import order', () => {
|
||||
beforeAll(async () => {
|
||||
await nextBuild(appDir)
|
||||
app = nextServer({
|
||||
dir: join(__dirname, '../'),
|
||||
dev: false,
|
||||
quiet: true,
|
||||
})
|
||||
|
||||
server = await startApp(app)
|
||||
appPort = server.address().port
|
||||
appPort = await findPort()
|
||||
app = await nextStart(appDir, appPort)
|
||||
})
|
||||
afterAll(() => stopApp(server))
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
const respectsSideEffects = async () => {
|
||||
const res = await fetchViaHTTP(appPort, '/')
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
it(
|
||||
'_app chunks should be attached to de dom before page chunks',
|
||||
respectsChunkAttachmentOrder
|
||||
)
|
||||
it(
|
||||
'root components should be imported in this order _document > _app > page in order to respect side effects',
|
||||
respectsSideEffects
|
||||
)
|
||||
})
|
||||
|
||||
const expectSideEffectsOrder = ['_document', '_app', 'page']
|
||||
describe('on dev server', () => {
|
||||
beforeAll(async () => {
|
||||
appPort = await findPort()
|
||||
app = await launchApp(join(__dirname, '../'), appPort)
|
||||
})
|
||||
|
||||
const sideEffectCalls = $('.side-effect-calls')
|
||||
|
||||
Array.from(sideEffectCalls).forEach((sideEffectCall, index) => {
|
||||
expect($(sideEffectCall).text()).toEqual(expectSideEffectsOrder[index])
|
||||
})
|
||||
}
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
it(
|
||||
'root components should be imported in this order _document > _app > page in order to respect side effects',
|
||||
respectsSideEffects
|
||||
)
|
||||
|
||||
const respectsChunkAttachmentOrder = async () => {
|
||||
const res = await fetchViaHTTP(appPort, '/')
|
||||
const html = await res.text()
|
||||
const $ = cheerio.load(html)
|
||||
|
||||
const requiredByRegex = /^\/_next\/static\/chunks\/(requiredBy\w*).*\.js/
|
||||
const chunks = Array.from($('head').contents())
|
||||
.filter(
|
||||
(child) =>
|
||||
child.type === 'script' &&
|
||||
child.name === 'script' &&
|
||||
child.attribs.src.match(requiredByRegex)
|
||||
)
|
||||
.map((child) => child.attribs.src.match(requiredByRegex)[1])
|
||||
|
||||
const requiredByAppIndex = chunks.indexOf('requiredByApp')
|
||||
const requiredByPageIndex = chunks.indexOf('requiredByPage')
|
||||
|
||||
expect(requiredByAppIndex).toBeLessThan(requiredByPageIndex)
|
||||
}
|
||||
|
||||
it(
|
||||
'_app chunks should be attached to de dom before page chunks',
|
||||
respectsChunkAttachmentOrder
|
||||
)
|
||||
|
||||
describe('on dev server', () => {
|
||||
beforeAll(async () => {
|
||||
appPort = await findPort()
|
||||
app = await launchApp(join(__dirname, '../'), appPort)
|
||||
})
|
||||
|
||||
afterAll(() => killApp(app))
|
||||
|
||||
it(
|
||||
'root components should be imported in this order _document > _app > page in order to respect side effects',
|
||||
respectsSideEffects
|
||||
)
|
||||
|
||||
it(
|
||||
'_app chunks should be attached to de dom before page chunks',
|
||||
respectsChunkAttachmentOrder
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -23,6 +23,10 @@ const httpOptions = {
|
|||
cert: readFileSync(join(__dirname, 'ssh/localhost.pem')),
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.error('- error unhandledRejection:', err)
|
||||
})
|
||||
|
||||
app.prepare().then(() => {
|
||||
const server = createServer(httpOptions, async (req, res) => {
|
||||
if (req.url === '/no-query') {
|
||||
|
|
|
@ -23,7 +23,8 @@ let server
|
|||
|
||||
const context = {}
|
||||
|
||||
describe.each([
|
||||
// TODO: investigate this test stalling in CI
|
||||
describe.skip.each([
|
||||
{ title: 'using HTTP', useHttps: false },
|
||||
{ title: 'using HTTPS', useHttps: true },
|
||||
])('Custom Server $title', ({ useHttps }) => {
|
||||
|
@ -38,7 +39,7 @@ describe.each([
|
|||
const startServer = async (optEnv = {}, opts) => {
|
||||
const scriptPath = join(appDir, 'server.js')
|
||||
context.appPort = appPort = await getPort()
|
||||
nextUrl = `http${useHttps ? 's' : ''}://127.0.0.1:${context.appPort}`
|
||||
nextUrl = `http${useHttps ? 's' : ''}://localhost:${context.appPort}`
|
||||
|
||||
const env = Object.assign(
|
||||
{ ...process.env },
|
||||
|
@ -55,7 +56,8 @@ describe.each([
|
|||
)
|
||||
}
|
||||
|
||||
describe('with dynamic assetPrefix', () => {
|
||||
// TODO: continue supporting this or remove it?
|
||||
describe.skip('with dynamic assetPrefix', () => {
|
||||
beforeAll(() => startServer())
|
||||
afterAll(() => killApp(server))
|
||||
|
||||
|
@ -280,7 +282,7 @@ describe.each([
|
|||
expect(stderr).toContain(
|
||||
'- error unhandledRejection: Error: unhandled rejection'
|
||||
)
|
||||
expect(stderr).toContain('server.js:33:22')
|
||||
expect(stderr).toContain('server.js:37:22')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -63,10 +63,11 @@ export function runTests(ctx) {
|
|||
|
||||
for (const locale of locales) {
|
||||
for (const asset of assets) {
|
||||
require('console').log({ locale, asset })
|
||||
// _next/static asset
|
||||
const res = await fetchViaHTTP(
|
||||
ctx.appPort,
|
||||
`${ctx.basePath || ''}/${locale}/_next/static/${asset}`,
|
||||
`${ctx.basePath || ''}/${locale}/_next/static/${encodeURI(asset)}`,
|
||||
undefined,
|
||||
{ redirect: 'manual' }
|
||||
)
|
||||
|
|
|
@ -14,6 +14,7 @@ describe('Production Usage without production build', () => {
|
|||
dir: appDir,
|
||||
dev: false,
|
||||
quiet: true,
|
||||
customServer: false,
|
||||
})
|
||||
await srv.prepare()
|
||||
}).rejects.toThrow(/Could not find a production build in the/)
|
||||
|
|
|
@ -7,4 +7,5 @@ module.exports = {
|
|||
},
|
||||
]
|
||||
},
|
||||
output: 'standalone',
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ describe('Required Server Files', () => {
|
|||
quiet: false,
|
||||
minimalMode: true,
|
||||
})
|
||||
await nextApp.prepare()
|
||||
appPort = await findPort()
|
||||
|
||||
server = http.createServer(async (req, res) => {
|
||||
|
@ -442,27 +443,21 @@ describe('Required Server Files', () => {
|
|||
errors = []
|
||||
const res = await fetchViaHTTP(appPort, '/errors/gip', { crash: '1' })
|
||||
expect(res.status).toBe(500)
|
||||
expect(await res.text()).toBe('error')
|
||||
expect(errors.length).toBe(1)
|
||||
expect(errors[0].message).toContain('gip hit an oops')
|
||||
expect(await res.text()).toBe('Internal Server Error')
|
||||
})
|
||||
|
||||
it('should bubble error correctly for gssp page', async () => {
|
||||
errors = []
|
||||
const res = await fetchViaHTTP(appPort, '/errors/gssp', { crash: '1' })
|
||||
expect(res.status).toBe(500)
|
||||
expect(await res.text()).toBe('error')
|
||||
expect(errors.length).toBe(1)
|
||||
expect(errors[0].message).toContain('gssp hit an oops')
|
||||
expect(await res.text()).toBe('Internal Server Error')
|
||||
})
|
||||
|
||||
it('should bubble error correctly for gsp page', async () => {
|
||||
errors = []
|
||||
const res = await fetchViaHTTP(appPort, '/errors/gsp/crash')
|
||||
expect(res.status).toBe(500)
|
||||
expect(await res.text()).toBe('error')
|
||||
expect(errors.length).toBe(1)
|
||||
expect(errors[0].message).toContain('gsp hit an oops')
|
||||
expect(await res.text()).toBe('Internal Server Error')
|
||||
})
|
||||
|
||||
it('should normalize optional values correctly for SSP page', async () => {
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
import path from 'path'
|
||||
import fs from 'fs-extra'
|
||||
import { check, findPort, killApp, launchApp, nextBuild } from 'next-test-utils'
|
||||
import {
|
||||
check,
|
||||
findPort,
|
||||
killApp,
|
||||
launchApp,
|
||||
nextBuild,
|
||||
renderViaHTTP,
|
||||
} from 'next-test-utils'
|
||||
|
||||
const appDir = path.join(__dirname, '..')
|
||||
|
||||
|
@ -48,6 +55,7 @@ describe('page features telemetry', () => {
|
|||
turbo: true,
|
||||
})
|
||||
await check(() => stderr, /NEXT_CLI_SESSION_STARTED/)
|
||||
await renderViaHTTP(port, '/hello')
|
||||
|
||||
if (app) {
|
||||
await killApp(app)
|
||||
|
@ -89,6 +97,7 @@ describe('page features telemetry', () => {
|
|||
})
|
||||
|
||||
await check(() => stderr, /NEXT_CLI_SESSION_STARTED/)
|
||||
await renderViaHTTP(port, '/hello')
|
||||
|
||||
if (app) {
|
||||
await killApp(app)
|
||||
|
@ -129,6 +138,7 @@ describe('page features telemetry', () => {
|
|||
})
|
||||
|
||||
await check(() => stderr, /NEXT_CLI_SESSION_STARTED/)
|
||||
await renderViaHTTP(port, '/hello')
|
||||
|
||||
if (app) {
|
||||
await killApp(app)
|
||||
|
|
|
@ -362,36 +362,40 @@ export class NextInstance {
|
|||
}
|
||||
|
||||
public async destroy(): Promise<void> {
|
||||
if (this.isDestroyed) {
|
||||
throw new Error(`next instance already destroyed`)
|
||||
}
|
||||
this.isDestroyed = true
|
||||
this.emit('destroy', [])
|
||||
await this.stop()
|
||||
try {
|
||||
if (this.isDestroyed) {
|
||||
throw new Error(`next instance already destroyed`)
|
||||
}
|
||||
this.isDestroyed = true
|
||||
this.emit('destroy', [])
|
||||
await this.stop().catch(console.error)
|
||||
|
||||
if (process.env.TRACE_PLAYWRIGHT) {
|
||||
await fs
|
||||
.copy(
|
||||
path.join(this.testDir, '.next/trace'),
|
||||
path.join(
|
||||
__dirname,
|
||||
'../../traces',
|
||||
`${path
|
||||
.relative(
|
||||
path.join(__dirname, '../../'),
|
||||
process.env.TEST_FILE_PATH
|
||||
)
|
||||
.replace(/\//g, '-')}`,
|
||||
`next-trace`
|
||||
if (process.env.TRACE_PLAYWRIGHT) {
|
||||
await fs
|
||||
.copy(
|
||||
path.join(this.testDir, '.next/trace'),
|
||||
path.join(
|
||||
__dirname,
|
||||
'../../traces',
|
||||
`${path
|
||||
.relative(
|
||||
path.join(__dirname, '../../'),
|
||||
process.env.TEST_FILE_PATH
|
||||
)
|
||||
.replace(/\//g, '-')}`,
|
||||
`next-trace`
|
||||
)
|
||||
)
|
||||
)
|
||||
.catch(() => {})
|
||||
}
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
if (!process.env.NEXT_TEST_SKIP_CLEANUP) {
|
||||
await fs.remove(this.testDir)
|
||||
if (!process.env.NEXT_TEST_SKIP_CLEANUP) {
|
||||
await fs.remove(this.testDir)
|
||||
}
|
||||
require('console').log(`destroyed next instance`)
|
||||
} catch (err) {
|
||||
require('console').error('Error while destroying', err)
|
||||
}
|
||||
require('console').log(`destroyed next instance`)
|
||||
}
|
||||
|
||||
public get url() {
|
||||
|
@ -420,25 +424,47 @@ export class NextInstance {
|
|||
public async readJSON(filename: string) {
|
||||
return fs.readJSON(path.join(this.testDir, filename))
|
||||
}
|
||||
private async handleDevWatchDelay(filename: string) {
|
||||
// to help alleviate flakiness with tests that create
|
||||
// dynamic routes // and then request it we give a buffer
|
||||
// of 500ms to allow WatchPack to detect the changed files
|
||||
// TODO: replace this with an event directly from WatchPack inside
|
||||
// router-server for better accuracy
|
||||
if (
|
||||
(global as any).isNextDev &&
|
||||
(filename.startsWith('app/') || filename.startsWith('pages/'))
|
||||
) {
|
||||
require('console').log('fs dev delay', filename)
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
}
|
||||
public async patchFile(filename: string, content: string) {
|
||||
const outputPath = path.join(this.testDir, filename)
|
||||
const newFile = !(await fs.pathExists(outputPath))
|
||||
await fs.ensureDir(path.dirname(outputPath))
|
||||
return fs.writeFile(outputPath, content)
|
||||
await fs.writeFile(outputPath, content)
|
||||
|
||||
if (newFile) {
|
||||
await this.handleDevWatchDelay(filename)
|
||||
}
|
||||
}
|
||||
public async renameFile(filename: string, newFilename: string) {
|
||||
return fs.rename(
|
||||
await fs.rename(
|
||||
path.join(this.testDir, filename),
|
||||
path.join(this.testDir, newFilename)
|
||||
)
|
||||
await this.handleDevWatchDelay(filename)
|
||||
}
|
||||
public async renameFolder(foldername: string, newFoldername: string) {
|
||||
return fs.move(
|
||||
await fs.move(
|
||||
path.join(this.testDir, foldername),
|
||||
path.join(this.testDir, newFoldername)
|
||||
)
|
||||
await this.handleDevWatchDelay(foldername)
|
||||
}
|
||||
public async deleteFile(filename: string) {
|
||||
return fs.remove(path.join(this.testDir, filename))
|
||||
await fs.remove(path.join(this.testDir, filename))
|
||||
await this.handleDevWatchDelay(filename)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,9 @@ createNextDescribe(
|
|||
{
|
||||
files: __dirname,
|
||||
startCommand: 'node server.js',
|
||||
dependencies: {
|
||||
'get-port': '5.1.1',
|
||||
},
|
||||
},
|
||||
({ next }) => {
|
||||
it.each(['a', 'b', 'c'])('can navigate to /%s', async (page) => {
|
||||
|
|
|
@ -1,36 +1,44 @@
|
|||
const { createServer } = require('http')
|
||||
const { parse } = require('url')
|
||||
const next = require('next')
|
||||
const getPort = require('get-port')
|
||||
|
||||
const hostname = 'localhost'
|
||||
const port = 3000
|
||||
// when using middleware `hostname` and `port` must be provided below
|
||||
const app = next({ hostname, port })
|
||||
const handle = app.getRequestHandler()
|
||||
async function main() {
|
||||
const port = await getPort()
|
||||
const hostname = 'localhost'
|
||||
// when using middleware `hostname` and `port` must be provided below
|
||||
const app = next({ hostname, port })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
app.prepare().then(() => {
|
||||
createServer(async (req, res) => {
|
||||
try {
|
||||
// Be sure to pass `true` as the second argument to `url.parse`.
|
||||
// This tells it to parse the query portion of the URL.
|
||||
const parsedUrl = parse(req.url, true)
|
||||
const { pathname, query } = parsedUrl
|
||||
app.prepare().then(() => {
|
||||
createServer(async (req, res) => {
|
||||
try {
|
||||
// Be sure to pass `true` as the second argument to `url.parse`.
|
||||
// This tells it to parse the query portion of the URL.
|
||||
const parsedUrl = parse(req.url, true)
|
||||
const { pathname, query } = parsedUrl
|
||||
|
||||
if (pathname === '/a') {
|
||||
await app.render(req, res, '/a', query)
|
||||
} else if (pathname === '/b') {
|
||||
await app.render(req, res, '/page-b', query)
|
||||
} else {
|
||||
await handle(req, res, parsedUrl)
|
||||
if (pathname === '/a') {
|
||||
await app.render(req, res, '/a', query)
|
||||
} else if (pathname === '/b') {
|
||||
await app.render(req, res, '/page-b', query)
|
||||
} else {
|
||||
await handle(req, res, parsedUrl)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error occurred handling', req.url, err)
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error occurred handling', req.url, err)
|
||||
res.statusCode = 500
|
||||
res.end('Internal Server Error')
|
||||
}
|
||||
}).listen(port, (err) => {
|
||||
if (err) throw err
|
||||
// Start mode
|
||||
console.log(`started server on url: http://${hostname}:${port}`)
|
||||
}).listen(port, '0.0.0.0', (err) => {
|
||||
if (err) throw err
|
||||
// Start mode
|
||||
console.log(`started server on url: http://${hostname}:${port}`)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
|
|
@ -71,7 +71,7 @@ describe('should set-up next', () => {
|
|||
testServer,
|
||||
(
|
||||
await fs.readFile(testServer, 'utf8')
|
||||
).replace('conf:', `minimalMode: ${minimalMode},conf:`)
|
||||
).replace('port:', `minimalMode: ${minimalMode},port:`)
|
||||
)
|
||||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
|
@ -161,6 +161,7 @@ describe('should set-up next', () => {
|
|||
['/api/isr/first', 'isr-page,/api/isr/[slug]/route'],
|
||||
['/api/isr/second', 'isr-page,/api/isr/[slug]/route'],
|
||||
]) {
|
||||
require('console').error('checking', { path, tags })
|
||||
const res = await fetchViaHTTP(appPort, path, undefined, {
|
||||
redirect: 'manual',
|
||||
})
|
||||
|
|
|
@ -107,7 +107,7 @@ describe('should set-up next', () => {
|
|||
testServer,
|
||||
(
|
||||
await fs.readFile(testServer, 'utf8')
|
||||
).replace('conf:', 'minimalMode: true,conf:')
|
||||
).replace('port:', 'minimalMode: true,port:')
|
||||
)
|
||||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
|
@ -587,35 +587,41 @@ describe('should set-up next', () => {
|
|||
})
|
||||
|
||||
it('should bubble error correctly for gip page', async () => {
|
||||
errors = []
|
||||
const res = await fetchViaHTTP(appPort, '/errors/gip', { crash: '1' })
|
||||
expect(res.status).toBe(500)
|
||||
expect(await res.text()).toBe('Internal Server Error')
|
||||
|
||||
await check(
|
||||
() => (errors[0].includes('gip hit an oops') ? 'success' : errors[0]),
|
||||
() =>
|
||||
errors.join('').includes('gip hit an oops')
|
||||
? 'success'
|
||||
: errors.join('\n'),
|
||||
'success'
|
||||
)
|
||||
})
|
||||
|
||||
it('should bubble error correctly for gssp page', async () => {
|
||||
errors = []
|
||||
const res = await fetchViaHTTP(appPort, '/errors/gssp', { crash: '1' })
|
||||
expect(res.status).toBe(500)
|
||||
expect(await res.text()).toBe('Internal Server Error')
|
||||
await check(
|
||||
() => (errors[0].includes('gssp hit an oops') ? 'success' : errors[0]),
|
||||
() =>
|
||||
errors.join('\n').includes('gssp hit an oops')
|
||||
? 'success'
|
||||
: errors.join('\n'),
|
||||
'success'
|
||||
)
|
||||
})
|
||||
|
||||
it('should bubble error correctly for gsp page', async () => {
|
||||
errors = []
|
||||
const res = await fetchViaHTTP(appPort, '/errors/gsp/crash')
|
||||
expect(res.status).toBe(500)
|
||||
expect(await res.text()).toBe('Internal Server Error')
|
||||
await check(
|
||||
() => (errors[0].includes('gsp hit an oops') ? 'success' : errors[0]),
|
||||
() =>
|
||||
errors.join('\n').includes('gsp hit an oops')
|
||||
? 'success'
|
||||
: errors.join('\n'),
|
||||
'success'
|
||||
)
|
||||
})
|
||||
|
@ -627,9 +633,9 @@ describe('should set-up next', () => {
|
|||
expect(await res.text()).toBe('Internal Server Error')
|
||||
await check(
|
||||
() =>
|
||||
errors[0].includes('some error from /api/error')
|
||||
errors.join('\n').includes('some error from /api/error')
|
||||
? 'success'
|
||||
: errors[0],
|
||||
: errors.join('\n'),
|
||||
'success'
|
||||
)
|
||||
})
|
||||
|
|
|
@ -113,7 +113,7 @@ describe('should set-up next', () => {
|
|||
testServer,
|
||||
(
|
||||
await fs.readFile(testServer, 'utf8')
|
||||
).replace('conf:', `minimalMode: ${minimalMode},conf:`)
|
||||
).replace('port:', `minimalMode: ${minimalMode},port:`)
|
||||
)
|
||||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
|
@ -956,7 +956,10 @@ describe('should set-up next', () => {
|
|||
expect(await res.text()).toBe('Internal Server Error')
|
||||
|
||||
await check(
|
||||
() => (errors[0].includes('gip hit an oops') ? 'success' : errors[0]),
|
||||
() =>
|
||||
errors.join('\n').includes('gip hit an oops')
|
||||
? 'success'
|
||||
: errors.join('\n'),
|
||||
'success'
|
||||
)
|
||||
})
|
||||
|
@ -967,7 +970,10 @@ describe('should set-up next', () => {
|
|||
expect(res.status).toBe(500)
|
||||
expect(await res.text()).toBe('Internal Server Error')
|
||||
await check(
|
||||
() => (errors[0].includes('gssp hit an oops') ? 'success' : errors[0]),
|
||||
() =>
|
||||
errors.join('\n').includes('gssp hit an oops')
|
||||
? 'success'
|
||||
: errors.join('\n'),
|
||||
'success'
|
||||
)
|
||||
})
|
||||
|
@ -978,7 +984,10 @@ describe('should set-up next', () => {
|
|||
expect(res.status).toBe(500)
|
||||
expect(await res.text()).toBe('Internal Server Error')
|
||||
await check(
|
||||
() => (errors[0].includes('gsp hit an oops') ? 'success' : errors[0]),
|
||||
() =>
|
||||
errors.join('\n').includes('gsp hit an oops')
|
||||
? 'success'
|
||||
: errors.join('\n'),
|
||||
'success'
|
||||
)
|
||||
})
|
||||
|
@ -990,9 +999,9 @@ describe('should set-up next', () => {
|
|||
expect(await res.text()).toBe('Internal Server Error')
|
||||
await check(
|
||||
() =>
|
||||
errors[0].includes('some error from /api/error')
|
||||
errors.join('\n').includes('some error from /api/error')
|
||||
? 'success'
|
||||
: errors[0],
|
||||
: errors.join('\n'),
|
||||
'success'
|
||||
)
|
||||
})
|
||||
|
@ -1267,10 +1276,6 @@ describe('should set-up next', () => {
|
|||
})
|
||||
|
||||
it('should run middleware correctly (without minimalMode, with wasm)', async () => {
|
||||
await next.destroy()
|
||||
await killApp(server)
|
||||
await setupNext({ nextEnv: false, minimalMode: false })
|
||||
|
||||
const standaloneDir = join(next.testDir, 'standalone')
|
||||
|
||||
const testServer = join(standaloneDir, 'server.js')
|
||||
|
@ -1313,8 +1318,5 @@ describe('should set-up next', () => {
|
|||
|
||||
expect(resImageResponse.status).toBe(200)
|
||||
expect(resImageResponse.headers.get('content-type')).toBe('image/png')
|
||||
|
||||
// when not in next env should be compress: true
|
||||
expect(fs.readFileSync(testServer, 'utf8')).toContain('"compress":true')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -58,7 +58,7 @@ describe('minimal-mode-response-cache', () => {
|
|||
testServer,
|
||||
(await fs.readFile(testServer, 'utf8'))
|
||||
.replace('console.error(err)', `console.error('top-level', err)`)
|
||||
.replace('conf:', 'minimalMode: true,conf:')
|
||||
.replace('port:', 'minimalMode: true,port:')
|
||||
)
|
||||
appPort = await findPort()
|
||||
server = await initNextServerScript(
|
||||
|
|
Loading…
Reference in a new issue