2017-02-26 20:45:16 +01:00
|
|
|
import { EventEmitter } from 'events'
|
2019-03-19 04:24:21 +01:00
|
|
|
import { join, posix } from 'path'
|
2021-10-24 23:04:26 +02:00
|
|
|
import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack'
|
2021-06-30 13:44:40 +02:00
|
|
|
import { normalizePagePath, normalizePathSep } from '../normalize-page-path'
|
|
|
|
import { pageNotFoundError } from '../require'
|
|
|
|
import { findPageFile } from '../lib/find-page-file'
|
|
|
|
import getRouteFromEntrypoint from '../get-route-from-entrypoint'
|
2021-10-20 19:52:11 +02:00
|
|
|
import { API_ROUTE, MIDDLEWARE_ROUTE } from '../../lib/constants'
|
2021-10-09 11:51:37 +02:00
|
|
|
import { reportTrigger } from '../../build/output'
|
2021-10-15 09:09:54 +02:00
|
|
|
import type ws from 'ws'
|
2021-10-26 18:50:56 +02:00
|
|
|
import { NextConfigComplete } from '../config-shared'
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2020-06-26 06:26:09 +02:00
|
|
|
export const ADDED = Symbol('added')
|
|
|
|
export const BUILDING = Symbol('building')
|
|
|
|
export const BUILT = Symbol('built')
|
|
|
|
|
|
|
|
export let entries: {
|
|
|
|
[page: string]: {
|
2021-09-30 15:52:26 +02:00
|
|
|
bundlePath: string
|
2020-06-26 06:26:09 +02:00
|
|
|
absolutePagePath: string
|
|
|
|
status?: typeof ADDED | typeof BUILDING | typeof BUILT
|
|
|
|
lastActiveTime?: number
|
2021-10-11 18:12:48 +02:00
|
|
|
dispose?: boolean
|
2020-06-26 06:26:09 +02:00
|
|
|
}
|
|
|
|
} = {}
|
2018-09-16 16:06:02 +02:00
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
export default function onDemandEntryHandler(
|
2020-06-26 06:26:09 +02:00
|
|
|
watcher: any,
|
2019-10-04 18:11:39 +02:00
|
|
|
multiCompiler: webpack.MultiCompiler,
|
2019-09-24 17:15:14 +02:00
|
|
|
{
|
|
|
|
pagesDir,
|
2021-10-26 18:50:56 +02:00
|
|
|
nextConfig,
|
2019-09-24 17:15:14 +02:00
|
|
|
maxInactiveAge,
|
2019-10-04 18:11:39 +02:00
|
|
|
pagesBufferLength,
|
|
|
|
}: {
|
|
|
|
pagesDir: string
|
2021-10-26 18:50:56 +02:00
|
|
|
nextConfig: NextConfigComplete
|
2019-10-04 18:11:39 +02:00
|
|
|
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
|
2020-06-26 06:26:09 +02:00
|
|
|
const invalidator = new Invalidator(watcher, multiCompiler)
|
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
let lastClientAccessPages = ['']
|
2019-10-04 18:11:39 +02:00
|
|
|
let doneCallbacks: 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) {
|
2020-06-26 06:26:09 +02:00
|
|
|
compiler.hooks.make.tap(
|
2019-10-04 18:11:39 +02:00
|
|
|
'NextJsOnDemandEntries',
|
2021-10-24 23:04:26 +02:00
|
|
|
(_compilation: webpack.Compilation) => {
|
2019-10-04 18:11:39 +02:00
|
|
|
invalidator.startBuilding()
|
|
|
|
}
|
|
|
|
)
|
2018-09-16 16:06:02 +02:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
function getPagePathsFromEntrypoints(
|
|
|
|
type: string,
|
|
|
|
entrypoints: any
|
|
|
|
): string[] {
|
2019-05-28 15:48:13 +02:00
|
|
|
const pagePaths = []
|
2020-06-04 19:32:45 +02:00
|
|
|
for (const entrypoint of entrypoints.values()) {
|
|
|
|
const page = getRouteFromEntrypoint(entrypoint.name)
|
|
|
|
if (page) {
|
2021-09-30 15:52:26 +02:00
|
|
|
pagePaths.push(`${type}${page}`)
|
2018-09-16 16:06:02 +02:00
|
|
|
}
|
2019-05-28 15:48:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return pagePaths
|
|
|
|
}
|
|
|
|
|
2020-05-18 21:24:37 +02:00
|
|
|
multiCompiler.hooks.done.tap('NextJsOnDemandEntries', (multiStats) => {
|
2021-09-09 10:14:30 +02:00
|
|
|
if (invalidator.rebuildAgain) {
|
|
|
|
return invalidator.doneBuilding()
|
|
|
|
}
|
2021-10-26 18:50:56 +02:00
|
|
|
const [clientStats, serverStats, serverWebStats] = multiStats.stats
|
2021-09-30 15:52:26 +02:00
|
|
|
const pagePaths = [
|
|
|
|
...getPagePathsFromEntrypoints(
|
|
|
|
'client',
|
|
|
|
clientStats.compilation.entrypoints
|
|
|
|
),
|
|
|
|
...getPagePathsFromEntrypoints(
|
|
|
|
'server',
|
|
|
|
serverStats.compilation.entrypoints
|
|
|
|
),
|
2021-10-26 18:50:56 +02:00
|
|
|
...(serverWebStats
|
|
|
|
? getPagePathsFromEntrypoints(
|
|
|
|
'server-web',
|
|
|
|
serverWebStats.compilation.entrypoints
|
|
|
|
)
|
|
|
|
: []),
|
2021-09-30 15:52:26 +02:00
|
|
|
]
|
2019-05-28 15:48:13 +02:00
|
|
|
|
2020-06-04 19:32:45 +02:00
|
|
|
for (const page of pagePaths) {
|
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
|
2019-10-04 18:11:39 +02:00
|
|
|
doneCallbacks!.emit(page)
|
2018-09-16 16:06:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
invalidator.doneBuilding()
|
2017-02-26 20:45:16 +01:00
|
|
|
})
|
|
|
|
|
2021-10-11 19:49:32 +02:00
|
|
|
const pingIntervalTime = Math.max(1000, Math.min(5000, maxInactiveAge))
|
|
|
|
|
2020-05-18 21:24:37 +02:00
|
|
|
const disposeHandler = setInterval(function () {
|
2021-09-30 15:52:26 +02:00
|
|
|
disposeInactiveEntries(watcher, lastClientAccessPages, maxInactiveAge)
|
2021-10-11 19:49:32 +02:00
|
|
|
}, pingIntervalTime + 1000)
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2018-01-31 12:37:41 +01:00
|
|
|
disposeHandler.unref()
|
|
|
|
|
2019-10-04 18:11:39 +02:00
|
|
|
function handlePing(pg: string) {
|
2020-06-04 19:32:45 +02:00
|
|
|
const page = normalizePathSep(pg)
|
2021-09-30 15:52:26 +02:00
|
|
|
const pageKey = `client${page}`
|
|
|
|
const entryInfo = entries[pageKey]
|
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
|
2021-09-30 15:52:26 +02:00
|
|
|
if (!lastClientAccessPages.includes(pageKey)) {
|
|
|
|
lastClientAccessPages.unshift(pageKey)
|
2019-02-15 22:22:21 +01:00
|
|
|
|
|
|
|
// Maintain the buffer max length
|
2021-09-30 15:52:26 +02:00
|
|
|
if (lastClientAccessPages.length > pagesBufferLength) {
|
|
|
|
lastClientAccessPages.pop()
|
2019-02-15 22:22:21 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
entryInfo.lastActiveTime = Date.now()
|
2021-10-11 18:12:48 +02:00
|
|
|
entryInfo.dispose = false
|
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 {
|
2021-09-30 15:52:26 +02:00
|
|
|
async ensurePage(page: string, clientOnly: boolean) {
|
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,
|
2021-10-26 18:50:56 +02:00
|
|
|
nextConfig.pageExtensions
|
2019-05-29 13:57:26 +02:00
|
|
|
)
|
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)
|
|
|
|
}
|
|
|
|
|
2021-10-21 12:52:50 +02:00
|
|
|
let bundlePath: string
|
|
|
|
let absolutePagePath: string
|
|
|
|
if (pagePath.startsWith('next/dist/pages/')) {
|
|
|
|
bundlePath = page
|
|
|
|
absolutePagePath = require.resolve(pagePath)
|
|
|
|
} else {
|
|
|
|
let pageUrl = pagePath.replace(/\\/g, '/')
|
|
|
|
|
|
|
|
pageUrl = `${pageUrl[0] !== '/' ? '/' : ''}${pageUrl
|
2021-10-26 18:50:56 +02:00
|
|
|
.replace(
|
|
|
|
new RegExp(`\\.+(?:${nextConfig.pageExtensions.join('|')})$`),
|
|
|
|
''
|
|
|
|
)
|
2021-10-21 12:52:50 +02:00
|
|
|
.replace(/\/index$/, '')}`
|
|
|
|
|
|
|
|
pageUrl = pageUrl === '' ? '/' : pageUrl
|
|
|
|
const bundleFile = normalizePagePath(pageUrl)
|
|
|
|
bundlePath = posix.join('pages', bundleFile)
|
|
|
|
absolutePagePath = join(pagesDir, pagePath)
|
|
|
|
page = posix.normalize(pageUrl)
|
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
const normalizedPage = normalizePathSep(page)
|
2019-03-19 04:24:21 +01:00
|
|
|
|
2021-10-20 19:52:11 +02:00
|
|
|
const isMiddleware = normalizedPage.match(MIDDLEWARE_ROUTE)
|
|
|
|
const isApiRoute = normalizedPage.match(API_ROUTE) && !isMiddleware
|
2021-10-26 18:50:56 +02:00
|
|
|
const isServerWeb = !!nextConfig.experimental.concurrentFeatures
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
let entriesChanged = false
|
2021-10-26 18:50:56 +02:00
|
|
|
const addPageEntry = (type: 'client' | 'server' | 'server-web') => {
|
2021-09-30 15:52:26 +02:00
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
|
|
// Makes sure the page that is being kept in on-demand-entries matches the webpack output
|
2021-10-21 12:52:50 +02:00
|
|
|
const pageKey = `${type}${page}`
|
2021-09-30 15:52:26 +02:00
|
|
|
const entryInfo = entries[pageKey]
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
if (entryInfo) {
|
|
|
|
entryInfo.lastActiveTime = Date.now()
|
2021-10-11 18:12:48 +02:00
|
|
|
entryInfo.dispose = false
|
2021-09-30 15:52:26 +02:00
|
|
|
if (entryInfo.status === BUILT) {
|
|
|
|
resolve()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
doneCallbacks!.once(pageKey, handleCallback)
|
2017-02-26 20:45:16 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
entriesChanged = true
|
|
|
|
|
|
|
|
entries[pageKey] = {
|
|
|
|
bundlePath,
|
|
|
|
absolutePagePath,
|
|
|
|
status: ADDED,
|
|
|
|
lastActiveTime: Date.now(),
|
2021-10-11 18:12:48 +02:00
|
|
|
dispose: false,
|
2021-09-30 15:52:26 +02:00
|
|
|
}
|
|
|
|
doneCallbacks!.once(pageKey, handleCallback)
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
function handleCallback(err: Error) {
|
|
|
|
if (err) return reject(err)
|
|
|
|
resolve()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
const promise = isApiRoute
|
|
|
|
? addPageEntry('server')
|
2021-10-20 19:52:11 +02:00
|
|
|
: clientOnly || isMiddleware
|
2021-09-30 15:52:26 +02:00
|
|
|
? addPageEntry('client')
|
2021-10-26 18:50:56 +02:00
|
|
|
: Promise.all([
|
|
|
|
addPageEntry('client'),
|
|
|
|
addPageEntry(isServerWeb ? 'server-web' : 'server'),
|
|
|
|
])
|
2021-09-30 15:52:26 +02:00
|
|
|
|
|
|
|
if (entriesChanged) {
|
2021-10-09 11:51:37 +02:00
|
|
|
reportTrigger(
|
2021-10-25 00:09:47 +02:00
|
|
|
isApiRoute
|
2021-10-09 11:51:37 +02:00
|
|
|
? `${normalizedPage} (server only)`
|
2021-10-25 00:09:47 +02:00
|
|
|
: clientOnly || isMiddleware
|
2021-10-09 11:51:37 +02:00
|
|
|
? `${normalizedPage} (client only)`
|
|
|
|
: normalizedPage
|
2021-09-30 15:52:26 +02:00
|
|
|
)
|
2017-02-27 21:05:10 +01:00
|
|
|
invalidator.invalidate()
|
2021-09-30 15:52:26 +02:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2021-09-30 15:52:26 +02:00
|
|
|
return promise
|
2017-02-26 20:45:16 +01:00
|
|
|
},
|
|
|
|
|
2021-10-15 09:09:54 +02:00
|
|
|
onHMR(client: ws) {
|
|
|
|
client.addEventListener('message', ({ data }) => {
|
|
|
|
data = typeof data !== 'string' ? data.toString() : data
|
|
|
|
try {
|
|
|
|
const parsedData = JSON.parse(data)
|
|
|
|
|
|
|
|
if (parsedData.event === 'ping') {
|
|
|
|
const result = handlePing(parsedData.page)
|
|
|
|
client.send(
|
|
|
|
JSON.stringify({
|
|
|
|
...result,
|
|
|
|
event: 'pong',
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} catch (_) {}
|
2020-07-01 17:34:00 +02: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(
|
2021-09-30 15:52:26 +02:00
|
|
|
_watcher: any,
|
|
|
|
lastClientAccessPages: any,
|
2019-10-04 18:11:39 +02:00
|
|
|
maxInactiveAge: number
|
2019-05-29 13:57:26 +02:00
|
|
|
) {
|
2020-05-18 21:24:37 +02:00
|
|
|
Object.keys(entries).forEach((page) => {
|
2021-10-11 18:12:48 +02:00
|
|
|
const { lastActiveTime, status, dispose } = entries[page]
|
|
|
|
|
|
|
|
// Skip pages already scheduled for disposing
|
|
|
|
if (dispose) return
|
2017-02-26 20:45:16 +01:00
|
|
|
|
|
|
|
// 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
|
2021-09-30 15:52:26 +02:00
|
|
|
if (lastClientAccessPages.includes(page)) return
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2020-06-26 06:26:09 +02:00
|
|
|
if (lastActiveTime && Date.now() - lastActiveTime > maxInactiveAge) {
|
2021-10-11 18:12:48 +02:00
|
|
|
entries[page].dispose = true
|
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
|
2020-06-26 06:26:09 +02:00
|
|
|
private watcher: any
|
2019-10-04 18:11:39 +02:00
|
|
|
private building: boolean
|
2021-09-09 10:14:30 +02:00
|
|
|
public rebuildAgain: boolean
|
2019-10-04 18:11:39 +02:00
|
|
|
|
2020-06-26 06:26:09 +02:00
|
|
|
constructor(watcher: any, multiCompiler: webpack.MultiCompiler) {
|
2018-09-16 16:06:02 +02:00
|
|
|
this.multiCompiler = multiCompiler
|
2020-06-26 06:26:09 +02:00
|
|
|
this.watcher = watcher
|
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
|
2020-06-26 06:26:09 +02:00
|
|
|
this.watcher.invalidate()
|
2017-02-27 21:05:10 +01:00
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|