Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor FS references in the Base Server #32179

Merged
merged 4 commits into from Dec 7, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
153 changes: 22 additions & 131 deletions packages/next/server/base-server.ts
Expand Up @@ -20,18 +20,15 @@ import type { ResponseCacheEntry, ResponseCacheValue } from './response-cache'
import type { UrlWithParsedQuery } from 'url'

import compression from 'next/dist/compiled/compression'
import fs from 'fs'
import Proxy from 'next/dist/compiled/http-proxy'
import { join, relative, resolve, sep } from 'path'
import { parse as parseQs, stringify as stringifyQs } from 'querystring'
import { format as formatUrl, parse as parseUrl } from 'url'
import { getRedirectStatus, modifyRouteRegex } from '../lib/load-custom-routes'
import {
BUILD_ID_FILE,
CLIENT_PUBLIC_FILES_PATH,
CLIENT_STATIC_FILES_PATH,
CLIENT_STATIC_FILES_RUNTIME,
PAGES_MANIFEST,
PERMANENT_REDIRECT_STATUS,
PRERENDER_MANIFEST,
ROUTES_MANIFEST,
Expand Down Expand Up @@ -219,6 +216,12 @@ export default abstract class Server {
public readonly hostname?: string
public readonly port?: number

protected abstract getHasStaticDir(): boolean
protected abstract getPagesManifest(): PagesManifest | undefined
protected abstract getBuildId(): string
protected abstract generatePublicRoutes(): Route[]
protected abstract getFilesystemPaths(): Set<string>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we think about trying to make these async when possible? I know that we can have some of this pre-calculated, but I feel like if it's easy during rewrite, making them async is just more flexible later on

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I think we can totally do that in an upcoming PR. Would be good to do the same for require too!


public constructor({
dir = '.',
quiet = false,
Expand All @@ -241,7 +244,7 @@ export default abstract class Server {

this.distDir = join(this.dir, this.nextConfig.distDir)
this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
this.hasStaticDir = !minimalMode && fs.existsSync(join(this.dir, 'static'))
this.hasStaticDir = !minimalMode && this.getHasStaticDir()

// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
Expand All @@ -253,7 +256,7 @@ export default abstract class Server {
compress,
} = this.nextConfig

this.buildId = this.readBuildId()
this.buildId = this.getBuildId()
this.minimalMode = minimalMode

this.renderOpts = {
Expand Down Expand Up @@ -303,18 +306,9 @@ export default abstract class Server {
this.distDir,
this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
)
const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)
const middlewareManifestPath = join(
join(this.distDir, SERVER_DIRECTORY),
MIDDLEWARE_MANIFEST
)

if (!dev) {
this.pagesManifest = require(pagesManifestPath)
if (!this.minimalMode) {
this.middlewareManifest = require(middlewareManifestPath)
}
}
this.pagesManifest = this.getPagesManifest()
this.middlewareManifest = this.getMiddlewareManifest()

this.customRoutes = this.getCustomRoutes()
this.router = new Router(this.generateRoutes())
Expand Down Expand Up @@ -622,6 +616,17 @@ export default abstract class Server {
return this.getPrerenderManifest().preview
}

protected getMiddlewareManifest(): MiddlewareManifest | undefined {
if (!this.minimalMode) {
const middlewareManifestPath = join(
join(this.distDir, SERVER_DIRECTORY),
MIDDLEWARE_MANIFEST
)
return require(middlewareManifestPath)
}
return undefined
}

protected getMiddleware() {
const middleware = this.middlewareManifest?.middleware || {}
return (
Expand Down Expand Up @@ -782,9 +787,7 @@ export default abstract class Server {
locales: string[]
} {
const server: Server = this
const publicRoutes = fs.existsSync(this.publicDir)
? this.generatePublicRoutes()
: []
const publicRoutes = this.generatePublicRoutes()

const staticFilesRoute = this.hasStaticDir
? [
Expand Down Expand Up @@ -1525,66 +1528,6 @@ export default abstract class Server {
return true
}

protected generatePublicRoutes(): Route[] {
const publicFiles = new Set(
recursiveReadDirSync(this.publicDir).map((p) =>
encodeURI(p.replace(/\\/g, '/'))
)
)

return [
{
match: route('/:path*'),
name: 'public folder catchall',
fn: async (req, res, params, parsedUrl) => {
const pathParts: string[] = params.path || []
const { basePath } = this.nextConfig

// if basePath is defined require it be present
if (basePath) {
const basePathParts = basePath.split('/')
// remove first empty value
basePathParts.shift()

if (
!basePathParts.every((part: string, idx: number) => {
return part === pathParts[idx]
})
) {
return { finished: false }
}

pathParts.splice(0, basePathParts.length)
}

let path = `/${pathParts.join('/')}`

if (!publicFiles.has(path)) {
// In `next-dev-server.ts`, we ensure encoded paths match
// decoded paths on the filesystem. So we need do the
// opposite here: make sure decoded paths match encoded.
path = encodeURI(path)
}

if (publicFiles.has(path)) {
await this.serveStatic(
req,
res,
join(this.publicDir, ...pathParts),
parsedUrl
)
return {
finished: true,
}
}
return {
finished: false,
}
},
} as Route,
]
}

protected getDynamicRoutes(): Array<RoutingItem> {
const addedPages = new Set<string>()

Expand Down Expand Up @@ -2578,43 +2521,6 @@ export default abstract class Server {
}
}

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 =
!this.minimalMode && fs.existsSync(join(this.distDir, 'static'))
? 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:
Expand Down Expand Up @@ -2655,21 +2561,6 @@ export default abstract class Server {
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 production build in the '${this.distDir}' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id`
)
}

throw err
}
}

protected get _isLikeServerless(): boolean {
return isTargetLikeServerless(this.nextConfig.target)
}
Expand Down
8 changes: 8 additions & 0 deletions packages/next/server/dev/next-dev-server.ts
Expand Up @@ -700,10 +700,18 @@ export default class DevServer extends Server {
})
}

protected getPagesManifest(): undefined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is getPagesManifest still required to be defined since it derives from the next-server?
same with getMiddlewareManifest

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes since I refactored the caller conditions in the base server a bit so they'are always called: https://github.com/vercel/next.js/pull/32179/files?diff=unified&w=0#diff-2b8e3503c1bebf856610a5551d9c5996435b99f45db389d4eeac1b671518875eL306-R310

Goal is to eventually get rid of ad-hoc dev server logic from the base server.

return undefined
}

protected getMiddleware(): never[] {
return []
}

protected getMiddlewareManifest(): undefined {
return undefined
}

protected async hasMiddleware(
pathname: string,
isSSR?: boolean
Expand Down
2 changes: 1 addition & 1 deletion packages/next/server/image-optimizer.ts
Expand Up @@ -14,7 +14,7 @@ import { NextConfig } from './config-shared'
import { fileExists } from '../lib/file-exists'
import { ImageConfig, imageConfigDefault } from './image-config'
import { processBuffer, decodeBuffer, Operation } from './lib/squoosh/main'
import Server from './next-server'
import type Server from './base-server'
import { sendEtagResponse } from './send-payload'
import { getContentType, getExtension } from './serve-static'
import chalk from 'chalk'
Expand Down
134 changes: 133 additions & 1 deletion packages/next/server/next-server.ts
@@ -1,4 +1,136 @@
import fs from 'fs'
import { join, relative } from 'path'

import { PAGES_MANIFEST, BUILD_ID_FILE } from '../shared/lib/constants'
import { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
import type { Route } from './router'
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
import { route } from './router'

import BaseServer from './base-server'
export * from './base-server'

export default class NextNodeServer extends BaseServer {}
export default class NextNodeServer extends BaseServer {
protected getHasStaticDir(): boolean {
return fs.existsSync(join(this.dir, 'static'))
}

protected getPagesManifest(): PagesManifest | undefined {
const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)
return require(pagesManifestPath)
}

protected getBuildId(): 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 production build in the '${this.distDir}' directory. Try building your app with 'next build' before starting the production server. https://nextjs.org/docs/messages/production-start-no-build-id`
)
}

throw err
}
}

protected generatePublicRoutes(): Route[] {
if (!fs.existsSync(this.publicDir)) return []

const publicFiles = new Set(
recursiveReadDirSync(this.publicDir).map((p) =>
encodeURI(p.replace(/\\/g, '/'))
)
)

return [
{
match: route('/:path*'),
name: 'public folder catchall',
fn: async (req, res, params, parsedUrl) => {
const pathParts: string[] = params.path || []
const { basePath } = this.nextConfig

// if basePath is defined require it be present
if (basePath) {
const basePathParts = basePath.split('/')
// remove first empty value
basePathParts.shift()

if (
!basePathParts.every((part: string, idx: number) => {
return part === pathParts[idx]
})
) {
return { finished: false }
}

pathParts.splice(0, basePathParts.length)
}

let path = `/${pathParts.join('/')}`

if (!publicFiles.has(path)) {
// In `next-dev-server.ts`, we ensure encoded paths match
// decoded paths on the filesystem. So we need do the
// opposite here: make sure decoded paths match encoded.
path = encodeURI(path)
}

if (publicFiles.has(path)) {
await this.serveStatic(
req,
res,
join(this.publicDir, ...pathParts),
parsedUrl
)
return {
finished: true,
}
}
return {
finished: false,
}
},
} as Route,
]
}

private _validFilesystemPathSet: Set<string> | null = null
protected 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 =
!this.minimalMode && fs.existsSync(join(this.distDir, 'static'))
? recursiveReadDirSync(join(this.distDir, 'static')).map((f) =>
join('.', relative(this.dir, this.distDir), 'static', f)
)
: []

return (this._validFilesystemPathSet = new Set<string>([
...nextFilesStatic,
...userFilesPublic,
...userFilesStatic,
]))
}
}