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 next-server some for easier page checking #9671

Merged
merged 7 commits into from
Dec 10, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions packages/next/next-server/server/api-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IncomingMessage } from 'http'
import { IncomingMessage, ServerResponse } from 'http'
import { NextApiResponse, NextApiRequest } from '../lib/utils'
import { Stream } from 'stream'
import getRawBody from 'raw-body'
Expand All @@ -11,12 +11,15 @@ export type NextApiRequestCookies = { [key: string]: string }
export type NextApiRequestQuery = { [key: string]: string | string[] }

export async function apiResolver(
req: NextApiRequest,
res: NextApiResponse,
req: IncomingMessage,
res: ServerResponse,
params: any,
resolverModule: any,
onError?: ({ err }: { err: any }) => Promise<void>
) {
const apiReq = req as NextApiRequest
const apiRes = res as NextApiResponse

try {
let config: PageConfig = {}
let bodyParser = true
Expand All @@ -33,32 +36,32 @@ export async function apiResolver(
}
}
// Parsing of cookies
setLazyProp({ req }, 'cookies', getCookieParser(req))
setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req))
// Parsing query string
setLazyProp({ req, params }, 'query', getQueryParser(req))
setLazyProp({ req: apiReq, params }, 'query', getQueryParser(req))
// // Parsing of body
if (bodyParser) {
req.body = await parseBody(
req,
apiReq.body = await parseBody(
apiReq,
config.api && config.api.bodyParser && config.api.bodyParser.sizeLimit
? config.api.bodyParser.sizeLimit
: '1mb'
)
}

res.status = statusCode => sendStatusCode(res, statusCode)
res.send = data => sendData(res, data)
res.json = data => sendJson(res, data)
apiRes.status = statusCode => sendStatusCode(apiRes, statusCode)
apiRes.send = data => sendData(apiRes, data)
apiRes.json = data => sendJson(apiRes, data)

const resolver = interopDefault(resolverModule)
resolver(req, res)
} catch (err) {
if (err instanceof ApiError) {
sendError(res, err.statusCode, err.message)
sendError(apiRes, err.statusCode, err.message)
} else {
console.error(err)
if (onError) await onError({ err })
sendError(res, 500, 'Internal Server Error')
sendError(apiRes, 500, 'Internal Server Error')
}
}
}
Expand Down
117 changes: 58 additions & 59 deletions packages/next/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ export default class Server {
pagesDir?: string
publicDir: string
hasStaticDir: boolean
pagesManifest: string
serverBuildDir: string
pagesManifest?: { [name: string]: string }
buildId: string
renderOpts: {
poweredByHeader: boolean
Expand Down Expand Up @@ -122,13 +123,6 @@ export default class Server {
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'))
this.pagesManifest = join(
this.distDir,
this.nextConfig.target === 'server'
? SERVER_DIRECTORY
: SERVERLESS_DIRECTORY,
PAGES_MANIFEST
)

// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
Expand Down Expand Up @@ -172,19 +166,26 @@ export default class Server {
})
}

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 serverPath = join(
this.distDir,
this._isLikeServerless ? 'serverless' : 'server'
)
const initServer = require(join(serverPath, 'init-server.js')).default
const initServer = require(join(this.serverBuildDir, 'init-server.js'))
.default
this.onErrorMiddleware = require(join(
serverPath,
this.serverBuildDir,
'on-error-server.js'
)).default
initServer()
Expand Down Expand Up @@ -503,6 +504,24 @@ export default class Server {
return routes
}

protected async getPagePath(pathname: string) {
Timer marked this conversation as resolved.
Show resolved Hide resolved
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,
Expand All @@ -512,84 +531,63 @@ export default class Server {
return false
}

// Used to build API page in development
protected async ensureApiPage(pathname: string) {}

/**
* 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: NextApiRequest,
res: NextApiResponse,
req: IncomingMessage,
res: ServerResponse,
pathname: string
) {
let page = pathname
let params: Params | boolean = false
let resolverFunction: any

try {
resolverFunction = await this.resolveApiRequest(pathname)
} catch (err) {}
let pageFound = await this.hasPage(page)

if (
this.dynamicRoutes &&
this.dynamicRoutes.length > 0 &&
!resolverFunction
) {
if (!pageFound && this.dynamicRoutes) {
for (const dynamicRoute of this.dynamicRoutes) {
params = dynamicRoute.match(pathname)
if (params) {
resolverFunction = await this.resolveApiRequest(dynamicRoute.page)
page = dynamicRoute.page
pageFound = true
break
}
}
}

if (!resolverFunction) {
if (!pageFound) {
return this.render404(req, res)
}
// Make sure the page is built before getting the path
// or else it won't be in the manifest yet
await this.ensureApiPage(page)

const builtPagePath = await this.getPagePath(page)
const pageModule = require(builtPagePath)

if (!this.renderOpts.dev && this._isLikeServerless) {
const mod = require(resolverFunction)
if (typeof mod.default === 'function') {
return mod.default(req, res)
if (typeof pageModule.default === 'function') {
return pageModule.default(req, res)
}
}

await apiResolver(
req,
res,
params,
resolverFunction ? require(resolverFunction) : undefined,
this.onErrorMiddleware
)
}

/**
* Resolves path to resolver function
* @param pathname path of request
*/
protected async resolveApiRequest(pathname: string): Promise<string | null> {
return getPagePath(
pathname,
this.distDir,
this._isLikeServerless,
this.renderOpts.dev
)
await apiResolver(req, res, params, pageModule, this.onErrorMiddleware)
}

protected generatePublicRoutes(): Route[] {
const routes: Route[] = []
const publicFiles = recursiveReadDirSync(this.publicDir)
const serverBuildPath = join(
this.distDir,
this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
)
const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))

publicFiles.forEach(path => {
const unixPath = path.replace(/\\/g, '/')
// Only include public files that will not replace a page path
if (!pagesManifest[unixPath]) {
// this should not occur now that we check this during build
if (!this.pagesManifest![unixPath]) {
routes.push({
match: route(unixPath),
type: 'route',
Expand All @@ -609,8 +607,9 @@ export default class Server {
}

protected getDynamicRoutes() {
const manifest = require(this.pagesManifest)
const dynamicRoutedPages = Object.keys(manifest).filter(isDynamicRoute)
const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
isDynamicRoute
)
return getSortedRoutes(dynamicRoutedPages).map(page => ({
page,
match: getRouteMatcher(getRouteRegex(page)),
Expand Down
34 changes: 12 additions & 22 deletions packages/next/server/next-dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,15 @@ export default class DevServer extends Server {
}
}

protected async hasPage(pathname: string): Promise<boolean> {
const pageFile = await findPageFile(
this.pagesDir!,
normalizePagePath(pathname),
this.nextConfig.pageExtensions
)
return !!pageFile
}

protected async _beforeCatchAllRender(
req: IncomingMessage,
res: ServerResponse,
Expand All @@ -256,13 +265,7 @@ export default class DevServer extends Server {
// check for a public file, throwing error if there's a
// conflicting page
if (await this.hasPublicFile(pathname!)) {
const pageFile = await findPageFile(
this.pagesDir!,
normalizePagePath(pathname!),
this.nextConfig.pageExtensions
)

if (pageFile) {
if (await this.hasPage(pathname!)) {
const err = new Error(
`A conflicting public file and page file was found for path ${pathname} https://err.sh/zeit/next.js/conflicting-public-file-page`
)
Expand Down Expand Up @@ -378,21 +381,8 @@ export default class DevServer extends Server {
return !snippet.includes('data-amp-development-mode-only')
}

/**
* Check if resolver function is build or request new build for this function
* @param {string} pathname
*/
protected async resolveApiRequest(pathname: string): Promise<string | null> {
try {
await this.hotReloader!.ensurePage(pathname)
} catch (err) {
// API route dosn't exist => return 404
if (err.code === 'ENOENT') {
return null
}
}
const resolvedPath = await super.resolveApiRequest(pathname)
return resolvedPath
protected async ensureApiPage(pathname: string) {
return this.hotReloader!.ensurePage(pathname)
}

async renderToHTML(
Expand Down