2017-02-26 20:45:16 +01:00
|
|
|
import { EventEmitter } from 'events'
|
2019-10-04 18:11:39 +02:00
|
|
|
import { IncomingMessage, ServerResponse } from 'http'
|
2019-03-19 04:24:21 +01:00
|
|
|
import { join, posix } from 'path'
|
2019-10-04 18:11:39 +02:00
|
|
|
import { stringify } from 'querystring'
|
2019-02-19 22:45:07 +01:00
|
|
|
import { parse } from 'url'
|
2019-10-04 18:11:39 +02:00
|
|
|
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'
|
2019-09-04 16:00:54 +02:00
|
|
|
import {
|
2019-10-04 18:11:39 +02:00
|
|
|
IS_BUNDLED_PAGE_REGEX,
|
2019-09-04 16:00:54 +02:00
|
|
|
ROUTE_NAME_REGEX,
|
|
|
|
} from '../next-server/lib/constants'
|
2019-10-04 18:11:39 +02:00
|
|
|
import { normalizePagePath } from '../next-server/server/normalize-page-path'
|
|
|
|
import { pageNotFoundError } from '../next-server/server/require'
|
2019-02-24 22:08:35 +01:00
|
|
|
import { findPageFile } from './lib/find-page-file'
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2017-03-03 01:17:41 +01:00
|
|
|
const ADDED = Symbol('added')
|
|
|
|
const BUILDING = Symbol('building')
|
|
|
|
const BUILT = Symbol('built')
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2018-09-16 16:06:02 +02:00
|
|
|
// Based on https://github.com/webpack/webpack/blob/master/lib/DynamicEntryPlugin.js#L29-L37
|
2019-10-04 18:11:39 +02:00
|
|
|
function addEntry(
|
|
|
|
compilation: webpack.compilation.Compilation,
|
|
|
|
context: string,
|
|
|
|
name: string,
|
|
|
|
entry: string[]
|
|
|
|
) {
|
2018-09-16 16:06:02 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
2019-05-17 13:25:46 +02:00
|
|
|
const dep = DynamicEntryPlugin.createDependency(entry, name)
|
2019-10-04 18:11:39 +02:00
|
|
|
compilation.addEntry(context, dep, name, (err: Error) => {
|
2018-09-16 16:06:02 +02:00
|
|
|
if (err) return reject(err)
|
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
export default function onDemandEntryHandler(
|
|
|
|
devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware,
|
|
|
|
multiCompiler: webpack.MultiCompiler,
|
2019-09-24 17:15:14 +02:00
|
|
|
{
|
|
|
|
buildId,
|
|
|
|
pagesDir,
|
|
|
|
reload,
|
|
|
|
pageExtensions,
|
|
|
|
maxInactiveAge,
|
2019-10-04 18:11:39 +02:00
|
|
|
pagesBufferLength,
|
|
|
|
}: {
|
|
|
|
buildId: string
|
|
|
|
pagesDir: string
|
|
|
|
reload: any
|
|
|
|
pageExtensions: string[]
|
|
|
|
maxInactiveAge: number
|
|
|
|
pagesBufferLength: number
|
2019-09-24 17:15:14 +02:00
|
|
|
}
|
2019-05-29 13:57:26 +02:00
|
|
|
) {
|
2019-02-19 22:45:07 +01:00
|
|
|
const { compilers } = multiCompiler
|
2018-09-16 16:06:02 +02:00
|
|
|
const invalidator = new Invalidator(devMiddleware, multiCompiler)
|
2019-10-04 18:11:39 +02:00
|
|
|
let entries: any = {}
|
2017-06-07 00:32:02 +02:00
|
|
|
let lastAccessPages = ['']
|
2019-10-04 18:11:39 +02:00
|
|
|
let doneCallbacks: EventEmitter | null = new EventEmitter()
|
2017-06-07 00:32:02 +02:00
|
|
|
let reloading = false
|
|
|
|
let stopped = false
|
2019-10-04 18:11:39 +02:00
|
|
|
let reloadCallbacks: EventEmitter | null = new EventEmitter()
|
2018-01-30 16:40:52 +01:00
|
|
|
|
2018-09-16 16:06:02 +02:00
|
|
|
for (const compiler of compilers) {
|
2019-10-04 18:11:39 +02:00
|
|
|
compiler.hooks.make.tapPromise(
|
|
|
|
'NextJsOnDemandEntries',
|
|
|
|
(compilation: webpack.compilation.Compilation) => {
|
|
|
|
invalidator.startBuilding()
|
2018-01-30 16:40:52 +01:00
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
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) {
|
2020-01-03 11:43:36 +01:00
|
|
|
// page was removed
|
2019-10-04 18:11:39 +02:00
|
|
|
delete entries[page]
|
|
|
|
return
|
|
|
|
}
|
2019-01-08 23:10:32 +01:00
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
entries[page].status = BUILDING
|
|
|
|
return addEntry(compilation, compiler.context, name, [
|
|
|
|
compiler.name === 'client'
|
|
|
|
? `next-client-pages-loader?${stringify({
|
|
|
|
page,
|
|
|
|
absolutePagePath,
|
|
|
|
})}!`
|
|
|
|
: absolutePagePath,
|
|
|
|
])
|
|
|
|
})
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
return Promise.all(allEntries).catch(err => console.error(err))
|
|
|
|
}
|
|
|
|
)
|
2018-09-16 16:06:02 +02:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
function findHardFailedPages(errors: any[]) {
|
2019-05-29 13:57:26 +02:00
|
|
|
return 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_REGEX.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
|
|
|
|
})
|
2018-09-16 16:06:02 +02:00
|
|
|
.map(e => e.module.chunks)
|
|
|
|
.reduce((a, b) => [...a, ...b], [])
|
2019-10-04 18:11:39 +02:00
|
|
|
.map((c: any) => {
|
|
|
|
const pageName = ROUTE_NAME_REGEX.exec(c.name)![1]
|
2018-09-16 16:06:02 +02:00
|
|
|
return normalizePage(`/${pageName}`)
|
|
|
|
})
|
2019-05-28 15:48:13 +02:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
function getPagePathsFromEntrypoints(entrypoints: any) {
|
2019-05-28 15:48:13 +02:00
|
|
|
const pagePaths = []
|
|
|
|
for (const [, entrypoint] of entrypoints.entries()) {
|
2018-09-16 16:06:02 +02:00
|
|
|
const result = ROUTE_NAME_REGEX.exec(entrypoint.name)
|
|
|
|
if (!result) {
|
|
|
|
continue
|
|
|
|
}
|
2017-06-07 00:32:02 +02:00
|
|
|
|
2018-09-16 16:06:02 +02:00
|
|
|
const pagePath = result[1]
|
2017-06-07 00:32:02 +02:00
|
|
|
|
2018-09-16 16:06:02 +02:00
|
|
|
if (!pagePath) {
|
|
|
|
continue
|
|
|
|
}
|
2018-01-30 16:40:52 +01:00
|
|
|
|
2019-05-28 15:48:13 +02:00
|
|
|
pagePaths.push(pagePath)
|
|
|
|
}
|
|
|
|
|
|
|
|
return pagePaths
|
|
|
|
}
|
|
|
|
|
2019-05-29 13:57:26 +02:00
|
|
|
multiCompiler.hooks.done.tap('NextJsOnDemandEntries', multiStats => {
|
2019-05-28 15:48:13 +02:00
|
|
|
const [clientStats, serverStats] = multiStats.stats
|
2019-05-29 13:57:26 +02:00
|
|
|
const hardFailedPages = [
|
|
|
|
...new Set([
|
|
|
|
...findHardFailedPages(clientStats.compilation.errors),
|
2019-10-04 18:11:39 +02:00
|
|
|
...findHardFailedPages(serverStats.compilation.errors),
|
|
|
|
]),
|
2019-05-29 13:57:26 +02:00
|
|
|
]
|
|
|
|
const pagePaths = new Set([
|
|
|
|
...getPagePathsFromEntrypoints(clientStats.compilation.entrypoints),
|
2019-10-04 18:11:39 +02:00
|
|
|
...getPagePathsFromEntrypoints(serverStats.compilation.entrypoints),
|
2019-05-29 13:57:26 +02:00
|
|
|
])
|
2019-05-28 15:48:13 +02:00
|
|
|
|
|
|
|
// compilation.entrypoints is a Map object, so iterating over it 0 is the key and 1 is the value
|
|
|
|
for (const pagePath of pagePaths) {
|
2018-09-16 16:06:02 +02:00
|
|
|
const page = normalizePage('/' + pagePath)
|
2018-01-30 16:40:52 +01:00
|
|
|
|
2018-09-16 16:06:02 +02:00
|
|
|
const entry = entries[page]
|
|
|
|
if (!entry) {
|
|
|
|
continue
|
|
|
|
}
|
2017-06-07 00:32:02 +02:00
|
|
|
|
2018-09-16 16:06:02 +02:00
|
|
|
if (entry.status !== BUILDING) {
|
|
|
|
continue
|
2017-02-26 20:45:16 +01:00
|
|
|
}
|
2018-09-16 16:06:02 +02:00
|
|
|
|
|
|
|
entry.status = BUILT
|
|
|
|
entry.lastActiveTime = Date.now()
|
2019-10-04 18:11:39 +02:00
|
|
|
doneCallbacks!.emit(page)
|
2018-09-16 16:06:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
invalidator.doneBuilding()
|
|
|
|
|
|
|
|
if (hardFailedPages.length > 0 && !reloading) {
|
2019-05-29 13:57:26 +02:00
|
|
|
console.log(
|
|
|
|
`> Reloading webpack due to inconsistant state of pages(s): ${hardFailedPages.join(
|
|
|
|
', '
|
|
|
|
)}`
|
|
|
|
)
|
2018-09-16 16:06:02 +02:00
|
|
|
reloading = true
|
|
|
|
reload()
|
|
|
|
.then(() => {
|
|
|
|
console.log('> Webpack reloaded.')
|
2019-10-04 18:11:39 +02:00
|
|
|
reloadCallbacks!.emit('done')
|
2018-09-16 16:06:02 +02:00
|
|
|
stop()
|
|
|
|
})
|
2019-10-04 18:11:39 +02:00
|
|
|
.catch((err: Error) => {
|
2018-09-16 16:06:02 +02:00
|
|
|
console.error(`> Webpack reloading failed: ${err.message}`)
|
|
|
|
console.error(err.stack)
|
|
|
|
process.exit(1)
|
|
|
|
})
|
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
})
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
const disposeHandler = setInterval(function() {
|
2017-06-07 00:32:02 +02:00
|
|
|
if (stopped) return
|
2019-05-29 13:57:26 +02:00
|
|
|
disposeInactiveEntries(
|
|
|
|
devMiddleware,
|
|
|
|
entries,
|
|
|
|
lastAccessPages,
|
|
|
|
maxInactiveAge
|
|
|
|
)
|
2017-02-26 20:45:16 +01:00
|
|
|
}, 5000)
|
|
|
|
|
2018-01-31 12:37:41 +01:00
|
|
|
disposeHandler.unref()
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
function stop() {
|
2017-06-07 00:32:02 +02:00
|
|
|
clearInterval(disposeHandler)
|
|
|
|
stopped = true
|
|
|
|
doneCallbacks = null
|
|
|
|
reloadCallbacks = null
|
|
|
|
}
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
function handlePing(pg: string) {
|
2019-02-15 22:22:21 +01:00
|
|
|
const page = normalizePage(pg)
|
|
|
|
const entryInfo = entries[page]
|
2019-02-19 21:58:47 +01:00
|
|
|
let toSend
|
2019-02-15 22:22:21 +01:00
|
|
|
|
2019-04-09 17:52:03 +02:00
|
|
|
// If there's no entry, it may have been invalidated and needs to be re-built.
|
2019-02-15 22:22:21 +01:00
|
|
|
if (!entryInfo) {
|
2020-01-03 11:43:36 +01:00
|
|
|
// if (page !== lastEntry) client pings, but there's no entry for page
|
2019-02-19 21:58:47 +01:00
|
|
|
return { invalid: true }
|
2019-02-15 22:22:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 404 is an on demand entry but when a new page is added we have to refresh the page
|
|
|
|
if (page === '/_error') {
|
2019-02-19 21:58:47 +01:00
|
|
|
toSend = { invalid: true }
|
2019-02-15 22:22:21 +01:00
|
|
|
} else {
|
2019-02-19 21:58:47 +01:00
|
|
|
toSend = { success: true }
|
2019-02-15 22:22:21 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// We don't need to maintain active state of anything other than BUILT entries
|
|
|
|
if (entryInfo.status !== BUILT) return
|
|
|
|
|
|
|
|
// If there's an entryInfo
|
|
|
|
if (!lastAccessPages.includes(page)) {
|
|
|
|
lastAccessPages.unshift(page)
|
|
|
|
|
|
|
|
// Maintain the buffer max length
|
|
|
|
if (lastAccessPages.length > pagesBufferLength) {
|
|
|
|
lastAccessPages.pop()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
entryInfo.lastActiveTime = Date.now()
|
2019-02-19 21:58:47 +01:00
|
|
|
return toSend
|
2019-02-15 22:22:21 +01:00
|
|
|
}
|
|
|
|
|
2017-02-26 20:45:16 +01:00
|
|
|
return {
|
2019-10-04 18:11:39 +02:00
|
|
|
waitUntilReloaded() {
|
2017-06-07 00:32:02 +02:00
|
|
|
if (!reloading) return Promise.resolve(true)
|
2019-05-29 13:57:26 +02:00
|
|
|
return new Promise(resolve => {
|
2019-10-04 18:11:39 +02:00
|
|
|
reloadCallbacks!.once('done', function() {
|
2017-06-07 00:32:02 +02:00
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
async ensurePage(page: string) {
|
2017-06-07 00:32:02 +02:00
|
|
|
await this.waitUntilReloaded()
|
2019-10-04 18:11:39 +02:00
|
|
|
let normalizedPagePath: string
|
2018-02-14 16:20:41 +01:00
|
|
|
try {
|
|
|
|
normalizedPagePath = normalizePagePath(page)
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
2019-10-04 18:11:39 +02:00
|
|
|
throw pageNotFoundError(page)
|
2018-02-14 16:20:41 +01:00
|
|
|
}
|
|
|
|
|
2019-05-29 13:57:26 +02:00
|
|
|
let pagePath = await findPageFile(
|
|
|
|
pagesDir,
|
|
|
|
normalizedPagePath,
|
|
|
|
pageExtensions
|
|
|
|
)
|
2019-02-03 15:34:28 +01:00
|
|
|
|
|
|
|
// Default the /_error route to the Next.js provided default page
|
2019-02-24 22:08:35 +01:00
|
|
|
if (page === '/_error' && pagePath === null) {
|
|
|
|
pagePath = 'next/dist/pages/_error'
|
2019-02-03 15:34:28 +01:00
|
|
|
}
|
2018-02-14 16:20:41 +01:00
|
|
|
|
2019-02-24 22:08:35 +01:00
|
|
|
if (pagePath === null) {
|
2018-02-14 16:20:41 +01:00
|
|
|
throw pageNotFoundError(normalizedPagePath)
|
|
|
|
}
|
|
|
|
|
2020-02-05 22:10:39 +01:00
|
|
|
let pageUrl = pagePath.replace(/\\/g, '/')
|
|
|
|
|
|
|
|
pageUrl = `${pageUrl[0] !== '/' ? '/' : ''}${pageUrl
|
2019-05-29 13:57:26 +02:00
|
|
|
.replace(new RegExp(`\\.+(?:${pageExtensions.join('|')})$`), '')
|
2020-02-05 22:10:39 +01:00
|
|
|
.replace(/\/index$/, '')}`
|
|
|
|
|
2019-01-08 23:10:32 +01:00
|
|
|
pageUrl = pageUrl === '' ? '/' : pageUrl
|
2020-02-05 22:10:39 +01:00
|
|
|
|
|
|
|
const bundleFile = `${normalizePagePath(pageUrl)}.js`
|
2019-01-08 23:10:32 +01:00
|
|
|
const name = join('static', buildId, 'pages', bundleFile)
|
2019-05-29 13:57:26 +02:00
|
|
|
const absolutePagePath = pagePath.startsWith('next/dist/pages')
|
|
|
|
? require.resolve(pagePath)
|
|
|
|
: join(pagesDir, pagePath)
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2019-03-19 04:24:21 +01:00
|
|
|
page = posix.normalize(pageUrl)
|
|
|
|
|
2019-04-11 20:59:26 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
2019-02-26 15:57:45 +01:00
|
|
|
// Makes sure the page that is being kept in on-demand-entries matches the webpack output
|
|
|
|
const normalizedPage = normalizePage(page)
|
|
|
|
const entryInfo = entries[normalizedPage]
|
2017-02-26 20:45:16 +01:00
|
|
|
|
|
|
|
if (entryInfo) {
|
|
|
|
if (entryInfo.status === BUILT) {
|
|
|
|
resolve()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (entryInfo.status === BUILDING) {
|
2019-10-04 18:11:39 +02:00
|
|
|
doneCallbacks!.once(normalizedPage, handleCallback)
|
2017-02-26 20:45:16 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-09 17:52:03 +02:00
|
|
|
Log.event(`build page: ${normalizedPage}`)
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2019-02-26 15:57:45 +01:00
|
|
|
entries[normalizedPage] = { name, absolutePagePath, status: ADDED }
|
2019-10-04 18:11:39 +02:00
|
|
|
doneCallbacks!.once(normalizedPage, handleCallback)
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2017-02-27 21:05:10 +01:00
|
|
|
invalidator.invalidate()
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
function handleCallback(err: Error) {
|
2017-02-26 20:45:16 +01:00
|
|
|
if (err) return reject(err)
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
middleware() {
|
|
|
|
return (req: IncomingMessage, res: ServerResponse, next: Function) => {
|
2017-06-07 00:32:02 +02:00
|
|
|
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
|
2019-10-04 18:11:39 +02:00
|
|
|
res.setHeader('Location', req.url!)
|
2017-06-07 00:32:02 +02:00
|
|
|
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.
|
2019-05-29 13:57:26 +02:00
|
|
|
this.waitUntilReloaded().then(() => {
|
|
|
|
res.statusCode = 302
|
2019-10-04 18:11:39 +02:00
|
|
|
res.setHeader('Location', req.url!)
|
2019-05-29 13:57:26 +02:00
|
|
|
res.end('302')
|
|
|
|
})
|
2017-06-07 00:32:02 +02:00
|
|
|
} else {
|
2019-10-04 18:11:39 +02:00
|
|
|
if (!/^\/_next\/webpack-hmr/.test(req.url!)) return next()
|
2017-06-07 00:32:02 +02:00
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
const { query } = parse(req.url!, true)
|
2019-02-19 21:58:47 +01:00
|
|
|
const page = query.page
|
|
|
|
if (!page) return next()
|
|
|
|
|
|
|
|
const runPing = () => {
|
2019-10-04 18:11:39 +02:00
|
|
|
const data = handlePing(query.page as string)
|
2019-02-22 10:22:23 +01:00
|
|
|
if (!data) return
|
2019-02-19 21:58:47 +01:00
|
|
|
res.write('data: ' + JSON.stringify(data) + '\n\n')
|
2019-02-15 22:22:21 +01:00
|
|
|
}
|
2019-02-19 21:58:47 +01:00
|
|
|
const pingInterval = setInterval(() => runPing(), 5000)
|
|
|
|
|
|
|
|
req.on('close', () => {
|
|
|
|
clearInterval(pingInterval)
|
|
|
|
})
|
2019-04-21 22:51:09 +02:00
|
|
|
next()
|
2017-06-07 00:32:02 +02:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
}
|
2019-10-04 18:11:39 +02:00
|
|
|
},
|
2017-02-26 20:45:16 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
function disposeInactiveEntries(
|
|
|
|
devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware,
|
|
|
|
entries: any,
|
|
|
|
lastAccessPages: any,
|
|
|
|
maxInactiveAge: number
|
2019-05-29 13:57:26 +02:00
|
|
|
) {
|
2019-10-04 18:11:39 +02:00
|
|
|
const disposingPages: any = []
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2019-05-29 13:57:26 +02:00
|
|
|
Object.keys(entries).forEach(page => {
|
2017-02-26 20:45:16 +01:00
|
|
|
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
|
2017-09-28 14:51:03 +02:00
|
|
|
if (lastAccessPages.includes(page)) return
|
2017-02-26 20:45:16 +01:00
|
|
|
|
|
|
|
if (Date.now() - lastActiveTime > maxInactiveAge) {
|
|
|
|
disposingPages.push(page)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if (disposingPages.length > 0) {
|
2019-10-04 18:11:39 +02:00
|
|
|
disposingPages.forEach((page: any) => {
|
2017-02-26 20:45:16 +01:00
|
|
|
delete entries[page]
|
|
|
|
})
|
2020-01-03 11:43:36 +01:00
|
|
|
// disposing inactive page(s)
|
2017-02-26 20:45:16 +01:00
|
|
|
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.
|
2019-10-04 18:11:39 +02:00
|
|
|
export function normalizePage(page: string) {
|
2018-09-16 16:06:02 +02:00
|
|
|
const unixPagePath = page.replace(/\\/g, '/')
|
|
|
|
if (unixPagePath === '/index' || unixPagePath === '/') {
|
2018-07-24 11:24:40 +02:00
|
|
|
return '/'
|
|
|
|
}
|
2018-09-16 16:06:02 +02:00
|
|
|
return unixPagePath.replace(/\/index$/, '')
|
2017-02-26 20:45:16 +01:00
|
|
|
}
|
|
|
|
|
2017-02-27 21:05:10 +01:00
|
|
|
// Make sure only one invalidation happens at a time
|
|
|
|
// Otherwise, webpack hash gets changed and it'll force the client to reload.
|
|
|
|
class Invalidator {
|
2019-10-04 18:11:39 +02:00
|
|
|
private multiCompiler: webpack.MultiCompiler
|
|
|
|
private devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware
|
|
|
|
private building: boolean
|
|
|
|
private rebuildAgain: boolean
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
devMiddleware: WebpackDevMiddleware.WebpackDevMiddleware,
|
|
|
|
multiCompiler: webpack.MultiCompiler
|
|
|
|
) {
|
2018-09-16 16:06:02 +02:00
|
|
|
this.multiCompiler = multiCompiler
|
2017-02-27 21:05:10 +01:00
|
|
|
this.devMiddleware = devMiddleware
|
2018-01-30 16:40:52 +01:00
|
|
|
// contains an array of types of compilers currently building
|
2017-02-27 21:05:10 +01:00
|
|
|
this.building = false
|
|
|
|
this.rebuildAgain = false
|
|
|
|
}
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
invalidate() {
|
2017-02-27 21:05:10 +01:00
|
|
|
// 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
|
2018-09-16 16:06:02 +02:00
|
|
|
// Work around a bug in webpack, calling `invalidate` on Watching.js
|
|
|
|
// doesn't trigger the invalid call used to keep track of the `.done` hook on multiCompiler
|
|
|
|
for (const compiler of this.multiCompiler.compilers) {
|
|
|
|
compiler.hooks.invalid.call()
|
|
|
|
}
|
2017-02-27 21:05:10 +01:00
|
|
|
this.devMiddleware.invalidate()
|
|
|
|
}
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
startBuilding() {
|
2017-02-27 21:05:10 +01:00
|
|
|
this.building = true
|
|
|
|
}
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
doneBuilding() {
|
2017-02-27 21:05:10 +01:00
|
|
|
this.building = false
|
2018-01-30 16:40:52 +01:00
|
|
|
|
2017-02-27 21:05:10 +01:00
|
|
|
if (this.rebuildAgain) {
|
|
|
|
this.rebuildAgain = false
|
|
|
|
this.invalidate()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|