diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index eeda0ee96c6a0..c9925716d8a9b 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -32,6 +32,7 @@ import { FileType, fileExists } from '../lib/file-exists' import { findPagesDir } from '../lib/find-pages-dir' import loadCustomRoutes, { CustomRoutes, + Header, normalizeRouteRegex, Redirect, Rewrite, @@ -170,47 +171,46 @@ export type PrerenderManifest = { preview: __ApiPreviewProps } -type CustomRoute = { +type ManifestBuiltRoute = { + /** + * The route pattern used to match requests for this route. + */ regex: string - statusCode?: number | undefined - permanent?: undefined - source: string - locale?: false | undefined - basePath?: false | undefined - destination?: string | undefined +} + +export type ManifestRewriteRoute = ManifestBuiltRoute & Rewrite +export type ManifestRedirectRoute = ManifestBuiltRoute & Redirect +export type ManifestHeaderRoute = ManifestBuiltRoute & Header + +export type ManifestRoute = ManifestBuiltRoute & { + page: string + namedRegex?: string + routeKeys?: { [key: string]: string } +} + +export type ManifestDataRoute = { + page: string + routeKeys?: { [key: string]: string } + dataRouteRegex: string + namedDataRouteRegex?: string } export type RoutesManifest = { version: number pages404: boolean basePath: string - redirects: Array + redirects: Array rewrites?: - | Array + | Array | { - beforeFiles: Array - afterFiles: Array - fallback: Array + beforeFiles: Array + afterFiles: Array + fallback: Array } - headers: Array - 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 - }> + headers: Array + staticRoutes: Array + dynamicRoutes: Array + dataRoutes: Array i18n?: { domains?: Array<{ http?: true @@ -231,6 +231,52 @@ export type RoutesManifest = { caseSensitive?: boolean } +export function buildCustomRoute( + type: 'header', + route: Header +): ManifestHeaderRoute +export function buildCustomRoute( + type: 'rewrite', + route: Rewrite +): ManifestRewriteRoute +export function buildCustomRoute( + type: 'redirect', + route: Redirect, + restrictedRedirectPaths: string[] +): ManifestRedirectRoute +export function buildCustomRoute( + type: RouteType, + route: Redirect | Rewrite | Header, + restrictedRedirectPaths?: string[] +): ManifestHeaderRoute | ManifestRewriteRoute | ManifestRedirectRoute { + const compiled = pathToRegexp(route.source, [], { + strict: true, + sensitive: false, + delimiter: '/', // default is `/#?`, but Next does not pass query info + }) + + let source = compiled.source + if (!route.internal) { + source = modifyRouteRegex( + source, + type === 'redirect' ? restrictedRedirectPaths : undefined + ) + } + + const regex = normalizeRouteRegex(source) + + if (type !== 'redirect') { + return { ...route, regex } + } + + return { + ...route, + statusCode: getRedirectStatus(route as Redirect), + permanent: undefined, + regex, + } +} + async function generateClientSsgManifest( prerenderManifest: PrerenderManifest, { @@ -710,44 +756,6 @@ export default async function build( config.basePath ? `${config.basePath}${p}` : p ) - const buildCustomRoute = ( - r: { - source: string - locale?: false - basePath?: false - statusCode?: number - destination?: string - }, - type: RouteType - ) => { - const keys: any[] = [] - - const routeRegex = pathToRegexp(r.source, keys, { - strict: true, - sensitive: false, - delimiter: '/', // default is `/#?`, but Next does not pass query info - }) - let regexSource = routeRegex.source - - if (!(r as any).internal) { - regexSource = modifyRouteRegex( - routeRegex.source, - type === 'redirect' ? restrictedRedirectPaths : undefined - ) - } - - return { - ...r, - ...(type === 'redirect' - ? { - statusCode: getRedirectStatus(r as Redirect), - permanent: undefined, - } - : {}), - regex: normalizeRouteRegex(regexSource), - } - } - const routesManifestPath = path.join(distDir, ROUTES_MANIFEST) const routesManifest: RoutesManifest = nextBuildSpan .traceChild('generate-routes-manifest') @@ -772,10 +780,10 @@ export default async function build( pages404: true, caseSensitive: !!config.experimental.caseSensitiveRoutes, basePath: config.basePath, - redirects: redirects.map((r: any) => - buildCustomRoute(r, 'redirect') + redirects: redirects.map((r) => + buildCustomRoute('redirect', r, restrictedRedirectPaths) ), - headers: headers.map((r: any) => buildCustomRoute(r, 'header')), + headers: headers.map((r) => buildCustomRoute('header', r)), dynamicRoutes, staticRoutes, dataRoutes: [], @@ -791,22 +799,23 @@ export default async function build( }) if (rewrites.beforeFiles.length === 0 && rewrites.fallback.length === 0) { - routesManifest.rewrites = rewrites.afterFiles.map((r: any) => - buildCustomRoute(r, 'rewrite') + routesManifest.rewrites = rewrites.afterFiles.map((r) => + buildCustomRoute('rewrite', r) ) } else { routesManifest.rewrites = { - beforeFiles: rewrites.beforeFiles.map((r: any) => - buildCustomRoute(r, 'rewrite') + beforeFiles: rewrites.beforeFiles.map((r) => + buildCustomRoute('rewrite', r) ), - afterFiles: rewrites.afterFiles.map((r: any) => - buildCustomRoute(r, 'rewrite') + afterFiles: rewrites.afterFiles.map((r) => + buildCustomRoute('rewrite', r) ), - fallback: rewrites.fallback.map((r: any) => - buildCustomRoute(r, 'rewrite') + fallback: rewrites.fallback.map((r) => + buildCustomRoute('rewrite', r) ), } } + const combinedRewrites: Rewrite[] = [ ...rewrites.beforeFiles, ...rewrites.afterFiles, @@ -2057,7 +2066,7 @@ export default async function build( await fs.copyFile(cachedTracePath, nextServerTraceOutput) return } - } catch (_) {} + } catch {} } const root = diff --git a/packages/next/src/build/load-jsconfig.ts b/packages/next/src/build/load-jsconfig.ts index 7dac1bab01bb1..c865463f977a5 100644 --- a/packages/next/src/build/load-jsconfig.ts +++ b/packages/next/src/build/load-jsconfig.ts @@ -51,7 +51,7 @@ export default async function loadJsConfig( }, ]) typeScriptPath = deps.resolved.get('typescript') - } catch (_) {} + } catch {} const tsConfigPath = path.join(dir, config.typescript.tsconfigPath) const useTypeScript = Boolean( typeScriptPath && (await fileExists(tsConfigPath)) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index e7e3d8e3e72d9..c50bbfcce00eb 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -3023,7 +3023,7 @@ export default async function getBaseWebpackConfig( if (rule(input)) { return true } - } catch (_) {} + } catch {} return false }) ) { diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts index a14f9da5faacf..d95d0d25c18ee 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts @@ -4,7 +4,7 @@ function getSocketProtocol(assetPrefix: string): string { try { // assetPrefix is a url protocol = new URL(assetPrefix).protocol - } catch (_) {} + } catch {} return protocol === 'http:' ? 'ws' : 'wss' } diff --git a/packages/next/src/client/dev/error-overlay/websocket.ts b/packages/next/src/client/dev/error-overlay/websocket.ts index 75c7db451f656..c29adf7299241 100644 --- a/packages/next/src/client/dev/error-overlay/websocket.ts +++ b/packages/next/src/client/dev/error-overlay/websocket.ts @@ -8,7 +8,7 @@ function getSocketProtocol(assetPrefix: string): string { try { // assetPrefix is a url protocol = new URL(assetPrefix).protocol - } catch (_) {} + } catch {} return protocol === 'http:' ? 'ws' : 'wss' } diff --git a/packages/next/src/export/index.ts b/packages/next/src/export/index.ts index 209855a8a033a..37801f74bb6df 100644 --- a/packages/next/src/export/index.ts +++ b/packages/next/src/export/index.ts @@ -269,7 +269,7 @@ export default async function exportApp( let prerenderManifest: PrerenderManifest | undefined = undefined try { prerenderManifest = require(join(distDir, PRERENDER_MANIFEST)) - } catch (_) {} + } catch {} let appRoutePathManifest: Record | undefined = undefined try { @@ -589,7 +589,7 @@ export default async function exportApp( )) as MiddlewareManifest hasMiddleware = Object.keys(middlewareManifest.middleware).length > 0 - } catch (_) {} + } catch {} // Warn if the user defines a path for an API page if (hasApiRoutes || hasMiddleware) { diff --git a/packages/next/src/lib/create-client-router-filter.ts b/packages/next/src/lib/create-client-router-filter.ts index 1de02c9802015..dcf0a6a0bd43c 100644 --- a/packages/next/src/lib/create-client-router-filter.ts +++ b/packages/next/src/lib/create-client-router-filter.ts @@ -47,7 +47,7 @@ export function createClientRouterFilter( try { tokens = tryToParsePath(source).tokens || [] - } catch (_) {} + } catch {} if (tokens.every((token) => typeof token === 'string')) { // only include static redirects initially diff --git a/packages/next/src/lib/load-custom-routes.ts b/packages/next/src/lib/load-custom-routes.ts index 45e07d4afc31c..232fc972759f2 100644 --- a/packages/next/src/lib/load-custom-routes.ts +++ b/packages/next/src/lib/load-custom-routes.ts @@ -25,6 +25,11 @@ export type Rewrite = { locale?: false has?: RouteHas[] missing?: RouteHas[] + + /** + * @internal - used internally for routing + */ + internal?: boolean } export type Header = { @@ -34,6 +39,11 @@ export type Header = { headers: Array<{ key: string; value: string }> has?: RouteHas[] missing?: RouteHas[] + + /** + * @internal - used internally for routing + */ + internal?: boolean } // internal type used for validation (not user facing) @@ -44,6 +54,11 @@ export type Redirect = { locale?: false has?: RouteHas[] missing?: RouteHas[] + + /** + * @internal - used internally for routing + */ + internal?: boolean } & ( | { statusCode?: never @@ -688,14 +703,14 @@ export default async function loadCustomRoutes( key: 'x-nextjs-data', }, ], - } as Redirect, + }, { source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)', destination: '/:notfile/', permanent: true, locale: config.i18n ? false : undefined, internal: true, - } as Redirect + } ) if (config.basePath) { redirects.unshift({ @@ -705,7 +720,7 @@ export default async function loadCustomRoutes( basePath: false, locale: config.i18n ? false : undefined, internal: true, - } as Redirect) + }) } } else { redirects.unshift({ @@ -714,7 +729,7 @@ export default async function loadCustomRoutes( permanent: true, locale: config.i18n ? false : undefined, internal: true, - } as Redirect) + }) if (config.basePath) { redirects.unshift({ source: config.basePath + '/', @@ -723,7 +738,7 @@ export default async function loadCustomRoutes( basePath: false, locale: config.i18n ? false : undefined, internal: true, - } as Redirect) + }) } } } diff --git a/packages/next/src/lib/metadata/resolvers/resolve-url.ts b/packages/next/src/lib/metadata/resolvers/resolve-url.ts index 2681aef4991b5..959ecaf9c41e3 100644 --- a/packages/next/src/lib/metadata/resolvers/resolve-url.ts +++ b/packages/next/src/lib/metadata/resolvers/resolve-url.ts @@ -58,7 +58,7 @@ function resolveUrl( // If we can construct a URL instance from url, ignore metadataBase const parsedUrl = new URL(url) return parsedUrl - } catch (_) {} + } catch {} if (!metadataBase) { metadataBase = createLocalMetadataBase() diff --git a/packages/next/src/lib/recursive-delete.ts b/packages/next/src/lib/recursive-delete.ts index 6db6e4ab0c005..78f3ea4948fc7 100644 --- a/packages/next/src/lib/recursive-delete.ts +++ b/packages/next/src/lib/recursive-delete.ts @@ -73,7 +73,7 @@ export async function recursiveDelete( : join(dirname(absolutePath), linkPath) ) isDirectory = stats.isDirectory() - } catch (_) {} + } catch {} } const pp = join(previousPath, part.name) diff --git a/packages/next/src/server/app-render/action-handler.ts b/packages/next/src/server/app-render/action-handler.ts index eb0822e571fb2..6f184fab120a4 100644 --- a/packages/next/src/server/app-render/action-handler.ts +++ b/packages/next/src/server/app-render/action-handler.ts @@ -481,7 +481,7 @@ export async function handleAction({ const promise = Promise.reject(err) try { await promise - } catch (_) {} + } catch {} return generateFlight({ skipFlight: false, actionResult: promise, @@ -497,7 +497,7 @@ export async function handleAction({ const promise = Promise.reject(err) try { await promise - } catch (_) {} + } catch {} return generateFlight({ actionResult: promise, diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts index c3e23e1bf004c..5baafaa487ae4 100644 --- a/packages/next/src/server/base-server.ts +++ b/packages/next/src/server/base-server.ts @@ -21,7 +21,11 @@ import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plug import type { OutgoingHttpHeaders } from 'http2' import type { BaseNextRequest, BaseNextResponse } from './base-http' import type { PayloadOptions } from './send-payload' -import type { PrerenderManifest } from '../build' +import type { + ManifestRewriteRoute, + ManifestRoute, + PrerenderManifest, +} from '../build' import type { ClientReferenceManifest } from '../build/webpack/plugins/flight-manifest-plugin' import type { NextFontManifest } from '../build/webpack/plugins/next-font-manifest-plugin' import type { PagesRouteModule } from './future/route-modules/pages/module' @@ -129,6 +133,19 @@ export interface MiddlewareRoutingItem { matchers?: MiddlewareMatcher[] } +/** + * The normalized route manifest is the same as the route manifest, but with + * the rewrites normalized to the object shape that the router expects. + */ +export type NormalizedRouteManifest = { + readonly dynamicRoutes: ReadonlyArray + readonly rewrites: { + readonly beforeFiles: ReadonlyArray + readonly afterFiles: ReadonlyArray + readonly fallback: ReadonlyArray + } +} + export interface Options { /** * Object containing the configuration next.config.js @@ -215,6 +232,9 @@ type ResponsePayload = { } export default abstract class Server { + public readonly hostname?: string + public readonly fetchHostname?: string + public readonly port?: number protected readonly dir: string protected readonly quiet: boolean protected readonly nextConfig: NextConfigComplete @@ -266,14 +286,11 @@ export default abstract class Server { appDirDevErrorLogger?: (err: any) => Promise strictNextHead: boolean } - protected serverOptions: ServerOptions - private responseCache: ResponseCacheBase - protected appPathRoutes?: Record - protected clientReferenceManifest?: ClientReferenceManifest + protected readonly serverOptions: Readonly + protected readonly appPathRoutes?: Record + protected readonly clientReferenceManifest?: ClientReferenceManifest protected nextFontManifest?: NextFontManifest - public readonly hostname?: string - public readonly fetchHostname?: string - public readonly port?: number + private readonly responseCache: ResponseCacheBase protected abstract getPublicDir(): string protected abstract getHasStaticDir(): boolean @@ -287,9 +304,11 @@ export default abstract class Server { query: NextParsedUrlQuery params: Params isAppPath: boolean + // The following parameters are used in the development server's + // implementation. sriEnabled?: boolean - appPaths?: string[] | null - shouldEnsure: boolean + appPaths?: ReadonlyArray | null + shouldEnsure?: boolean }): Promise protected abstract getFontManifest(): FontManifest | undefined protected abstract getPrerenderManifest(): PrerenderManifest @@ -328,9 +347,7 @@ export default abstract class Server { renderOpts: RenderOpts ): Promise - protected async getPrefetchRsc(_pathname: string): Promise { - return null - } + protected abstract getPrefetchRsc(pathname: string): Promise protected abstract getIncrementalCache(options: { requestHeaders: Record @@ -470,13 +487,13 @@ export default abstract class Server { this.appPathRoutes = this.getAppPathRoutes() // Configure the routes. - const { matchers } = this.getRoutes() - this.matchers = matchers + this.matchers = this.getRouteMatchers() // Start route compilation. We don't wait for the routes to finish loading // because we use the `waitTillReady` promise below in `handleRequest` to // wait. Also we can't `await` in the constructor. - matchers.reload() + void this.matchers.reload() + this.setAssetPrefix(assetPrefix) this.responseCache = this.getResponseCache({ dev }) } @@ -608,9 +625,7 @@ export default abstract class Server { return { finished: false } } - protected getRoutes(): { - matchers: RouteMatcherManager - } { + protected getRouteMatchers(): RouteMatcherManager { // Create a new manifest loader that get's the manifests from the server. const manifestLoader = new ServerManifestLoader((name) => { switch (name) { @@ -655,7 +670,7 @@ export default abstract class Server { ) } - return { matchers } + return matchers } public logError(err: Error): void { @@ -1120,7 +1135,7 @@ export default abstract class Server { 'http://n' ) protocol = parsedFullUrl.protocol as 'https:' | 'http:' - } catch (_) {} + } catch {} const incrementalCache = this.getIncrementalCache({ requestHeaders: Object.assign({}, req.headers), @@ -1290,11 +1305,11 @@ export default abstract class Server { return this.handleRequest.bind(this) } - protected async handleUpgrade( - _req: BaseNextRequest, - _socket: any, - _head?: any - ): Promise {} + protected abstract handleUpgrade( + req: BaseNextRequest, + socket: any, + head?: any + ): Promise public setAssetPrefix(prefix?: string): void { this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : '' @@ -1870,7 +1885,7 @@ export default abstract class Server { 'http://n' ) protocol = parsedFullUrl.protocol as 'https:' | 'http:' - } catch (_) {} + } catch {} // use existing incrementalCache instance if available const incrementalCache = @@ -2529,27 +2544,9 @@ export default abstract class Server { ) } - 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 - } + protected abstract getMiddleware(): MiddlewareRoutingItem | undefined + protected abstract getFallbackErrorComponents(): Promise + protected abstract getRoutesManifest(): NormalizedRouteManifest | undefined private async renderToResponseImpl( ctx: RequestContext @@ -2973,11 +2970,6 @@ export default abstract class Server { }) } - protected async getFallbackErrorComponents(): Promise { - // The development server will provide an implementation for this - return null - } - public async render404( req: BaseNextRequest, res: BaseNextResponse, diff --git a/packages/next/src/server/dev/hot-reloader-types.ts b/packages/next/src/server/dev/hot-reloader-types.ts index 4ad4e3b71ce26..d3058a989fc2c 100644 --- a/packages/next/src/server/dev/hot-reloader-types.ts +++ b/packages/next/src/server/dev/hot-reloader-types.ts @@ -37,7 +37,7 @@ export interface NextJsHotReloaderInterface { }: { page: string clientOnly: boolean - appPaths?: string[] | null + appPaths?: ReadonlyArray | null isApp?: boolean match?: RouteMatch }): Promise diff --git a/packages/next/src/server/dev/hot-reloader-webpack.ts b/packages/next/src/server/dev/hot-reloader-webpack.ts index e08cecbfe8fbe..bfec2c3e43ee9 100644 --- a/packages/next/src/server/dev/hot-reloader-webpack.ts +++ b/packages/next/src/server/dev/hot-reloader-webpack.ts @@ -1456,7 +1456,7 @@ export default class HotReloader implements NextJsHotReloaderInterface { }: { page: string clientOnly: boolean - appPaths?: string[] | null + appPaths?: ReadonlyArray | null isApp?: boolean match?: RouteMatch }): Promise { diff --git a/packages/next/src/server/dev/next-dev-server.ts b/packages/next/src/server/dev/next-dev-server.ts index 6c75285d77afe..985d283931bdf 100644 --- a/packages/next/src/server/dev/next-dev-server.ts +++ b/packages/next/src/server/dev/next-dev-server.ts @@ -1,4 +1,3 @@ -import type { CustomRoutes } from '../../lib/load-custom-routes' import type { FindComponentsResult } from '../next-server' import type { LoadComponentsReturnType } from '../load-components' import type { Options as ServerOptions } from '../next-server' @@ -10,6 +9,11 @@ import type { BaseNextRequest, BaseNextResponse } from '../base-http' import type { FallbackMode, MiddlewareRoutingItem } from '../base-server' import type { FunctionComponent } from 'react' import type { RouteMatch } from '../future/route-matches/route-match' +import type { RouteMatcherManager } from '../future/route-matcher-managers/route-matcher-manager' +import type { + NextParsedUrlQuery, + NextUrlWithParsedQuery, +} from '../request-meta' import { Worker } from 'next/dist/compiled/jest-worker' import { join as pathJoin } from 'path' @@ -59,7 +63,6 @@ import { DefaultFileReader } from '../future/route-matcher-providers/dev/helpers import { NextBuildContext } from '../../build/build-context' import { IncrementalCache } from '../lib/incremental-cache' import LRUCache from 'next/dist/compiled/lru-cache' -import { NextUrlWithParsedQuery } from '../request-meta' import { errorToJSON } from '../render' import { getMiddlewareRouteMatcher } from '../../shared/lib/router/utils/middleware-route-matcher' import { @@ -185,7 +188,7 @@ export default class DevServer extends Server { this.appDir = appDir } - protected getRoutes() { + protected getRouteMatchers(): RouteMatcherManager { const { pagesDir, appDir } = findPagesDir( this.dir, !!this.nextConfig.experimental.appDir @@ -201,9 +204,8 @@ export default class DevServer extends Server { }, } - const routes = super.getRoutes() const matchers = new DevRouteMatcherManager( - routes.matchers, + super.getRouteMatchers(), ensurer, this.dir ) @@ -257,7 +259,7 @@ export default class DevServer extends Server { ) } - return { matchers } + return matchers } protected getBuildId(): string { @@ -507,16 +509,6 @@ export default class DevServer extends Server { ) } - // override production loading of routes-manifest - protected getCustomRoutes(): CustomRoutes { - // actual routes will be loaded asynchronously during .prepare() - return { - redirects: [], - rewrites: { beforeFiles: [], afterFiles: [], fallback: [] }, - headers: [], - } - } - protected getPagesManifest(): PagesManifest | undefined { return ( NodeManifestLoader.require( @@ -745,20 +737,20 @@ export default class DevServer extends Server { protected async ensurePage(opts: { page: string clientOnly: boolean - appPaths?: string[] | null + appPaths?: ReadonlyArray | null match?: RouteMatch }): Promise { - if (this.isRenderWorker) { - await invokeIpcMethod({ - fetchHostname: this.fetchHostname, - method: 'ensurePage', - args: [opts], - ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT, - ipcKey: process.env.__NEXT_PRIVATE_ROUTER_IPC_KEY, - }) - return + if (!this.isRenderWorker) { + throw new Error('Invariant ensurePage called outside render worker') } - throw new Error('Invariant ensurePage called outside render worker') + + await invokeIpcMethod({ + fetchHostname: this.fetchHostname, + method: 'ensurePage', + args: [opts], + ipcPort: process.env.__NEXT_PRIVATE_ROUTER_IPC_PORT, + ipcKey: process.env.__NEXT_PRIVATE_ROUTER_IPC_KEY, + }) } protected async findPageComponents({ @@ -770,10 +762,11 @@ export default class DevServer extends Server { shouldEnsure, }: { pathname: string - query: ParsedUrlQuery + query: NextParsedUrlQuery params: Params isAppPath: boolean - appPaths?: string[] | null + sriEnabled?: boolean + appPaths?: ReadonlyArray | null shouldEnsure: boolean }): Promise { await this.devReady @@ -803,6 +796,7 @@ export default class DevServer extends Server { query, params, isAppPath, + shouldEnsure, }) } catch (err) { if ((err as any).code !== 'ENOENT') { diff --git a/packages/next/src/server/dev/on-demand-entry-handler.ts b/packages/next/src/server/dev/on-demand-entry-handler.ts index c30388e454a0a..d53aeb87e1409 100644 --- a/packages/next/src/server/dev/on-demand-entry-handler.ts +++ b/packages/next/src/server/dev/on-demand-entry-handler.ts @@ -34,8 +34,7 @@ import { RSC_MODULE_TYPES, } from '../../shared/lib/constants' import { RouteMatch } from '../future/route-matches/route-match' -import { RouteKind } from '../future/route-kind' -import { AppPageRouteMatch } from '../future/route-matches/app-page-route-match' +import { isAppPageRouteMatch } from '../future/route-matches/app-page-route-match' const debug = origDebug('next:on-demand-entry-handler') @@ -717,9 +716,8 @@ export function onDemandEntryHandler({ // If the route is actually an app page route, then we should have access // to the app route match, and therefore, the appPaths from it. - if (match?.definition.kind === RouteKind.APP_PAGE) { - const { definition: route } = match as AppPageRouteMatch - appPaths = route.appPaths + if (!appPaths && match && isAppPageRouteMatch(match)) { + appPaths = match.definition.appPaths } try { @@ -974,7 +972,7 @@ export function onDemandEntryHandler({ }) ) } - } catch (_) {} + } catch {} }) }, } diff --git a/packages/next/src/server/future/route-matchers/app-page-route-matcher.ts b/packages/next/src/server/future/route-matchers/app-page-route-matcher.ts index 2cbf6725cefa3..fc154acdd608e 100644 --- a/packages/next/src/server/future/route-matchers/app-page-route-matcher.ts +++ b/packages/next/src/server/future/route-matchers/app-page-route-matcher.ts @@ -3,6 +3,6 @@ import { AppPageRouteDefinition } from '../route-definitions/app-page-route-defi export class AppPageRouteMatcher extends RouteMatcher { public get identity(): string { - return `${this.definition.pathname}?__nextPage=${this.definition.page}}` + return `${this.definition.pathname}?__nextPage=${this.definition.page}` } } diff --git a/packages/next/src/server/future/route-matches/app-page-route-match.ts b/packages/next/src/server/future/route-matches/app-page-route-match.ts index ad9a62f92c4f3..0d644abcab6c8 100644 --- a/packages/next/src/server/future/route-matches/app-page-route-match.ts +++ b/packages/next/src/server/future/route-matches/app-page-route-match.ts @@ -4,7 +4,20 @@ import type { AppPageRouteDefinition, } from '../route-definitions/app-page-route-definition' +import { RouteKind } from '../route-kind' + export interface AppPageRouteMatch extends RouteMatch {} +/** + * Checks if the given match is an App Page route match. + * @param match the match to check + * @returns true if the match is an App Page route match, false otherwise + */ +export function isAppPageRouteMatch( + match: RouteMatch +): match is AppPageRouteMatch { + return match.definition.kind === RouteKind.APP_PAGE +} + export interface AppPageInterceptingRouteMatch extends RouteMatch {} diff --git a/packages/next/src/server/future/route-matches/pages-api-route-match.ts b/packages/next/src/server/future/route-matches/pages-api-route-match.ts index c52c8f2f3f783..435202354e51c 100644 --- a/packages/next/src/server/future/route-matches/pages-api-route-match.ts +++ b/packages/next/src/server/future/route-matches/pages-api-route-match.ts @@ -1,5 +1,18 @@ import type { RouteMatch } from './route-match' import type { PagesAPIRouteDefinition } from '../route-definitions/pages-api-route-definition' +import { RouteKind } from '../route-kind' + export interface PagesAPIRouteMatch extends RouteMatch {} + +/** + * Checks if the given match is a Pages API route match. + * @param match the match to check + * @returns true if the match is a Pages API route match, false otherwise + */ +export function isPagesAPIRouteMatch( + match: RouteMatch +): match is PagesAPIRouteMatch { + return match.definition.kind === RouteKind.PAGES_API +} diff --git a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts index 27743e72704da..ab829a20d4110 100644 --- a/packages/next/src/server/lib/incremental-cache/file-system-cache.ts +++ b/packages/next/src/server/lib/incremental-cache/file-system-cache.ts @@ -212,7 +212,7 @@ export default class FileSystemCache implements CacheHandler { await this.fs.readFile(filePath.replace(/\.html$/, '.meta')) ).toString('utf-8') ) - } catch (_) {} + } catch {} } data = { diff --git a/packages/next/src/server/lib/router-utils/filesystem.ts b/packages/next/src/server/lib/router-utils/filesystem.ts index e09af618061d7..18c25496e52c8 100644 --- a/packages/next/src/server/lib/router-utils/filesystem.ts +++ b/packages/next/src/server/lib/router-utils/filesystem.ts @@ -1,4 +1,8 @@ -import type { PrerenderManifest, RoutesManifest } from '../../../build' +import type { + ManifestRoute, + PrerenderManifest, + RoutesManifest, +} from '../../../build' import type { NextConfigComplete } from '../../config-shared' import type { MiddlewareManifest } from '../../../build/webpack/plugins/middleware-plugin' @@ -14,7 +18,10 @@ 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 { + PatchMatcher, + 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' @@ -54,12 +61,19 @@ export type FsOutput = { const debug = setupDebug('next:router-server:filesystem') +export type FilesystemDynamicRoute = ManifestRoute & { + /** + * The path matcher that can be used to match paths against this route. + */ + match: PatchMatcher +} + export const buildCustomRoute = ( type: 'redirect' | 'header' | 'rewrite' | 'before_files_rewrite', item: T & { source: string }, basePath?: string, caseSensitive?: boolean -): T & { match: ReturnType; check?: boolean } => { +): T & { match: PatchMatcher; check?: boolean } => { const restrictedRedirectPaths = ['/_next'].map((p) => basePath ? `${basePath}${p}` : p ) @@ -91,18 +105,20 @@ export async function setupFsCheck(opts: { arg: (files: Map) => void ) => void }) { - const getItemsLru = new LRUCache({ - max: 1024 * 1024, - length(value, key) { - if (!value) return key?.length || 0 - return ( - (key || '').length + - (value.fsPath || '').length + - value.itemPath.length + - value.type.length - ) - }, - }) + const getItemsLru = !opts.dev + ? new LRUCache({ + max: 1024 * 1024, + length(value, key) { + if (!value) return key?.length || 0 + return ( + (key || '').length + + (value.fsPath || '').length + + value.itemPath.length + + value.type.length + ) + }, + }) + : undefined // routes that have _next/data endpoints (SSG/SSP) const nextDataRoutes = new Set() @@ -112,9 +128,7 @@ export async function setupFsCheck(opts: { const appFiles = new Set() const pageFiles = new Set() - let dynamicRoutes: (RoutesManifest['dynamicRoutes'][0] & { - match: ReturnType - })[] = [] + let dynamicRoutes: FilesystemDynamicRoute[] = [] let middlewareMatcher: | ReturnType @@ -259,17 +273,20 @@ export async function setupFsCheck(opts: { } 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) - ? { + rewrites: routesManifest.rewrites + ? Array.isArray(routesManifest.rewrites) + ? { + beforeFiles: [], + afterFiles: routesManifest.rewrites, + fallback: [], + } + : routesManifest.rewrites + : { beforeFiles: [], - afterFiles: routesManifest.rewrites, + afterFiles: [], fallback: [], - } - : routesManifest.rewrites, - // @ts-expect-error additional fields in manifest type + }, headers: routesManifest.headers, } } else { @@ -382,7 +399,7 @@ export async function setupFsCheck(opts: { async getItem(itemPath: string): Promise { const originalItemPath = itemPath const itemKey = originalItemPath - const lruResult = getItemsLru.get(itemKey) + const lruResult = getItemsLru?.get(itemKey) if (lruResult) { return lruResult @@ -409,7 +426,7 @@ export async function setupFsCheck(opts: { try { decodedItemPath = decodeURIComponent(itemPath) - } catch (_) {} + } catch {} if (itemPath === '/_next/image') { return { @@ -448,7 +465,7 @@ export async function setupFsCheck(opts: { try { curDecodedItemPath = decodeURIComponent(curItemPath) - } catch (_) {} + } catch {} } } @@ -460,7 +477,7 @@ export async function setupFsCheck(opts: { try { curDecodedItemPath = decodeURIComponent(curItemPath) - } catch (_) {} + } catch {} } if ( @@ -496,7 +513,7 @@ export async function setupFsCheck(opts: { try { curDecodedItemPath = decodeURIComponent(curItemPath) - } catch (_) {} + } catch {} } // check decoded variant as well @@ -554,7 +571,7 @@ export async function setupFsCheck(opts: { const tempItemPath = decodeURIComponent(curItemPath) fsPath = path.posix.join(itemsRoot, tempItemPath) found = await fileExists(fsPath, FileType.File) - } catch (_) {} + } catch {} if (!found) { continue @@ -588,16 +605,12 @@ export async function setupFsCheck(opts: { itemPath: curItemPath, } - if (!opts.dev) { - getItemsLru.set(itemKey, itemResult) - } + getItemsLru?.set(itemKey, itemResult) return itemResult } } - if (!opts.dev) { - getItemsLru.set(itemKey, null) - } + getItemsLru?.set(itemKey, null) return null }, getDynamicRoutes() { diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index 2e1c1d0bbaaa7..3ae8d5995036d 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -3,6 +3,7 @@ import type { FsOutput } from './filesystem' import type { IncomingMessage } from 'http' import type { NextConfigComplete } from '../../config-shared' import type { RenderWorker, initialize } from '../router-server' +import type { PatchMatcher } from '../../../shared/lib/router/utils/path-match' import url from 'url' import { Redirect } from '../../../../types' @@ -20,7 +21,6 @@ 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' @@ -54,30 +54,34 @@ export function getResolveRoutes( renderWorkerOpts: Parameters[0], ensureMiddleware?: () => Promise ) { - const routes: ({ - match: ReturnType + type Route = { + /** + * The path matcher to check if this route applies to this request. + */ + match: PatchMatcher check?: boolean name?: string - internal?: boolean } & Partial
& - Partial)[] = [ + Partial + + const routes: Route[] = [ // _next/data with middleware handling - { match: () => ({} as any), name: 'middleware_next_data' }, + { match: () => ({}), name: 'middleware_next_data' }, ...(opts.minimalMode ? [] : fsChecker.headers), ...(opts.minimalMode ? [] : fsChecker.redirects), // check middleware (using matchers) - { match: () => ({} as any), name: 'middleware' }, + { match: () => ({}), name: 'middleware' }, ...(opts.minimalMode ? [] : fsChecker.rewrites.beforeFiles), // check middleware (using matchers) - { match: () => ({} as any), name: 'before_files_end' }, + { match: () => ({}), name: 'before_files_end' }, // we check exact matches on fs before continuing to // after files rewrites - { match: () => ({} as any), name: 'check_fs' }, + { match: () => ({}), name: 'check_fs' }, ...(opts.minimalMode ? [] : fsChecker.rewrites.afterFiles), @@ -85,7 +89,7 @@ export function getResolveRoutes( // fallback rewrites { check: true, - match: () => ({} as any), + match: () => ({}), name: 'after files check: true', }, diff --git a/packages/next/src/server/lib/router-utils/setup-dev.ts b/packages/next/src/server/lib/router-utils/setup-dev.ts index b1b5cb416e764..d6fa63cd6c7c6 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev.ts @@ -14,7 +14,7 @@ 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 { FilesystemDynamicRoute, buildCustomRoute } from './filesystem' import * as Log from '../../../build/output/log' import HotReloader, { matchNextPageBundleRequest, @@ -1353,17 +1353,16 @@ async function startWatcher(opts: SetupOpts) { // before it has been built and is populated in the _buildManifest const sortedRoutes = getSortedRoutes(routedPages) - opts.fsChecker.dynamicRoutes = sortedRoutes - .map((page) => { + opts.fsChecker.dynamicRoutes = sortedRoutes.map( + (page): FilesystemDynamicRoute => { const regex = getRouteRegex(page) return { + regex: regex.re.toString(), match: getRouteMatcher(regex), page, - re: regex.re, - groups: regex.groups, } - }) - .filter(Boolean) as any + } + ) const dataRoutes: typeof opts.fsChecker.dynamicRoutes = [] diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index 45f190c589cca..d2b563493ebe6 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -179,7 +179,8 @@ async function loadComponentsImpl({ const Document = interopDefault(DocumentMod) const App = interopDefault(AppMod) - const { getServerSideProps, getStaticProps, getStaticPaths } = ComponentMod + const { getServerSideProps, getStaticProps, getStaticPaths, routeModule } = + ComponentMod return { App, @@ -196,7 +197,7 @@ async function loadComponentsImpl({ serverActionsManifest, isAppPath, pathname, - routeModule: ComponentMod.routeModule, + routeModule, } } diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 61442178b9acb..9bbfef0786a06 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -15,7 +15,7 @@ import { import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin' import type RenderResult from './render-result' import type { FetchEventResult } from './web/types' -import type { PrerenderManifest } from '../build' +import type { PrerenderManifest, RoutesManifest } from '../build' import { BaseNextRequest, BaseNextResponse } from './base-http' import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin' import type { PayloadOptions } from './send-payload' @@ -60,11 +60,12 @@ import BaseServer, { MiddlewareRoutingItem, NoFallbackError, RequestContext, + NormalizedRouteManifest, } from './base-server' import { getMaybePagePath, getPagePath, requireFontManifest } from './require' import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' -import { loadComponents } from './load-components' +import { LoadComponentsReturnType, loadComponents } from './load-components' import isError, { getProperError } from '../lib/is-error' import { FontManifest } from './font-utils' import { splitCookiesString, toNodeOutgoingHttpHeaders } from './web/utils' @@ -80,9 +81,11 @@ import { IncrementalCache } from './lib/incremental-cache' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { setHttpClientAndAgentOptions } from './setup-http-agent-env' -import { RouteKind } from './future/route-kind' -import { PagesAPIRouteMatch } from './future/route-matches/pages-api-route-match' +import { + PagesAPIRouteMatch, + isPagesAPIRouteMatch, +} from './future/route-matches/pages-api-route-match' import { MatchOptions } from './future/route-matcher-managers/route-matcher-manager' import { INSTRUMENTATION_HOOK_FILENAME } from '../lib/constants' import { getTracer } from './lib/trace/tracer' @@ -197,24 +200,18 @@ export default class NextNodeServer extends BaseServer { } if (!options.dev) { - const routesManifest = this.getRoutesManifest() as { - dynamicRoutes: { - page: string - regex: string - namedRegex?: string - routeKeys?: { [key: string]: string } - }[] - } - this.dynamicRoutes = routesManifest.dynamicRoutes.map((r) => { + const { dynamicRoutes = [] } = this.getRoutesManifest() ?? {} + this.dynamicRoutes = dynamicRoutes.map((r) => { + // TODO: can we just re-use the regex from the manifest? const regex = getRouteRegex(r.page) const match = getRouteMatcher(regex) return { match, page: r.page, - regex: regex.re, + re: regex.re, } - }) as any + }) } // ensure options are set when loadConfig isn't called @@ -227,6 +224,11 @@ export default class NextNodeServer extends BaseServer { } } + protected async handleUpgrade(): Promise { + // The web server does not support web sockets, it's only used for HMR in + // development. + } + protected async prepareImpl() { await super.prepareImpl() if ( @@ -590,24 +592,29 @@ export default class NextNodeServer extends BaseServer { }: { pathname: string query: NextParsedUrlQuery - params: Params | null + params: Params isAppPath: boolean + // The following parameters are used in the development server's + // implementation. + sriEnabled?: boolean + appPaths?: ReadonlyArray | null + shouldEnsure: boolean }): Promise { - let route = pathname - if (isAppPath) { - // When in App we get page instead of route - route = pathname.replace(/\/[^/]*$/, '') - } - return getTracer().trace( NextNodeServerSpan.findPageComponents, { spanName: `resolving page into components`, attributes: { - 'next.route': route, + 'next.route': isAppPath ? normalizeAppPath(pathname) : pathname, }, }, - () => this.findPageComponentsImpl({ pathname, query, params, isAppPath }) + () => + this.findPageComponentsImpl({ + pathname, + query, + params, + isAppPath, + }) ) } @@ -619,7 +626,7 @@ export default class NextNodeServer extends BaseServer { }: { pathname: string query: NextParsedUrlQuery - params: Params | null + params: Params isAppPath: boolean }): Promise { const paths: string[] = [pathname] @@ -809,12 +816,13 @@ export default class NextNodeServer extends BaseServer { parsedUrl: NextUrlWithParsedQuery ) { let { pathname, query } = parsedUrl - if (!pathname) { - throw new Error('pathname is undefined') + throw new Error('Invariant: pathname is undefined') } + + // This is a catch-all route, there should be no fallbacks so mark it as + // such. query._nextBubbleNoFallback = '1' - const bubbleNoFallback = true try { // next.js core assumes page path without trailing slash @@ -825,63 +833,57 @@ export default class NextNodeServer extends BaseServer { } const match = await this.matchers.match(pathname, options) - // Try to handle the given route with the configured handlers. - if (match) { - // Add the match to the request so we don't have to re-run the matcher - // for the same request. - addRequestMeta(req, '_nextMatch', match) - - // TODO-APP: move this to a route handler - const edgeFunctionsPages = this.getEdgeFunctionsPages() - for (const edgeFunctionsPage of edgeFunctionsPages) { - if (edgeFunctionsPage === match.definition.page) { - if (this.nextConfig.output === 'export') { - await this.render404(req, res, parsedUrl) - return { finished: true } - } - delete query._nextBubbleNoFallback - delete query[NEXT_RSC_UNION_QUERY] - - const handledAsEdgeFunction = await this.runEdgeFunction({ - req, - res, - query, - params: match.params, - page: match.definition.page, - match, - appPaths: null, - }) - - if (handledAsEdgeFunction) { - return { finished: true } - } - } + // If we don't have a match, try to render it anyways. + if (!match) { + await this.render(req, res, pathname, query, parsedUrl, true) + + return { finished: true } + } + + // Add the match to the request so we don't have to re-run the matcher + // for the same request. + addRequestMeta(req, '_nextMatch', match) + + // TODO-APP: move this to a route handler + const edgeFunctionsPages = this.getEdgeFunctionsPages() + for (const edgeFunctionsPage of edgeFunctionsPages) { + // If the page doesn't match the edge function page, skip it. + if (edgeFunctionsPage !== match.definition.page) continue + + if (this.nextConfig.output === 'export') { + await this.render404(req, res, parsedUrl) + return { finished: true } } - let handled = false - - // If the route was detected as being a Pages API route, then handle - // it. - // TODO: move this behavior into a route handler. - if (match.definition.kind === RouteKind.PAGES_API) { - if (this.nextConfig.output === 'export') { - await this.render404(req, res, parsedUrl) - return { finished: true } - } - delete query._nextBubbleNoFallback - - handled = await this.handleApiRequest( - req, - res, - query, - // TODO: see if we can add a runtime check for this - match as PagesAPIRouteMatch - ) - if (handled) return { finished: true } + delete query._nextBubbleNoFallback + delete query[NEXT_RSC_UNION_QUERY] + + const handled = await this.runEdgeFunction({ + req, + res, + query, + params: match.params, + page: match.definition.page, + match, + appPaths: null, + }) + + // If we handled the request, we can return early. + if (handled) return { finished: true } + } + + // If the route was detected as being a Pages API route, then handle + // it. + // TODO: move this behavior into a route handler. + if (isPagesAPIRouteMatch(match)) { + if (this.nextConfig.output === 'export') { + await this.render404(req, res, parsedUrl) + return { finished: true } } - // else if (match.definition.kind === RouteKind.METADATA_ROUTE) { - // handled = await this.handlers.handle(match, req, res) - // if (handled) return { finished: true } - // } + + delete query._nextBubbleNoFallback + + const handled = await this.handleApiRequest(req, res, query, match) + if (handled) return { finished: true } } await this.render(req, res, pathname, query, parsedUrl, true) @@ -890,7 +892,7 @@ export default class NextNodeServer extends BaseServer { finished: true, } } catch (err: any) { - if (err instanceof NoFallbackError && bubbleNoFallback) { + if (err instanceof NoFallbackError) { if (this.isRenderWorker) { res.setHeader('x-no-fallback', '1') res.send() @@ -925,19 +927,25 @@ export default class NextNodeServer extends BaseServer { } // Used in development only, overloaded in next-dev-server - protected async logErrorWithOriginalStack(..._args: any[]): Promise { + protected async logErrorWithOriginalStack( + _err?: unknown, + _type?: 'unhandledRejection' | 'uncaughtException' | 'warning' | 'app-dir' + ): Promise { throw new Error( - 'logErrorWithOriginalStack can only be called on the development server' + 'Invariant: logErrorWithOriginalStack can only be called on the development server' ) } + // Used in development only, overloaded in next-dev-server protected async ensurePage(_opts: { page: string clientOnly: boolean - appPaths?: string[] | null + appPaths?: ReadonlyArray | null match?: RouteMatch }): Promise { - throw new Error('ensurePage can only be called on the development server') + throw new Error( + 'Invariant: ensurePage can only be called on the development server' + ) } /** @@ -1641,18 +1649,28 @@ export default class NextNodeServer extends BaseServer { return (this._cachedPreviewManifest = manifest) } - protected getRoutesManifest() { + protected getRoutesManifest(): NormalizedRouteManifest | undefined { return getTracer().trace(NextNodeServerSpan.getRoutesManifest, () => { - const manifest = require(join(this.distDir, ROUTES_MANIFEST)) + const manifest: RoutesManifest = require(join( + this.distDir, + ROUTES_MANIFEST + )) + + let rewrites = manifest.rewrites ?? { + beforeFiles: [], + afterFiles: [], + fallback: [], + } - if (Array.isArray(manifest.rewrites)) { - manifest.rewrites = { + if (Array.isArray(rewrites)) { + rewrites = { beforeFiles: [], - afterFiles: manifest.rewrites, + afterFiles: rewrites, fallback: [], } } - return manifest + + return { ...manifest, rewrites } }) } @@ -1796,4 +1814,10 @@ export default class NextNodeServer extends BaseServer { protected get serverDistDir() { return join(this.distDir, SERVER_DIRECTORY) } + + protected async getFallbackErrorComponents(): Promise { + // Not implemented for production use cases, this is implemented on the + // development server. + return null + } } diff --git a/packages/next/src/server/require.ts b/packages/next/src/server/require.ts index 393fc5bfef621..bbc8e91dc2835 100644 --- a/packages/next/src/server/require.ts +++ b/packages/next/src/server/require.ts @@ -1,4 +1,4 @@ -import { join } from 'path' +import path from 'path' import { FONT_MANIFEST, PAGES_MANIFEST, @@ -15,17 +15,11 @@ import { loadManifest } from './load-manifest' import { promises } from 'fs' const isDev = process.env.NODE_ENV === 'development' -const pagePathCache = isDev - ? { - get: (_key: string) => { - return null - }, - set: () => {}, - has: () => false, - } - : new LRUCache({ +const pagePathCache = !isDev + ? new LRUCache({ max: 1000, }) + : null export function getMaybePagePath( page: string, @@ -35,21 +29,22 @@ export function getMaybePagePath( ): string | null { const cacheKey = `${page}:${distDir}:${locales}:${isAppPath}` - if (pagePathCache.has(cacheKey)) { - return pagePathCache.get(cacheKey) as string | null - } + let pagePath = pagePathCache?.get(cacheKey) - const serverBuildPath = join(distDir, SERVER_DIRECTORY) + // If we have a cached path, we can return it directly. + if (pagePath) return pagePath + + const serverBuildPath = path.join(distDir, SERVER_DIRECTORY) let appPathsManifest: undefined | PagesManifest if (isAppPath) { appPathsManifest = loadManifest( - join(serverBuildPath, APP_PATHS_MANIFEST), + path.join(serverBuildPath, APP_PATHS_MANIFEST), !isDev ) } const pagesManifest = loadManifest( - join(serverBuildPath, PAGES_MANIFEST), + path.join(serverBuildPath, PAGES_MANIFEST), !isDev ) as PagesManifest @@ -74,7 +69,6 @@ export function getMaybePagePath( } return curPath } - let pagePath: string | undefined if (appPathsManifest) { pagePath = checkManifest(appPathsManifest) @@ -85,14 +79,14 @@ export function getMaybePagePath( } if (!pagePath) { - pagePathCache.set(cacheKey, null) + pagePathCache?.set(cacheKey, null) return null } - const path = join(serverBuildPath, pagePath) - pagePathCache.set(cacheKey, path) + pagePath = path.join(serverBuildPath, pagePath) - return path + pagePathCache?.set(cacheKey, pagePath) + return pagePath } export function getPagePath( @@ -129,7 +123,7 @@ export function requirePage( } export function requireFontManifest(distDir: string) { - const serverBuildPath = join(distDir, SERVER_DIRECTORY) - const fontManifest = loadManifest(join(serverBuildPath, FONT_MANIFEST)) + const serverBuildPath = path.join(distDir, SERVER_DIRECTORY) + const fontManifest = loadManifest(path.join(serverBuildPath, FONT_MANIFEST)) return fontManifest } diff --git a/packages/next/src/server/server-utils.ts b/packages/next/src/server/server-utils.ts index 336b5d6aaaec1..4bfb79cfb62d3 100644 --- a/packages/next/src/server/server-utils.ts +++ b/packages/next/src/server/server-utils.ts @@ -1,4 +1,4 @@ -import type { IncomingMessage, ServerResponse } from 'http' +import type { IncomingMessage } from 'http' import type { Rewrite } from '../lib/load-custom-routes' import type { RouteMatchFn } from '../shared/lib/router/utils/route-matcher' import type { NextConfig } from './config' @@ -14,13 +14,6 @@ import { matchHas, prepareDestination, } from '../shared/lib/router/utils/prepare-destination' -import { acceptLanguage } from './accept-header' -import { detectLocaleCookie } from '../shared/lib/i18n/detect-locale-cookie' -import { detectDomainLocale } from '../shared/lib/i18n/detect-domain-locale' -import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' -import cookie from 'next/dist/compiled/cookie' -import { TEMPORARY_REDIRECT_STATUS } from '../shared/lib/constants' -import { addRequestMeta } from './request-meta' import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash' import { normalizeRscPath } from '../shared/lib/router/utils/app-paths' import { NEXT_QUERY_PARAM_PREFIX } from '../lib/constants' @@ -103,9 +96,9 @@ export function getUtils({ i18n?: NextConfig['i18n'] basePath: string rewrites: { - fallback?: Rewrite[] - afterFiles?: Rewrite[] - beforeFiles?: Rewrite[] + fallback?: ReadonlyArray + afterFiles?: ReadonlyArray + beforeFiles?: ReadonlyArray } pageIsDynamic: boolean trailingSlash?: boolean @@ -426,152 +419,7 @@ export function getUtils({ } } - function handleLocale( - req: IncomingMessage, - res: ServerResponse, - parsedUrl: UrlWithParsedQuery, - routeNoAssetPath: string, - shouldNotRedirect: boolean - ) { - if (!i18n) return - const pathname = parsedUrl.pathname || '/' - - let defaultLocale = i18n.defaultLocale - let detectedLocale = detectLocaleCookie(req, i18n.locales) - let acceptPreferredLocale - try { - acceptPreferredLocale = - i18n.localeDetection !== false - ? acceptLanguage(req.headers['accept-language'], i18n.locales) - : detectedLocale - } catch (_) { - acceptPreferredLocale = detectedLocale - } - - const { host } = req.headers || {} - // remove port from host and remove port if present - const hostname = host && host.split(':')[0].toLowerCase() - - const detectedDomain = detectDomainLocale(i18n.domains, hostname) - if (detectedDomain) { - defaultLocale = detectedDomain.defaultLocale - detectedLocale = defaultLocale - addRequestMeta(req as any, '__nextIsLocaleDomain', true) - } - - // if not domain specific locale use accept-language preferred - detectedLocale = detectedLocale || acceptPreferredLocale - - let localeDomainRedirect - const localePathResult = normalizeLocalePath(pathname, i18n.locales) - - routeNoAssetPath = normalizeLocalePath( - routeNoAssetPath, - i18n.locales - ).pathname - - if (localePathResult.detectedLocale) { - detectedLocale = localePathResult.detectedLocale - req.url = formatUrl({ - ...parsedUrl, - pathname: localePathResult.pathname, - }) - addRequestMeta(req as any, '__nextStrippedLocale', true) - parsedUrl.pathname = localePathResult.pathname - } - - // If a detected locale is a domain specific locale and we aren't already - // on that domain and path prefix redirect to it to prevent duplicate - // content from multiple domains - if (detectedDomain) { - const localeToCheck = localePathResult.detectedLocale - ? detectedLocale - : acceptPreferredLocale - - const matchedDomain = detectDomainLocale( - i18n.domains, - undefined, - localeToCheck - ) - - if (matchedDomain && matchedDomain.domain !== detectedDomain.domain) { - localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${ - matchedDomain.domain - }/${localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck}` - } - } - - const denormalizedPagePath = denormalizePagePath(pathname) - const detectedDefaultLocale = - !detectedLocale || - detectedLocale.toLowerCase() === defaultLocale.toLowerCase() - const shouldStripDefaultLocale = false - // detectedDefaultLocale && - // denormalizedPagePath.toLowerCase() === \`/\${i18n.defaultLocale.toLowerCase()}\` - - const shouldAddLocalePrefix = - !detectedDefaultLocale && denormalizedPagePath === '/' - - detectedLocale = detectedLocale || i18n.defaultLocale - - if ( - !shouldNotRedirect && - !req.headers['x-vercel-id'] && - i18n.localeDetection !== false && - (localeDomainRedirect || - shouldAddLocalePrefix || - shouldStripDefaultLocale) - ) { - // set the NEXT_LOCALE cookie when a user visits the default locale - // with the locale prefix so that they aren't redirected back to - // their accept-language preferred locale - if (shouldStripDefaultLocale && acceptPreferredLocale !== defaultLocale) { - const previous = res.getHeader('set-cookie') - - res.setHeader('set-cookie', [ - ...(typeof previous === 'string' - ? [previous] - : Array.isArray(previous) - ? previous - : []), - cookie.serialize('NEXT_LOCALE', defaultLocale, { - httpOnly: true, - path: '/', - }), - ]) - } - - res.setHeader( - 'Location', - formatUrl({ - // make sure to include any query values when redirecting - ...parsedUrl, - pathname: localeDomainRedirect - ? localeDomainRedirect - : shouldStripDefaultLocale - ? basePath || '/' - : `${basePath}/${detectedLocale}`, - }) - ) - res.statusCode = TEMPORARY_REDIRECT_STATUS - res.end() - return - } - - detectedLocale = - localePathResult.detectedLocale || - (detectedDomain && detectedDomain.defaultLocale) || - defaultLocale - - return { - defaultLocale, - detectedLocale, - routeNoAssetPath, - } - } - return { - handleLocale, handleRewrites, handleBasePath, defaultRouteRegex, diff --git a/packages/next/src/server/web-server.ts b/packages/next/src/server/web-server.ts index 4226e211303a2..b347467d9ed43 100644 --- a/packages/next/src/server/web-server.ts +++ b/packages/next/src/server/web-server.ts @@ -6,10 +6,14 @@ import type { Params } from '../shared/lib/router/utils/route-matcher' import type { PayloadOptions } from './send-payload' import type { LoadComponentsReturnType } from './load-components' import type { BaseNextRequest, BaseNextResponse } from './base-http' -import type { UrlWithParsedQuery } from 'url' import { byteLength } from './api-utils/web' -import BaseServer, { NoFallbackError, Options } from './base-server' +import BaseServer, { + MiddlewareRoutingItem, + NoFallbackError, + NormalizedRouteManifest, + Options, +} from './base-server' import { generateETag } from './lib/etag' import { addRequestMeta } from './request-meta' import WebResponseCache from './response-cache/web' @@ -47,10 +51,6 @@ export default class NextWebServer extends BaseServer { Object.assign(this.renderOpts, options.webServerConfig.extendRenderOpts) } - protected handleCompression() { - // For the web server layer, compression is automatically handled by the - // upstream proxy (edge runtime or node server) and we can simply skip here. - } protected getIncrementalCache({ requestHeaders, }: { @@ -80,50 +80,19 @@ export default class NextWebServer extends BaseServer { protected getResponseCache() { return new WebResponseCache(this.minimalMode) } - protected getCustomRoutes() { - return { - headers: [], - rewrites: { - fallback: [], - afterFiles: [], - beforeFiles: [], - }, - redirects: [], - } - } - protected async run( - req: BaseNextRequest, - res: BaseNextResponse, - parsedUrl: UrlWithParsedQuery - ): Promise { - super.run(req, res, parsedUrl) - } + protected async hasPage(page: string) { return page === this.serverOptions.webServerConfig.page } - protected getPublicDir() { - // Public files are not handled by the web server. - return '' - } + protected getBuildId() { return this.serverOptions.webServerConfig.extendRenderOpts.buildId } - protected loadEnvConfig() { - // The web server does not need to load the env config. This is done by the - // runtime already. - } + protected getHasAppDir() { return this.serverOptions.webServerConfig.pagesType === 'app' } - protected getHasStaticDir() { - return false - } - protected async getFallback() { - return '' - } - protected getFontManifest() { - return undefined - } + protected getPagesManifest() { return { // keep same theme but server path doesn't need to be accurate @@ -131,21 +100,21 @@ export default class NextWebServer extends BaseServer { .normalizedPage]: `server${this.serverOptions.webServerConfig.page}.js`, } } + protected getAppPathsManifest() { const page = this.serverOptions.webServerConfig.page return { [this.serverOptions.webServerConfig.page]: `app${page}.js`, } } - protected getFilesystemPaths() { - return new Set() - } + protected attachRequestMeta( req: WebNextRequest, parsedUrl: NextUrlWithParsedQuery ) { addRequestMeta(req, '__NEXT_INIT_QUERY', { ...parsedUrl.query }) } + protected getPrerenderManifest() { const { prerenderManifest } = this.serverOptions.webServerConfig if (this.renderOpts?.dev || !prerenderManifest) { @@ -228,11 +197,6 @@ export default class NextWebServer extends BaseServer { } } - // Edge API requests are handled separately in minimal mode. - protected async handleApiRequest() { - return false - } - protected renderHTML( req: WebNextRequest, res: WebNextResponse, @@ -328,10 +292,6 @@ export default class NextWebServer extends BaseServer { res.send() } - protected async runApi() { - // @TODO - return true - } protected async findPageComponents({ pathname, @@ -356,4 +316,73 @@ export default class NextWebServer extends BaseServer { components: result, } } + + // Below are methods that are not implemented by the web server as they are + // handled by the upstream proxy (edge runtime or node server). + + protected async runApi() { + // This web server does not need to handle API requests. + return true + } + + protected async handleApiRequest() { + // Edge API requests are handled separately in minimal mode. + return false + } + + protected loadEnvConfig() { + // The web server does not need to load the env config. This is done by the + // runtime already. + } + + protected getPublicDir() { + // Public files are not handled by the web server. + return '' + } + + protected getHasStaticDir() { + return false + } + + protected async getFallback() { + return '' + } + + protected getFontManifest() { + return undefined + } + + protected handleCompression() { + // For the web server layer, compression is automatically handled by the + // upstream proxy (edge runtime or node server) and we can simply skip here. + } + + protected async handleUpgrade(): Promise { + // The web server does not support web sockets. + } + + protected async getFallbackErrorComponents(): Promise { + // The web server does not need to handle fallback errors in production. + return null + } + + protected getRoutesManifest(): NormalizedRouteManifest | undefined { + // The web server does not need to handle rewrite rules. This is done by the + // upstream proxy (edge runtime or node server). + return undefined + } + + protected getMiddleware(): MiddlewareRoutingItem | undefined { + // The web server does not need to handle middleware. This is done by the + // upstream proxy (edge runtime or node server). + return undefined + } + + protected getFilesystemPaths() { + return new Set() + } + + protected async getPrefetchRsc(): Promise { + return null + } } diff --git a/packages/next/src/shared/lib/router/utils/path-match.ts b/packages/next/src/shared/lib/router/utils/path-match.ts index fd0e6cb245c9a..e011897d22f89 100644 --- a/packages/next/src/shared/lib/router/utils/path-match.ts +++ b/packages/next/src/shared/lib/router/utils/path-match.ts @@ -25,12 +25,17 @@ interface Options { sensitive?: boolean } +export type PatchMatcher = ( + pathname?: string | null, + params?: Record +) => Record | false + /** * Generates a path matcher function for a given path and options based on * path-to-regexp. By default the match will be case insensitive, non strict * and delimited by `/`. */ -export function getPathMatch(path: string, options?: Options) { +export function getPathMatch(path: string, options?: Options): PatchMatcher { const keys: Key[] = [] const regexp = pathToRegexp(path, keys, { delimiter: '/', @@ -39,7 +44,7 @@ export function getPathMatch(path: string, options?: Options) { strict: options?.strict, }) - const matcher = regexpToFunction( + const matcher = regexpToFunction>( options?.regexModifier ? new RegExp(options.regexModifier(regexp.source), regexp.flags) : regexp, @@ -52,14 +57,14 @@ 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 ( - pathname?: string | null, - params?: any - ): false | { [key: string]: any } => { - const res = pathname == null ? false : matcher(pathname) - if (!res) { - return false - } + return (pathname, params) => { + // If no pathname is provided it's not a match. + if (typeof pathname !== 'string') return false + + const match = matcher(pathname) + + // If the path did not match `false` will be returned. + if (!match) return false /** * If unnamed params are not allowed they must be removed from @@ -69,11 +74,11 @@ export function getPathMatch(path: string, options?: Options) { if (options?.removeUnnamedParams) { for (const key of keys) { if (typeof key.name === 'number') { - delete (res.params as any)[key.name] + delete match.params[key.name] } } } - return { ...params, ...res.params } + return { ...params, ...match.params } } } diff --git a/packages/next/src/telemetry/events/swc-load-failure.ts b/packages/next/src/telemetry/events/swc-load-failure.ts index cf23580b45212..2101d9fbb8216 100644 --- a/packages/next/src/telemetry/events/swc-load-failure.ts +++ b/packages/next/src/telemetry/events/swc-load-failure.ts @@ -31,7 +31,7 @@ export async function eventSwcLoadFailure( try { // @ts-ignore glibcVersion = process.report?.getReport().header.glibcVersionRuntime - } catch (_) {} + } catch {} try { const pkgNames = Object.keys(optionalDependencies || {}).filter((pkg) => @@ -43,13 +43,13 @@ export async function eventSwcLoadFailure( try { const { version } = require(`${pkg}/package.json`) installedPkgs.push(`${pkg}@${version}`) - } catch (_) {} + } catch {} } if (installedPkgs.length > 0) { installedSwcPackages = installedPkgs.sort().join(',') } - } catch (_) {} + } catch {} telemetry.record({ eventName: EVENT_PLUGIN_PRESENT,