mirror of
https://github.com/terribleplan/next.js.git
synced 2024-01-19 02:48:18 +00:00
Merge master into v3-beta.
This commit is contained in:
commit
8cb3e89455
|
@ -6,7 +6,7 @@ environment:
|
|||
# Install scripts. (runs after repo cloning)
|
||||
install:
|
||||
# Install Google Chrome for e2e testing
|
||||
- choco install googlechrome
|
||||
- choco install --ignore-checksums googlechrome
|
||||
# Get the latest stable version of Node.js or io.js
|
||||
- ps: Install-Product node $env:nodejs_version
|
||||
# install modules
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import {
|
||||
IS_BUNDLED_PAGE,
|
||||
MATCH_ROUTE_NAME
|
||||
} from '../../utils'
|
||||
|
||||
export default class PagesPlugin {
|
||||
apply (compiler) {
|
||||
const isBundledPage = /^bundles[/\\]pages.*\.js$/
|
||||
const matchRouteName = /^bundles[/\\]pages[/\\](.*)\.js$/
|
||||
|
||||
compiler.plugin('after-compile', (compilation, callback) => {
|
||||
const pages = Object
|
||||
.keys(compilation.namedChunks)
|
||||
.map(key => compilation.namedChunks[key])
|
||||
.filter(chunk => isBundledPage.test(chunk.name))
|
||||
.filter(chunk => IS_BUNDLED_PAGE.test(chunk.name))
|
||||
|
||||
pages.forEach((chunk) => {
|
||||
const page = compilation.assets[chunk.name]
|
||||
const pageName = matchRouteName.exec(chunk.name)[1]
|
||||
const pageName = MATCH_ROUTE_NAME.exec(chunk.name)[1]
|
||||
let routeName = `/${pageName.replace(/[/\\]?index$/, '')}`
|
||||
|
||||
// We need to convert \ into / when we are in windows
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { join, relative, sep } from 'path'
|
||||
import webpackDevMiddleware from 'webpack-dev-middleware'
|
||||
import webpackHotMiddleware from 'webpack-hot-middleware'
|
||||
import WebpackDevMiddleware from 'webpack-dev-middleware'
|
||||
import WebpackHotMiddleware from 'webpack-hot-middleware'
|
||||
import onDemandEntryHandler from './on-demand-entry-handler'
|
||||
import isWindowsBash from 'is-windows-bash'
|
||||
import webpack from './build/webpack'
|
||||
import clean from './build/clean'
|
||||
import getConfig from './config'
|
||||
|
||||
const isBundledPage = /^bundles[/\\]pages.*\.js$/
|
||||
import {
|
||||
IS_BUNDLED_PAGE
|
||||
} from './utils'
|
||||
|
||||
export default class HotReloader {
|
||||
constructor (dir, { quiet, conf } = {}) {
|
||||
|
@ -44,14 +45,17 @@ export default class HotReloader {
|
|||
clean(this.dir)
|
||||
])
|
||||
|
||||
this.prepareMiddlewares(compiler)
|
||||
const buildTools = await this.prepareBuildTools(compiler)
|
||||
this.assignBuildTools(buildTools)
|
||||
|
||||
this.stats = await this.waitUntilValid()
|
||||
}
|
||||
|
||||
async stop () {
|
||||
if (this.webpackDevMiddleware) {
|
||||
async stop (webpackDevMiddleware) {
|
||||
const middleware = webpackDevMiddleware || this.webpackDevMiddleware
|
||||
if (middleware) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.webpackDevMiddleware.close((err) => {
|
||||
middleware.close((err) => {
|
||||
if (err) return reject(err)
|
||||
resolve()
|
||||
})
|
||||
|
@ -59,7 +63,35 @@ export default class HotReloader {
|
|||
}
|
||||
}
|
||||
|
||||
async prepareMiddlewares (compiler) {
|
||||
async reload () {
|
||||
this.stats = null
|
||||
|
||||
const [compiler] = await Promise.all([
|
||||
webpack(this.dir, { dev: true, quiet: this.quiet }),
|
||||
clean(this.dir)
|
||||
])
|
||||
|
||||
const buildTools = await this.prepareBuildTools(compiler)
|
||||
this.stats = await this.waitUntilValid(buildTools.webpackDevMiddleware)
|
||||
|
||||
const oldWebpackDevMiddleware = this.webpackDevMiddleware
|
||||
|
||||
this.assignBuildTools(buildTools)
|
||||
await this.stop(oldWebpackDevMiddleware)
|
||||
}
|
||||
|
||||
assignBuildTools ({ webpackDevMiddleware, webpackHotMiddleware, onDemandEntries }) {
|
||||
this.webpackDevMiddleware = webpackDevMiddleware
|
||||
this.webpackHotMiddleware = webpackHotMiddleware
|
||||
this.onDemandEntries = onDemandEntries
|
||||
this.middlewares = [
|
||||
webpackDevMiddleware,
|
||||
webpackHotMiddleware,
|
||||
onDemandEntries.middleware()
|
||||
]
|
||||
}
|
||||
|
||||
async prepareBuildTools (compiler) {
|
||||
compiler.plugin('after-emit', (compilation, callback) => {
|
||||
const { assets } = compilation
|
||||
|
||||
|
@ -83,7 +115,7 @@ export default class HotReloader {
|
|||
const chunkNames = new Set(
|
||||
compilation.chunks
|
||||
.map((c) => c.name)
|
||||
.filter(name => isBundledPage.test(name))
|
||||
.filter(name => IS_BUNDLED_PAGE.test(name))
|
||||
)
|
||||
|
||||
const failedChunkNames = new Set(compilation.errors
|
||||
|
@ -95,7 +127,7 @@ export default class HotReloader {
|
|||
|
||||
const chunkHashes = new Map(
|
||||
compilation.chunks
|
||||
.filter(c => isBundledPage.test(c.name))
|
||||
.filter(c => IS_BUNDLED_PAGE.test(c.name))
|
||||
.map((c) => [c.name, c.hash])
|
||||
)
|
||||
|
||||
|
@ -163,33 +195,38 @@ export default class HotReloader {
|
|||
webpackDevMiddlewareConfig = this.config.webpackDevMiddleware(webpackDevMiddlewareConfig)
|
||||
}
|
||||
|
||||
this.webpackDevMiddleware = webpackDevMiddleware(compiler, webpackDevMiddlewareConfig)
|
||||
const webpackDevMiddleware = WebpackDevMiddleware(compiler, webpackDevMiddlewareConfig)
|
||||
|
||||
this.webpackHotMiddleware = webpackHotMiddleware(compiler, {
|
||||
const webpackHotMiddleware = WebpackHotMiddleware(compiler, {
|
||||
path: '/_next/webpack-hmr',
|
||||
log: false,
|
||||
heartbeat: 2500
|
||||
})
|
||||
this.onDemandEntries = onDemandEntryHandler(this.webpackDevMiddleware, compiler, {
|
||||
const onDemandEntries = onDemandEntryHandler(webpackDevMiddleware, compiler, {
|
||||
dir: this.dir,
|
||||
dev: true,
|
||||
reload: this.reload.bind(this),
|
||||
...this.config.onDemandEntries
|
||||
})
|
||||
|
||||
this.middlewares = [
|
||||
this.webpackDevMiddleware,
|
||||
this.webpackHotMiddleware,
|
||||
this.onDemandEntries.middleware()
|
||||
]
|
||||
return {
|
||||
webpackDevMiddleware,
|
||||
webpackHotMiddleware,
|
||||
onDemandEntries
|
||||
}
|
||||
}
|
||||
|
||||
waitUntilValid () {
|
||||
waitUntilValid (webpackDevMiddleware) {
|
||||
const middleware = webpackDevMiddleware || this.webpackDevMiddleware
|
||||
return new Promise((resolve) => {
|
||||
this.webpackDevMiddleware.waitUntilValid(resolve)
|
||||
middleware.waitUntilValid(resolve)
|
||||
})
|
||||
}
|
||||
|
||||
getCompilationErrors () {
|
||||
async getCompilationErrors () {
|
||||
// When we are reloading, we need to wait until it's reloaded properly.
|
||||
await this.onDemandEntries.waitUntilReloaded()
|
||||
|
||||
if (!this.compilationErrors) {
|
||||
this.compilationErrors = new Map()
|
||||
|
||||
|
|
|
@ -174,7 +174,7 @@ export default class Server {
|
|||
return await renderScriptError(req, res, page, error, {}, this.renderOpts)
|
||||
}
|
||||
|
||||
const compilationErr = this.getCompilationError(page)
|
||||
const compilationErr = await this.getCompilationError(page, req, res)
|
||||
if (compilationErr) {
|
||||
const customFields = { statusCode: 500 }
|
||||
return await renderScriptError(req, res, page, compilationErr, customFields, this.renderOpts)
|
||||
|
@ -253,7 +253,7 @@ export default class Server {
|
|||
|
||||
async renderToHTML (req, res, pathname, query) {
|
||||
if (this.dev) {
|
||||
const compilationErr = this.getCompilationError(pathname)
|
||||
const compilationErr = await this.getCompilationError(pathname)
|
||||
if (compilationErr) {
|
||||
res.statusCode = 500
|
||||
return this.renderErrorToHTML(compilationErr, req, res, pathname, query)
|
||||
|
@ -281,7 +281,7 @@ export default class Server {
|
|||
|
||||
async renderErrorToHTML (err, req, res, pathname, query) {
|
||||
if (this.dev) {
|
||||
const compilationErr = this.getCompilationError('/_error')
|
||||
const compilationErr = await this.getCompilationError('/_error')
|
||||
if (compilationErr) {
|
||||
res.statusCode = 500
|
||||
return renderErrorToHTML(compilationErr, req, res, pathname, query, this.renderOpts)
|
||||
|
@ -362,10 +362,10 @@ export default class Server {
|
|||
return true
|
||||
}
|
||||
|
||||
getCompilationError (page) {
|
||||
async getCompilationError (page, req, res) {
|
||||
if (!this.hotReloader) return
|
||||
|
||||
const errors = this.hotReloader.getCompilationErrors()
|
||||
const errors = await this.hotReloader.getCompilationErrors()
|
||||
if (!errors.size) return
|
||||
|
||||
const id = join(this.dir, this.dist, 'bundles', 'pages', page)
|
||||
|
|
|
@ -4,6 +4,7 @@ import { join } from 'path'
|
|||
import { parse } from 'url'
|
||||
import resolvePath from './resolve'
|
||||
import touch from 'touch'
|
||||
import { MATCH_ROUTE_NAME, IS_BUNDLED_PAGE } from './utils'
|
||||
|
||||
const ADDED = Symbol('added')
|
||||
const BUILDING = Symbol('building')
|
||||
|
@ -12,13 +13,17 @@ const BUILT = Symbol('built')
|
|||
export default function onDemandEntryHandler (devMiddleware, compiler, {
|
||||
dir,
|
||||
dev,
|
||||
reload,
|
||||
maxInactiveAge = 1000 * 25
|
||||
}) {
|
||||
const entries = {}
|
||||
const lastAccessPages = ['']
|
||||
const doneCallbacks = new EventEmitter()
|
||||
let entries = {}
|
||||
let lastAccessPages = ['']
|
||||
let doneCallbacks = new EventEmitter()
|
||||
const invalidator = new Invalidator(devMiddleware)
|
||||
let touchedAPage = false
|
||||
let reloading = false
|
||||
let stopped = false
|
||||
let reloadCallbacks = new EventEmitter()
|
||||
|
||||
compiler.plugin('make', function (compilation, done) {
|
||||
invalidator.startBuilding()
|
||||
|
@ -35,6 +40,27 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
})
|
||||
|
||||
compiler.plugin('done', function (stats) {
|
||||
const { compilation } = stats
|
||||
const hardFailedPages = compilation.errors
|
||||
.filter(e => {
|
||||
// Make sure to only pick errors which marked with missing modules
|
||||
const hasNoModuleFoundError = /ENOENT/.test(e.message) || /Module not found/.test(e.message)
|
||||
if (!hasNoModuleFoundError) return false
|
||||
|
||||
// The page itself is missing. So this is a failed page.
|
||||
if (IS_BUNDLED_PAGE.test(e.module.name)) return true
|
||||
|
||||
// No dependencies means this is a top level page.
|
||||
// So this is a failed page.
|
||||
return e.module.dependencies.length === 0
|
||||
})
|
||||
.map(e => e.module.chunks)
|
||||
.reduce((a, b) => [...a, ...b], [])
|
||||
.map(c => {
|
||||
const pageName = MATCH_ROUTE_NAME.exec(c.name)[1]
|
||||
return normalizePage(`/${pageName}`)
|
||||
})
|
||||
|
||||
// Call all the doneCallbacks
|
||||
Object.keys(entries).forEach((page) => {
|
||||
const entryInfo = entries[page]
|
||||
|
@ -57,14 +83,48 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
})
|
||||
|
||||
invalidator.doneBuilding()
|
||||
|
||||
if (hardFailedPages.length > 0 && !reloading) {
|
||||
console.log(`> Reloading webpack due to inconsistant state of pages(s): ${hardFailedPages.join(', ')}`)
|
||||
reloading = true
|
||||
reload()
|
||||
.then(() => {
|
||||
console.log('> Webpack reloaded.')
|
||||
reloadCallbacks.emit('done')
|
||||
stop()
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(`> Webpack reloading failed: ${err.message}`)
|
||||
console.error(err.stack)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
setInterval(function () {
|
||||
const disposeHandler = setInterval(function () {
|
||||
if (stopped) return
|
||||
disposeInactiveEntries(devMiddleware, entries, lastAccessPages, maxInactiveAge)
|
||||
}, 5000)
|
||||
|
||||
function stop () {
|
||||
clearInterval(disposeHandler)
|
||||
stopped = true
|
||||
doneCallbacks = null
|
||||
reloadCallbacks = null
|
||||
}
|
||||
|
||||
return {
|
||||
waitUntilReloaded () {
|
||||
if (!reloading) return Promise.resolve(true)
|
||||
return new Promise((resolve) => {
|
||||
reloadCallbacks.once('done', function () {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
async ensurePage (page) {
|
||||
await this.waitUntilReloaded()
|
||||
page = normalizePage(page)
|
||||
|
||||
const pagePath = join(dir, 'pages', page)
|
||||
|
@ -103,31 +163,49 @@ export default function onDemandEntryHandler (devMiddleware, compiler, {
|
|||
},
|
||||
|
||||
middleware () {
|
||||
return function (req, res, next) {
|
||||
if (!/^\/_next\/on-demand-entries-ping/.test(req.url)) return next()
|
||||
return (req, res, next) => {
|
||||
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.end('302')
|
||||
} else if (reloading) {
|
||||
// Webpack config is reloading. So, we need to wait until it's done and
|
||||
// reload user's browser.
|
||||
// So the user could connect to the new handler and webpack setup.
|
||||
this.waitUntilReloaded()
|
||||
.then(() => {
|
||||
res.statusCode = 302
|
||||
res.setHeader('Location', req.url)
|
||||
res.end('302')
|
||||
})
|
||||
} else {
|
||||
if (!/^\/_next\/on-demand-entries-ping/.test(req.url)) return next()
|
||||
|
||||
const { query } = parse(req.url, true)
|
||||
const page = normalizePage(query.page)
|
||||
const entryInfo = entries[page]
|
||||
const { query } = parse(req.url, true)
|
||||
const page = normalizePage(query.page)
|
||||
const entryInfo = entries[page]
|
||||
|
||||
// If there's no entry.
|
||||
// Then it seems like an weird issue.
|
||||
if (!entryInfo) {
|
||||
const message = `Client pings, but there's no entry for page: ${page}`
|
||||
console.error(message)
|
||||
sendJson(res, { invalid: true })
|
||||
return
|
||||
// If there's no entry.
|
||||
// Then it seems like an weird issue.
|
||||
if (!entryInfo) {
|
||||
const message = `Client pings, but there's no entry for page: ${page}`
|
||||
console.error(message)
|
||||
sendJson(res, { invalid: true })
|
||||
return
|
||||
}
|
||||
|
||||
sendJson(res, { success: true })
|
||||
|
||||
// We don't need to maintain active state of anything other than BUILT entries
|
||||
if (entryInfo.status !== BUILT) return
|
||||
|
||||
// If there's an entryInfo
|
||||
lastAccessPages.pop()
|
||||
lastAccessPages.unshift(page)
|
||||
entryInfo.lastActiveTime = Date.now()
|
||||
}
|
||||
|
||||
sendJson(res, { success: true })
|
||||
|
||||
// We don't need to maintain active state of anything other than BUILT entries
|
||||
if (entryInfo.status !== BUILT) return
|
||||
|
||||
// If there's an entryInfo
|
||||
lastAccessPages.pop()
|
||||
lastAccessPages.unshift(page)
|
||||
entryInfo.lastActiveTime = Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
2
server/utils.js
Normal file
2
server/utils.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const IS_BUNDLED_PAGE = /^bundles[/\\]pages.*\.js$/
|
||||
export const MATCH_ROUTE_NAME = /^bundles[/\\]pages[/\\](.*)\.js$/
|
7
test/integration/basic/pages/hmr/contact.js
Normal file
7
test/integration/basic/pages/hmr/contact.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export default () => (
|
||||
<div className='hmr-contact-page'>
|
||||
<p>
|
||||
This is the contact page.
|
||||
</p>
|
||||
</div>
|
||||
)
|
|
@ -1,7 +1,8 @@
|
|||
/* global describe, it, expect */
|
||||
import webdriver from 'next-webdriver'
|
||||
import { readFileSync, writeFileSync } from 'fs'
|
||||
import { readFileSync, writeFileSync, renameSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
import { waitFor } from 'next-test-utils'
|
||||
|
||||
export default (context, render) => {
|
||||
describe('Hot Module Reloading', () => {
|
||||
|
@ -36,5 +37,45 @@ export default (context, render) => {
|
|||
browser.close()
|
||||
})
|
||||
})
|
||||
|
||||
describe('delete a page and add it back', () => {
|
||||
it('should load the page properly', async () => {
|
||||
const browser = await webdriver(context.appPort, '/hmr/contact')
|
||||
const text = await browser
|
||||
.elementByCss('p').text()
|
||||
expect(text).toBe('This is the contact page.')
|
||||
|
||||
const contactPagePath = join(__dirname, '../', 'pages', 'hmr', 'contact.js')
|
||||
const newContactPagePath = join(__dirname, '../', 'pages', 'hmr', '_contact.js')
|
||||
|
||||
// Rename the file to mimic a deleted page
|
||||
renameSync(contactPagePath, newContactPagePath)
|
||||
|
||||
// wait until the 404 page comes
|
||||
while (true) {
|
||||
try {
|
||||
const pageContent = await browser.elementByCss('body').text()
|
||||
if (/This page could not be found/.test(pageContent)) break
|
||||
} catch (ex) {}
|
||||
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
// Rename the file back to the original filename
|
||||
renameSync(newContactPagePath, contactPagePath)
|
||||
|
||||
// wait until the page comes back
|
||||
while (true) {
|
||||
try {
|
||||
const pageContent = await browser.elementByCss('body').text()
|
||||
if (/This is the contact page/.test(pageContent)) break
|
||||
} catch (ex) {}
|
||||
|
||||
await waitFor(1000)
|
||||
}
|
||||
|
||||
browser.close()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue