mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
c97aca50e5
* Show webpack errors in all pages. When there's a webpack error that means HMR failed too. So, showing other pages won't makes sense since user can't edit them and get changes via HMR. That's why now we show the error on all pages. * Remove exact propType checks from the error component. When there's an error, something this check shows in the console. That means it could accept more props. Also this is not a public API. So, we don't want to do propType checks.
409 lines
12 KiB
JavaScript
409 lines
12 KiB
JavaScript
import { resolve, join, sep } from 'path'
|
|
import { parse as parseUrl } from 'url'
|
|
import { parse as parseQs } from 'querystring'
|
|
import fs from 'fs'
|
|
import http, { STATUS_CODES } from 'http'
|
|
import {
|
|
renderToHTML,
|
|
renderErrorToHTML,
|
|
sendHTML,
|
|
serveStatic,
|
|
renderScript,
|
|
renderScriptError
|
|
} from './render'
|
|
import Router from './router'
|
|
import { getAvailableChunks } from './utils'
|
|
import getConfig from './config'
|
|
// We need to go up one more level since we are in the `dist` directory
|
|
import pkg from '../../package'
|
|
|
|
const internalPrefixes = [
|
|
/^\/_next\//,
|
|
/^\/static\//
|
|
]
|
|
|
|
const blockedPages = {
|
|
'/_document': true,
|
|
'/_error': true
|
|
}
|
|
|
|
export default class Server {
|
|
constructor ({ dir = '.', dev = false, staticMarkup = false, quiet = false, conf = null } = {}) {
|
|
this.dir = resolve(dir)
|
|
this.dev = dev
|
|
this.quiet = quiet
|
|
this.router = new Router()
|
|
this.hotReloader = dev ? this.getHotReloader(this.dir, { quiet, conf }) : null
|
|
this.http = null
|
|
this.config = getConfig(this.dir, conf)
|
|
this.dist = this.config.distDir
|
|
if (!dev && !fs.existsSync(resolve(dir, this.dist, 'BUILD_ID'))) {
|
|
console.error(`> Could not find a valid build in the '${this.dist}' directory! Try building your app with 'next build' before starting the server.`)
|
|
process.exit(1)
|
|
}
|
|
this.buildStats = !dev ? require(join(this.dir, this.dist, 'build-stats.json')) : null
|
|
this.buildId = !dev ? this.readBuildId() : '-'
|
|
this.renderOpts = {
|
|
dev,
|
|
staticMarkup,
|
|
dir: this.dir,
|
|
hotReloader: this.hotReloader,
|
|
buildStats: this.buildStats,
|
|
buildId: this.buildId,
|
|
assetPrefix: this.config.assetPrefix.replace(/\/$/, ''),
|
|
availableChunks: dev ? {} : getAvailableChunks(this.dir, this.dist)
|
|
}
|
|
|
|
this.defineRoutes()
|
|
}
|
|
|
|
getHotReloader (dir, options) {
|
|
const HotReloader = require('./hot-reloader').default
|
|
return new HotReloader(dir, options)
|
|
}
|
|
|
|
handleRequest (req, res, parsedUrl) {
|
|
// Parse url if parsedUrl not provided
|
|
if (!parsedUrl || typeof parsedUrl !== 'object') {
|
|
parsedUrl = parseUrl(req.url, true)
|
|
}
|
|
|
|
// Parse the querystring ourselves if the user doesn't handle querystring parsing
|
|
if (typeof parsedUrl.query === 'string') {
|
|
parsedUrl.query = parseQs(parsedUrl.query)
|
|
}
|
|
|
|
res.statusCode = 200
|
|
return this.run(req, res, parsedUrl)
|
|
.catch((err) => {
|
|
if (!this.quiet) console.error(err)
|
|
res.statusCode = 500
|
|
res.end(STATUS_CODES[500])
|
|
})
|
|
}
|
|
|
|
getRequestHandler () {
|
|
return this.handleRequest.bind(this)
|
|
}
|
|
|
|
async prepare () {
|
|
if (this.hotReloader) {
|
|
await this.hotReloader.start()
|
|
}
|
|
}
|
|
|
|
async close () {
|
|
if (this.hotReloader) {
|
|
await this.hotReloader.stop()
|
|
}
|
|
|
|
if (this.http) {
|
|
await new Promise((resolve, reject) => {
|
|
this.http.close((err) => {
|
|
if (err) return reject(err)
|
|
return resolve()
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
defineRoutes () {
|
|
const routes = {
|
|
'/_next-prefetcher.js': async (req, res, params) => {
|
|
const p = join(__dirname, '../client/next-prefetcher-bundle.js')
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
// This is to support, webpack dynamic imports in production.
|
|
'/_next/webpack/chunks/:name': async (req, res, params) => {
|
|
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
|
|
const p = join(this.dir, this.dist, 'chunks', params.name)
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
// This is to support, webpack dynamic import support with HMR
|
|
'/_next/webpack/:id': async (req, res, params) => {
|
|
const p = join(this.dir, this.dist, 'chunks', params.id)
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
'/_next/:hash/manifest.js': async (req, res, params) => {
|
|
if (!this.dev) return this.send404(res)
|
|
|
|
this.handleBuildHash('manifest.js', params.hash, res)
|
|
const p = join(this.dir, this.dist, 'manifest.js')
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
'/_next/:hash/main.js': async (req, res, params) => {
|
|
if (!this.dev) return this.send404(res)
|
|
|
|
this.handleBuildHash('main.js', params.hash, res)
|
|
const p = join(this.dir, this.dist, 'main.js')
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
'/_next/:hash/commons.js': async (req, res, params) => {
|
|
if (!this.dev) return this.send404(res)
|
|
|
|
this.handleBuildHash('commons.js', params.hash, res)
|
|
const p = join(this.dir, this.dist, 'commons.js')
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
'/_next/:hash/app.js': async (req, res, params) => {
|
|
if (this.dev) return this.send404(res)
|
|
|
|
this.handleBuildHash('app.js', params.hash, res)
|
|
const p = join(this.dir, this.dist, 'app.js')
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
'/_next/:buildId/page/_error*': async (req, res, params) => {
|
|
if (!this.handleBuildId(params.buildId, res)) {
|
|
const error = new Error('INVALID_BUILD_ID')
|
|
const customFields = { buildIdMismatched: true }
|
|
|
|
return await renderScriptError(req, res, '/_error', error, customFields, this.renderOpts)
|
|
}
|
|
|
|
const p = join(this.dir, `${this.dist}/bundles/pages/_error.js`)
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
'/_next/:buildId/page/:path*': async (req, res, params) => {
|
|
const paths = params.path || ['']
|
|
const page = `/${paths.join('/')}`
|
|
|
|
if (!this.handleBuildId(params.buildId, res)) {
|
|
const error = new Error('INVALID_BUILD_ID')
|
|
const customFields = { buildIdMismatched: true }
|
|
|
|
return await renderScriptError(req, res, page, error, customFields, this.renderOpts)
|
|
}
|
|
|
|
if (this.dev) {
|
|
try {
|
|
await this.hotReloader.ensurePage(page)
|
|
} catch (error) {
|
|
return await renderScriptError(req, res, page, error, {}, this.renderOpts)
|
|
}
|
|
|
|
const compilationErr = await this.getCompilationError()
|
|
if (compilationErr) {
|
|
const customFields = { statusCode: 500 }
|
|
return await renderScriptError(req, res, page, compilationErr, customFields, this.renderOpts)
|
|
}
|
|
}
|
|
|
|
await renderScript(req, res, page, this.renderOpts)
|
|
},
|
|
|
|
'/_next/:path+': async (req, res, params) => {
|
|
const p = join(__dirname, '..', 'client', ...(params.path || []))
|
|
await this.serveStatic(req, res, p)
|
|
},
|
|
|
|
'/static/:path+': async (req, res, params) => {
|
|
const p = join(this.dir, 'static', ...(params.path || []))
|
|
await this.serveStatic(req, res, p)
|
|
}
|
|
}
|
|
|
|
if (this.config.useFileSystemPublicRoutes) {
|
|
routes['/:path*'] = async (req, res, params, parsedUrl) => {
|
|
const { pathname, query } = parsedUrl
|
|
await this.render(req, res, pathname, query)
|
|
}
|
|
}
|
|
|
|
for (const method of ['GET', 'HEAD']) {
|
|
for (const p of Object.keys(routes)) {
|
|
this.router.add(method, p, routes[p])
|
|
}
|
|
}
|
|
}
|
|
|
|
async start (port, hostname) {
|
|
await this.prepare()
|
|
this.http = http.createServer(this.getRequestHandler())
|
|
await new Promise((resolve, reject) => {
|
|
// This code catches EADDRINUSE error if the port is already in use
|
|
this.http.on('error', reject)
|
|
this.http.on('listening', () => resolve())
|
|
this.http.listen(port, hostname)
|
|
})
|
|
}
|
|
|
|
async run (req, res, parsedUrl) {
|
|
if (this.hotReloader) {
|
|
await this.hotReloader.run(req, res)
|
|
}
|
|
|
|
const fn = this.router.match(req, res, parsedUrl)
|
|
if (fn) {
|
|
await fn()
|
|
return
|
|
}
|
|
|
|
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
await this.render404(req, res, parsedUrl)
|
|
} else {
|
|
res.statusCode = 501
|
|
res.end(STATUS_CODES[501])
|
|
}
|
|
}
|
|
|
|
async render (req, res, pathname, query, parsedUrl) {
|
|
if (this.isInternalUrl(req)) {
|
|
return this.handleRequest(req, res, parsedUrl)
|
|
}
|
|
|
|
if (blockedPages[pathname]) {
|
|
return await this.render404(req, res, parsedUrl)
|
|
}
|
|
|
|
if (this.config.poweredByHeader) {
|
|
res.setHeader('X-Powered-By', `Next.js ${pkg.version}`)
|
|
}
|
|
const html = await this.renderToHTML(req, res, pathname, query)
|
|
return sendHTML(req, res, html, req.method, this.renderOpts)
|
|
}
|
|
|
|
async renderToHTML (req, res, pathname, query) {
|
|
if (this.dev) {
|
|
const compilationErr = await this.getCompilationError()
|
|
if (compilationErr) {
|
|
res.statusCode = 500
|
|
return this.renderErrorToHTML(compilationErr, req, res, pathname, query)
|
|
}
|
|
}
|
|
|
|
try {
|
|
return await renderToHTML(req, res, pathname, query, this.renderOpts)
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') {
|
|
res.statusCode = 404
|
|
return this.renderErrorToHTML(null, req, res, pathname, query)
|
|
} else {
|
|
if (!this.quiet) console.error(err)
|
|
res.statusCode = 500
|
|
return this.renderErrorToHTML(err, req, res, pathname, query)
|
|
}
|
|
}
|
|
}
|
|
|
|
async renderError (err, req, res, pathname, query) {
|
|
const html = await this.renderErrorToHTML(err, req, res, pathname, query)
|
|
return sendHTML(req, res, html, req.method, this.renderOpts)
|
|
}
|
|
|
|
async renderErrorToHTML (err, req, res, pathname, query) {
|
|
if (this.dev) {
|
|
const compilationErr = await this.getCompilationError()
|
|
if (compilationErr) {
|
|
res.statusCode = 500
|
|
return renderErrorToHTML(compilationErr, req, res, pathname, query, this.renderOpts)
|
|
}
|
|
}
|
|
|
|
try {
|
|
return await renderErrorToHTML(err, req, res, pathname, query, this.renderOpts)
|
|
} catch (err2) {
|
|
if (this.dev) {
|
|
if (!this.quiet) console.error(err2)
|
|
res.statusCode = 500
|
|
return renderErrorToHTML(err2, req, res, pathname, query, this.renderOpts)
|
|
} else {
|
|
throw err2
|
|
}
|
|
}
|
|
}
|
|
|
|
async render404 (req, res, parsedUrl = parseUrl(req.url, true)) {
|
|
const { pathname, query } = parsedUrl
|
|
res.statusCode = 404
|
|
return this.renderError(null, req, res, pathname, query)
|
|
}
|
|
|
|
async serveStatic (req, res, path) {
|
|
if (!this.isServeableUrl(path)) {
|
|
return this.render404(req, res)
|
|
}
|
|
|
|
try {
|
|
return await serveStatic(req, res, path)
|
|
} catch (err) {
|
|
if (err.code === 'ENOENT') {
|
|
this.render404(req, res)
|
|
} else {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
isServeableUrl (path) {
|
|
const resolved = resolve(path)
|
|
if (
|
|
resolved.indexOf(join(this.dir, this.dist) + sep) !== 0 &&
|
|
resolved.indexOf(join(this.dir, 'static') + sep) !== 0
|
|
) {
|
|
// Seems like the user is trying to traverse the filesystem.
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
isInternalUrl (req) {
|
|
for (const prefix of internalPrefixes) {
|
|
if (prefix.test(req.url)) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
readBuildId () {
|
|
const buildIdPath = join(this.dir, this.dist, 'BUILD_ID')
|
|
const buildId = fs.readFileSync(buildIdPath, 'utf8')
|
|
return buildId.trim()
|
|
}
|
|
|
|
handleBuildId (buildId, res) {
|
|
if (this.dev) return true
|
|
if (buildId !== this.renderOpts.buildId) {
|
|
return false
|
|
}
|
|
|
|
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
|
|
return true
|
|
}
|
|
|
|
async getCompilationError () {
|
|
if (!this.hotReloader) return
|
|
|
|
const errors = await this.hotReloader.getCompilationErrors()
|
|
if (!errors.size) return
|
|
|
|
// Return the very first error we found.
|
|
return Array.from(errors.values())[0][0]
|
|
}
|
|
|
|
handleBuildHash (filename, hash, res) {
|
|
if (this.dev) return
|
|
|
|
if (hash !== this.buildStats[filename].hash) {
|
|
throw new Error(`Invalid Build File Hash(${hash}) for chunk: ${filename}`)
|
|
}
|
|
|
|
res.setHeader('Cache-Control', 'max-age=365000000, immutable')
|
|
}
|
|
|
|
send404 (res) {
|
|
res.statusCode = 404
|
|
res.end('404 - Not Found')
|
|
}
|
|
}
|