1b36f0c029
Disambiguate between pages/index.js and pages/index/index.js so that they resolve differently. It all started with a bug in pagesmanifest that propagated throughout the codebase. After fixing pagesmanifest I was able to remove a few hacks here and there and more logic is shared now. especially the logic that resolves an entrypoint back into a route path. To sum up what happened: - `getRouteFromEntrypoint` is the inverse operation of `getPageFile` that's under `pages/_document.tsx` - `denormalizePagePath` is the inverse operation of `normalizePagePath`. Everything is refactored in terms of these operations, that makes their behavior uniform and easier to update/patch in a central place. Before there were subtle differences between those that made `index/index.js` hard to handle. Some potential follow up on this PR: - [`hot-reloader`](https://github.com/vercel/next.js/pull/13699/files#diff-6161346d2c5f4b7abc87059d8768c44bR207) still has one place that does very similar behavior to `getRouteFromEntrypoint`. It can probably be rewritten in terms of `getRouteFromEntrypoint`. - There are a few places where `denormalizePagePath(normalizePagePath(...))` is happening. This is a sign that `normalizePagePath` is doing some validation that is independent of its rewriting logic. That should probably be factored out in its own function. after that I should probably investigate whether `normalizePagePath` is even still needed at all. - a lot of code is doing `.replace(/\\/g, '')`. If wanted, that could be replaced with `normalizePathSep`. - It looks to me like some logic that's spread across the project can be centralized in 4 functions - `getRouteFromEntrypoint` (part of this PR) - its inverse `getEntrypointFromRoute` (already exists in `_document.tsx` as `getPageFile`) - `getRouteFromPageFile` - its inverse `getPageFileFromRoute` (already exists as `findPageFile ` in `server/lib/find-page-file.ts`) It could be beneficial to structure the code to keep these fuctionalities close together and name them similarly. - revise `index.amp` handling in pagesmanifest. I left it alone in this PR to keep it scoped, but it may be broken wrt nested index files as well. It might even make sense to reshape the pagesmanifest altogether to handle html/json/amp/... better
1479 lines
42 KiB
TypeScript
1479 lines
42 KiB
TypeScript
import compression from 'next/dist/compiled/compression'
|
|
import fs from 'fs'
|
|
import chalk from 'next/dist/compiled/chalk'
|
|
import { IncomingMessage, ServerResponse } from 'http'
|
|
import Proxy from 'next/dist/compiled/http-proxy'
|
|
import { join, relative, resolve, sep } from 'path'
|
|
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
|
|
import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url'
|
|
import { PrerenderManifest } from '../../build'
|
|
import {
|
|
getRedirectStatus,
|
|
Header,
|
|
Redirect,
|
|
Rewrite,
|
|
RouteType,
|
|
} from '../../lib/check-custom-routes'
|
|
import { withCoalescedInvoke } from '../../lib/coalesced-function'
|
|
import {
|
|
BUILD_ID_FILE,
|
|
CLIENT_PUBLIC_FILES_PATH,
|
|
CLIENT_STATIC_FILES_PATH,
|
|
CLIENT_STATIC_FILES_RUNTIME,
|
|
PAGES_MANIFEST,
|
|
PHASE_PRODUCTION_SERVER,
|
|
PRERENDER_MANIFEST,
|
|
ROUTES_MANIFEST,
|
|
SERVERLESS_DIRECTORY,
|
|
SERVER_DIRECTORY,
|
|
} from '../lib/constants'
|
|
import {
|
|
getRouteMatcher,
|
|
getRouteRegex,
|
|
getSortedRoutes,
|
|
isDynamicRoute,
|
|
} from '../lib/router/utils'
|
|
import * as envConfig from '../lib/runtime-config'
|
|
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
|
|
import { apiResolver, tryGetPreviewData, __ApiPreviewProps } from './api-utils'
|
|
import loadConfig, { isTargetLikeServerless } from './config'
|
|
import pathMatch from './lib/path-match'
|
|
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
|
|
import { loadComponents, LoadComponentsReturnType } from './load-components'
|
|
import { normalizePagePath } from './normalize-page-path'
|
|
import { RenderOpts, RenderOptsPartial, renderToHTML } from './render'
|
|
import { getPagePath } from './require'
|
|
import Router, {
|
|
DynamicRoutes,
|
|
PageChecker,
|
|
Params,
|
|
prepareDestination,
|
|
route,
|
|
Route,
|
|
} from './router'
|
|
import { sendHTML } from './send-html'
|
|
import { sendPayload } from './send-payload'
|
|
import { serveStatic } from './serve-static'
|
|
import {
|
|
getFallback,
|
|
getSprCache,
|
|
initializeSprCache,
|
|
setSprCache,
|
|
} from './spr-cache'
|
|
import { execOnce } from '../lib/utils'
|
|
import { isBlockedPage } from './utils'
|
|
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
|
|
import { loadEnvConfig } from '../../lib/load-env-config'
|
|
import './node-polyfill-fetch'
|
|
import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin'
|
|
|
|
const getCustomRouteMatcher = pathMatch(true)
|
|
|
|
type NextConfig = any
|
|
|
|
type Middleware = (
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
next: (err?: Error) => void
|
|
) => void
|
|
|
|
type FindComponentsResult = {
|
|
components: LoadComponentsReturnType
|
|
query: ParsedUrlQuery
|
|
}
|
|
|
|
export type ServerConstructor = {
|
|
/**
|
|
* Where the Next project is located - @default '.'
|
|
*/
|
|
dir?: string
|
|
staticMarkup?: boolean
|
|
/**
|
|
* Hide error messages containing server information - @default false
|
|
*/
|
|
quiet?: boolean
|
|
/**
|
|
* Object what you would use in next.config.js - @default {}
|
|
*/
|
|
conf?: NextConfig
|
|
dev?: boolean
|
|
customServer?: boolean
|
|
}
|
|
|
|
export default class Server {
|
|
dir: string
|
|
quiet: boolean
|
|
nextConfig: NextConfig
|
|
distDir: string
|
|
pagesDir?: string
|
|
publicDir: string
|
|
hasStaticDir: boolean
|
|
serverBuildDir: string
|
|
pagesManifest?: PagesManifest
|
|
buildId: string
|
|
renderOpts: {
|
|
poweredByHeader: boolean
|
|
staticMarkup: boolean
|
|
buildId: string
|
|
generateEtags: boolean
|
|
runtimeConfig?: { [key: string]: any }
|
|
assetPrefix?: string
|
|
canonicalBase: string
|
|
dev?: boolean
|
|
previewProps: __ApiPreviewProps
|
|
customServer?: boolean
|
|
ampOptimizerConfig?: { [key: string]: any }
|
|
basePath: string
|
|
}
|
|
private compression?: Middleware
|
|
private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
|
|
router: Router
|
|
protected dynamicRoutes?: DynamicRoutes
|
|
protected customRoutes?: {
|
|
rewrites: Rewrite[]
|
|
redirects: Redirect[]
|
|
headers: Header[]
|
|
}
|
|
protected staticPathsWorker?: import('jest-worker').default & {
|
|
loadStaticPaths: typeof import('../../server/static-paths-worker').loadStaticPaths
|
|
}
|
|
|
|
public constructor({
|
|
dir = '.',
|
|
staticMarkup = false,
|
|
quiet = false,
|
|
conf = null,
|
|
dev = false,
|
|
customServer = true,
|
|
}: ServerConstructor = {}) {
|
|
this.dir = resolve(dir)
|
|
this.quiet = quiet
|
|
const phase = this.currentPhase()
|
|
loadEnvConfig(this.dir, dev)
|
|
|
|
this.nextConfig = loadConfig(phase, this.dir, conf)
|
|
this.distDir = join(this.dir, this.nextConfig.distDir)
|
|
this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
|
|
this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
|
|
|
|
// Only serverRuntimeConfig needs the default
|
|
// publicRuntimeConfig gets it's default in client/index.js
|
|
const {
|
|
serverRuntimeConfig = {},
|
|
publicRuntimeConfig,
|
|
assetPrefix,
|
|
generateEtags,
|
|
compress,
|
|
} = this.nextConfig
|
|
|
|
this.buildId = this.readBuildId()
|
|
|
|
this.renderOpts = {
|
|
poweredByHeader: this.nextConfig.poweredByHeader,
|
|
canonicalBase: this.nextConfig.amp.canonicalBase,
|
|
staticMarkup,
|
|
buildId: this.buildId,
|
|
generateEtags,
|
|
previewProps: this.getPreviewProps(),
|
|
customServer: customServer === true ? true : undefined,
|
|
ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
|
|
basePath: this.nextConfig.experimental.basePath,
|
|
}
|
|
|
|
// Only the `publicRuntimeConfig` key is exposed to the client side
|
|
// It'll be rendered as part of __NEXT_DATA__ on the client side
|
|
if (Object.keys(publicRuntimeConfig).length > 0) {
|
|
this.renderOpts.runtimeConfig = publicRuntimeConfig
|
|
}
|
|
|
|
if (compress && this.nextConfig.target === 'server') {
|
|
this.compression = compression() as Middleware
|
|
}
|
|
|
|
// Initialize next/config with the environment configuration
|
|
envConfig.setConfig({
|
|
serverRuntimeConfig,
|
|
publicRuntimeConfig,
|
|
})
|
|
|
|
this.serverBuildDir = join(
|
|
this.distDir,
|
|
this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
|
|
)
|
|
const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)
|
|
|
|
if (!dev) {
|
|
this.pagesManifest = require(pagesManifestPath)
|
|
}
|
|
|
|
this.router = new Router(this.generateRoutes())
|
|
this.setAssetPrefix(assetPrefix)
|
|
|
|
// call init-server middleware, this is also handled
|
|
// individually in serverless bundles when deployed
|
|
if (!dev && this.nextConfig.experimental.plugins) {
|
|
const initServer = require(join(this.serverBuildDir, 'init-server.js'))
|
|
.default
|
|
this.onErrorMiddleware = require(join(
|
|
this.serverBuildDir,
|
|
'on-error-server.js'
|
|
)).default
|
|
initServer()
|
|
}
|
|
|
|
initializeSprCache({
|
|
dev,
|
|
distDir: this.distDir,
|
|
pagesDir: join(
|
|
this.distDir,
|
|
this._isLikeServerless
|
|
? SERVERLESS_DIRECTORY
|
|
: `${SERVER_DIRECTORY}/static/${this.buildId}`,
|
|
'pages'
|
|
),
|
|
flushToDisk: this.nextConfig.experimental.sprFlushToDisk,
|
|
})
|
|
}
|
|
|
|
protected currentPhase(): string {
|
|
return PHASE_PRODUCTION_SERVER
|
|
}
|
|
|
|
private logError(err: Error): void {
|
|
if (this.onErrorMiddleware) {
|
|
this.onErrorMiddleware({ err })
|
|
}
|
|
if (this.quiet) return
|
|
// tslint:disable-next-line
|
|
console.error(err)
|
|
}
|
|
|
|
private async handleRequest(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
parsedUrl?: UrlWithParsedQuery
|
|
): Promise<void> {
|
|
// Parse url if parsedUrl not provided
|
|
if (!parsedUrl || typeof parsedUrl !== 'object') {
|
|
const url: any = req.url
|
|
parsedUrl = parseUrl(url, true)
|
|
}
|
|
|
|
// Parse the querystring ourselves if the user doesn't handle querystring parsing
|
|
if (typeof parsedUrl.query === 'string') {
|
|
parsedUrl.query = parseQs(parsedUrl.query)
|
|
}
|
|
|
|
const { basePath } = this.nextConfig.experimental
|
|
|
|
// if basePath is set require it be present
|
|
if (basePath && !req.url!.startsWith(basePath)) {
|
|
return this.render404(req, res, parsedUrl)
|
|
} else {
|
|
// If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
|
|
parsedUrl.pathname = parsedUrl.pathname!.replace(basePath, '') || '/'
|
|
req.url = req.url!.replace(basePath, '')
|
|
}
|
|
|
|
res.statusCode = 200
|
|
try {
|
|
return await this.run(req, res, parsedUrl)
|
|
} catch (err) {
|
|
this.logError(err)
|
|
res.statusCode = 500
|
|
res.end('Internal Server Error')
|
|
}
|
|
}
|
|
|
|
public getRequestHandler() {
|
|
return this.handleRequest.bind(this)
|
|
}
|
|
|
|
public setAssetPrefix(prefix?: string): void {
|
|
this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
|
|
}
|
|
|
|
// Backwards compatibility
|
|
public async prepare(): Promise<void> {}
|
|
|
|
// Backwards compatibility
|
|
protected async close(): Promise<void> {}
|
|
|
|
protected setImmutableAssetCacheControl(res: ServerResponse): void {
|
|
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
|
|
}
|
|
|
|
protected getCustomRoutes() {
|
|
return require(join(this.distDir, ROUTES_MANIFEST))
|
|
}
|
|
|
|
private _cachedPreviewManifest: PrerenderManifest | undefined
|
|
protected getPrerenderManifest(): PrerenderManifest {
|
|
if (this._cachedPreviewManifest) {
|
|
return this._cachedPreviewManifest
|
|
}
|
|
const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
|
|
return (this._cachedPreviewManifest = manifest)
|
|
}
|
|
|
|
protected getPreviewProps(): __ApiPreviewProps {
|
|
return this.getPrerenderManifest().preview
|
|
}
|
|
|
|
protected generateRoutes(): {
|
|
headers: Route[]
|
|
rewrites: Route[]
|
|
fsRoutes: Route[]
|
|
redirects: Route[]
|
|
catchAllRoute: Route
|
|
pageChecker: PageChecker
|
|
useFileSystemPublicRoutes: boolean
|
|
dynamicRoutes: DynamicRoutes | undefined
|
|
} {
|
|
this.customRoutes = this.getCustomRoutes()
|
|
|
|
const publicRoutes = fs.existsSync(this.publicDir)
|
|
? this.generatePublicRoutes()
|
|
: []
|
|
|
|
const staticFilesRoute = this.hasStaticDir
|
|
? [
|
|
{
|
|
// It's very important to keep this route's param optional.
|
|
// (but it should support as many params as needed, separated by '/')
|
|
// Otherwise this will lead to a pretty simple DOS attack.
|
|
// See more: https://github.com/vercel/next.js/issues/2617
|
|
match: route('/static/:path*'),
|
|
name: 'static catchall',
|
|
fn: async (req, res, params, parsedUrl) => {
|
|
const p = join(
|
|
this.dir,
|
|
'static',
|
|
...(params.path || []).map(encodeURIComponent)
|
|
)
|
|
await this.serveStatic(req, res, p, parsedUrl)
|
|
return {
|
|
finished: true,
|
|
}
|
|
},
|
|
} as Route,
|
|
]
|
|
: []
|
|
|
|
let headers: Route[] = []
|
|
let rewrites: Route[] = []
|
|
let redirects: Route[] = []
|
|
|
|
const fsRoutes: Route[] = [
|
|
{
|
|
match: route('/_next/static/:path*'),
|
|
type: 'route',
|
|
name: '_next/static catchall',
|
|
fn: async (req, res, params, parsedUrl) => {
|
|
// The commons folder holds commonschunk files
|
|
// The chunks folder holds dynamic entries
|
|
// The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
|
|
|
|
// make sure to 404 for /_next/static itself
|
|
if (!params.path) {
|
|
await this.render404(req, res, parsedUrl)
|
|
return {
|
|
finished: true,
|
|
}
|
|
}
|
|
|
|
if (
|
|
params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
|
|
params.path[0] === 'chunks' ||
|
|
params.path[0] === 'css' ||
|
|
params.path[0] === 'media' ||
|
|
params.path[0] === this.buildId
|
|
) {
|
|
this.setImmutableAssetCacheControl(res)
|
|
}
|
|
const p = join(
|
|
this.distDir,
|
|
CLIENT_STATIC_FILES_PATH,
|
|
...(params.path || [])
|
|
)
|
|
await this.serveStatic(req, res, p, parsedUrl)
|
|
return {
|
|
finished: true,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
match: route('/_next/data/:path*'),
|
|
type: 'route',
|
|
name: '_next/data catchall',
|
|
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()
|
|
|
|
// show 404 if it doesn't end with .json
|
|
if (!params.path[params.path.length - 1].endsWith('.json')) {
|
|
await this.render404(req, res, _parsedUrl)
|
|
return {
|
|
finished: true,
|
|
}
|
|
}
|
|
|
|
// re-create page's pathname
|
|
const pathname = `/${params.path
|
|
// we need to re-encode the params since they are decoded
|
|
// by path-match and we are re-building the URL
|
|
.map((param: string) => encodeURIComponent(param))
|
|
.join('/')}`
|
|
.replace(/\.json$/, '')
|
|
.replace(/\/index$/, '/')
|
|
|
|
const parsedUrl = parseUrl(pathname, true)
|
|
|
|
await this.render(
|
|
req,
|
|
res,
|
|
pathname,
|
|
{ ..._parsedUrl.query, _nextDataReq: '1' },
|
|
parsedUrl
|
|
)
|
|
return {
|
|
finished: true,
|
|
}
|
|
},
|
|
},
|
|
{
|
|
match: route('/_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,
|
|
}
|
|
},
|
|
},
|
|
...publicRoutes,
|
|
...staticFilesRoute,
|
|
]
|
|
|
|
if (this.customRoutes) {
|
|
const getCustomRoute = (
|
|
r: Rewrite | Redirect | Header,
|
|
type: RouteType
|
|
) =>
|
|
({
|
|
...r,
|
|
type,
|
|
match: getCustomRouteMatcher(r.source),
|
|
name: type,
|
|
fn: async (_req, _res, _params, _parsedUrl) => ({ finished: false }),
|
|
} as Route & Rewrite & Header)
|
|
|
|
const updateHeaderValue = (value: string, params: Params): string => {
|
|
if (!value.includes(':')) {
|
|
return value
|
|
}
|
|
const { parsedDestination } = prepareDestination(value, params, {})
|
|
|
|
if (
|
|
!parsedDestination.pathname ||
|
|
!parsedDestination.pathname.startsWith('/')
|
|
) {
|
|
// the value needs to start with a forward-slash to be compiled
|
|
// correctly
|
|
return compilePathToRegex(`/${value}`, { validate: false })(
|
|
params
|
|
).substr(1)
|
|
}
|
|
return formatUrl(parsedDestination)
|
|
}
|
|
|
|
// Headers come very first
|
|
headers = this.customRoutes.headers.map((r) => {
|
|
const headerRoute = getCustomRoute(r, 'header')
|
|
return {
|
|
match: headerRoute.match,
|
|
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 as Header).headers) {
|
|
let { key, value } = header
|
|
if (hasParams) {
|
|
key = updateHeaderValue(key, params)
|
|
value = updateHeaderValue(value, params)
|
|
}
|
|
res.setHeader(key, value)
|
|
}
|
|
return { finished: false }
|
|
},
|
|
} as Route
|
|
})
|
|
|
|
redirects = this.customRoutes.redirects.map((redirect) => {
|
|
const redirectRoute = getCustomRoute(redirect, 'redirect')
|
|
return {
|
|
type: redirectRoute.type,
|
|
match: redirectRoute.match,
|
|
statusCode: redirectRoute.statusCode,
|
|
name: `Redirect route`,
|
|
fn: async (_req, res, params, parsedUrl) => {
|
|
const { parsedDestination } = prepareDestination(
|
|
redirectRoute.destination,
|
|
params,
|
|
parsedUrl.query
|
|
)
|
|
const updatedDestination = formatUrl(parsedDestination)
|
|
|
|
res.setHeader('Location', updatedDestination)
|
|
res.statusCode = getRedirectStatus(redirectRoute as Redirect)
|
|
|
|
// Since IE11 doesn't support the 308 header add backwards
|
|
// compatibility using refresh header
|
|
if (res.statusCode === 308) {
|
|
res.setHeader('Refresh', `0;url=${updatedDestination}`)
|
|
}
|
|
|
|
res.end()
|
|
return {
|
|
finished: true,
|
|
}
|
|
},
|
|
} as Route
|
|
})
|
|
|
|
rewrites = this.customRoutes.rewrites.map((rewrite) => {
|
|
const rewriteRoute = getCustomRoute(rewrite, 'rewrite')
|
|
return {
|
|
check: true,
|
|
type: rewriteRoute.type,
|
|
name: `Rewrite route`,
|
|
match: rewriteRoute.match,
|
|
fn: async (req, res, params, parsedUrl) => {
|
|
const { newUrl, parsedDestination } = prepareDestination(
|
|
rewriteRoute.destination,
|
|
params,
|
|
parsedUrl.query,
|
|
true
|
|
)
|
|
|
|
// external rewrite, proxy it
|
|
if (parsedDestination.protocol) {
|
|
const target = formatUrl(parsedDestination)
|
|
const proxy = new Proxy({
|
|
target,
|
|
changeOrigin: true,
|
|
ignorePath: true,
|
|
})
|
|
proxy.web(req, res)
|
|
|
|
proxy.on('error', (err: Error) => {
|
|
console.error(`Error occurred proxying ${target}`, err)
|
|
})
|
|
return {
|
|
finished: true,
|
|
}
|
|
}
|
|
;(req as any)._nextDidRewrite = true
|
|
;(req as any)._nextRewroteUrl = newUrl
|
|
|
|
return {
|
|
finished: false,
|
|
pathname: newUrl,
|
|
query: parsedDestination.query,
|
|
}
|
|
},
|
|
} as Route
|
|
})
|
|
}
|
|
|
|
const catchAllRoute: Route = {
|
|
match: route('/:path*'),
|
|
type: 'route',
|
|
name: 'Catchall render',
|
|
fn: async (req, res, params, parsedUrl) => {
|
|
const { pathname, query } = parsedUrl
|
|
if (!pathname) {
|
|
throw new Error('pathname is undefined')
|
|
}
|
|
|
|
if (params?.path?.[0] === 'api') {
|
|
const handled = await this.handleApiRequest(
|
|
req as NextApiRequest,
|
|
res as NextApiResponse,
|
|
pathname,
|
|
query
|
|
)
|
|
if (handled) {
|
|
return { finished: true }
|
|
}
|
|
}
|
|
|
|
await this.render(req, res, pathname, query, parsedUrl)
|
|
return {
|
|
finished: true,
|
|
}
|
|
},
|
|
}
|
|
|
|
const { useFileSystemPublicRoutes } = this.nextConfig
|
|
|
|
if (useFileSystemPublicRoutes) {
|
|
this.dynamicRoutes = this.getDynamicRoutes()
|
|
}
|
|
|
|
return {
|
|
headers,
|
|
fsRoutes,
|
|
rewrites,
|
|
redirects,
|
|
catchAllRoute,
|
|
useFileSystemPublicRoutes,
|
|
dynamicRoutes: this.dynamicRoutes,
|
|
pageChecker: this.hasPage.bind(this),
|
|
}
|
|
}
|
|
|
|
private async getPagePath(pathname: string): Promise<string> {
|
|
return getPagePath(
|
|
pathname,
|
|
this.distDir,
|
|
this._isLikeServerless,
|
|
this.renderOpts.dev
|
|
)
|
|
}
|
|
|
|
protected async hasPage(pathname: string): Promise<boolean> {
|
|
let found = false
|
|
try {
|
|
found = !!(await this.getPagePath(pathname))
|
|
} catch (_) {}
|
|
|
|
return found
|
|
}
|
|
|
|
protected async _beforeCatchAllRender(
|
|
_req: IncomingMessage,
|
|
_res: ServerResponse,
|
|
_params: Params,
|
|
_parsedUrl: UrlWithParsedQuery
|
|
): Promise<boolean> {
|
|
return false
|
|
}
|
|
|
|
// Used to build API page in development
|
|
protected async ensureApiPage(_pathname: string): Promise<void> {}
|
|
|
|
/**
|
|
* Resolves `API` request, in development builds on demand
|
|
* @param req http request
|
|
* @param res http response
|
|
* @param pathname path of request
|
|
*/
|
|
private async handleApiRequest(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
pathname: string,
|
|
query: ParsedUrlQuery
|
|
): Promise<boolean> {
|
|
let page = pathname
|
|
let params: Params | boolean = false
|
|
let pageFound = await this.hasPage(page)
|
|
|
|
if (!pageFound && this.dynamicRoutes) {
|
|
for (const dynamicRoute of this.dynamicRoutes) {
|
|
params = dynamicRoute.match(pathname)
|
|
if (dynamicRoute.page.startsWith('/api') && params) {
|
|
page = dynamicRoute.page
|
|
pageFound = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!pageFound) {
|
|
return false
|
|
}
|
|
// Make sure the page is built before getting the path
|
|
// or else it won't be in the manifest yet
|
|
await this.ensureApiPage(page)
|
|
|
|
let builtPagePath
|
|
try {
|
|
builtPagePath = await this.getPagePath(page)
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') {
|
|
return false
|
|
}
|
|
throw err
|
|
}
|
|
|
|
const pageModule = require(builtPagePath)
|
|
query = { ...query, ...params }
|
|
|
|
if (!this.renderOpts.dev && this._isLikeServerless) {
|
|
if (typeof pageModule.default === 'function') {
|
|
prepareServerlessUrl(req, query)
|
|
await pageModule.default(req, res)
|
|
return true
|
|
}
|
|
}
|
|
|
|
await apiResolver(
|
|
req,
|
|
res,
|
|
query,
|
|
pageModule,
|
|
this.renderOpts.previewProps,
|
|
false,
|
|
this.onErrorMiddleware
|
|
)
|
|
return true
|
|
}
|
|
|
|
protected generatePublicRoutes(): Route[] {
|
|
const publicFiles = new Set(
|
|
recursiveReadDirSync(this.publicDir).map((p) => p.replace(/\\/g, '/'))
|
|
)
|
|
|
|
return [
|
|
{
|
|
match: route('/:path*'),
|
|
name: 'public folder catchall',
|
|
fn: async (req, res, params, parsedUrl) => {
|
|
const pathParts: string[] = params.path || []
|
|
const path = `/${pathParts.join('/')}`
|
|
|
|
if (publicFiles.has(path)) {
|
|
await this.serveStatic(
|
|
req,
|
|
res,
|
|
// we need to re-encode it since send decodes it
|
|
join(this.publicDir, ...pathParts.map(encodeURIComponent)),
|
|
parsedUrl
|
|
)
|
|
return {
|
|
finished: true,
|
|
}
|
|
}
|
|
return {
|
|
finished: false,
|
|
}
|
|
},
|
|
} as Route,
|
|
]
|
|
}
|
|
|
|
protected getDynamicRoutes() {
|
|
return getSortedRoutes(Object.keys(this.pagesManifest!))
|
|
.filter(isDynamicRoute)
|
|
.map((page) => ({
|
|
page,
|
|
match: getRouteMatcher(getRouteRegex(page)),
|
|
}))
|
|
}
|
|
|
|
private handleCompression(req: IncomingMessage, res: ServerResponse): void {
|
|
if (this.compression) {
|
|
this.compression(req, res, () => {})
|
|
}
|
|
}
|
|
|
|
protected async run(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
parsedUrl: UrlWithParsedQuery
|
|
): Promise<void> {
|
|
this.handleCompression(req, res)
|
|
|
|
try {
|
|
const matched = await this.router.execute(req, res, parsedUrl)
|
|
if (matched) {
|
|
return
|
|
}
|
|
} catch (err) {
|
|
if (err.code === 'DECODE_FAILED') {
|
|
res.statusCode = 400
|
|
return this.renderError(null, req, res, '/_error', {})
|
|
}
|
|
throw err
|
|
}
|
|
|
|
await this.render404(req, res, parsedUrl)
|
|
}
|
|
|
|
protected async sendHTML(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
html: string
|
|
): Promise<void> {
|
|
const { generateEtags, poweredByHeader } = this.renderOpts
|
|
return sendHTML(req, res, html, { generateEtags, poweredByHeader })
|
|
}
|
|
|
|
public async render(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
pathname: string,
|
|
query: ParsedUrlQuery = {},
|
|
parsedUrl?: UrlWithParsedQuery
|
|
): Promise<void> {
|
|
if (!pathname.startsWith('/')) {
|
|
console.warn(
|
|
`Cannot render page with path "${pathname}", did you mean "/${pathname}"?. See more info here: https://err.sh/next.js/render-no-starting-slash`
|
|
)
|
|
}
|
|
|
|
if (
|
|
this.renderOpts.customServer &&
|
|
pathname === '/index' &&
|
|
!(await this.hasPage('/index'))
|
|
) {
|
|
// maintain backwards compatibility for custom server
|
|
// (see custom-server integration tests)
|
|
pathname = '/'
|
|
}
|
|
|
|
const url: any = req.url
|
|
|
|
// we allow custom servers to call render for all URLs
|
|
// so check if we need to serve a static _next file or not.
|
|
// we don't modify the URL for _next/data request but still
|
|
// call render so we special case this to prevent an infinite loop
|
|
if (
|
|
!query._nextDataReq &&
|
|
(url.match(/^\/_next\//) ||
|
|
(this.hasStaticDir && url.match(/^\/static\//)))
|
|
) {
|
|
return this.handleRequest(req, res, parsedUrl)
|
|
}
|
|
|
|
if (isBlockedPage(pathname)) {
|
|
return this.render404(req, res, parsedUrl)
|
|
}
|
|
|
|
const html = await this.renderToHTML(req, res, pathname, query)
|
|
// Request was ended by the user
|
|
if (html === null) {
|
|
return
|
|
}
|
|
|
|
return this.sendHTML(req, res, html)
|
|
}
|
|
|
|
private async findPageComponents(
|
|
pathname: string,
|
|
query: ParsedUrlQuery = {},
|
|
params: Params | null = null
|
|
): Promise<FindComponentsResult | null> {
|
|
const paths = [
|
|
// try serving a static AMP version first
|
|
query.amp ? normalizePagePath(pathname) + '.amp' : null,
|
|
pathname,
|
|
].filter(Boolean)
|
|
for (const pagePath of paths) {
|
|
try {
|
|
const components = await loadComponents(
|
|
this.distDir,
|
|
this.buildId,
|
|
pagePath!,
|
|
!this.renderOpts.dev && this._isLikeServerless
|
|
)
|
|
return {
|
|
components,
|
|
query: {
|
|
...(components.getStaticProps
|
|
? { _nextDataReq: query._nextDataReq, amp: query.amp }
|
|
: query),
|
|
...(params || {}),
|
|
},
|
|
}
|
|
} catch (err) {
|
|
if (err.code !== 'ENOENT') throw err
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
private async getStaticPaths(
|
|
pathname: string
|
|
): Promise<{
|
|
staticPaths: string[] | undefined
|
|
hasStaticFallback: boolean
|
|
}> {
|
|
// we lazy load the staticPaths to prevent the user
|
|
// from waiting on them for the page to load in dev mode
|
|
let staticPaths: string[] | undefined
|
|
let hasStaticFallback = false
|
|
|
|
if (!this.renderOpts.dev) {
|
|
// `staticPaths` is intentionally set to `undefined` as it should've
|
|
// been caught when checking disk data.
|
|
staticPaths = undefined
|
|
|
|
// Read whether or not fallback should exist from the manifest.
|
|
hasStaticFallback =
|
|
typeof this.getPrerenderManifest().dynamicRoutes[pathname].fallback ===
|
|
'string'
|
|
} else {
|
|
const __getStaticPaths = async () => {
|
|
const paths = await this.staticPathsWorker!.loadStaticPaths(
|
|
this.distDir,
|
|
this.buildId,
|
|
pathname,
|
|
!this.renderOpts.dev && this._isLikeServerless
|
|
)
|
|
return paths
|
|
}
|
|
;({ paths: staticPaths, fallback: hasStaticFallback } = (
|
|
await withCoalescedInvoke(__getStaticPaths)(
|
|
`staticPaths-${pathname}`,
|
|
[]
|
|
)
|
|
).value)
|
|
}
|
|
|
|
return { staticPaths, hasStaticFallback }
|
|
}
|
|
|
|
private async renderToHTMLWithComponents(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
pathname: string,
|
|
{ components, query }: FindComponentsResult,
|
|
opts: RenderOptsPartial
|
|
): Promise<string | null> {
|
|
// we need to ensure the status code if /404 is visited directly
|
|
if (pathname === '/404') {
|
|
res.statusCode = 404
|
|
}
|
|
|
|
// handle static page
|
|
if (typeof components.Component === 'string') {
|
|
return components.Component
|
|
}
|
|
|
|
// check request state
|
|
const isLikeServerless =
|
|
typeof components.Component === 'object' &&
|
|
typeof (components.Component as any).renderReqToHTML === 'function'
|
|
const isSSG = !!components.getStaticProps
|
|
const isServerProps = !!components.getServerSideProps
|
|
const hasStaticPaths = !!components.getStaticPaths
|
|
|
|
if (!query.amp) {
|
|
delete query.amp
|
|
}
|
|
|
|
// Toggle whether or not this is a Data request
|
|
const isDataReq = !!query._nextDataReq && (isSSG || isServerProps)
|
|
delete query._nextDataReq
|
|
|
|
let previewData: string | false | object | undefined
|
|
let isPreviewMode = false
|
|
|
|
if (isServerProps || isSSG) {
|
|
previewData = tryGetPreviewData(req, res, this.renderOpts.previewProps)
|
|
isPreviewMode = previewData !== false
|
|
}
|
|
|
|
// Compute the iSSG cache key. We use the rewroteUrl since
|
|
// pages with fallback: false are allowed to be rewritten to
|
|
// and we need to look up the path by the rewritten path
|
|
let urlPathname = (req as any)._nextRewroteUrl
|
|
? (req as any)._nextRewroteUrl
|
|
: `${parseUrl(req.url || '').pathname!}`
|
|
|
|
// remove trailing slash
|
|
urlPathname = urlPathname.replace(/(?!^)\/$/, '')
|
|
|
|
// remove /_next/data prefix from urlPathname so it matches
|
|
// for direct page visit and /_next/data visit
|
|
if (isDataReq && urlPathname.includes(this.buildId)) {
|
|
urlPathname = (urlPathname.split(this.buildId).pop() || '/')
|
|
.replace(/\.json$/, '')
|
|
.replace(/\/index$/, '/')
|
|
}
|
|
|
|
const ssgCacheKey =
|
|
isPreviewMode || !isSSG
|
|
? undefined // Preview mode bypasses the cache
|
|
: `${urlPathname}${query.amp ? '.amp' : ''}`
|
|
|
|
// Complete the response with cached data if its present
|
|
const cachedData = ssgCacheKey ? await getSprCache(ssgCacheKey) : undefined
|
|
|
|
if (cachedData) {
|
|
const data = isDataReq
|
|
? JSON.stringify(cachedData.pageData)
|
|
: cachedData.html
|
|
|
|
sendPayload(
|
|
res,
|
|
data,
|
|
isDataReq ? 'json' : 'html',
|
|
!this.renderOpts.dev
|
|
? {
|
|
private: isPreviewMode,
|
|
stateful: false, // GSP response
|
|
revalidate:
|
|
cachedData.curRevalidate !== undefined
|
|
? cachedData.curRevalidate
|
|
: /* default to minimum revalidate (this should be an invariant) */ 1,
|
|
}
|
|
: undefined
|
|
)
|
|
|
|
// Stop the request chain here if the data we sent was up-to-date
|
|
if (!cachedData.isStale) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// If we're here, that means data is missing or it's stale.
|
|
const maybeCoalesceInvoke = ssgCacheKey
|
|
? (fn: any) => withCoalescedInvoke(fn).bind(null, ssgCacheKey, [])
|
|
: (fn: any) => async () => {
|
|
const value = await fn()
|
|
return { isOrigin: true, value }
|
|
}
|
|
|
|
const doRender = maybeCoalesceInvoke(async function (): Promise<{
|
|
html: string | null
|
|
pageData: any
|
|
sprRevalidate: number | false
|
|
}> {
|
|
let pageData: any
|
|
let html: string | null
|
|
let sprRevalidate: number | false
|
|
|
|
let renderResult
|
|
// handle serverless
|
|
if (isLikeServerless) {
|
|
renderResult = await (components.Component as any).renderReqToHTML(
|
|
req,
|
|
res,
|
|
'passthrough'
|
|
)
|
|
|
|
html = renderResult.html
|
|
pageData = renderResult.renderOpts.pageData
|
|
sprRevalidate = renderResult.renderOpts.revalidate
|
|
} else {
|
|
const renderOpts: RenderOpts = {
|
|
...components,
|
|
...opts,
|
|
isDataReq,
|
|
}
|
|
renderResult = await renderToHTML(req, res, pathname, query, renderOpts)
|
|
|
|
html = renderResult
|
|
// TODO: change this to a different passing mechanism
|
|
pageData = (renderOpts as any).pageData
|
|
sprRevalidate = (renderOpts as any).revalidate
|
|
}
|
|
|
|
return { html, pageData, sprRevalidate }
|
|
})
|
|
|
|
const isProduction = !this.renderOpts.dev
|
|
const isDynamicPathname = isDynamicRoute(pathname)
|
|
const didRespond = isResSent(res)
|
|
|
|
const { staticPaths, hasStaticFallback } = hasStaticPaths
|
|
? await this.getStaticPaths(pathname)
|
|
: { staticPaths: undefined, hasStaticFallback: false }
|
|
|
|
// const isForcedBlocking =
|
|
// req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking'
|
|
|
|
// When we did not respond from cache, we need to choose to block on
|
|
// rendering or return a skeleton.
|
|
//
|
|
// * Data requests always block.
|
|
//
|
|
// * Preview mode toggles all pages to be resolved in a blocking manner.
|
|
//
|
|
// * Non-dynamic pages should block (though this is an impossible
|
|
// case in production).
|
|
//
|
|
// * Dynamic pages should return their skeleton if not defined in
|
|
// getStaticPaths, then finish the data request on the client-side.
|
|
//
|
|
if (
|
|
ssgCacheKey &&
|
|
!didRespond &&
|
|
!isDataReq &&
|
|
!isPreviewMode &&
|
|
isDynamicPathname &&
|
|
// Development should trigger fallback when the path is not in
|
|
// `getStaticPaths`
|
|
(isProduction || !staticPaths || !staticPaths.includes(urlPathname))
|
|
) {
|
|
if (
|
|
// In development, fall through to render to handle missing
|
|
// getStaticPaths.
|
|
(isProduction || staticPaths) &&
|
|
// When fallback isn't present, abort this render so we 404
|
|
!hasStaticFallback
|
|
) {
|
|
throw new NoFallbackError()
|
|
}
|
|
|
|
let html: string
|
|
|
|
// Production already emitted the fallback as static HTML.
|
|
if (isProduction) {
|
|
html = await getFallback(pathname)
|
|
}
|
|
// We need to generate the fallback on-demand for development.
|
|
else {
|
|
query.__nextFallback = 'true'
|
|
if (isLikeServerless) {
|
|
prepareServerlessUrl(req, query)
|
|
}
|
|
const { value: renderResult } = await doRender()
|
|
html = renderResult.html
|
|
}
|
|
|
|
sendPayload(res, html, 'html')
|
|
return null
|
|
}
|
|
|
|
const {
|
|
isOrigin,
|
|
value: { html, pageData, sprRevalidate },
|
|
} = await doRender()
|
|
let resHtml = html
|
|
if (!isResSent(res) && (isSSG || isDataReq || isServerProps)) {
|
|
sendPayload(
|
|
res,
|
|
isDataReq ? JSON.stringify(pageData) : html,
|
|
isDataReq ? 'json' : 'html',
|
|
!this.renderOpts.dev || (isServerProps && !isDataReq)
|
|
? {
|
|
private: isPreviewMode,
|
|
stateful: !isSSG,
|
|
revalidate: sprRevalidate,
|
|
}
|
|
: undefined
|
|
)
|
|
resHtml = null
|
|
}
|
|
|
|
// Update the SPR cache if the head request and cacheable
|
|
if (isOrigin && ssgCacheKey) {
|
|
await setSprCache(ssgCacheKey, { html: html!, pageData }, sprRevalidate)
|
|
}
|
|
|
|
return resHtml
|
|
}
|
|
|
|
public async renderToHTML(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
pathname: string,
|
|
query: ParsedUrlQuery = {}
|
|
): Promise<string | null> {
|
|
try {
|
|
const result = await this.findPageComponents(pathname, query)
|
|
if (result) {
|
|
try {
|
|
return await this.renderToHTMLWithComponents(
|
|
req,
|
|
res,
|
|
pathname,
|
|
result,
|
|
{ ...this.renderOpts }
|
|
)
|
|
} catch (err) {
|
|
if (!(err instanceof NoFallbackError)) {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.dynamicRoutes) {
|
|
for (const dynamicRoute of this.dynamicRoutes) {
|
|
const params = dynamicRoute.match(pathname)
|
|
if (!params) {
|
|
continue
|
|
}
|
|
|
|
const dynamicRouteResult = await this.findPageComponents(
|
|
dynamicRoute.page,
|
|
query,
|
|
params
|
|
)
|
|
if (dynamicRouteResult) {
|
|
try {
|
|
return await this.renderToHTMLWithComponents(
|
|
req,
|
|
res,
|
|
dynamicRoute.page,
|
|
dynamicRouteResult,
|
|
{ ...this.renderOpts, params }
|
|
)
|
|
} catch (err) {
|
|
if (!(err instanceof NoFallbackError)) {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
this.logError(err)
|
|
res.statusCode = 500
|
|
return await this.renderErrorToHTML(err, req, res, pathname, query)
|
|
}
|
|
|
|
res.statusCode = 404
|
|
return await this.renderErrorToHTML(null, req, res, pathname, query)
|
|
}
|
|
|
|
public async renderError(
|
|
err: Error | null,
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
pathname: string,
|
|
query: ParsedUrlQuery = {}
|
|
): Promise<void> {
|
|
res.setHeader(
|
|
'Cache-Control',
|
|
'no-cache, no-store, max-age=0, must-revalidate'
|
|
)
|
|
const html = await this.renderErrorToHTML(err, req, res, pathname, query)
|
|
if (html === null) {
|
|
return
|
|
}
|
|
return this.sendHTML(req, res, html)
|
|
}
|
|
|
|
private customErrorNo404Warn = execOnce(() => {
|
|
console.warn(
|
|
chalk.bold.yellow(`Warning: `) +
|
|
chalk.yellow(
|
|
`You have added a custom /_error page without a custom /404 page. This prevents the 404 page from being auto statically optimized.\nSee here for info: https://err.sh/next.js/custom-error-no-custom-404`
|
|
)
|
|
)
|
|
})
|
|
|
|
public async renderErrorToHTML(
|
|
err: Error | null,
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
_pathname: string,
|
|
query: ParsedUrlQuery = {}
|
|
) {
|
|
let result: null | FindComponentsResult = null
|
|
|
|
const is404 = res.statusCode === 404
|
|
let using404Page = false
|
|
|
|
// use static 404 page if available and is 404 response
|
|
if (is404) {
|
|
result = await this.findPageComponents('/404')
|
|
using404Page = result !== null
|
|
}
|
|
|
|
if (!result) {
|
|
result = await this.findPageComponents('/_error', query)
|
|
}
|
|
|
|
if (
|
|
process.env.NODE_ENV !== 'production' &&
|
|
!using404Page &&
|
|
(await this.hasPage('/_error')) &&
|
|
!(await this.hasPage('/404'))
|
|
) {
|
|
this.customErrorNo404Warn()
|
|
}
|
|
|
|
let html: string | null
|
|
try {
|
|
try {
|
|
html = await this.renderToHTMLWithComponents(
|
|
req,
|
|
res,
|
|
using404Page ? '/404' : '/_error',
|
|
result!,
|
|
{
|
|
...this.renderOpts,
|
|
err,
|
|
}
|
|
)
|
|
} catch (maybeFallbackError) {
|
|
if (maybeFallbackError instanceof NoFallbackError) {
|
|
throw new Error('invariant: failed to render error page')
|
|
}
|
|
throw maybeFallbackError
|
|
}
|
|
} catch (renderToHtmlError) {
|
|
console.error(renderToHtmlError)
|
|
res.statusCode = 500
|
|
html = 'Internal Server Error'
|
|
}
|
|
return html
|
|
}
|
|
|
|
public async render404(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
parsedUrl?: UrlWithParsedQuery
|
|
): Promise<void> {
|
|
const url: any = req.url
|
|
const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
|
|
res.statusCode = 404
|
|
return this.renderError(null, req, res, pathname!, query)
|
|
}
|
|
|
|
public async serveStatic(
|
|
req: IncomingMessage,
|
|
res: ServerResponse,
|
|
path: string,
|
|
parsedUrl?: UrlWithParsedQuery
|
|
): Promise<void> {
|
|
if (!this.isServeableUrl(path)) {
|
|
return this.render404(req, res, parsedUrl)
|
|
}
|
|
|
|
if (!(req.method === 'GET' || req.method === 'HEAD')) {
|
|
res.statusCode = 405
|
|
res.setHeader('Allow', ['GET', 'HEAD'])
|
|
return this.renderError(null, req, res, path)
|
|
}
|
|
|
|
try {
|
|
await serveStatic(req, res, path)
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT' || err.statusCode === 404) {
|
|
this.render404(req, res, parsedUrl)
|
|
} else if (err.statusCode === 412) {
|
|
res.statusCode = 412
|
|
return this.renderError(err, req, res, path)
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
private _validFilesystemPathSet: Set<string> | null = null
|
|
private getFilesystemPaths(): Set<string> {
|
|
if (this._validFilesystemPathSet) {
|
|
return this._validFilesystemPathSet
|
|
}
|
|
|
|
const pathUserFilesStatic = join(this.dir, 'static')
|
|
let userFilesStatic: string[] = []
|
|
if (this.hasStaticDir && fs.existsSync(pathUserFilesStatic)) {
|
|
userFilesStatic = recursiveReadDirSync(pathUserFilesStatic).map((f) =>
|
|
join('.', 'static', f)
|
|
)
|
|
}
|
|
|
|
let userFilesPublic: string[] = []
|
|
if (this.publicDir && fs.existsSync(this.publicDir)) {
|
|
userFilesPublic = recursiveReadDirSync(this.publicDir).map((f) =>
|
|
join('.', 'public', f)
|
|
)
|
|
}
|
|
|
|
let nextFilesStatic: string[] = []
|
|
nextFilesStatic = recursiveReadDirSync(
|
|
join(this.distDir, 'static')
|
|
).map((f) => join('.', relative(this.dir, this.distDir), 'static', f))
|
|
|
|
return (this._validFilesystemPathSet = new Set<string>([
|
|
...nextFilesStatic,
|
|
...userFilesPublic,
|
|
...userFilesStatic,
|
|
]))
|
|
}
|
|
|
|
protected isServeableUrl(untrustedFileUrl: string): boolean {
|
|
// This method mimics what the version of `send` we use does:
|
|
// 1. decodeURIComponent:
|
|
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L989
|
|
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L518-L522
|
|
// 2. resolve:
|
|
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L561
|
|
|
|
let decodedUntrustedFilePath: string
|
|
try {
|
|
// (1) Decode the URL so we have the proper file name
|
|
decodedUntrustedFilePath = decodeURIComponent(untrustedFileUrl)
|
|
} catch {
|
|
return false
|
|
}
|
|
|
|
// (2) Resolve "up paths" to determine real request
|
|
const untrustedFilePath = resolve(decodedUntrustedFilePath)
|
|
|
|
// don't allow null bytes anywhere in the file path
|
|
if (untrustedFilePath.indexOf('\0') !== -1) {
|
|
return false
|
|
}
|
|
|
|
// Check if .next/static, static and public are in the path.
|
|
// If not the path is not available.
|
|
if (
|
|
(untrustedFilePath.startsWith(join(this.distDir, 'static') + sep) ||
|
|
untrustedFilePath.startsWith(join(this.dir, 'static') + sep) ||
|
|
untrustedFilePath.startsWith(join(this.dir, 'public') + sep)) === false
|
|
) {
|
|
return false
|
|
}
|
|
|
|
// Check against the real filesystem paths
|
|
const filesystemUrls = this.getFilesystemPaths()
|
|
const resolved = relative(this.dir, untrustedFilePath)
|
|
return filesystemUrls.has(resolved)
|
|
}
|
|
|
|
protected readBuildId(): string {
|
|
const buildIdFile = join(this.distDir, BUILD_ID_FILE)
|
|
try {
|
|
return fs.readFileSync(buildIdFile, 'utf8').trim()
|
|
} catch (err) {
|
|
if (!fs.existsSync(buildIdFile)) {
|
|
throw new Error(
|
|
`Could not find a valid build in the '${this.distDir}' directory! Try building your app with 'next build' before starting the server.`
|
|
)
|
|
}
|
|
|
|
throw err
|
|
}
|
|
}
|
|
|
|
private get _isLikeServerless(): boolean {
|
|
return isTargetLikeServerless(this.nextConfig.target)
|
|
}
|
|
}
|
|
|
|
function prepareServerlessUrl(
|
|
req: IncomingMessage,
|
|
query: ParsedUrlQuery
|
|
): void {
|
|
const curUrl = parseUrl(req.url!, true)
|
|
req.url = formatUrl({
|
|
...curUrl,
|
|
search: undefined,
|
|
query: {
|
|
...curUrl.query,
|
|
...query,
|
|
},
|
|
})
|
|
}
|
|
|
|
class NoFallbackError extends Error {}
|