1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Convert next-server.js to typescript (#5844)

This commit is contained in:
Tim Neutkens 2018-12-09 22:46:45 +01:00 committed by GitHub
parent 61894816ba
commit 8b6173917a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 131 additions and 79 deletions

View file

@ -28,6 +28,7 @@
"standard": { "standard": {
"parser": "babel-eslint", "parser": "babel-eslint",
"ignore": [ "ignore": [
"packages/next-server/server/next-server.ts",
"**/*.d.ts", "**/*.d.ts",
"**/node_modules/**", "**/node_modules/**",
"examples/with-ioc/**", "examples/with-ioc/**",

View file

@ -1,7 +1,8 @@
/* eslint-disable import/first, no-return-await */ /* eslint-disable import/first */
import {IncomingMessage, ServerResponse} from 'http'
import { resolve, join, sep } from 'path' import { resolve, join, sep } from 'path'
import { parse as parseUrl } from 'url' import { parse as parseUrl, UrlWithParsedQuery } from 'url'
import { parse as parseQs } from 'querystring' import { parse as parseQs, ParsedUrlQuery } from 'querystring'
import fs from 'fs' import fs from 'fs'
import { import {
renderToHTML, renderToHTML,
@ -9,16 +10,39 @@ import {
} from './render' } from './render'
import {sendHTML} from './send-html' import {sendHTML} from './send-html'
import {serveStatic} from './serve-static' import {serveStatic} from './serve-static'
import Router, {route} from './router' import Router, {route, Route} from './router'
import { isInternalUrl, isBlockedPage } from './utils' import { isInternalUrl, isBlockedPage } from './utils'
import loadConfig from 'next-server/next-config' import loadConfig from 'next-server/next-config'
import {PHASE_PRODUCTION_SERVER, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME} from 'next-server/constants' import {PHASE_PRODUCTION_SERVER, BUILD_ID_FILE, CLIENT_STATIC_FILES_PATH, CLIENT_STATIC_FILES_RUNTIME} from 'next-server/constants'
import * as asset from '../lib/asset' import * as asset from '../lib/asset'
import * as envConfig from '../lib/runtime-config' import * as envConfig from '../lib/runtime-config'
import { isResSent } from '../lib/utils'
type NextConfig = any
type ServerConstructor = {
dir?: string,
staticMarkup?: boolean,
quiet?: boolean,
conf?: NextConfig
}
export default class Server { export default class Server {
constructor ({ dir = '.', staticMarkup = false, quiet = false, conf = null } = {}) { dir: string
quiet: boolean
nextConfig: NextConfig
distDir: string
buildId: string
renderOpts: {
staticMarkup: boolean,
distDir: string,
buildId: string,
generateEtags: boolean,
runtimeConfig?: {[key: string]: any},
assetPrefix?: string
}
router: Router
public constructor ({ dir = '.', staticMarkup = false, quiet = false, conf = null }: ServerConstructor = {}) {
this.dir = resolve(dir) this.dir = resolve(dir)
this.quiet = quiet this.quiet = quiet
const phase = this.currentPhase() const phase = this.currentPhase()
@ -54,14 +78,15 @@ export default class Server {
this.setAssetPrefix(assetPrefix) this.setAssetPrefix(assetPrefix)
} }
currentPhase () { private currentPhase (): string {
return PHASE_PRODUCTION_SERVER return PHASE_PRODUCTION_SERVER
} }
handleRequest (req, res, parsedUrl) { private handleRequest (req: IncomingMessage, res: ServerResponse, parsedUrl?: UrlWithParsedQuery): Promise<void> {
// Parse url if parsedUrl not provided // Parse url if parsedUrl not provided
if (!parsedUrl || typeof parsedUrl !== 'object') { if (!parsedUrl || typeof parsedUrl !== 'object') {
parsedUrl = parseUrl(req.url, true) const url: any = req.url
parsedUrl = parseUrl(url, true)
} }
// Parse the querystring ourselves if the user doesn't handle querystring parsing // Parse the querystring ourselves if the user doesn't handle querystring parsing
@ -78,30 +103,30 @@ export default class Server {
}) })
} }
getRequestHandler () { public getRequestHandler () {
return this.handleRequest.bind(this) return this.handleRequest.bind(this)
} }
setAssetPrefix (prefix) { public setAssetPrefix (prefix?: string) {
this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : '' this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
asset.setAssetPrefix(this.renderOpts.assetPrefix) asset.setAssetPrefix(this.renderOpts.assetPrefix)
} }
// Backwards compatibility // Backwards compatibility
async prepare () {} public async prepare (): Promise<void> {}
// Backwards compatibility // Backwards compatibility
async close () {} private async close (): Promise<void> {}
setImmutableAssetCacheControl (res) { private setImmutableAssetCacheControl (res: ServerResponse) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
} }
generateRoutes () { private generateRoutes (): Route[] {
const routes = [ const routes: Route[] = [
{ {
match: route('/_next/static/:path*'), match: route('/_next/static/:path*'),
fn: async (req, res, params) => { fn: async (req, res, params, parsedUrl) => {
// The commons folder holds commonschunk files // The commons folder holds commonschunk files
// The chunks folder holds dynamic entries // 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. // The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
@ -109,13 +134,13 @@ export default class Server {
this.setImmutableAssetCacheControl(res) this.setImmutableAssetCacheControl(res)
} }
const p = join(this.distDir, CLIENT_STATIC_FILES_PATH, ...(params.path || [])) const p = join(this.distDir, CLIENT_STATIC_FILES_PATH, ...(params.path || []))
await this.serveStatic(req, res, p) await this.serveStatic(req, res, p, parsedUrl)
} }
}, },
{ {
match: route('/_next/:path*'), match: route('/_next/:path*'),
// This path is needed because `render()` does a check for `/_next` and the calls the routing again // This path is needed because `render()` does a check for `/_next` and the calls the routing again
fn: async (req, res, params, parsedUrl) => { fn: async (req, res, _params, parsedUrl) => {
await this.render404(req, res, parsedUrl) await this.render404(req, res, parsedUrl)
} }
}, },
@ -125,9 +150,9 @@ export default class Server {
// Otherwise this will lead to a pretty simple DOS attack. // Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/zeit/next.js/issues/2617 // See more: https://github.com/zeit/next.js/issues/2617
match: route('/static/:path*'), match: route('/static/:path*'),
fn: async (req, res, params) => { fn: async (req, res, params, parsedUrl) => {
const p = join(this.dir, 'static', ...(params.path || [])) const p = join(this.dir, 'static', ...(params.path || []))
await this.serveStatic(req, res, p) await this.serveStatic(req, res, p, parsedUrl)
} }
} }
] ]
@ -139,8 +164,11 @@ export default class Server {
// See more: https://github.com/zeit/next.js/issues/2617 // See more: https://github.com/zeit/next.js/issues/2617
routes.push({ routes.push({
match: route('/:path*'), match: route('/:path*'),
fn: async (req, res, params, parsedUrl) => { fn: async (req, res, _params, parsedUrl) => {
const { pathname, query } = parsedUrl const { pathname, query } = parsedUrl
if(!pathname) {
throw new Error('pathname is undefined')
}
await this.render(req, res, pathname, query, parsedUrl) await this.render(req, res, pathname, query, parsedUrl)
} }
}) })
@ -149,7 +177,7 @@ export default class Server {
return routes return routes
} }
async run (req, res, parsedUrl) { private async run (req: IncomingMessage, res: ServerResponse, parsedUrl: UrlWithParsedQuery) {
try { try {
const fn = this.router.match(req, res, parsedUrl) const fn = this.router.match(req, res, parsedUrl)
if (fn) { if (fn) {
@ -172,32 +200,40 @@ export default class Server {
} }
} }
async render (req, res, pathname, query, parsedUrl) { private async sendHTML(req: IncomingMessage, res: ServerResponse, html: string) {
if (isInternalUrl(req.url)) { const {generateEtags} = this.renderOpts
return sendHTML(req, res, html, {generateEtags})
}
public async render (req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery, parsedUrl: UrlWithParsedQuery): Promise<void> {
const url: any = req.url
if (isInternalUrl(url)) {
return this.handleRequest(req, res, parsedUrl) return this.handleRequest(req, res, parsedUrl)
} }
if (isBlockedPage(pathname)) { if (isBlockedPage(pathname)) {
return await this.render404(req, res, parsedUrl) return this.render404(req, res, parsedUrl)
} }
const html = await this.renderToHTML(req, res, pathname, query) const html = await this.renderToHTML(req, res, pathname, query)
if (isResSent(res)) { // Request was ended by the user
if (html === null) {
return return
} }
if (this.nextConfig.poweredByHeader) { if (this.nextConfig.poweredByHeader) {
res.setHeader('X-Powered-By', 'Next.js ' + process.env.NEXT_VERSION) res.setHeader('X-Powered-By', 'Next.js ' + process.env.NEXT_VERSION)
} }
return sendHTML(req, res, html, this.renderOpts) return this.sendHTML(req, res, html)
} }
async renderToHTML (req, res, pathname, query) { public async renderToHTML (req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery): Promise<string|null> {
try { try {
return await renderToHTML(req, res, pathname, query, this.renderOpts) // To make sure the try/catch is executed
const html = await renderToHTML(req, res, pathname, query, this.renderOpts)
return html
} catch (err) { } catch (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
res.statusCode = 404
return this.renderErrorToHTML(null, req, res, pathname, query) return this.renderErrorToHTML(null, req, res, pathname, query)
} else { } else {
if (!this.quiet) console.error(err) if (!this.quiet) console.error(err)
@ -207,39 +243,43 @@ export default class Server {
} }
} }
async renderError (err, 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') res.setHeader('Cache-Control', 'no-cache, no-store, max-age=0, must-revalidate')
const html = await this.renderErrorToHTML(err, req, res, pathname, query) const html = await this.renderErrorToHTML(err, req, res, pathname, query)
return sendHTML(req, res, html, this.renderOpts) return this.sendHTML(req, res, html)
} }
async renderErrorToHTML (err, req, res, pathname, query) { public async renderErrorToHTML (err: Error|null, req: IncomingMessage, res: ServerResponse, pathname: string, query: ParsedUrlQuery) {
return renderErrorToHTML(err, req, res, pathname, query, this.renderOpts) return renderErrorToHTML(err, req, res, pathname, query, this.renderOpts)
} }
async render404 (req, res, parsedUrl = parseUrl(req.url, true)) { public async render404 (req: IncomingMessage, res: ServerResponse, parsedUrl?: UrlWithParsedQuery): Promise<void> {
const { pathname, query } = parsedUrl const url: any = req.url
const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
if(!pathname) {
throw new Error('pathname is undefined')
}
res.statusCode = 404 res.statusCode = 404
return this.renderError(null, req, res, pathname, query) return this.renderError(null, req, res, pathname, query)
} }
async serveStatic (req, res, path) { public async serveStatic (req: IncomingMessage, res: ServerResponse, path: string, parsedUrl?: UrlWithParsedQuery): Promise<void> {
if (!this.isServeableUrl(path)) { if (!this.isServeableUrl(path)) {
return this.render404(req, res) return this.render404(req, res, parsedUrl)
} }
try { try {
return await serveStatic(req, res, path) await serveStatic(req, res, path)
} catch (err) { } catch (err) {
if (err.code === 'ENOENT' || err.statusCode === 404) { if (err.code === 'ENOENT' || err.statusCode === 404) {
this.render404(req, res) this.render404(req, res, parsedUrl)
} else { } else {
throw err throw err
} }
} }
} }
isServeableUrl (path) { private isServeableUrl (path: string): boolean {
const resolved = resolve(path) const resolved = resolve(path)
if ( if (
resolved.indexOf(join(this.distDir) + sep) !== 0 && resolved.indexOf(join(this.distDir) + sep) !== 0 &&
@ -252,7 +292,7 @@ export default class Server {
return true return true
} }
readBuildId () { private readBuildId (): string {
const buildIdFile = join(this.distDir, BUILD_ID_FILE) const buildIdFile = join(this.distDir, BUILD_ID_FILE)
try { try {
return fs.readFileSync(buildIdFile, 'utf8').trim() return fs.readFileSync(buildIdFile, 'utf8').trim()

View file

@ -64,7 +64,7 @@ async function doRender (req, res, pathname, query, {
] ]
// the response might be finshed on the getinitialprops call // the response might be finshed on the getinitialprops call
if (isResSent(res)) return if (isResSent(res)) return null
let reactLoadableModules = [] let reactLoadableModules = []
const renderPage = (options = Page => Page) => { const renderPage = (options = Page => Page) => {
@ -114,7 +114,7 @@ async function doRender (req, res, pathname, query, {
const dynamicImports = [...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)] const dynamicImports = [...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)]
const dynamicImportsIds = dynamicImports.map((bundle) => bundle.id) const dynamicImportsIds = dynamicImports.map((bundle) => bundle.id)
if (isResSent(res)) return if (isResSent(res)) return null
if (!Document.prototype || !Document.prototype.isReactComponent) throw new Error('_document.js is not exporting a React component') if (!Document.prototype || !Document.prototype.isReactComponent) throw new Error('_document.js is not exporting a React component')
const doc = <Document {...{ const doc = <Document {...{

View file

@ -1,27 +0,0 @@
import pathMatch from './lib/path-match'
export const route = pathMatch()
export default class Router {
constructor (routes = []) {
this.routes = routes
}
add (route) {
this.routes.unshift(route)
}
match (req, res, parsedUrl) {
if (req.method !== 'GET' && req.method !== 'HEAD') {
return
}
const { pathname } = parsedUrl
for (const route of this.routes) {
const params = route.match(pathname)
if (params) {
return () => route.fn(req, res, params, parsedUrl)
}
}
}
}

View file

@ -0,0 +1,37 @@
import {IncomingMessage, ServerResponse} from 'http'
import {UrlWithParsedQuery} from 'url'
import pathMatch from './lib/path-match'
export const route = pathMatch()
type Params = {[param: string]: string}
export type Route = {
match: (pathname: string|undefined) => false|Params,
fn: (req: IncomingMessage, res: ServerResponse, params: Params, parsedUrl: UrlWithParsedQuery) => void
}
export default class Router {
routes: Route[]
constructor (routes: Route[] = []) {
this.routes = routes
}
add (route: Route) {
this.routes.unshift(route)
}
match (req: IncomingMessage, res: ServerResponse, parsedUrl: UrlWithParsedQuery) {
if (req.method !== 'GET' && req.method !== 'HEAD') {
return
}
const { pathname } = parsedUrl
for (const route of this.routes) {
const params = route.match(pathname)
if (params) {
return () => route.fn(req, res, params, parsedUrl)
}
}
}
}

View file

@ -3,7 +3,7 @@ import generateETag from 'etag'
import fresh from 'fresh' import fresh from 'fresh'
import { isResSent } from '../lib/utils' import { isResSent } from '../lib/utils'
export function sendHTML (req: IncomingMessage, res: ServerResponse, html: string, { dev, generateEtags }: {dev: boolean, generateEtags: boolean}) { export function sendHTML (req: IncomingMessage, res: ServerResponse, html: string, { generateEtags }: {generateEtags: boolean}) {
if (isResSent(res)) return if (isResSent(res)) return
const etag = generateEtags ? generateETag(html) : undefined const etag = generateEtags ? generateETag(html) : undefined
@ -13,11 +13,6 @@ export function sendHTML (req: IncomingMessage, res: ServerResponse, html: strin
return return
} }
if (dev) {
// In dev, we should not cache pages for any reason.
res.setHeader('Cache-Control', 'no-store, must-revalidate')
}
if (etag) { if (etag) {
res.setHeader('ETag', etag) res.setHeader('ETag', etag)
} }

View file

@ -5,7 +5,7 @@ const internalPrefixes = [
/^\/static\// /^\/static\//
] ]
export function isInternalUrl (url) { export function isInternalUrl (url: string): boolean {
for (const prefix of internalPrefixes) { for (const prefix of internalPrefixes) {
if (prefix.test(url)) { if (prefix.test(url)) {
return true return true
@ -15,6 +15,6 @@ export function isInternalUrl (url) {
return false return false
} }
export function isBlockedPage (pathname) { export function isBlockedPage (pathname: string): boolean {
return (BLOCKED_PAGES.indexOf(pathname) !== -1) return (BLOCKED_PAGES.indexOf(pathname) !== -1)
} }

View file

@ -120,6 +120,12 @@ export default class DevServer extends Server {
} }
} }
sendHTML (req, res, html) {
// In dev, we should not cache pages for any reason.
res.setHeader('Cache-Control', 'no-store, must-revalidate')
return super.sendHTML(req, res, html)
}
setImmutableAssetCacheControl (res) { setImmutableAssetCacheControl (res) {
res.setHeader('Cache-Control', 'no-store, must-revalidate') res.setHeader('Cache-Control', 'no-store, must-revalidate')
} }