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": {
"parser": "babel-eslint",
"ignore": [
"packages/next-server/server/next-server.ts",
"**/*.d.ts",
"**/node_modules/**",
"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 { parse as parseUrl } from 'url'
import { parse as parseQs } from 'querystring'
import { parse as parseUrl, UrlWithParsedQuery } from 'url'
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
import fs from 'fs'
import {
renderToHTML,
@ -9,16 +10,39 @@ import {
} from './render'
import {sendHTML} from './send-html'
import {serveStatic} from './serve-static'
import Router, {route} from './router'
import Router, {route, Route} from './router'
import { isInternalUrl, isBlockedPage } from './utils'
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 * as asset from '../lib/asset'
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 {
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.quiet = quiet
const phase = this.currentPhase()
@ -54,14 +78,15 @@ export default class Server {
this.setAssetPrefix(assetPrefix)
}
currentPhase () {
private currentPhase (): string {
return PHASE_PRODUCTION_SERVER
}
handleRequest (req, res, parsedUrl) {
private handleRequest (req: IncomingMessage, res: ServerResponse, parsedUrl?: UrlWithParsedQuery): Promise<void> {
// Parse url if parsedUrl not provided
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
@ -78,30 +103,30 @@ export default class Server {
})
}
getRequestHandler () {
public getRequestHandler () {
return this.handleRequest.bind(this)
}
setAssetPrefix (prefix) {
public setAssetPrefix (prefix?: string) {
this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
asset.setAssetPrefix(this.renderOpts.assetPrefix)
}
// Backwards compatibility
async prepare () {}
public async prepare (): Promise<void> {}
// Backwards compatibility
async close () {}
private async close (): Promise<void> {}
setImmutableAssetCacheControl (res) {
private setImmutableAssetCacheControl (res: ServerResponse) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
}
generateRoutes () {
const routes = [
private generateRoutes (): Route[] {
const routes: Route[] = [
{
match: route('/_next/static/:path*'),
fn: async (req, res, params) => {
fn: async (req, res, params, parsedUrl) => {
// The commons folder holds commonschunk files
// 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.
@ -109,13 +134,13 @@ export default class Server {
this.setImmutableAssetCacheControl(res)
}
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*'),
// 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)
}
},
@ -125,9 +150,9 @@ export default class Server {
// Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/zeit/next.js/issues/2617
match: route('/static/:path*'),
fn: async (req, res, params) => {
fn: async (req, res, params, parsedUrl) => {
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
routes.push({
match: route('/:path*'),
fn: async (req, res, params, parsedUrl) => {
fn: async (req, res, _params, parsedUrl) => {
const { pathname, query } = parsedUrl
if(!pathname) {
throw new Error('pathname is undefined')
}
await this.render(req, res, pathname, query, parsedUrl)
}
})
@ -149,7 +177,7 @@ export default class Server {
return routes
}
async run (req, res, parsedUrl) {
private async run (req: IncomingMessage, res: ServerResponse, parsedUrl: UrlWithParsedQuery) {
try {
const fn = this.router.match(req, res, parsedUrl)
if (fn) {
@ -172,32 +200,40 @@ export default class Server {
}
}
async render (req, res, pathname, query, parsedUrl) {
if (isInternalUrl(req.url)) {
private async sendHTML(req: IncomingMessage, res: ServerResponse, html: string) {
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)
}
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)
if (isResSent(res)) {
// Request was ended by the user
if (html === null) {
return
}
if (this.nextConfig.poweredByHeader) {
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 {
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) {
if (err.code === 'ENOENT') {
res.statusCode = 404
return this.renderErrorToHTML(null, req, res, pathname, query)
} else {
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')
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)
}
async render404 (req, res, parsedUrl = parseUrl(req.url, true)) {
const { pathname, query } = parsedUrl
public async render404 (req: IncomingMessage, res: ServerResponse, parsedUrl?: UrlWithParsedQuery): Promise<void> {
const url: any = req.url
const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
if(!pathname) {
throw new Error('pathname is undefined')
}
res.statusCode = 404
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)) {
return this.render404(req, res)
return this.render404(req, res, parsedUrl)
}
try {
return await serveStatic(req, res, path)
await serveStatic(req, res, path)
} catch (err) {
if (err.code === 'ENOENT' || err.statusCode === 404) {
this.render404(req, res)
this.render404(req, res, parsedUrl)
} else {
throw err
}
}
}
isServeableUrl (path) {
private isServeableUrl (path: string): boolean {
const resolved = resolve(path)
if (
resolved.indexOf(join(this.distDir) + sep) !== 0 &&
@ -252,7 +292,7 @@ export default class Server {
return true
}
readBuildId () {
private readBuildId (): string {
const buildIdFile = join(this.distDir, BUILD_ID_FILE)
try {
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
if (isResSent(res)) return
if (isResSent(res)) return null
let reactLoadableModules = []
const renderPage = (options = Page => Page) => {
@ -114,7 +114,7 @@ async function doRender (req, res, pathname, query, {
const dynamicImports = [...getDynamicImportBundles(reactLoadableManifest, reactLoadableModules)]
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')
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 { 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
const etag = generateEtags ? generateETag(html) : undefined
@ -13,11 +13,6 @@ export function sendHTML (req: IncomingMessage, res: ServerResponse, html: strin
return
}
if (dev) {
// In dev, we should not cache pages for any reason.
res.setHeader('Cache-Control', 'no-store, must-revalidate')
}
if (etag) {
res.setHeader('ETag', etag)
}

View file

@ -5,7 +5,7 @@ const internalPrefixes = [
/^\/static\//
]
export function isInternalUrl (url) {
export function isInternalUrl (url: string): boolean {
for (const prefix of internalPrefixes) {
if (prefix.test(url)) {
return true
@ -15,6 +15,6 @@ export function isInternalUrl (url) {
return false
}
export function isBlockedPage (pathname) {
export function isBlockedPage (pathname: string): boolean {
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) {
res.setHeader('Cache-Control', 'no-store, must-revalidate')
}