2022-04-27 11:50:29 +02:00
|
|
|
import type ws from 'ws'
|
2022-08-22 18:32:30 +02:00
|
|
|
import origDebug from 'next/dist/compiled/debug'
|
2022-08-16 11:55:37 +02:00
|
|
|
import type { webpack } from 'next/dist/compiled/webpack/webpack'
|
2022-04-27 11:50:29 +02:00
|
|
|
import type { NextConfigComplete } from '../config-shared'
|
2017-02-26 20:45:16 +01:00
|
|
|
import { EventEmitter } from 'events'
|
2022-04-27 11:50:29 +02:00
|
|
|
import { findPageFile } from '../lib/find-page-file'
|
2022-05-20 14:24:00 +02:00
|
|
|
import { runDependingOnPageType } from '../../build/entries'
|
2019-03-19 04:24:21 +01:00
|
|
|
import { join, posix } from 'path'
|
2022-04-30 13:19:27 +02:00
|
|
|
import { normalizePathSep } from '../../shared/lib/page-path/normalize-path-sep'
|
|
|
|
import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path'
|
|
|
|
import { ensureLeadingSlash } from '../../shared/lib/page-path/ensure-leading-slash'
|
|
|
|
import { removePagePathTail } from '../../shared/lib/page-path/remove-page-path-tail'
|
2021-10-09 11:51:37 +02:00
|
|
|
import { reportTrigger } from '../../build/output'
|
2022-04-27 11:50:29 +02:00
|
|
|
import getRouteFromEntrypoint from '../get-route-from-entrypoint'
|
2022-05-13 19:48:53 +02:00
|
|
|
import { serverComponentRegex } from '../../build/webpack/loaders/utils'
|
2022-05-20 14:24:00 +02:00
|
|
|
import { getPageStaticInfo } from '../../build/analysis/get-page-static-info'
|
2022-06-08 16:10:05 +02:00
|
|
|
import { isMiddlewareFile, isMiddlewareFilename } from '../../build/utils'
|
2022-06-06 20:35:26 +02:00
|
|
|
import { PageNotFoundError } from '../../shared/lib/utils'
|
2022-07-11 14:34:10 +02:00
|
|
|
import { DynamicParamTypesShort, FlightRouterState } from '../app-render'
|
2022-08-12 15:01:19 +02:00
|
|
|
import {
|
|
|
|
CompilerNameValues,
|
|
|
|
COMPILER_INDEXES,
|
|
|
|
COMPILER_NAMES,
|
|
|
|
} from '../../shared/lib/constants'
|
|
|
|
|
2022-08-22 18:32:30 +02:00
|
|
|
const debug = origDebug('next:on-demand-entry-handler')
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
/**
|
|
|
|
* Returns object keys with type inferred from the object key
|
|
|
|
*/
|
|
|
|
const keys = Object.keys as <T>(o: T) => Extract<keyof T, string>[]
|
|
|
|
|
|
|
|
const COMPILER_KEYS = keys(COMPILER_INDEXES)
|
2022-07-10 19:18:48 +02:00
|
|
|
|
|
|
|
function treePathToEntrypoint(
|
|
|
|
segmentPath: string[],
|
|
|
|
parentPath?: string
|
|
|
|
): string {
|
|
|
|
const [parallelRouteKey, segment] = segmentPath
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
// TODO-APP: modify this path to cover parallelRouteKey convention
|
2022-07-10 19:18:48 +02:00
|
|
|
const path =
|
|
|
|
(parentPath ? parentPath + '/' : '') +
|
2022-09-06 19:03:21 +02:00
|
|
|
(parallelRouteKey !== 'children' && !segment.startsWith('@')
|
|
|
|
? parallelRouteKey + '/'
|
|
|
|
: '') +
|
2022-07-10 19:18:48 +02:00
|
|
|
(segment === '' ? 'page' : segment)
|
|
|
|
|
|
|
|
// Last segment
|
|
|
|
if (segmentPath.length === 2) {
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
|
|
|
|
const childSegmentPath = segmentPath.slice(2)
|
|
|
|
return treePathToEntrypoint(childSegmentPath, path)
|
|
|
|
}
|
|
|
|
|
2022-07-11 14:34:10 +02:00
|
|
|
function convertDynamicParamTypeToSyntax(
|
|
|
|
dynamicParamTypeShort: DynamicParamTypesShort,
|
|
|
|
param: string
|
|
|
|
) {
|
|
|
|
switch (dynamicParamTypeShort) {
|
|
|
|
case 'c':
|
|
|
|
return `[...${param}]`
|
|
|
|
case 'oc':
|
|
|
|
return `[[...${param}]]`
|
|
|
|
case 'd':
|
|
|
|
return `[${param}]`
|
|
|
|
default:
|
|
|
|
throw new Error('Unknown dynamic param type')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-07-10 19:18:48 +02:00
|
|
|
function getEntrypointsFromTree(
|
|
|
|
tree: FlightRouterState,
|
|
|
|
isFirst: boolean,
|
|
|
|
parentPath: string[] = []
|
|
|
|
) {
|
|
|
|
const [segment, parallelRoutes] = tree
|
|
|
|
|
2022-07-11 14:34:10 +02:00
|
|
|
const currentSegment = Array.isArray(segment)
|
|
|
|
? convertDynamicParamTypeToSyntax(segment[2], segment[0])
|
|
|
|
: segment
|
2022-07-10 19:18:48 +02:00
|
|
|
|
|
|
|
const currentPath = [...parentPath, currentSegment]
|
|
|
|
|
|
|
|
if (!isFirst && currentSegment === '') {
|
|
|
|
// TODO get rid of '' at the start of tree
|
|
|
|
return [treePathToEntrypoint(currentPath.slice(1))]
|
|
|
|
}
|
|
|
|
|
|
|
|
return Object.keys(parallelRoutes).reduce(
|
|
|
|
(paths: string[], key: string): string[] => {
|
|
|
|
const childTree = parallelRoutes[key]
|
|
|
|
const childPages = getEntrypointsFromTree(childTree, false, [
|
|
|
|
...currentPath,
|
|
|
|
key,
|
|
|
|
])
|
|
|
|
return [...paths, ...childPages]
|
|
|
|
},
|
|
|
|
[]
|
|
|
|
)
|
|
|
|
}
|
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')
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
interface EntryType {
|
|
|
|
/**
|
|
|
|
* Tells if a page is scheduled to be disposed.
|
|
|
|
*/
|
|
|
|
dispose?: boolean
|
|
|
|
/**
|
|
|
|
* Timestamp with the last time the page was active.
|
|
|
|
*/
|
|
|
|
lastActiveTime?: number
|
|
|
|
/**
|
|
|
|
* Page build status.
|
|
|
|
*/
|
|
|
|
status?: typeof ADDED | typeof BUILDING | typeof BUILT
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Path to the page file relative to the dist folder with no extension.
|
|
|
|
* For example: `pages/about/index`
|
|
|
|
*/
|
|
|
|
bundlePath: string
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Webpack request to create a dependency for.
|
|
|
|
*/
|
|
|
|
request: string
|
|
|
|
}
|
|
|
|
|
|
|
|
// Shadowing check in ESLint does not account for enum
|
|
|
|
// eslint-disable-next-line no-shadow
|
|
|
|
export const enum EntryTypes {
|
|
|
|
ENTRY,
|
|
|
|
CHILD_ENTRY,
|
|
|
|
}
|
|
|
|
interface Entry extends EntryType {
|
|
|
|
type: EntryTypes.ENTRY
|
|
|
|
/**
|
|
|
|
* The absolute page to the page file. Used for detecting if the file was removed. For example:
|
|
|
|
* `/Users/Rick/project/pages/about/index.js`
|
|
|
|
*/
|
|
|
|
absolutePagePath: string
|
2022-09-06 19:03:21 +02:00
|
|
|
/**
|
|
|
|
* All parallel pages that match the same entry, for example:
|
|
|
|
* ['/parallel/@bar/nested/@a/page', '/parallel/@bar/nested/@b/page', '/parallel/@foo/nested/@a/page', '/parallel/@foo/nested/@b/page']
|
|
|
|
*/
|
|
|
|
appPaths: string[] | null
|
2022-08-12 15:01:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
interface ChildEntry extends EntryType {
|
|
|
|
type: EntryTypes.CHILD_ENTRY
|
|
|
|
/**
|
|
|
|
* Which parent entries use this childEntry.
|
|
|
|
*/
|
|
|
|
parentEntries: Set<string>
|
|
|
|
}
|
|
|
|
|
2021-11-06 12:27:40 +01:00
|
|
|
export const entries: {
|
2022-04-30 13:19:27 +02:00
|
|
|
/**
|
|
|
|
* The key composed of the compiler name and the page. For example:
|
|
|
|
* `edge-server/about`
|
|
|
|
*/
|
2022-08-12 15:01:19 +02:00
|
|
|
[entryName: string]: Entry | ChildEntry
|
2020-06-26 06:26:09 +02:00
|
|
|
} = {}
|
2018-09-16 16:06:02 +02:00
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
let invalidator: Invalidator
|
|
|
|
export const getInvalidator = () => invalidator
|
|
|
|
|
|
|
|
const doneCallbacks: EventEmitter | null = new EventEmitter()
|
|
|
|
const lastClientAccessPages = ['']
|
|
|
|
const lastServerAccessPagesForAppDir = ['']
|
|
|
|
|
|
|
|
type BuildingTracker = Set<CompilerNameValues>
|
|
|
|
type RebuildTracker = Set<CompilerNameValues>
|
|
|
|
|
|
|
|
// Make sure only one invalidation happens at a time
|
2022-08-11 23:32:52 +02:00
|
|
|
// Otherwise, webpack hash gets changed and it'll force the client to reload.
|
|
|
|
class Invalidator {
|
|
|
|
private multiCompiler: webpack.MultiCompiler
|
2022-08-12 15:01:19 +02:00
|
|
|
|
|
|
|
private building: BuildingTracker = new Set()
|
|
|
|
private rebuildAgain: RebuildTracker = new Set()
|
2022-08-11 23:32:52 +02:00
|
|
|
|
|
|
|
constructor(multiCompiler: webpack.MultiCompiler) {
|
|
|
|
this.multiCompiler = multiCompiler
|
|
|
|
}
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
public shouldRebuildAll() {
|
|
|
|
return this.rebuildAgain.size > 0
|
|
|
|
}
|
2022-08-11 23:32:52 +02:00
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
invalidate(compilerKeys: typeof COMPILER_KEYS = COMPILER_KEYS): void {
|
|
|
|
for (const key of compilerKeys) {
|
|
|
|
// 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.
|
2022-08-11 23:32:52 +02:00
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
if (this.building.has(key)) {
|
|
|
|
this.rebuildAgain.add(key)
|
|
|
|
continue
|
2022-08-11 23:32:52 +02:00
|
|
|
}
|
2022-08-12 15:01:19 +02:00
|
|
|
|
|
|
|
this.multiCompiler.compilers[COMPILER_INDEXES[key]].watching?.invalidate()
|
|
|
|
this.building.add(key)
|
2022-08-11 23:32:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
public startBuilding(compilerKey: keyof typeof COMPILER_INDEXES) {
|
|
|
|
this.building.add(compilerKey)
|
2022-08-11 23:32:52 +02:00
|
|
|
}
|
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
public doneBuilding() {
|
|
|
|
const rebuild: typeof COMPILER_KEYS = []
|
|
|
|
for (const key of COMPILER_KEYS) {
|
|
|
|
this.building.delete(key)
|
2022-08-11 23:32:52 +02:00
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
if (this.rebuildAgain.has(key)) {
|
|
|
|
rebuild.push(key)
|
|
|
|
this.rebuildAgain.delete(key)
|
|
|
|
}
|
2022-08-11 23:32:52 +02:00
|
|
|
}
|
2022-08-12 15:01:19 +02:00
|
|
|
this.invalidate(rebuild)
|
2022-08-11 23:32:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
function disposeInactiveEntries(maxInactiveAge: number) {
|
|
|
|
Object.keys(entries).forEach((entryKey) => {
|
|
|
|
const entryData = entries[entryKey]
|
|
|
|
const { lastActiveTime, status, dispose } = entryData
|
|
|
|
|
|
|
|
// TODO-APP: implement disposing of CHILD_ENTRY
|
|
|
|
if (entryData.type === EntryTypes.CHILD_ENTRY) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dispose)
|
|
|
|
// Skip pages already scheduled for disposing
|
|
|
|
return
|
|
|
|
|
|
|
|
// 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 (
|
|
|
|
lastClientAccessPages.includes(entryKey) ||
|
|
|
|
lastServerAccessPagesForAppDir.includes(entryKey)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
if (lastActiveTime && Date.now() - lastActiveTime > maxInactiveAge) {
|
|
|
|
entries[entryKey].dispose = true
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-09-03 02:13:47 +02:00
|
|
|
// Normalize both app paths and page paths
|
2022-08-15 16:29:51 +02:00
|
|
|
function tryToNormalizePagePath(page: string) {
|
|
|
|
try {
|
|
|
|
return normalizePagePath(page)
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err)
|
|
|
|
throw new PageNotFoundError(page)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attempts to find a page file path from the given pages absolute directory,
|
|
|
|
* a page and allowed extensions. If the page can't be found it will throw an
|
|
|
|
* error. It defaults the `/_error` page to Next.js internal error page.
|
|
|
|
*
|
|
|
|
* @param rootDir Absolute path to the project root.
|
|
|
|
* @param pagesDir Absolute path to the pages folder with trailing `/pages`.
|
|
|
|
* @param normalizedPagePath The page normalized (it will be denormalized).
|
|
|
|
* @param pageExtensions Array of page extensions.
|
|
|
|
*/
|
|
|
|
async function findPagePathData(
|
|
|
|
rootDir: string,
|
|
|
|
page: string,
|
|
|
|
extensions: string[],
|
2022-09-03 02:13:47 +02:00
|
|
|
pagesDir?: string,
|
2022-08-15 16:29:51 +02:00
|
|
|
appDir?: string
|
|
|
|
) {
|
|
|
|
const normalizedPagePath = tryToNormalizePagePath(page)
|
|
|
|
let pagePath: string | null = null
|
|
|
|
|
|
|
|
if (isMiddlewareFile(normalizedPagePath)) {
|
2022-09-03 02:13:47 +02:00
|
|
|
pagePath = await findPageFile(
|
|
|
|
rootDir,
|
|
|
|
normalizedPagePath,
|
|
|
|
extensions,
|
|
|
|
false
|
|
|
|
)
|
2022-08-15 16:29:51 +02:00
|
|
|
|
|
|
|
if (!pagePath) {
|
|
|
|
throw new PageNotFoundError(normalizedPagePath)
|
|
|
|
}
|
|
|
|
|
|
|
|
const pageUrl = ensureLeadingSlash(
|
|
|
|
removePagePathTail(normalizePathSep(pagePath), {
|
|
|
|
extensions,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
return {
|
|
|
|
absolutePagePath: join(rootDir, pagePath),
|
|
|
|
bundlePath: normalizedPagePath.slice(1),
|
|
|
|
page: posix.normalize(pageUrl),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check appDir first falling back to pagesDir
|
|
|
|
if (appDir) {
|
2022-09-03 02:13:47 +02:00
|
|
|
pagePath = await findPageFile(appDir, normalizedPagePath, extensions, true)
|
2022-08-15 16:29:51 +02:00
|
|
|
if (pagePath) {
|
|
|
|
const pageUrl = ensureLeadingSlash(
|
|
|
|
removePagePathTail(normalizePathSep(pagePath), {
|
|
|
|
keepIndex: true,
|
|
|
|
extensions,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
return {
|
|
|
|
absolutePagePath: join(appDir, pagePath),
|
|
|
|
bundlePath: posix.join('app', normalizePagePath(pageUrl)),
|
|
|
|
page: posix.normalize(pageUrl),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-03 02:13:47 +02:00
|
|
|
if (!pagePath && pagesDir) {
|
|
|
|
pagePath = await findPageFile(
|
|
|
|
pagesDir,
|
|
|
|
normalizedPagePath,
|
|
|
|
extensions,
|
|
|
|
false
|
|
|
|
)
|
2022-08-15 16:29:51 +02:00
|
|
|
}
|
|
|
|
|
2022-09-03 02:13:47 +02:00
|
|
|
if (pagePath !== null && pagesDir) {
|
2022-08-15 16:29:51 +02:00
|
|
|
const pageUrl = ensureLeadingSlash(
|
|
|
|
removePagePathTail(normalizePathSep(pagePath), {
|
|
|
|
extensions,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
|
|
|
|
return {
|
|
|
|
absolutePagePath: join(pagesDir, pagePath),
|
|
|
|
bundlePath: posix.join('pages', normalizePagePath(pageUrl)),
|
|
|
|
page: posix.normalize(pageUrl),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (page === '/_error') {
|
|
|
|
return {
|
|
|
|
absolutePagePath: require.resolve('next/dist/pages/_error'),
|
|
|
|
bundlePath: page,
|
|
|
|
page: normalizePathSep(page),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new PageNotFoundError(normalizedPagePath)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
export function onDemandEntryHandler({
|
|
|
|
maxInactiveAge,
|
|
|
|
multiCompiler,
|
|
|
|
nextConfig,
|
|
|
|
pagesBufferLength,
|
|
|
|
pagesDir,
|
2022-05-19 17:46:21 +02:00
|
|
|
rootDir,
|
2022-05-25 11:46:26 +02:00
|
|
|
appDir,
|
2022-04-30 13:19:27 +02:00
|
|
|
}: {
|
|
|
|
maxInactiveAge: number
|
|
|
|
multiCompiler: webpack.MultiCompiler
|
|
|
|
nextConfig: NextConfigComplete
|
|
|
|
pagesBufferLength: number
|
2022-09-03 02:13:47 +02:00
|
|
|
pagesDir?: string
|
2022-05-19 17:46:21 +02:00
|
|
|
rootDir: string
|
2022-05-25 11:46:26 +02:00
|
|
|
appDir?: string
|
2022-04-30 13:19:27 +02:00
|
|
|
}) {
|
2022-06-24 20:50:49 +02:00
|
|
|
invalidator = new Invalidator(multiCompiler)
|
2018-01-30 16:40:52 +01:00
|
|
|
|
2022-08-12 15:01:19 +02:00
|
|
|
const startBuilding = (compilation: webpack.Compilation) => {
|
|
|
|
const compilationName = compilation.name as any as CompilerNameValues
|
|
|
|
invalidator.startBuilding(compilationName)
|
2022-05-13 19:48:53 +02:00
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
for (const compiler of multiCompiler.compilers) {
|
2022-05-13 19:48:53 +02:00
|
|
|
compiler.hooks.make.tap('NextJsOnDemandEntries', 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(
|
2022-08-12 15:01:19 +02:00
|
|
|
type: CompilerNameValues,
|
2022-05-03 12:37:23 +02:00
|
|
|
entrypoints: Map<string, { name?: string }>,
|
|
|
|
root?: boolean
|
2022-04-30 13:19:27 +02:00
|
|
|
) {
|
|
|
|
const pagePaths: string[] = []
|
2020-06-04 19:32:45 +02:00
|
|
|
for (const entrypoint of entrypoints.values()) {
|
2022-05-03 12:37:23 +02:00
|
|
|
const page = getRouteFromEntrypoint(entrypoint.name!, root)
|
2020-06-04 19:32:45 +02:00
|
|
|
if (page) {
|
2021-09-30 15:52:26 +02:00
|
|
|
pagePaths.push(`${type}${page}`)
|
2022-06-08 16:10:05 +02:00
|
|
|
} else if (
|
|
|
|
(root && entrypoint.name === 'root') ||
|
|
|
|
isMiddlewareFilename(entrypoint.name)
|
|
|
|
) {
|
2022-05-19 17:46:21 +02:00
|
|
|
pagePaths.push(`${type}/${entrypoint.name}`)
|
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) => {
|
2022-08-12 15:01:19 +02:00
|
|
|
if (invalidator.shouldRebuildAll()) {
|
2021-09-09 10:14:30 +02:00
|
|
|
return invalidator.doneBuilding()
|
|
|
|
}
|
2022-02-08 14:16:46 +01:00
|
|
|
const [clientStats, serverStats, edgeServerStats] = multiStats.stats
|
2022-05-25 11:46:26 +02:00
|
|
|
const root = !!appDir
|
2021-09-30 15:52:26 +02:00
|
|
|
const pagePaths = [
|
|
|
|
...getPagePathsFromEntrypoints(
|
2022-08-12 15:01:19 +02:00
|
|
|
COMPILER_NAMES.client,
|
2022-05-03 12:37:23 +02:00
|
|
|
clientStats.compilation.entrypoints,
|
|
|
|
root
|
2021-09-30 15:52:26 +02:00
|
|
|
),
|
|
|
|
...getPagePathsFromEntrypoints(
|
2022-08-12 15:01:19 +02:00
|
|
|
COMPILER_NAMES.server,
|
2022-05-03 12:37:23 +02:00
|
|
|
serverStats.compilation.entrypoints,
|
|
|
|
root
|
2021-09-30 15:52:26 +02:00
|
|
|
),
|
2022-02-08 14:16:46 +01:00
|
|
|
...(edgeServerStats
|
2021-10-26 18:50:56 +02:00
|
|
|
? getPagePathsFromEntrypoints(
|
2022-08-12 15:01:19 +02:00
|
|
|
COMPILER_NAMES.edgeServer,
|
2022-05-03 12:37:23 +02:00
|
|
|
edgeServerStats.compilation.entrypoints,
|
|
|
|
root
|
2021-10-26 18:50:56 +02:00
|
|
|
)
|
|
|
|
: []),
|
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))
|
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
setInterval(function () {
|
2022-08-11 05:27:48 +02:00
|
|
|
disposeInactiveEntries(maxInactiveAge)
|
2022-04-30 13:19:27 +02:00
|
|
|
}, pingIntervalTime + 1000).unref()
|
2018-01-31 12:37:41 +01:00
|
|
|
|
2022-07-10 19:18:48 +02:00
|
|
|
function handleAppDirPing(
|
|
|
|
tree: FlightRouterState
|
|
|
|
): { success: true } | { invalid: true } {
|
|
|
|
const pages = getEntrypointsFromTree(tree, true)
|
2022-08-23 09:23:43 +02:00
|
|
|
let toSend: { invalid: true } | { success: true } = { invalid: true }
|
2022-07-10 19:18:48 +02:00
|
|
|
|
|
|
|
for (const page of pages) {
|
2022-08-23 09:23:43 +02:00
|
|
|
for (const compilerType of [
|
|
|
|
COMPILER_NAMES.client,
|
|
|
|
COMPILER_NAMES.server,
|
|
|
|
COMPILER_NAMES.edgeServer,
|
|
|
|
]) {
|
|
|
|
const pageKey = `${compilerType}/${page}`
|
|
|
|
const entryInfo = entries[pageKey]
|
|
|
|
|
|
|
|
// If there's no entry, it may have been invalidated and needs to be re-built.
|
|
|
|
if (!entryInfo) {
|
|
|
|
// if (page !== lastEntry) client pings, but there's no entry for page
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// We don't need to maintain active state of anything other than BUILT entries
|
|
|
|
if (entryInfo.status !== BUILT) continue
|
|
|
|
|
|
|
|
// If there's an entryInfo
|
|
|
|
if (!lastServerAccessPagesForAppDir.includes(pageKey)) {
|
|
|
|
lastServerAccessPagesForAppDir.unshift(pageKey)
|
|
|
|
|
|
|
|
// Maintain the buffer max length
|
|
|
|
// TODO: verify that the current pageKey is not at the end of the array as multiple entrypoints can exist
|
|
|
|
if (lastServerAccessPagesForAppDir.length > pagesBufferLength) {
|
|
|
|
lastServerAccessPagesForAppDir.pop()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
entryInfo.lastActiveTime = Date.now()
|
|
|
|
entryInfo.dispose = false
|
|
|
|
toSend = { success: true }
|
|
|
|
}
|
|
|
|
}
|
2022-09-06 19:03:21 +02:00
|
|
|
|
2022-08-23 09:23:43 +02:00
|
|
|
return toSend
|
|
|
|
}
|
|
|
|
|
|
|
|
function handlePing(pg: string) {
|
|
|
|
const page = normalizePathSep(pg)
|
|
|
|
let toSend: { invalid: true } | { success: true } = { invalid: true }
|
|
|
|
|
|
|
|
for (const compilerType of [
|
|
|
|
COMPILER_NAMES.client,
|
|
|
|
COMPILER_NAMES.server,
|
|
|
|
COMPILER_NAMES.edgeServer,
|
|
|
|
]) {
|
|
|
|
const pageKey = `${compilerType}${page}`
|
2022-07-10 19:18:48 +02:00
|
|
|
const entryInfo = entries[pageKey]
|
|
|
|
|
|
|
|
// If there's no entry, it may have been invalidated and needs to be re-built.
|
|
|
|
if (!entryInfo) {
|
|
|
|
// if (page !== lastEntry) client pings, but there's no entry for page
|
2022-08-23 09:23:43 +02:00
|
|
|
if (compilerType === COMPILER_NAMES.client) {
|
|
|
|
return { invalid: true }
|
|
|
|
}
|
|
|
|
continue
|
2022-07-10 19:18:48 +02:00
|
|
|
}
|
|
|
|
|
2022-08-23 09:23:43 +02:00
|
|
|
// 404 is an on demand entry but when a new page is added we have to refresh the page
|
|
|
|
toSend = page === '/_error' ? { invalid: true } : { success: true }
|
|
|
|
|
2022-07-10 19:18:48 +02:00
|
|
|
// We don't need to maintain active state of anything other than BUILT entries
|
|
|
|
if (entryInfo.status !== BUILT) continue
|
|
|
|
|
|
|
|
// If there's an entryInfo
|
2022-08-23 09:23:43 +02:00
|
|
|
if (!lastClientAccessPages.includes(pageKey)) {
|
|
|
|
lastClientAccessPages.unshift(pageKey)
|
2022-07-10 19:18:48 +02:00
|
|
|
|
|
|
|
// Maintain the buffer max length
|
2022-08-23 09:23:43 +02:00
|
|
|
if (lastClientAccessPages.length > pagesBufferLength) {
|
|
|
|
lastClientAccessPages.pop()
|
2022-07-10 19:18:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
entryInfo.lastActiveTime = Date.now()
|
|
|
|
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 {
|
2022-09-06 19:03:21 +02:00
|
|
|
async ensurePage({
|
|
|
|
page,
|
|
|
|
clientOnly,
|
|
|
|
appPaths = null,
|
|
|
|
}: {
|
|
|
|
page: string
|
|
|
|
clientOnly: boolean
|
|
|
|
appPaths?: string[] | null
|
|
|
|
}): Promise<void> {
|
2022-08-16 20:05:03 +02:00
|
|
|
const stalledTime = 60
|
|
|
|
const stalledEnsureTimeout = setTimeout(() => {
|
2022-08-22 18:32:30 +02:00
|
|
|
debug(
|
2022-08-16 20:05:03 +02:00
|
|
|
`Ensuring ${page} has taken longer than ${stalledTime}s, if this continues to stall this may be a bug`
|
|
|
|
)
|
|
|
|
}, stalledTime * 1000)
|
|
|
|
|
2022-08-22 18:59:23 +02:00
|
|
|
try {
|
|
|
|
const pagePathData = await findPagePathData(
|
|
|
|
rootDir,
|
|
|
|
page,
|
|
|
|
nextConfig.pageExtensions,
|
2022-09-03 02:13:47 +02:00
|
|
|
pagesDir,
|
2022-08-22 18:59:23 +02:00
|
|
|
appDir
|
|
|
|
)
|
|
|
|
|
|
|
|
const isServerComponent = serverComponentRegex.test(
|
|
|
|
pagePathData.absolutePagePath
|
|
|
|
)
|
|
|
|
const isInsideAppDir =
|
|
|
|
appDir && pagePathData.absolutePagePath.startsWith(appDir)
|
|
|
|
|
|
|
|
const addEntry = (
|
|
|
|
compilerType: CompilerNameValues
|
|
|
|
): {
|
|
|
|
entryKey: string
|
|
|
|
newEntry: boolean
|
|
|
|
shouldInvalidate: boolean
|
|
|
|
} => {
|
|
|
|
const entryKey = `${compilerType}${pagePathData.page}`
|
|
|
|
|
|
|
|
if (entries[entryKey]) {
|
|
|
|
entries[entryKey].dispose = false
|
|
|
|
entries[entryKey].lastActiveTime = Date.now()
|
|
|
|
if (entries[entryKey].status === BUILT) {
|
|
|
|
return {
|
|
|
|
entryKey,
|
|
|
|
newEntry: false,
|
|
|
|
shouldInvalidate: false,
|
|
|
|
}
|
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2022-08-12 19:41:04 +02:00
|
|
|
return {
|
2022-08-12 23:40:41 +02:00
|
|
|
entryKey,
|
2022-08-12 19:41:04 +02:00
|
|
|
newEntry: false,
|
2022-08-22 18:59:23 +02:00
|
|
|
shouldInvalidate: true,
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
2021-09-30 15:52:26 +02:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2022-08-22 18:59:23 +02:00
|
|
|
entries[entryKey] = {
|
|
|
|
type: EntryTypes.ENTRY,
|
2022-09-06 19:03:21 +02:00
|
|
|
appPaths,
|
2022-08-22 18:59:23 +02:00
|
|
|
absolutePagePath: pagePathData.absolutePagePath,
|
|
|
|
request: pagePathData.absolutePagePath,
|
|
|
|
bundlePath: pagePathData.bundlePath,
|
|
|
|
dispose: false,
|
|
|
|
lastActiveTime: Date.now(),
|
|
|
|
status: ADDED,
|
|
|
|
}
|
|
|
|
|
2022-08-12 19:41:04 +02:00
|
|
|
return {
|
2022-08-22 18:59:23 +02:00
|
|
|
entryKey: entryKey,
|
|
|
|
newEntry: true,
|
2022-08-12 19:41:04 +02:00
|
|
|
shouldInvalidate: true,
|
2022-08-12 15:01:19 +02:00
|
|
|
}
|
2022-08-12 19:41:04 +02:00
|
|
|
}
|
|
|
|
|
2022-08-22 18:59:23 +02:00
|
|
|
const staticInfo = await getPageStaticInfo({
|
|
|
|
pageFilePath: pagePathData.absolutePagePath,
|
|
|
|
nextConfig,
|
|
|
|
})
|
2022-08-12 15:01:19 +02:00
|
|
|
|
2022-08-22 18:59:23 +02:00
|
|
|
const added = new Map<CompilerNameValues, ReturnType<typeof addEntry>>()
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2022-08-22 18:59:23 +02:00
|
|
|
await runDependingOnPageType({
|
|
|
|
page: pagePathData.page,
|
|
|
|
pageRuntime: staticInfo.runtime,
|
|
|
|
onClient: () => {
|
|
|
|
// Skip adding the client entry for app / Server Components.
|
|
|
|
if (isServerComponent || isInsideAppDir) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
added.set(COMPILER_NAMES.client, addEntry(COMPILER_NAMES.client))
|
|
|
|
},
|
|
|
|
onServer: () => {
|
|
|
|
added.set(COMPILER_NAMES.server, addEntry(COMPILER_NAMES.server))
|
|
|
|
},
|
|
|
|
onEdgeServer: () => {
|
|
|
|
added.set(
|
|
|
|
COMPILER_NAMES.edgeServer,
|
|
|
|
addEntry(COMPILER_NAMES.edgeServer)
|
|
|
|
)
|
|
|
|
},
|
|
|
|
})
|
2022-05-20 14:24:00 +02:00
|
|
|
|
2022-08-22 18:59:23 +02:00
|
|
|
const addedValues = [...added.values()]
|
|
|
|
const entriesThatShouldBeInvalidated = addedValues.filter(
|
|
|
|
(entry) => entry.shouldInvalidate
|
|
|
|
)
|
|
|
|
const hasNewEntry = addedValues.some((entry) => entry.newEntry)
|
2022-08-12 19:41:04 +02:00
|
|
|
|
2022-08-22 18:59:23 +02:00
|
|
|
if (hasNewEntry) {
|
|
|
|
reportTrigger(
|
|
|
|
!clientOnly && hasNewEntry
|
|
|
|
? `${pagePathData.page} (client and server)`
|
|
|
|
: pagePathData.page
|
2022-08-12 19:41:04 +02:00
|
|
|
)
|
2022-08-22 18:59:23 +02:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2022-08-22 18:59:23 +02:00
|
|
|
if (entriesThatShouldBeInvalidated.length > 0) {
|
|
|
|
const invalidatePromises = entriesThatShouldBeInvalidated.map(
|
|
|
|
({ entryKey }) => {
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
|
|
doneCallbacks!.once(entryKey, (err: Error) => {
|
|
|
|
if (err) {
|
|
|
|
return reject(err)
|
|
|
|
}
|
|
|
|
resolve()
|
|
|
|
})
|
2022-08-12 19:41:04 +02:00
|
|
|
})
|
2022-08-22 18:59:23 +02:00
|
|
|
}
|
|
|
|
)
|
|
|
|
invalidator.invalidate([...added.keys()])
|
|
|
|
await Promise.all(invalidatePromises)
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
clearTimeout(stalledEnsureTimeout)
|
2022-08-12 19:41:04 +02:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
},
|
|
|
|
|
2021-10-15 09:09:54 +02:00
|
|
|
onHMR(client: ws) {
|
|
|
|
client.addEventListener('message', ({ data }) => {
|
|
|
|
try {
|
2022-04-30 13:19:27 +02:00
|
|
|
const parsedData = JSON.parse(
|
|
|
|
typeof data !== 'string' ? data.toString() : data
|
|
|
|
)
|
2021-10-15 09:09:54 +02:00
|
|
|
|
|
|
|
if (parsedData.event === 'ping') {
|
2022-07-10 19:18:48 +02:00
|
|
|
const result = parsedData.appDirRoute
|
|
|
|
? handleAppDirPing(parsedData.tree)
|
|
|
|
: handlePing(parsedData.page)
|
2021-10-15 09:09:54 +02:00
|
|
|
client.send(
|
|
|
|
JSON.stringify({
|
|
|
|
...result,
|
2022-07-10 19:18:48 +02:00
|
|
|
[parsedData.appDirRoute ? 'action' : 'event']: 'pong',
|
2021-10-15 09:09:54 +02:00
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
} 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
|
|
|
}
|
|
|
|
}
|