2018-03-27 18:11:03 +00:00
/* eslint-disable import/first, no-return-await */
2017-06-01 00:16:32 +00:00
import { resolve , join , sep } from 'path'
2017-03-01 23:30:07 +00:00
import { parse as parseUrl } from 'url'
import { parse as parseQs } from 'querystring'
2017-03-07 18:43:56 +00:00
import fs from 'fs'
2017-01-12 15:38:43 +00:00
import http , { STATUS _CODES } from 'http'
2018-06-14 17:30:14 +00:00
import promisify from '../lib/promisify'
2016-12-16 20:33:08 +00:00
import {
renderToHTML ,
renderErrorToHTML ,
2016-12-31 12:46:23 +00:00
sendHTML ,
2017-04-04 19:55:56 +00:00
serveStatic ,
renderScriptError
2016-12-16 20:33:08 +00:00
} from './render'
2016-10-05 23:52:50 +00:00
import Router from './router'
2018-07-24 09:24:40 +00:00
import { isInternalUrl } from './utils'
2018-06-04 09:38:46 +00:00
import loadConfig from './config'
2018-06-04 13:45:39 +00:00
import { PHASE _PRODUCTION _SERVER , PHASE _DEVELOPMENT _SERVER , BLOCKED _PAGES , BUILD _ID _FILE } from '../lib/constants'
2018-01-30 15:40:52 +00:00
import * as asset from '../lib/asset'
2018-02-26 11:03:27 +00:00
import * as envConfig from '../lib/runtime-config'
2018-02-01 22:25:30 +00:00
import { isResSent } from '../lib/utils'
2017-04-07 17:58:35 +00:00
2018-06-04 09:38:46 +00:00
// We need to go up one more level since we are in the `dist` directory
import pkg from '../../package'
2018-03-30 14:59:42 +00:00
const access = promisify ( fs . access )
2016-10-05 23:52:50 +00:00
export default class Server {
2017-05-31 08:06:07 +00:00
constructor ( { dir = '.' , dev = false , staticMarkup = false , quiet = false , conf = null } = { } ) {
2016-10-06 11:05:52 +00:00
this . dir = resolve ( dir )
this . dev = dev
2016-12-16 20:33:08 +00:00
this . quiet = quiet
2016-10-05 23:52:50 +00:00
this . router = new Router ( )
2016-12-16 20:33:08 +00:00
this . http = null
2018-02-23 13:42:06 +00:00
const phase = dev ? PHASE _DEVELOPMENT _SERVER : PHASE _PRODUCTION _SERVER
2018-06-04 09:38:46 +00:00
this . nextConfig = loadConfig ( phase , this . dir , conf )
2018-06-13 18:30:55 +00:00
this . distDir = join ( this . dir , this . nextConfig . distDir )
2018-02-05 12:39:32 +00:00
2018-03-13 13:18:59 +00:00
// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
const { serverRuntimeConfig = { } , publicRuntimeConfig , assetPrefix , generateEtags } = this . nextConfig
2018-06-04 13:45:39 +00:00
if ( ! dev && ! fs . existsSync ( resolve ( this . distDir , BUILD _ID _FILE ) ) ) {
console . error ( ` > Could not find a valid build in the ' ${ this . distDir } ' directory! Try building your app with 'next build' before starting the server. ` )
2017-06-26 20:18:56 +00:00
process . exit ( 1 )
}
2017-03-07 18:43:56 +00:00
this . buildId = ! dev ? this . readBuildId ( ) : '-'
2018-06-25 21:06:46 +00:00
this . hotReloader = dev ? this . getHotReloader ( this . dir , { quiet , config : this . nextConfig , buildId : this . buildId } ) : null
2017-03-07 18:43:56 +00:00
this . renderOpts = {
dev ,
staticMarkup ,
2018-06-04 13:45:39 +00:00
distDir : this . distDir ,
2017-03-07 18:43:56 +00:00
hotReloader : this . hotReloader ,
2017-04-18 04:18:43 +00:00
buildId : this . buildId ,
2018-03-13 13:18:59 +00:00
generateEtags
2017-03-07 18:43:56 +00:00
}
2016-12-16 20:33:08 +00:00
2018-02-27 16:50:14 +00:00
// Only the `publicRuntimeConfig` key is exposed to the client side
// It'll be rendered as part of __NEXT_DATA__ on the client side
if ( publicRuntimeConfig ) {
this . renderOpts . runtimeConfig = publicRuntimeConfig
2018-02-26 11:03:27 +00:00
}
2018-02-27 16:50:14 +00:00
// Initialize next/config with the environment configuration
envConfig . setConfig ( {
serverRuntimeConfig ,
publicRuntimeConfig
} )
this . setAssetPrefix ( assetPrefix )
2016-12-16 20:33:08 +00:00
}
2016-10-05 23:52:50 +00:00
2017-07-15 10:29:10 +00:00
getHotReloader ( dir , options ) {
const HotReloader = require ( './hot-reloader' ) . default
return new HotReloader ( dir , options )
}
2017-04-07 17:58:35 +00:00
handleRequest ( req , res , parsedUrl ) {
// Parse url if parsedUrl not provided
2017-07-02 05:52:39 +00:00
if ( ! parsedUrl || typeof parsedUrl !== 'object' ) {
2017-04-07 17:58:35 +00:00
parsedUrl = parseUrl ( req . url , true )
}
2017-02-09 03:22:48 +00:00
2017-04-07 17:58:35 +00:00
// Parse the querystring ourselves if the user doesn't handle querystring parsing
if ( typeof parsedUrl . query === 'string' ) {
parsedUrl . query = parseQs ( parsedUrl . query )
2016-12-16 20:33:08 +00:00
}
2017-04-07 17:58:35 +00:00
2017-06-19 07:27:35 +00:00
res . statusCode = 200
2017-04-07 17:58:35 +00:00
return this . run ( req , res , parsedUrl )
2017-06-19 07:27:35 +00:00
. catch ( ( err ) => {
if ( ! this . quiet ) console . error ( err )
res . statusCode = 500
res . end ( STATUS _CODES [ 500 ] )
} )
2017-04-07 17:58:35 +00:00
}
getRequestHandler ( ) {
return this . handleRequest . bind ( this )
2016-10-05 23:52:50 +00:00
}
2018-02-02 14:43:36 +00:00
setAssetPrefix ( prefix ) {
2018-02-03 16:12:01 +00:00
this . renderOpts . assetPrefix = prefix ? prefix . replace ( /\/$/ , '' ) : ''
2018-02-02 14:43:36 +00:00
asset . setAssetPrefix ( this . renderOpts . assetPrefix )
}
2016-12-16 20:33:08 +00:00
async prepare ( ) {
2018-04-05 01:48:11 +00:00
await this . defineRoutes ( )
2016-10-17 07:07:41 +00:00
if ( this . hotReloader ) {
await this . hotReloader . start ( )
}
}
2016-12-17 04:04:40 +00:00
async close ( ) {
if ( this . hotReloader ) {
await this . hotReloader . stop ( )
}
2017-01-12 04:14:49 +00:00
if ( this . http ) {
await new Promise ( ( resolve , reject ) => {
this . http . close ( ( err ) => {
if ( err ) return reject ( err )
return resolve ( )
} )
} )
}
2016-12-17 04:04:40 +00:00
}
2018-04-05 01:48:11 +00:00
async defineRoutes ( ) {
2017-01-12 15:38:43 +00:00
const routes = {
2018-01-30 15:40:52 +00:00
'/_next/:buildId/page/:path*.js.map' : async ( req , res , params ) => {
const paths = params . path || [ '' ]
const page = ` / ${ paths . join ( '/' ) } `
if ( this . dev ) {
try {
await this . hotReloader . ensurePage ( page )
} catch ( err ) {
await this . render404 ( req , res )
}
}
2018-06-04 13:45:39 +00:00
const path = join ( this . distDir , 'bundles' , 'pages' , ` ${ page } .js.map ` )
2018-01-30 15:40:52 +00:00
await serveStatic ( req , res , path )
} ,
2018-01-13 07:34:48 +00:00
'/_next/:buildId/page/:path*.js' : async ( req , res , params ) => {
2017-04-03 18:10:24 +00:00
const paths = params . path || [ '' ]
2018-01-13 07:34:48 +00:00
const page = ` / ${ paths . join ( '/' ) } `
2017-04-03 18:10:24 +00:00
if ( ! this . handleBuildId ( params . buildId , res ) ) {
2017-04-04 19:55:56 +00:00
const error = new Error ( 'INVALID_BUILD_ID' )
2018-02-21 17:41:25 +00:00
return await renderScriptError ( req , res , page , error )
2017-04-03 18:10:24 +00:00
}
2018-04-12 08:33:22 +00:00
if ( this . dev && page !== '/_error' && page !== '/_app' ) {
2017-04-04 20:35:25 +00:00
try {
await this . hotReloader . ensurePage ( page )
} catch ( error ) {
2018-02-21 17:41:25 +00:00
return await renderScriptError ( req , res , page , error )
2017-04-04 20:35:25 +00:00
}
2018-07-24 09:24:40 +00:00
const compilationErr = await this . getCompilationError ( page )
2017-04-04 19:55:56 +00:00
if ( compilationErr ) {
2018-02-21 17:41:25 +00:00
return await renderScriptError ( req , res , page , compilationErr )
2017-04-04 19:55:56 +00:00
}
}
2018-06-04 13:45:39 +00:00
const p = join ( this . distDir , 'bundles' , 'pages' , ` ${ page } .js ` )
2018-02-01 08:26:08 +00:00
// [production] If the page is not exists, we need to send a proper Next.js style 404
// Otherwise, it'll affect the multi-zones feature.
2018-03-30 14:59:42 +00:00
try {
await access ( p , ( fs . constants || fs ) . R _OK )
} catch ( err ) {
2018-02-21 17:41:25 +00:00
return await renderScriptError ( req , res , page , { code : 'ENOENT' } )
2018-02-01 08:26:08 +00:00
}
2018-01-30 15:40:52 +00:00
await this . serveStatic ( req , res , p )
} ,
'/_next/static/:path*' : async ( req , res , params ) => {
2018-04-12 07:47:42 +00:00
// The commons folder holds commonschunk files
2018-07-24 09:24:40 +00:00
// The chunks folder holds dynamic entries
2018-04-12 07:47:42 +00:00
// In development they don't have a hash, and shouldn't be cached by the browser.
2018-07-24 09:24:40 +00:00
if ( params . path [ 0 ] === 'commons' || params . path [ 0 ] === 'chunks' ) {
2018-05-19 19:43:18 +00:00
if ( this . dev ) {
res . setHeader ( 'Cache-Control' , 'no-store, must-revalidate' )
} else {
res . setHeader ( 'Cache-Control' , 'public, max-age=31536000, immutable' )
}
2018-04-12 07:47:42 +00:00
}
2018-06-04 13:45:39 +00:00
const p = join ( this . distDir , 'static' , ... ( params . path || [ ] ) )
2018-01-30 15:40:52 +00:00
await this . serveStatic ( req , res , p )
2017-04-03 18:10:24 +00:00
} ,
2017-07-24 06:13:45 +00:00
// It's very important keep this route's param optional.
2017-07-26 17:01:49 +00:00
// (but it should support as many as params, seperated by '/')
2017-07-24 06:13:45 +00:00
// Othewise this will lead to a pretty simple DOS attack.
// See more: https://github.com/zeit/next.js/issues/2617
2017-07-26 17:01:49 +00:00
'/static/:path*' : async ( req , res , params ) => {
2017-01-12 15:38:43 +00:00
const p = join ( this . dir , 'static' , ... ( params . path || [ ] ) )
await this . serveStatic ( req , res , p )
2017-05-27 15:40:15 +00:00
}
}
2017-01-12 15:38:43 +00:00
2018-02-26 11:03:27 +00:00
if ( this . nextConfig . useFileSystemPublicRoutes ) {
2018-04-05 01:48:11 +00:00
// Makes `next export` exportPathMap work in development mode.
// So that the user doesn't have to define a custom server reading the exportPathMap
if ( this . dev && this . nextConfig . exportPathMap ) {
console . log ( 'Defining routes from exportPathMap' )
const exportPathMap = await this . nextConfig . exportPathMap ( { } ) // In development we can't give a default path mapping
for ( const path in exportPathMap ) {
2018-05-25 12:27:18 +00:00
const { page , query = { } } = exportPathMap [ path ]
2018-04-05 01:48:11 +00:00
routes [ path ] = async ( req , res , params , parsedUrl ) => {
2018-06-28 06:37:57 +00:00
const { query : urlQuery } = parsedUrl
Object . keys ( urlQuery )
. filter ( key => query [ key ] === undefined )
. forEach ( key => console . warn ( ` Url defines a query parameter ' ${ key } ' that is missing in exportPathMap ` ) )
const mergedQuery = { ... urlQuery , ... query }
await this . render ( req , res , page , mergedQuery , parsedUrl )
2018-04-05 01:48:11 +00:00
}
}
}
2018-07-24 09:24:40 +00:00
// In development we expose all compiled files for react-error-overlay's line show feature
if ( this . dev ) {
routes [ '/_next/development/:path*' ] = async ( req , res , params ) => {
const p = join ( this . distDir , ... ( params . path || [ ] ) )
await this . serveStatic ( req , res , p )
}
}
// It's very important keep this route's param optional.
// (but it should support as many as params, seperated by '/')
// Othewise this will lead to a pretty simple DOS attack.
// See more: https://github.com/zeit/next.js/issues/2617
routes [ '/_next/:path*' ] = async ( req , res , params ) => {
const p = join ( _ _dirname , '..' , 'client' , ... ( params . path || [ ] ) )
await this . serveStatic ( req , res , p )
}
2017-05-27 15:40:15 +00:00
routes [ '/:path*' ] = async ( req , res , params , parsedUrl ) => {
2017-02-02 06:51:08 +00:00
const { pathname , query } = parsedUrl
2018-04-05 01:48:11 +00:00
await this . render ( req , res , pathname , query , parsedUrl )
2017-01-12 15:38:43 +00:00
}
}
2016-10-05 23:52:50 +00:00
2017-01-12 15:38:43 +00:00
for ( const method of [ 'GET' , 'HEAD' ] ) {
for ( const p of Object . keys ( routes ) ) {
this . router . add ( method , p , routes [ p ] )
}
}
2016-12-16 20:33:08 +00:00
}
2016-10-05 23:52:50 +00:00
2017-02-12 11:26:10 +00:00
async start ( port , hostname ) {
2016-12-16 20:33:08 +00:00
await this . prepare ( )
this . http = http . createServer ( this . getRequestHandler ( ) )
await new Promise ( ( resolve , reject ) => {
2017-02-01 20:36:23 +00:00
// This code catches EADDRINUSE error if the port is already in use
this . http . on ( 'error' , reject )
this . http . on ( 'listening' , ( ) => resolve ( ) )
2017-02-12 16:23:42 +00:00
this . http . listen ( port , hostname )
2016-10-05 23:52:50 +00:00
} )
}
2017-02-02 06:51:08 +00:00
async run ( req , res , parsedUrl ) {
2016-11-23 18:32:49 +00:00
if ( this . hotReloader ) {
await this . hotReloader . run ( req , res )
}
2017-02-02 06:51:08 +00:00
const fn = this . router . match ( req , res , parsedUrl )
2016-10-05 23:52:50 +00:00
if ( fn ) {
await fn ( )
2017-01-12 15:38:43 +00:00
return
}
if ( req . method === 'GET' || req . method === 'HEAD' ) {
2017-02-02 06:51:08 +00:00
await this . render404 ( req , res , parsedUrl )
2017-01-12 15:38:43 +00:00
} else {
res . statusCode = 501
res . end ( STATUS _CODES [ 501 ] )
2016-10-05 23:52:50 +00:00
}
}
2017-04-07 17:58:35 +00:00
async render ( req , res , pathname , query , parsedUrl ) {
2018-01-30 15:40:52 +00:00
if ( isInternalUrl ( req . url ) ) {
2017-04-07 17:58:35 +00:00
return this . handleRequest ( req , res , parsedUrl )
}
2018-06-04 13:45:39 +00:00
if ( BLOCKED _PAGES . indexOf ( pathname ) !== - 1 ) {
2017-07-06 12:29:25 +00:00
return await this . render404 ( req , res , parsedUrl )
}
2016-12-16 20:33:08 +00:00
const html = await this . renderToHTML ( req , res , pathname , query )
2018-02-01 22:25:30 +00:00
if ( isResSent ( res ) ) {
return
}
2018-02-26 11:03:27 +00:00
if ( this . nextConfig . poweredByHeader ) {
2018-02-14 17:02:48 +00:00
res . setHeader ( 'X-Powered-By' , ` Next.js ${ pkg . version } ` )
}
2017-05-25 16:28:08 +00:00
return sendHTML ( req , res , html , req . method , this . renderOpts )
2016-12-16 20:33:08 +00:00
}
2016-10-19 12:41:45 +00:00
2016-12-16 20:33:08 +00:00
async renderToHTML ( req , res , pathname , query ) {
if ( this . dev ) {
2018-07-24 09:24:40 +00:00
const compilationErr = await this . getCompilationError ( pathname )
2016-12-16 20:33:08 +00:00
if ( compilationErr ) {
res . statusCode = 500
return this . renderErrorToHTML ( compilationErr , req , res , pathname , query )
}
2016-11-24 14:03:16 +00:00
}
try {
2018-01-30 15:40:52 +00:00
const out = await renderToHTML ( req , res , pathname , query , this . renderOpts )
return out
2016-11-24 14:03:16 +00:00
} catch ( err ) {
2016-12-16 20:33:08 +00:00
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 )
2016-10-05 23:52:50 +00:00
}
}
2016-11-24 14:03:16 +00:00
}
2016-12-16 20:33:08 +00:00
async renderError ( err , req , res , pathname , query ) {
const html = await this . renderErrorToHTML ( err , req , res , pathname , query )
2017-05-25 16:28:08 +00:00
return sendHTML ( req , res , html , req . method , this . renderOpts )
2016-10-05 23:52:50 +00:00
}
2016-12-16 20:33:08 +00:00
async renderErrorToHTML ( err , req , res , pathname , query ) {
if ( this . dev ) {
2018-07-24 09:24:40 +00:00
const compilationErr = await this . getCompilationError ( pathname )
2016-12-16 20:33:08 +00:00
if ( compilationErr ) {
res . statusCode = 500
return renderErrorToHTML ( compilationErr , req , res , pathname , query , this . renderOpts )
}
2016-11-24 14:03:16 +00:00
}
2016-10-19 12:41:45 +00:00
2016-11-24 14:03:16 +00:00
try {
2016-12-16 20:33:08 +00:00
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 )
2016-11-24 14:03:16 +00:00
} else {
2016-12-16 20:33:08 +00:00
throw err2
2016-10-05 23:52:50 +00:00
}
}
2016-11-24 14:03:16 +00:00
}
2017-03-01 23:30:07 +00:00
async render404 ( req , res , parsedUrl = parseUrl ( req . url , true ) ) {
2017-02-02 06:51:08 +00:00
const { pathname , query } = parsedUrl
2016-12-16 20:33:08 +00:00
res . statusCode = 404
2017-02-16 02:48:35 +00:00
return this . renderError ( null , req , res , pathname , query )
2016-12-16 20:33:08 +00:00
}
2016-11-03 15:12:37 +00:00
2017-02-16 02:48:35 +00:00
async serveStatic ( req , res , path ) {
2017-06-01 00:16:32 +00:00
if ( ! this . isServeableUrl ( path ) ) {
return this . render404 ( req , res )
}
2017-01-01 19:36:37 +00:00
try {
2017-02-16 02:48:35 +00:00
return await serveStatic ( req , res , path )
2017-01-01 19:36:37 +00:00
} catch ( err ) {
if ( err . code === 'ENOENT' ) {
this . render404 ( req , res )
} else {
throw err
}
}
}
2017-06-01 00:16:32 +00:00
isServeableUrl ( path ) {
const resolved = resolve ( path )
if (
2018-06-04 13:45:39 +00:00
resolved . indexOf ( join ( this . distDir ) + sep ) !== 0 &&
2017-06-01 00:16:32 +00:00
resolved . indexOf ( join ( this . dir , 'static' ) + sep ) !== 0
) {
// Seems like the user is trying to traverse the filesystem.
return false
}
return true
}
2017-03-07 18:43:56 +00:00
readBuildId ( ) {
2018-06-04 13:45:39 +00:00
const buildIdPath = join ( this . distDir , BUILD _ID _FILE )
2017-03-07 18:43:56 +00:00
const buildId = fs . readFileSync ( buildIdPath , 'utf8' )
return buildId . trim ( )
2017-01-11 20:16:18 +00:00
}
handleBuildId ( buildId , res ) {
2017-10-30 14:57:35 +00:00
if ( this . dev ) {
res . setHeader ( 'Cache-Control' , 'no-store, must-revalidate' )
return true
}
2017-01-11 20:16:18 +00:00
if ( buildId !== this . renderOpts . buildId ) {
2017-02-20 23:48:17 +00:00
return false
2017-01-11 20:16:18 +00:00
}
2018-05-19 19:43:18 +00:00
res . setHeader ( 'Cache-Control' , 'public, max-age=31536000, immutable' )
2017-02-20 23:48:17 +00:00
return true
2017-01-11 20:16:18 +00:00
}
2018-07-24 09:24:40 +00:00
async getCompilationError ( page ) {
2016-10-19 12:41:45 +00:00
if ( ! this . hotReloader ) return
2018-07-24 09:24:40 +00:00
const errors = await this . hotReloader . getCompilationErrors ( page )
if ( errors . length === 0 ) return
2016-10-19 12:41:45 +00:00
2017-07-18 07:00:23 +00:00
// Return the very first error we found.
2018-07-24 09:24:40 +00:00
return errors [ 0 ]
2016-10-19 12:41:45 +00:00
}
2017-02-20 23:48:17 +00:00
2017-05-23 17:30:14 +00:00
send404 ( res ) {
res . statusCode = 404
res . end ( '404 - Not Found' )
}
2017-02-20 23:48:17 +00:00
}