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

222 lines
6.1 KiB
JavaScript
Raw Normal View History

import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'
import { EventEmitter } from 'events'
import { join } from 'path'
import { parse } from 'url'
import resolvePath from './resolve'
import touch from 'touch'
const ADDED = Symbol('added')
const BUILDING = Symbol('building')
const BUILT = Symbol('built')
export default function onDemandEntryHandler (devMiddleware, compiler, {
dir,
dev,
maxInactiveAge = 1000 * 25
}) {
const entries = {}
const lastAccessPages = ['']
const doneCallbacks = new EventEmitter()
const invalidator = new Invalidator(devMiddleware)
let touchedAPage = false
compiler.plugin('make', function (compilation, done) {
invalidator.startBuilding()
const allEntries = Object.keys(entries).map((page) => {
const { name, entry } = entries[page]
entries[page].status = BUILDING
return addEntry(compilation, this.context, name, entry)
})
Promise.all(allEntries)
.then(() => done())
.catch(done)
})
compiler.plugin('done', function (stats) {
// Call all the doneCallbacks
Object.keys(entries).forEach((page) => {
const entryInfo = entries[page]
if (entryInfo.status !== BUILDING) return
// With this, we are triggering a filesystem based watch trigger
// It'll memorize some timestamp related info related to common files used
// in the page
// That'll reduce the page building time significantly.
if (!touchedAPage) {
setTimeout(() => {
touch.sync(entryInfo.pathname)
}, 1000)
touchedAPage = true
}
entryInfo.status = BUILT
entries[page].lastActiveTime = Date.now()
doneCallbacks.emit(page)
})
invalidator.doneBuilding()
})
setInterval(function () {
disposeInactiveEntries(devMiddleware, entries, lastAccessPages, maxInactiveAge)
}, 5000)
return {
async ensurePage (page) {
page = normalizePage(page)
const pagePath = join(dir, 'pages', page)
const pathname = await resolvePath(pagePath)
const name = join('bundles', pathname.substring(dir.length))
const entry = [`${pathname}?entry`]
await new Promise((resolve, reject) => {
const entryInfo = entries[page]
if (entryInfo) {
if (entryInfo.status === BUILT) {
resolve()
return
}
if (entryInfo.status === BUILDING) {
doneCallbacks.on(page, processCallback)
return
}
}
console.log(`> Building page: ${page}`)
entries[page] = { name, entry, pathname, status: ADDED }
doneCallbacks.on(page, processCallback)
invalidator.invalidate()
function processCallback (err) {
if (err) return reject(err)
resolve()
}
})
},
middleware () {
return function (req, res, next) {
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]
// 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()
}
}
}
}
function addEntry (compilation, context, name, entry) {
return new Promise((resolve, reject) => {
const dep = DynamicEntryPlugin.createDependency(entry, name)
compilation.addEntry(context, dep, name, (err) => {
if (err) return reject(err)
resolve()
})
})
}
function disposeInactiveEntries (devMiddleware, entries, lastAccessPages, maxInactiveAge) {
const disposingPages = []
Object.keys(entries).forEach((page) => {
const { lastActiveTime, status } = entries[page]
// This means this entry is currently building or just added
// We don't need to dispose those entries.
if (status !== BUILT) return
// We should not build the last accessed page even we didn't get any pings
// Sometimes, it's possible our XHR ping to wait before completing other requests.
// In that case, we should not dispose the current viewing page
if (lastAccessPages[0] === page) return
if (Date.now() - lastActiveTime > maxInactiveAge) {
disposingPages.push(page)
}
})
if (disposingPages.length > 0) {
disposingPages.forEach((page) => {
delete entries[page]
})
console.log(`> Disposing inactive page(s): ${disposingPages.join(', ')}`)
devMiddleware.invalidate()
}
}
// /index and / is the same. So, we need to identify both pages as the same.
// This also applies to sub pages as well.
function normalizePage (page) {
return page.replace(/\/index$/, '/')
}
function sendJson (res, payload) {
res.setHeader('Content-Type', 'application/json')
res.status = 200
res.end(JSON.stringify(payload))
}
// 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) {
this.devMiddleware = devMiddleware
this.building = false
this.rebuildAgain = false
}
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.
// So, it can re-build the queued pages at once.
if (this.building) {
this.rebuildAgain = true
return
}
this.building = true
this.devMiddleware.invalidate()
}
startBuilding () {
this.building = true
}
doneBuilding () {
this.building = false
if (this.rebuildAgain) {
this.rebuildAgain = false
this.invalidate()
}
}
}