未验证 提交 e9b2e25c 编写于 作者: J Joe Haddad 提交者: GitHub

Convert Dev Server to TypeScript (#8966)

* Convert Dev Server to TypeScript
This converts the Next.js Development Server to TypeScript in prep for upcoming changes.

* Convert remaining necessary

* Fix some type errors

* Adjust types
上级 c9d9ff6a
......@@ -85,7 +85,7 @@ export default class Server {
}
private compression?: Middleware
router: Router
private dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
protected dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
public constructor({
dir = '.',
......@@ -166,7 +166,7 @@ export default class Server {
})
}
private currentPhase(): string {
protected currentPhase(): string {
return PHASE_PRODUCTION_SERVER
}
......@@ -212,13 +212,13 @@ export default class Server {
public async prepare(): Promise<void> {}
// Backwards compatibility
private async close(): Promise<void> {}
protected async close(): Promise<void> {}
private setImmutableAssetCacheControl(res: ServerResponse) {
protected setImmutableAssetCacheControl(res: ServerResponse) {
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
}
private generateRoutes(): Route[] {
protected generateRoutes(): Route[] {
const routes: Route[] = [
{
match: route('/_next/static/:path*'),
......@@ -379,7 +379,7 @@ export default class Server {
* Resolves path to resolver function
* @param pathname path of request
*/
private resolveApiRequest(pathname: string) {
protected async resolveApiRequest(pathname: string): Promise<string | null> {
return getPagePath(
pathname,
this.distDir,
......@@ -388,7 +388,7 @@ export default class Server {
)
}
private generatePublicRoutes(): Route[] {
protected generatePublicRoutes(): Route[] {
const routes: Route[] = []
const publicFiles = recursiveReadDirSync(this.publicDir)
const serverBuildPath = join(
......@@ -414,7 +414,7 @@ export default class Server {
return routes
}
private getDynamicRoutes() {
protected getDynamicRoutes() {
const manifest = require(this.pagesManifest)
const dynamicRoutedPages = Object.keys(manifest).filter(isDynamicRoute)
return getSortedRoutes(dynamicRoutedPages).map(page => ({
......@@ -429,7 +429,7 @@ export default class Server {
}
}
private async run(
protected async run(
req: IncomingMessage,
res: ServerResponse,
parsedUrl: UrlWithParsedQuery
......@@ -453,7 +453,7 @@ export default class Server {
await this.render404(req, res, parsedUrl)
}
private async sendHTML(
protected async sendHTML(
req: IncomingMessage,
res: ServerResponse,
html: string
......@@ -848,7 +848,7 @@ export default class Server {
return true
}
private readBuildId(): string {
protected readBuildId(): string {
const buildIdFile = join(this.distDir, BUILD_ID_FILE)
try {
return fs.readFileSync(buildIdFile, 'utf8').trim()
......
......@@ -163,6 +163,8 @@
"@types/send": "0.14.4",
"@types/styled-jsx": "2.2.8",
"@types/text-table": "0.2.1",
"@types/webpack-dev-middleware": "2.0.3",
"@types/webpack-hot-middleware": "2.16.5",
"@types/webpack-sources": "0.1.5",
"@zeit/ncc": "0.18.5",
"arg": "4.1.0",
......
import React from 'react'
import Head from '../next-server/lib/head'
// This component is only rendered on the server side.
export default function ErrorDebug ({ error, info }) {
export default function ErrorDebug({
error,
info,
}: {
error: Error
info: any
}) {
return (
<div style={styles.errorDebug}>
<div style={styles.errorDebug as object}>
<Head>
<meta name='viewport' content='width=device-width, initial-scale=1.0' />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</Head>
<StackTrace error={error} info={info} />
</div>
)
}
const StackTrace = ({ error: { name, message, stack }, info }) => (
const StackTrace = ({
error: { name, message, stack },
info,
}: {
error: Error
info: any
}) => (
<div>
<div style={styles.heading}>{message || name}</div>
<pre style={styles.stack}>{stack}</pre>
{info && <pre style={styles.stack}>{info.componentStack}</pre>}
<div style={styles.heading as object}>{message || name}</div>
<pre style={styles.stack as object}>{stack}</pre>
{info && <pre style={styles.stack as object}>{info.componentStack}</pre>}
</div>
)
......@@ -33,7 +46,7 @@ export const styles = {
top: 0,
bottom: 0,
zIndex: 9999,
color: '#000000'
color: '#000000',
},
stack: {
......@@ -45,7 +58,7 @@ export const styles = {
margin: 0,
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
marginTop: '16px'
marginTop: '16px',
},
heading: {
......@@ -56,6 +69,6 @@ export const styles = {
lineHeight: '28px',
color: '#000000',
marginBottom: '0px',
marginTop: '0px'
}
marginTop: '0px',
},
}
import { relative as relativePath, join, normalize, sep } from 'path'
import fs from 'fs'
import { IncomingMessage, ServerResponse } from 'http'
import { join, normalize, relative as relativePath, sep } from 'path'
import { promisify } from 'util'
import webpack from 'webpack'
import WebpackDevMiddleware from 'webpack-dev-middleware'
import WebpackHotMiddleware from 'webpack-hot-middleware'
import errorOverlayMiddleware from './lib/error-overlay-middleware'
import onDemandEntryHandler, { normalizePage } from './on-demand-entry-handler'
import webpack from 'webpack'
import { createEntrypoints, createPagesMapping } from '../build/entries'
import { watchCompilers } from '../build/output'
import getBaseWebpackConfig from '../build/webpack-config'
import { NEXT_PROJECT_ROOT_DIST_CLIENT } from '../lib/constants'
import { fileExists } from '../lib/file-exists'
import { recursiveDelete } from '../lib/recursive-delete'
import {
BLOCKED_PAGES,
CLIENT_STATIC_FILES_RUNTIME_AMP,
IS_BUNDLED_PAGE_REGEX,
ROUTE_NAME_REGEX,
BLOCKED_PAGES,
CLIENT_STATIC_FILES_RUNTIME_AMP
} from '../next-server/lib/constants'
import { NEXT_PROJECT_ROOT_DIST_CLIENT } from '../lib/constants'
import { route } from '../next-server/server/router'
import { createPagesMapping, createEntrypoints } from '../build/entries'
import { watchCompilers } from '../build/output'
import errorOverlayMiddleware from './lib/error-overlay-middleware'
import { findPageFile } from './lib/find-page-file'
import { recursiveDelete } from '../lib/recursive-delete'
import { fileExists } from '../lib/file-exists'
import { promisify } from 'util'
import fs from 'fs'
import onDemandEntryHandler, { normalizePage } from './on-demand-entry-handler'
import { NextHandleFunction } from 'connect'
import { UrlObject } from 'url'
const access = promisify(fs.access)
const readFile = promisify(fs.readFile)
export async function renderScriptError (res, error) {
export async function renderScriptError(res: ServerResponse, error: Error) {
// Asks CDNs and others to not to cache the errored page
res.setHeader(
'Cache-Control',
'no-cache, no-store, max-age=0, must-revalidate'
)
if (error.code === 'ENOENT' || error.message === 'INVALID_BUILD_ID') {
if (
(error as any).code === 'ENOENT' ||
error.message === 'INVALID_BUILD_ID'
) {
res.statusCode = 404
res.end('404 - Not Found')
return
......@@ -42,7 +49,7 @@ export async function renderScriptError (res, error) {
res.end('500 - Internal Error')
}
function addCorsSupport (req, res) {
function addCorsSupport(req: IncomingMessage, res: ServerResponse) {
if (!req.headers.origin) {
return { preflight: false }
}
......@@ -51,10 +58,9 @@ function addCorsSupport (req, res) {
res.setHeader('Access-Control-Allow-Methods', 'OPTIONS, GET')
// Based on https://github.com/primus/access-control/blob/4cf1bc0e54b086c91e6aa44fb14966fa5ef7549c/index.js#L158
if (req.headers['access-control-request-headers']) {
res.setHeader(
'Access-Control-Allow-Headers',
req.headers['access-control-request-headers']
)
res.setHeader('Access-Control-Allow-Headers', req.headers[
'access-control-request-headers'
] as string)
}
if (req.method === 'OPTIONS') {
......@@ -71,7 +77,7 @@ const matchNextPageBundleRequest = route(
)
// Recursively look up the issuer till it ends up at the root
function findEntryModule (issuer) {
function findEntryModule(issuer: any): any {
if (issuer.issuer) {
return findEntryModule(issuer.issuer)
}
......@@ -79,8 +85,11 @@ function findEntryModule (issuer) {
return issuer
}
function erroredPages (compilation, options = { enhanceName: name => name }) {
const failedPages = {}
function erroredPages(
compilation: webpack.compilation.Compilation,
options = { enhanceName: (name: string) => name }
) {
const failedPages: { [page: string]: any[] } = {}
for (const error of compilation.errors) {
if (!error.origin) {
continue
......@@ -110,7 +119,29 @@ function erroredPages (compilation, options = { enhanceName: name => name }) {
}
export default class HotReloader {
constructor (dir, { config, pagesDir, buildId } = {}) {
private dir: string
private buildId: string
private middlewares: any[]
private pagesDir: string
private webpackDevMiddleware: WebpackDevMiddleware.WebpackDevMiddleware | null
private webpackHotMiddleware:
| (NextHandleFunction & WebpackHotMiddleware.EventStream)
| null
private initialized: boolean
private config: any
private stats: any
private serverPrevDocumentHash: string | null
private prevChunkNames?: Set<any>
private onDemandEntries: any
constructor(
dir: string,
{
config,
pagesDir,
buildId,
}: { config: object; pagesDir: string; buildId: string }
) {
this.buildId = buildId
this.dir = dir
this.middlewares = []
......@@ -124,7 +155,7 @@ export default class HotReloader {
this.config = config
}
async run (req, res, parsedUrl) {
async run(req: IncomingMessage, res: ServerResponse, parsedUrl: UrlObject) {
// Usually CORS support is not needed for the hot-reloader (this is dev only feature)
// With when the app runs for multi-zones support behind a proxy,
// the current page is trying to access this URL via assetPrefix.
......@@ -138,7 +169,10 @@ export default class HotReloader {
// we have to compile the page using on-demand-entries, this middleware will handle doing that
// by adding the page to on-demand-entries, waiting till it's done
// and then the bundle will be served like usual by the actual route in server/index.js
const handlePageBundleRequest = async (res, parsedUrl) => {
const handlePageBundleRequest = async (
res: ServerResponse,
parsedUrl: UrlObject
) => {
const { pathname } = parsedUrl
const params = matchNextPageBundleRequest(pathname)
if (!params) {
......@@ -186,11 +220,11 @@ export default class HotReloader {
return {}
}
const { finished } = await handlePageBundleRequest(res, parsedUrl)
const { finished } = (await handlePageBundleRequest(res, parsedUrl)) as any
for (const fn of this.middlewares) {
await new Promise((resolve, reject) => {
fn(req, res, err => {
fn(req, res, (err: Error) => {
if (err) return reject(err)
resolve()
})
......@@ -200,18 +234,18 @@ export default class HotReloader {
return { finished }
}
async clean () {
async clean() {
return recursiveDelete(join(this.dir, this.config.distDir))
}
async getWebpackConfig () {
async getWebpackConfig() {
const pagePaths = await Promise.all([
findPageFile(this.pagesDir, '/_app', this.config.pageExtensions),
findPageFile(this.pagesDir, '/_document', this.config.pageExtensions)
findPageFile(this.pagesDir, '/_document', this.config.pageExtensions),
])
const pages = createPagesMapping(
pagePaths.filter(i => i !== null),
pagePaths.filter(i => i !== null) as string[],
this.config.pageExtensions
)
const entrypoints = createEntrypoints(
......@@ -221,7 +255,7 @@ export default class HotReloader {
this.config
)
let additionalClientEntrypoints = {}
let additionalClientEntrypoints: { [file: string]: string } = {}
additionalClientEntrypoints[CLIENT_STATIC_FILES_RUNTIME_AMP] =
`.${sep}` +
relativePath(
......@@ -236,7 +270,7 @@ export default class HotReloader {
config: this.config,
buildId: this.buildId,
pagesDir: this.pagesDir,
entrypoints: { ...entrypoints.client, ...additionalClientEntrypoints }
entrypoints: { ...entrypoints.client, ...additionalClientEntrypoints },
}),
getBaseWebpackConfig(this.dir, {
dev: true,
......@@ -244,12 +278,12 @@ export default class HotReloader {
config: this.config,
buildId: this.buildId,
pagesDir: this.pagesDir,
entrypoints: entrypoints.server
})
entrypoints: entrypoints.server,
}),
])
}
async start () {
async start() {
await this.clean()
const configs = await this.getWebpackConfig()
......@@ -259,14 +293,14 @@ export default class HotReloader {
const buildTools = await this.prepareBuildTools(multiCompiler)
this.assignBuildTools(buildTools)
this.stats = (await this.waitUntilValid()).stats[0]
this.stats = ((await this.waitUntilValid()) as any).stats[0]
}
async stop (webpackDevMiddleware) {
async stop(webpackDevMiddleware?: WebpackDevMiddleware.WebpackDevMiddleware) {
const middleware = webpackDevMiddleware || this.webpackDevMiddleware
if (middleware) {
return new Promise((resolve, reject) => {
middleware.close(err => {
;(middleware.close as any)((err: any) => {
if (err) return reject(err)
resolve()
})
......@@ -274,7 +308,7 @@ export default class HotReloader {
}
}
async reload () {
async reload() {
this.stats = null
await this.clean()
......@@ -288,13 +322,17 @@ export default class HotReloader {
const oldWebpackDevMiddleware = this.webpackDevMiddleware
this.assignBuildTools(buildTools)
await this.stop(oldWebpackDevMiddleware)
await this.stop(oldWebpackDevMiddleware!)
}
assignBuildTools ({
assignBuildTools({
webpackDevMiddleware,
webpackHotMiddleware,
onDemandEntries
onDemandEntries,
}: {
webpackDevMiddleware: WebpackDevMiddleware.WebpackDevMiddleware
webpackHotMiddleware: NextHandleFunction & WebpackHotMiddleware.EventStream
onDemandEntries: any
}) {
this.webpackDevMiddleware = webpackDevMiddleware
this.webpackHotMiddleware = webpackHotMiddleware
......@@ -304,11 +342,11 @@ export default class HotReloader {
// must come before hotMiddleware
onDemandEntries.middleware(),
webpackHotMiddleware,
errorOverlayMiddleware({ dir: this.dir })
errorOverlayMiddleware({ dir: this.dir }),
]
}
async prepareBuildTools (multiCompiler) {
async prepareBuildTools(multiCompiler: webpack.MultiCompiler) {
const tsConfigPath = join(this.dir, 'tsconfig.json')
const useTypeScript = await fileExists(tsConfigPath)
......@@ -370,13 +408,13 @@ export default class HotReloader {
if (this.initialized) {
// detect chunks which have to be replaced with a new template
// e.g, pages/index.js <-> pages/_error.js
const addedPages = diff(chunkNames, this.prevChunkNames)
const removedPages = diff(this.prevChunkNames, chunkNames)
const addedPages = diff(chunkNames, this.prevChunkNames!)
const removedPages = diff(this.prevChunkNames!, chunkNames)
if (addedPages.size > 0) {
for (const addedPage of addedPages) {
let page =
'/' + ROUTE_NAME_REGEX.exec(addedPage)[1].replace(/\\/g, '/')
'/' + ROUTE_NAME_REGEX.exec(addedPage)![1].replace(/\\/g, '/')
page = page === '/index' ? '/' : page
this.send('addedPage', page)
}
......@@ -385,7 +423,7 @@ export default class HotReloader {
if (removedPages.size > 0) {
for (const removedPage of removedPages) {
let page =
'/' + ROUTE_NAME_REGEX.exec(removedPage)[1].replace(/\\/g, '/')
'/' + ROUTE_NAME_REGEX.exec(removedPage)![1].replace(/\\/g, '/')
page = page === '/index' ? '/' : page
this.send('removedPage', page)
}
......@@ -402,7 +440,7 @@ export default class HotReloader {
const ignored = [
/[\\/]\.git[\\/]/,
/[\\/]\.next[\\/]/,
/[\\/]node_modules[\\/]/
/[\\/]node_modules[\\/]/,
]
let webpackDevMiddlewareConfig = {
......@@ -410,7 +448,7 @@ export default class HotReloader {
noInfo: true,
logLevel: 'silent',
watchOptions: { ignored },
writeToDisk: true
writeToDisk: true,
}
if (this.config.webpackDevMiddleware) {
......@@ -434,7 +472,7 @@ export default class HotReloader {
{
path: '/_next/webpack-hmr',
log: false,
heartbeat: 2500
heartbeat: 2500,
}
)
......@@ -450,25 +488,27 @@ export default class HotReloader {
pageExtensions: this.config.pageExtensions,
publicRuntimeConfig: this.config.publicRuntimeConfig,
serverRuntimeConfig: this.config.serverRuntimeConfig,
...this.config.onDemandEntries
...this.config.onDemandEntries,
}
)
return {
webpackDevMiddleware,
webpackHotMiddleware,
onDemandEntries
onDemandEntries,
}
}
waitUntilValid (webpackDevMiddleware) {
waitUntilValid(
webpackDevMiddleware?: WebpackDevMiddleware.WebpackDevMiddleware
) {
const middleware = webpackDevMiddleware || this.webpackDevMiddleware
return new Promise(resolve => {
middleware.waitUntilValid(resolve)
middleware!.waitUntilValid(resolve)
})
}
async getCompilationErrors (page) {
async getCompilationErrors(page: string) {
const normalizedPage = normalizePage(page)
// When we are reloading, we need to wait until it's reloaded properly.
await this.onDemandEntries.waitUntilReloaded()
......@@ -476,9 +516,9 @@ export default class HotReloader {
if (this.stats.hasErrors()) {
const { compilation } = this.stats
const failedPages = erroredPages(compilation, {
enhanceName (name) {
return '/' + ROUTE_NAME_REGEX.exec(name)[1]
}
enhanceName(name) {
return '/' + ROUTE_NAME_REGEX.exec(name)![1]
},
})
// If there is an error related to the requesting page we display it instead of the first error
......@@ -496,11 +536,11 @@ export default class HotReloader {
return []
}
send = (action, ...args) => {
this.webpackHotMiddleware.publish({ action, data: args })
send = (action: string, ...args: any[]) => {
this.webpackHotMiddleware!.publish({ action, data: args })
}
async ensurePage (page) {
async ensurePage(page: string) {
// Make sure we don't re-build or dispose prebuilt pages
if (page !== '/_error' && BLOCKED_PAGES.indexOf(page) !== -1) {
return
......@@ -509,6 +549,6 @@ export default class HotReloader {
}
}
function diff (a, b) {
function diff(a: Set<any>, b: Set<any>) {
return new Set([...a].filter(v => !b.has(v)))
}
......@@ -2,18 +2,19 @@ import url from 'url'
import launchEditor from 'launch-editor'
import fs from 'fs'
import path from 'path'
import { IncomingMessage, ServerResponse } from 'http'
export default function errorOverlayMiddleware (options) {
return (req, res, next) => {
if (req.url.startsWith('/_next/development/open-stack-frame-in-editor')) {
const query = url.parse(req.url, true).query
const lineNumber = parseInt(query.lineNumber, 10) || 1
const colNumber = parseInt(query.colNumber, 10) || 1
export default function errorOverlayMiddleware(options: { dir: string }) {
return (req: IncomingMessage, res: ServerResponse, next: Function) => {
if (req.url!.startsWith('/_next/development/open-stack-frame-in-editor')) {
const query = url.parse(req.url!, true).query
const lineNumber = parseInt(query.lineNumber as string, 10) || 1
const colNumber = parseInt(query.colNumber as string, 10) || 1
let resolvedFileName = query.fileName
if (!fs.existsSync(resolvedFileName)) {
resolvedFileName = path.join(options.dir, resolvedFileName)
if (!fs.existsSync(resolvedFileName as string)) {
resolvedFileName = path.join(options.dir, resolvedFileName as string)
}
launchEditor(`${resolvedFileName}:${lineNumber}:${colNumber}`)
......
import Server from '../next-server/server/next-server'
import AmpHtmlValidator from 'amphtml-validator'
import fs from 'fs'
import { IncomingMessage, ServerResponse } from 'http'
import { join, relative } from 'path'
import React from 'react'
import { UrlWithParsedQuery } from 'url'
import { promisify } from 'util'
import HotReloader from './hot-reloader'
import { route } from '../next-server/server/router'
import { PHASE_DEVELOPMENT_SERVER } from '../next-server/lib/constants'
import ErrorDebug from './error-debug'
import AmpHtmlValidator from 'amphtml-validator'
import Watchpack from 'watchpack'
import { ampValidation } from '../build/output/index'
import * as Log from '../build/output/log'
import { PUBLIC_DIR_MIDDLEWARE_CONFLICT } from '../lib/constants'
import { findPagesDir } from '../lib/find-pages-dir'
import { verifyTypeScriptSetup } from '../lib/verifyTypeScriptSetup'
import Watchpack from 'watchpack'
import { setDistDir as setTelemetryDir } from '../telemetry/storage'
import { recordVersion } from '../telemetry/events'
import fs from 'fs'
import { PHASE_DEVELOPMENT_SERVER } from '../next-server/lib/constants'
import {
getRouteMatcher,
getRouteRegex,
getSortedRoutes,
isDynamicRoute
isDynamicRoute,
} from '../next-server/lib/router/utils'
import React from 'react'
import { findPageFile } from './lib/find-page-file'
import Server, { ServerConstructor } from '../next-server/server/next-server'
import { normalizePagePath } from '../next-server/server/normalize-page-path'
import { PUBLIC_DIR_MIDDLEWARE_CONFLICT } from '../lib/constants'
import { findPagesDir } from '../lib/find-pages-dir'
import { route } from '../next-server/server/router'
import { recordVersion } from '../telemetry/events'
import { setDistDir as setTelemetryDir } from '../telemetry/storage'
import ErrorDebug from './error-debug'
import HotReloader from './hot-reloader'
import { findPageFile } from './lib/find-page-file'
if (typeof React.Suspense === 'undefined') {
throw new Error(
......@@ -34,14 +37,22 @@ if (typeof React.Suspense === 'undefined') {
const fsStat = promisify(fs.stat)
export default class DevServer extends Server {
constructor (options) {
private devReady: Promise<void>
private setDevReady?: Function
private webpackWatcher?: Watchpack | null
private hotReloader?: HotReloader
constructor(options: ServerConstructor) {
super({ ...options, dev: true })
this.renderOpts.dev = true
this.renderOpts.ErrorDebug = ErrorDebug
;(this.renderOpts as any).ErrorDebug = ErrorDebug
this.devReady = new Promise(resolve => {
this.setDevReady = resolve
})
this.renderOpts.ampValidator = (html, pathname) => {
;(this.renderOpts as any).ampValidator = (
html: string,
pathname: string
) => {
return AmpHtmlValidator.getInstance().then(validator => {
const result = validator.validateString(html)
ampValidation(
......@@ -56,15 +67,15 @@ export default class DevServer extends Server {
this.pagesDir = findPagesDir(this.dir)
}
currentPhase () {
protected currentPhase() {
return PHASE_DEVELOPMENT_SERVER
}
readBuildId () {
protected readBuildId() {
return 'development'
}
async addExportPathMapRoutes () {
async addExportPathMapRoutes() {
// 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.nextConfig.exportPathMap) {
......@@ -76,7 +87,7 @@ export default class DevServer extends Server {
dir: this.dir,
outDir: null,
distDir: this.distDir,
buildId: this.buildId
buildId: this.buildId,
}
) // In development we can't give a default path mapping
for (const path in exportPathMap) {
......@@ -99,13 +110,13 @@ export default class DevServer extends Server {
const mergedQuery = { ...urlQuery, ...query }
await this.render(req, res, page, mergedQuery, parsedUrl)
}
},
})
}
}
}
async startWatcher () {
async startWatcher() {
if (this.webpackWatcher) {
return
}
......@@ -115,7 +126,7 @@ export default class DevServer extends Server {
const pagesDir = this.pagesDir
// Watchpack doesn't emit an event for an empty directory
fs.readdir(pagesDir, (_, files) => {
fs.readdir(pagesDir!, (_, files) => {
if (files && files.length) {
return
}
......@@ -127,7 +138,7 @@ export default class DevServer extends Server {
})
let wp = (this.webpackWatcher = new Watchpack())
wp.watch([], [pagesDir], 0)
wp.watch([], [pagesDir!], 0)
wp.on('aggregated', () => {
const dynamicRoutedPages = []
......@@ -137,7 +148,8 @@ export default class DevServer extends Server {
continue
}
let pageName = '/' + relative(pagesDir, fileName).replace(/\\+/g, '/')
let pageName =
'/' + relative(pagesDir!, fileName).replace(/\\+/g, '/')
pageName = pageName.replace(
new RegExp(`\\.+(?:${this.nextConfig.pageExtensions.join('|')})$`),
......@@ -155,7 +167,7 @@ export default class DevServer extends Server {
this.dynamicRoutes = getSortedRoutes(dynamicRoutedPages).map(page => ({
page,
match: getRouteMatcher(getRouteRegex(page))
match: getRouteMatcher(getRouteRegex(page)),
}))
if (!resolved) {
......@@ -166,7 +178,7 @@ export default class DevServer extends Server {
})
}
async stopWatcher () {
async stopWatcher() {
if (!this.webpackWatcher) {
return
}
......@@ -175,36 +187,40 @@ export default class DevServer extends Server {
this.webpackWatcher = null
}
async prepare () {
await verifyTypeScriptSetup(this.dir, this.pagesDir)
async prepare() {
await verifyTypeScriptSetup(this.dir, this.pagesDir!)
this.hotReloader = new HotReloader(this.dir, {
pagesDir: this.pagesDir,
pagesDir: this.pagesDir!,
config: this.nextConfig,
buildId: this.buildId
buildId: this.buildId,
})
await super.prepare()
await this.addExportPathMapRoutes()
await this.hotReloader.start()
await this.startWatcher()
this.setDevReady()
this.setDevReady!()
setTelemetryDir(this.distDir)
recordVersion({ cliCommand: 'dev' })
}
async close () {
protected async close() {
await this.stopWatcher()
if (this.hotReloader) {
await this.hotReloader.stop()
}
}
async run (req, res, parsedUrl) {
async run(
req: IncomingMessage,
res: ServerResponse,
parsedUrl: UrlWithParsedQuery
) {
await this.devReady
const { pathname } = parsedUrl
if (pathname.startsWith('/_next')) {
if (pathname!.startsWith('/_next')) {
try {
await fsStat(join(this.publicDir, '_next'))
throw new Error(PUBLIC_DIR_MIDDLEWARE_CONFLICT)
......@@ -214,11 +230,11 @@ export default class DevServer extends Server {
// check for a public file, throwing error if there's a
// conflicting page
if (this.nextConfig.experimental.publicDirectory) {
if (await this.hasPublicFile(pathname)) {
if (await this.hasPublicFile(pathname!)) {
const pageFile = await findPageFile(
this.pagesDir,
this.pagesDir!,
normalizePagePath(pathname),
normalizePagePath(pathname!),
this.nextConfig.pageExtensions
)
......@@ -227,13 +243,15 @@ export default class DevServer extends Server {
`A conflicting public file and page file was found for path ${pathname} https://err.sh/zeit/next.js/conflicting-public-file-page`
)
res.statusCode = 500
return this.renderError(err, req, res, pathname, {})
return this.renderError(err, req, res, pathname!, {})
}
return this.servePublic(req, res, pathname)
return this.servePublic(req, res, pathname!)
}
}
const { finished } = await this.hotReloader.run(req, res, parsedUrl)
const { finished } = (await this.hotReloader!.run(req, res, parsedUrl)) || {
finished: false,
}
if (finished) {
return
}
......@@ -241,7 +259,7 @@ export default class DevServer extends Server {
return super.run(req, res, parsedUrl)
}
generateRoutes () {
generateRoutes() {
const routes = super.generateRoutes()
// In development we expose all compiled files for react-error-overlay's line show feature
......@@ -251,23 +269,26 @@ export default class DevServer extends Server {
fn: async (req, res, params) => {
const p = join(this.distDir, ...(params.path || []))
await this.serveStatic(req, res, p)
}
},
})
return routes
}
// In development public files are not added to the router but handled as a fallback instead
generatePublicRoutes () {
protected generatePublicRoutes() {
return []
}
// In development dynamic routes cannot be known ahead of time
getDynamicRoutes () {
protected getDynamicRoutes() {
return []
}
_filterAmpDevelopmentScript (html, event) {
_filterAmpDevelopmentScript(
html: string,
event: { line: number; col: number; code: string }
) {
if (event.code !== 'DISALLOWED_SCRIPT_TAG') {
return true
}
......@@ -292,9 +313,9 @@ export default class DevServer extends Server {
* Check if resolver function is build or request new build for this function
* @param {string} pathname
*/
async resolveApiRequest (pathname) {
protected async resolveApiRequest(pathname: string): Promise<string | null> {
try {
await this.hotReloader.ensurePage(pathname)
await this.hotReloader!.ensurePage(pathname)
} catch (err) {
// API route dosn't exist => return 404
if (err.code === 'ENOENT') {
......@@ -305,7 +326,13 @@ export default class DevServer extends Server {
return resolvedPath
}
async renderToHTML (req, res, pathname, query, options = {}) {
async renderToHTML(
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: { [key: string]: string },
options = {}
) {
const compilationErr = await this.getCompilationError(pathname)
if (compilationErr) {
res.statusCode = 500
......@@ -314,18 +341,18 @@ export default class DevServer extends Server {
// In dev mode we use on demand entries to compile the page before rendering
try {
await this.hotReloader.ensurePage(pathname).catch(async err => {
if (err.code !== 'ENOENT') {
await this.hotReloader!.ensurePage(pathname).catch(async (err: Error) => {
if ((err as any).code !== 'ENOENT') {
throw err
}
for (const dynamicRoute of this.dynamicRoutes) {
for (const dynamicRoute of this.dynamicRoutes || []) {
const params = dynamicRoute.match(pathname)
if (!params) {
continue
}
return this.hotReloader.ensurePage(dynamicRoute.page).then(() => {
return this.hotReloader!.ensurePage(dynamicRoute.page).then(() => {
pathname = dynamicRoute.page
query = Object.assign({}, query, params)
})
......@@ -344,8 +371,14 @@ export default class DevServer extends Server {
return html
}
async renderErrorToHTML (err, req, res, pathname, query) {
await this.hotReloader.ensurePage('/_error')
async renderErrorToHTML(
err: Error | null,
req: IncomingMessage,
res: ServerResponse,
pathname: string,
query: { [key: string]: string }
) {
await this.hotReloader!.ensurePage('/_error')
const compilationErr = await this.getCompilationError(pathname)
if (compilationErr) {
......@@ -370,22 +403,22 @@ export default class DevServer extends Server {
}
}
sendHTML (req, res, html) {
sendHTML(req: IncomingMessage, res: ServerResponse, html: string) {
// 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) {
protected setImmutableAssetCacheControl(res: ServerResponse) {
res.setHeader('Cache-Control', 'no-store, must-revalidate')
}
servePublic (req, res, path) {
servePublic(req: IncomingMessage, res: ServerResponse, path: string) {
const p = join(this.publicDir, path)
return this.serveStatic(req, res, p)
}
async hasPublicFile (path) {
async hasPublicFile(path: string) {
try {
const info = await fsStat(join(this.publicDir, path))
return info.isFile()
......@@ -394,8 +427,8 @@ export default class DevServer extends Server {
}
}
async getCompilationError (page) {
const errors = await this.hotReloader.getCompilationErrors(page)
async getCompilationError(page: string) {
const errors = await this.hotReloader!.getCompilationErrors(page)
if (errors.length === 0) return
// Return the very first error we found.
......
import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
import { EventEmitter } from 'events'
import { IncomingMessage, ServerResponse } from 'http'
import { join, posix } from 'path'
import { stringify } from 'querystring'
import { parse } from 'url'
import { pageNotFoundError } from '../next-server/server/require'
import { normalizePagePath } from '../next-server/server/normalize-page-path'
import webpack from 'webpack'
import WebpackDevMiddleware from 'webpack-dev-middleware'
import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
import { isWriteable } from '../build/is-writeable'
import * as Log from '../build/output/log'
import { API_ROUTE } from '../lib/constants'
import {
IS_BUNDLED_PAGE_REGEX,
ROUTE_NAME_REGEX,
IS_BUNDLED_PAGE_REGEX
} from '../next-server/lib/constants'
import { stringify } from 'querystring'
import { normalizePagePath } from '../next-server/server/normalize-page-path'
import { pageNotFoundError } from '../next-server/server/require'
import { findPageFile } from './lib/find-page-file'
import { isWriteable } from '../build/is-writeable'
import * as Log from '../build/output/log'
import { API_ROUTE } from '../lib/constants'
const ADDED = Symbol('added')
const BUILDING = Symbol('building')
const BUILT = Symbol('built')
// Based on https://github.com/webpack/webpack/blob/master/lib/DynamicEntryPlugin.js#L29-L37
function addEntry (compilation, context, name, entry) {
function addEntry(
compilation: webpack.compilation.Compilation,
context: string,
name: string,
entry: string[]
) {
return new Promise((resolve, reject) => {
const dep = DynamicEntryPlugin.createDependency(entry, name)
compilation.addEntry(context, dep, name, err => {
compilation.addEntry(context, dep, name, (err: Error) => {
if (err) return reject(err)
resolve()
})
})
}
export default function onDemandEntryHandler (
devMiddleware,
multiCompiler,
export default function onDemandEntryHandler(
devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware,
multiCompiler: webpack.MultiCompiler,
{
buildId,
pagesDir,
reload,
pageExtensions,
maxInactiveAge,
pagesBufferLength
pagesBufferLength,
}: {
buildId: string
pagesDir: string
reload: any
pageExtensions: string[]
maxInactiveAge: number
pagesBufferLength: number
}
) {
const { compilers } = multiCompiler
const invalidator = new Invalidator(devMiddleware, multiCompiler)
let entries = {}
let entries: any = {}
let lastAccessPages = ['']
let doneCallbacks = new EventEmitter()
let doneCallbacks: EventEmitter | null = new EventEmitter()
let reloading = false
let stopped = false
let reloadCallbacks = new EventEmitter()
let lastEntry = null
let reloadCallbacks: EventEmitter | null = new EventEmitter()
let lastEntry: string | null = null
for (const compiler of compilers) {
compiler.hooks.make.tapPromise('NextJsOnDemandEntries', compilation => {
invalidator.startBuilding()
compiler.hooks.make.tapPromise(
'NextJsOnDemandEntries',
(compilation: webpack.compilation.Compilation) => {
invalidator.startBuilding()
const allEntries = Object.keys(entries).map(async page => {
if (compiler.name === 'client' && page.match(API_ROUTE)) {
return
}
const { name, absolutePagePath } = entries[page]
const pageExists = await isWriteable(absolutePagePath)
if (!pageExists) {
Log.event('page was removed', page)
delete entries[page]
return
}
const allEntries = Object.keys(entries).map(async page => {
if (compiler.name === 'client' && page.match(API_ROUTE)) {
return
}
const { name, absolutePagePath } = entries[page]
const pageExists = await isWriteable(absolutePagePath)
if (!pageExists) {
Log.event('page was removed', page)
delete entries[page]
return
}
entries[page].status = BUILDING
return addEntry(compilation, compiler.context, name, [
compiler.name === 'client'
? `next-client-pages-loader?${stringify({
page,
absolutePagePath
})}!`
: absolutePagePath
])
})
entries[page].status = BUILDING
return addEntry(compilation, compiler.context, name, [
compiler.name === 'client'
? `next-client-pages-loader?${stringify({
page,
absolutePagePath,
})}!`
: absolutePagePath,
])
})
return Promise.all(allEntries).catch(err => console.error(err))
})
return Promise.all(allEntries).catch(err => console.error(err))
}
)
}
function findHardFailedPages (errors) {
function findHardFailedPages(errors: any[]) {
return errors
.filter(e => {
// Make sure to only pick errors which marked with missing modules
......@@ -99,13 +118,13 @@ export default function onDemandEntryHandler (
})
.map(e => e.module.chunks)
.reduce((a, b) => [...a, ...b], [])
.map(c => {
const pageName = ROUTE_NAME_REGEX.exec(c.name)[1]
.map((c: any) => {
const pageName = ROUTE_NAME_REGEX.exec(c.name)![1]
return normalizePage(`/${pageName}`)
})
}
function getPagePathsFromEntrypoints (entrypoints) {
function getPagePathsFromEntrypoints(entrypoints: any) {
const pagePaths = []
for (const [, entrypoint] of entrypoints.entries()) {
const result = ROUTE_NAME_REGEX.exec(entrypoint.name)
......@@ -130,12 +149,12 @@ export default function onDemandEntryHandler (
const hardFailedPages = [
...new Set([
...findHardFailedPages(clientStats.compilation.errors),
...findHardFailedPages(serverStats.compilation.errors)
])
...findHardFailedPages(serverStats.compilation.errors),
]),
]
const pagePaths = new Set([
...getPagePathsFromEntrypoints(clientStats.compilation.entrypoints),
...getPagePathsFromEntrypoints(serverStats.compilation.entrypoints)
...getPagePathsFromEntrypoints(serverStats.compilation.entrypoints),
])
// compilation.entrypoints is a Map object, so iterating over it 0 is the key and 1 is the value
......@@ -153,7 +172,7 @@ export default function onDemandEntryHandler (
entry.status = BUILT
entry.lastActiveTime = Date.now()
doneCallbacks.emit(page)
doneCallbacks!.emit(page)
}
invalidator.doneBuilding()
......@@ -168,10 +187,10 @@ export default function onDemandEntryHandler (
reload()
.then(() => {
console.log('> Webpack reloaded.')
reloadCallbacks.emit('done')
reloadCallbacks!.emit('done')
stop()
})
.catch(err => {
.catch((err: Error) => {
console.error(`> Webpack reloading failed: ${err.message}`)
console.error(err.stack)
process.exit(1)
......@@ -179,7 +198,7 @@ export default function onDemandEntryHandler (
}
})
const disposeHandler = setInterval(function () {
const disposeHandler = setInterval(function() {
if (stopped) return
disposeInactiveEntries(
devMiddleware,
......@@ -191,14 +210,14 @@ export default function onDemandEntryHandler (
disposeHandler.unref()
function stop () {
function stop() {
clearInterval(disposeHandler)
stopped = true
doneCallbacks = null
reloadCallbacks = null
}
function handlePing (pg) {
function handlePing(pg: string) {
const page = normalizePage(pg)
const entryInfo = entries[page]
let toSend
......@@ -236,23 +255,23 @@ export default function onDemandEntryHandler (
}
return {
waitUntilReloaded () {
waitUntilReloaded() {
if (!reloading) return Promise.resolve(true)
return new Promise(resolve => {
reloadCallbacks.once('done', function () {
reloadCallbacks!.once('done', function() {
resolve()
})
})
},
async ensurePage (page) {
async ensurePage(page: string) {
await this.waitUntilReloaded()
let normalizedPagePath
let normalizedPagePath: string
try {
normalizedPagePath = normalizePagePath(page)
} catch (err) {
console.error(err)
throw pageNotFoundError(normalizedPagePath)
throw pageNotFoundError(page)
}
let pagePath = await findPageFile(
......@@ -294,7 +313,7 @@ export default function onDemandEntryHandler (
}
if (entryInfo.status === BUILDING) {
doneCallbacks.once(normalizedPage, handleCallback)
doneCallbacks!.once(normalizedPage, handleCallback)
return
}
}
......@@ -302,24 +321,24 @@ export default function onDemandEntryHandler (
Log.event(`build page: ${normalizedPage}`)
entries[normalizedPage] = { name, absolutePagePath, status: ADDED }
doneCallbacks.once(normalizedPage, handleCallback)
doneCallbacks!.once(normalizedPage, handleCallback)
invalidator.invalidate()
function handleCallback (err) {
function handleCallback(err: Error) {
if (err) return reject(err)
resolve()
}
})
},
middleware () {
return (req, res, next) => {
middleware() {
return (req: IncomingMessage, res: ServerResponse, next: Function) => {
if (stopped) {
// If this handler is stopped, we need to reload the user's browser.
// So the user could connect to the actually running handler.
res.statusCode = 302
res.setHeader('Location', req.url)
res.setHeader('Location', req.url!)
res.end('302')
} else if (reloading) {
// Webpack config is reloading. So, we need to wait until it's done and
......@@ -327,18 +346,18 @@ export default function onDemandEntryHandler (
// So the user could connect to the new handler and webpack setup.
this.waitUntilReloaded().then(() => {
res.statusCode = 302
res.setHeader('Location', req.url)
res.setHeader('Location', req.url!)
res.end('302')
})
} else {
if (!/^\/_next\/webpack-hmr/.test(req.url)) return next()
if (!/^\/_next\/webpack-hmr/.test(req.url!)) return next()
const { query } = parse(req.url, true)
const { query } = parse(req.url!, true)
const page = query.page
if (!page) return next()
const runPing = () => {
const data = handlePing(query.page)
const data = handlePing(query.page as string)
if (!data) return
res.write('data: ' + JSON.stringify(data) + '\n\n')
}
......@@ -352,17 +371,17 @@ export default function onDemandEntryHandler (
next()
}
}
}
},
}
}
function disposeInactiveEntries (
devMiddleware,
entries,
lastAccessPages,
maxInactiveAge
function disposeInactiveEntries(
devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware,
entries: any,
lastAccessPages: any,
maxInactiveAge: number
) {
const disposingPages = []
const disposingPages: any = []
Object.keys(entries).forEach(page => {
const { lastActiveTime, status } = entries[page]
......@@ -382,7 +401,7 @@ function disposeInactiveEntries (
})
if (disposingPages.length > 0) {
disposingPages.forEach(page => {
disposingPages.forEach((page: any) => {
delete entries[page]
})
Log.event(`disposing inactive page(s): ${disposingPages.join(', ')}`)
......@@ -392,7 +411,7 @@ function disposeInactiveEntries (
// /index and / is the same. So, we need to identify both pages as the same.
// This also applies to sub pages as well.
export function normalizePage (page) {
export function normalizePage(page: string) {
const unixPagePath = page.replace(/\\/g, '/')
if (unixPagePath === '/index' || unixPagePath === '/') {
return '/'
......@@ -403,7 +422,15 @@ export function normalizePage (page) {
// Make sure only one invalidation happens at a time
// Otherwise, webpack hash gets changed and it'll force the client to reload.
class Invalidator {
constructor (devMiddleware, multiCompiler) {
private multiCompiler: webpack.MultiCompiler
private devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware
private building: boolean
private rebuildAgain: boolean
constructor(
devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware,
multiCompiler: webpack.MultiCompiler
) {
this.multiCompiler = multiCompiler
this.devMiddleware = devMiddleware
// contains an array of types of compilers currently building
......@@ -411,7 +438,7 @@ class Invalidator {
this.rebuildAgain = false
}
invalidate () {
invalidate() {
// If there's a current build is processing, we won't abort it by invalidating.
// (If aborted, it'll cause a client side hard reload)
// But let it to invalidate just after the completion.
......@@ -430,11 +457,11 @@ class Invalidator {
this.devMiddleware.invalidate()
}
startBuilding () {
startBuilding() {
this.building = true
}
doneBuilding () {
doneBuilding() {
this.building = false
if (this.rebuildAgain) {
......
declare module '@babel/plugin-transform-modules-commonjs'
declare module 'webpack/lib/GraphHelpers'
declare module 'webpack/lib/DynamicEntryPlugin'
declare module 'unfetch'
declare module 'launch-editor'
declare module 'styled-jsx/server'
declare module 'async-retry'
......@@ -93,3 +95,19 @@ declare module NodeJS {
crossOrigin?: string
}
}
declare module 'watchpack' {
import { EventEmitter } from 'events'
class Watchpack extends EventEmitter {
watch(files: string[], directories: string[], startTime?: number): void
close(): void
getTimeInfoEntries(): Map<
string,
{ safeTime: number; timestamp: number; accuracy?: number }
>
}
export default Watchpack
}
......@@ -2205,6 +2205,13 @@
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03"
integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==
"@types/memory-fs@*":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@types/memory-fs/-/memory-fs-0.3.2.tgz#5d4753f9b390cb077c8c8af97bc96463399ceccd"
integrity sha512-j5AcZo7dbMxHoOimcHEIh0JZe5e1b8q8AqGSpZJrYc7xOgCIP79cIjTdx5jSDLtySnQDwkDTqwlC7Xw7uXw7qg==
dependencies:
"@types/node" "*"
"@types/mime@*":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
......@@ -2411,6 +2418,24 @@
"@types/unist" "*"
"@types/vfile-message" "*"
"@types/webpack-dev-middleware@2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/webpack-dev-middleware/-/webpack-dev-middleware-2.0.3.tgz#aefb145281b3326a052325d583d2339debbf1be3"
integrity sha512-DzNJJ6ah/6t1n8sfAgQyEbZ/OMmFcF9j9P3aesnm7G6/iBFR/qiGin8K89J0RmaWIBzhTMdDg3I5PmKmSv7N9w==
dependencies:
"@types/connect" "*"
"@types/memory-fs" "*"
"@types/webpack" "*"
loglevel "^1.6.2"
"@types/webpack-hot-middleware@2.16.5":
version "2.16.5"
resolved "https://registry.yarnpkg.com/@types/webpack-hot-middleware/-/webpack-hot-middleware-2.16.5.tgz#5271eada42f34670a7ae79ddb6f1c419a19c985f"
integrity sha512-41qSQeyRGZkWSi366jMQVsLo5fdLT8EgmvHNoBwcCtwZcHrQk6An6tD+ZfC0zMdNHzVEFlzQvT2mTte8zDxqNw==
dependencies:
"@types/connect" "*"
"@types/webpack" "*"
"@types/webpack-sources@0.1.5":
version "0.1.5"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92"
......@@ -9227,6 +9252,11 @@ log-update@^2.3.0:
cli-cursor "^2.0.0"
wrap-ansi "^3.0.1"
loglevel@^1.6.2:
version "1.6.4"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.4.tgz#f408f4f006db8354d0577dcf6d33485b3cb90d56"
integrity sha512-p0b6mOGKcGa+7nnmKbpzR6qloPbrgLcnio++E+14Vo/XffOGwZtRpUhr8dTH/x2oCMmEoIU0Zwm3ZauhvYD17g==
lolex@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册