Skip to content

Commit

Permalink
Refactor onDemandEntryHandler
Browse files Browse the repository at this point in the history
  • Loading branch information
javivelasco committed Apr 30, 2022
1 parent d501720 commit b81564f
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 123 deletions.
9 changes: 6 additions & 3 deletions packages/next/server/dev/hot-reloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import { BLOCKED_PAGES } from '../../shared/lib/constants'
import { __ApiPreviewProps } from '../api-utils'
import { getPathMatch } from '../../shared/lib/router/utils/path-match'
import { findPageFile } from '../lib/find-page-file'
import onDemandEntryHandler, {
entries,
import {
BUILDING,
entries,
onDemandEntryHandler,
} from './on-demand-entry-handler'
import { denormalizePagePath, normalizePathSep } from '../page-path-utils'
import getRouteFromEntrypoint from '../get-route-from-entrypoint'
Expand Down Expand Up @@ -830,7 +831,9 @@ export default class HotReloader {
)
})

this.onDemandEntries = onDemandEntryHandler(this.watcher, multiCompiler, {
this.onDemandEntries = onDemandEntryHandler({
multiCompiler,
watcher: this.watcher,
pagesDir: this.pagesDir,
nextConfig: this.config,
...(this.config.onDemandEntries as {
Expand Down
243 changes: 123 additions & 120 deletions packages/next/server/dev/on-demand-entry-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,56 @@ export const BUILDING = Symbol('building')
export const BUILT = Symbol('built')

export const entries: {
/**
* The key composed of the compiler name and the page. For example:
* `edge-server/about`
*/
[page: string]: {
bundlePath: string
/**
* The absolute page to the page file. For example:
* `/Users/Rick/project/pages/about/index.js`
*/
absolutePagePath: string
status?: typeof ADDED | typeof BUILDING | typeof BUILT
lastActiveTime?: number
/**
* Path to the page file relative to the dist folder with no extension.
* For example: `pages/about/index`
*/
bundlePath: string
/**
* 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
}
} = {}

export default function onDemandEntryHandler(
watcher: any,
multiCompiler: webpack.MultiCompiler,
{
pagesDir,
nextConfig,
maxInactiveAge,
pagesBufferLength,
}: {
pagesDir: string
nextConfig: NextConfigComplete
maxInactiveAge: number
pagesBufferLength: number
}
) {
const { compilers } = multiCompiler
const invalidator = new Invalidator(watcher, multiCompiler)

let lastClientAccessPages = ['']
let doneCallbacks: EventEmitter | null = new EventEmitter()
export function onDemandEntryHandler({
maxInactiveAge,
multiCompiler,
nextConfig,
pagesBufferLength,
pagesDir,
watcher,
}: {
maxInactiveAge: number
multiCompiler: webpack.MultiCompiler
nextConfig: NextConfigComplete
pagesBufferLength: number
pagesDir: string
watcher: any
}) {
const invalidator = new Invalidator(watcher)
const doneCallbacks: EventEmitter | null = new EventEmitter()
const lastClientAccessPages = ['']

for (const compiler of compilers) {
for (const compiler of multiCompiler.compilers) {
compiler.hooks.make.tap(
'NextJsOnDemandEntries',
(_compilation: webpack.Compilation) => {
Expand All @@ -56,12 +75,12 @@ export default function onDemandEntryHandler(
}

function getPagePathsFromEntrypoints(
type: string,
entrypoints: any
): string[] {
const pagePaths = []
type: 'client' | 'server' | 'edge-server',
entrypoints: Map<string, { name?: string }>
) {
const pagePaths: string[] = []
for (const entrypoint of entrypoints.values()) {
const page = getRouteFromEntrypoint(entrypoint.name)
const page = getRouteFromEntrypoint(entrypoint.name!)
if (page) {
pagePaths.push(`${type}${page}`)
}
Expand Down Expand Up @@ -111,17 +130,14 @@ export default function onDemandEntryHandler(

const pingIntervalTime = Math.max(1000, Math.min(5000, maxInactiveAge))

const disposeHandler = setInterval(function () {
disposeInactiveEntries(watcher, lastClientAccessPages, maxInactiveAge)
}, pingIntervalTime + 1000)

disposeHandler.unref()
setInterval(function () {
disposeInactiveEntries(lastClientAccessPages, maxInactiveAge)
}, pingIntervalTime + 1000).unref()

function handlePing(pg: string) {
const page = normalizePathSep(pg)
const pageKey = `client${page}`
const entryInfo = entries[pageKey]
let toSend

// If there's no entry, it may have been invalidated and needs to be re-built.
if (!entryInfo) {
Expand All @@ -130,11 +146,7 @@ export default function onDemandEntryHandler(
}

// 404 is an on demand entry but when a new page is added we have to refresh the page
if (page === '/_error') {
toSend = { invalid: true }
} else {
toSend = { success: true }
}
const toSend = page === '/_error' ? { invalid: true } : { success: true }

// We don't need to maintain active state of anything other than BUILT entries
if (entryInfo.status !== BUILT) return
Expand All @@ -154,64 +166,59 @@ export default function onDemandEntryHandler(
}

return {
async ensurePage(_page: string, clientOnly: boolean) {
const { absolutePagePath, bundlePath, page } = await getPageInfo({
pageExtensions: nextConfig.pageExtensions,
page: _page,
async ensurePage(page: string, clientOnly: boolean) {
const pagePathData = await findPagePathData(
pagesDir,
})
page,
nextConfig.pageExtensions
)

let entriesChanged = false
let entryAdded = false

const addPageEntry = (type: 'client' | 'server' | 'edge-server') => {
return new Promise<void>((resolve, reject) => {
// Makes sure the page that is being kept in on-demand-entries matches the webpack output
const pageKey = `${type}${page}`
const entryInfo = entries[pageKey]

if (entryInfo) {
entryInfo.lastActiveTime = Date.now()
entryInfo.dispose = false
if (entryInfo.status === BUILT) {
const pageKey = `${type}${pagePathData.page}`
if (entries[pageKey]) {
entries[pageKey].dispose = false
entries[pageKey].lastActiveTime = Date.now()
if (entries[pageKey].status === BUILT) {
resolve()
return
}

doneCallbacks!.once(pageKey, handleCallback)
return
}

entriesChanged = true

entries[pageKey] = {
bundlePath,
absolutePagePath,
status: ADDED,
lastActiveTime: Date.now(),
dispose: false,
} else {
entryAdded = true
entries[pageKey] = {
absolutePagePath: pagePathData.absolutePagePath,
bundlePath: pagePathData.bundlePath,
dispose: false,
lastActiveTime: Date.now(),
status: ADDED,
}
}
doneCallbacks!.once(pageKey, handleCallback)

function handleCallback(err: Error) {
doneCallbacks!.once(pageKey, (err: Error) => {
if (err) return reject(err)
resolve()
}
})
})
}

const promises = runDependingOnPageType({
page,
pageRuntime: await getPageRuntime(absolutePagePath, nextConfig),
page: pagePathData.page,
pageRuntime: await getPageRuntime(
pagePathData.absolutePagePath,
nextConfig
),
onClient: () => addPageEntry('client'),
onServer: () => addPageEntry('server'),
onEdgeServer: () => addPageEntry('edge-server'),
})

if (entriesChanged) {
if (entryAdded) {
reportTrigger(
!clientOnly && promises.length > 1
? `${page} (client and server)`
: page
? `${pagePathData.page} (client and server)`
: pagePathData.page
)
invalidator.invalidate()
}
Expand All @@ -221,9 +228,10 @@ export default function onDemandEntryHandler(

onHMR(client: ws) {
client.addEventListener('message', ({ data }) => {
data = typeof data !== 'string' ? data.toString() : data
try {
const parsedData = JSON.parse(data)
const parsedData = JSON.parse(
typeof data !== 'string' ? data.toString() : data
)

if (parsedData.event === 'ping') {
const result = handlePing(parsedData.page)
Expand All @@ -241,8 +249,7 @@ export default function onDemandEntryHandler(
}

function disposeInactiveEntries(
_watcher: any,
lastClientAccessPages: any,
lastClientAccessPages: string[],
maxInactiveAge: number
) {
Object.keys(entries).forEach((page) => {
Expand All @@ -269,13 +276,11 @@ function disposeInactiveEntries(
// Make sure only one invalidation happens at a time
// Otherwise, webpack hash gets changed and it'll force the client to reload.
class Invalidator {
private multiCompiler: webpack.MultiCompiler
private watcher: any
private building: boolean
public rebuildAgain: boolean

constructor(watcher: any, multiCompiler: webpack.MultiCompiler) {
this.multiCompiler = multiCompiler
constructor(watcher: any) {
this.watcher = watcher
// contains an array of types of compilers currently building
this.building = false
Expand Down Expand Up @@ -310,52 +315,50 @@ class Invalidator {
}
}

async function getPageInfo(opts: {
page: string
pageExtensions: string[]
pagesDir: string
}) {
const { page, pagesDir, pageExtensions } = opts

let normalizedPagePath: string

try {
normalizedPagePath = normalizePagePath(page)
} catch (err) {
console.error(err)
throw 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 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(
pagesDir: string,
page: string,
extensions: string[]
) {
const normalizedPagePath = tryToNormalizePagePath(page)
const pagePath = await findPageFile(pagesDir, normalizedPagePath, extensions)
if (pagePath !== null) {
const pageUrl = ensureLeadingSlash(
removePagePathTail(normalizePathSep(pagePath), extensions)
)

let pagePath = await findPageFile(
pagesDir,
normalizedPagePath,
pageExtensions
)

if (pagePath === null) {
// Default the /_error route to the Next.js provided default page
if (page === '/_error') {
pagePath = 'next/dist/pages/_error'
} else {
throw pageNotFoundError(normalizedPagePath)
return {
absolutePagePath: join(pagesDir, pagePath),
bundlePath: posix.join('pages', normalizePagePath(pageUrl)),
page: posix.normalize(pageUrl),
}
}

if (pagePath.startsWith('next/dist/pages/')) {
if (page === '/_error') {
return {
page: normalizePathSep(page),
absolutePagePath: require.resolve('next/dist/pages/_error'),
bundlePath: page,
absolutePagePath: require.resolve(pagePath),
page: normalizePathSep(page),
}
} else {
throw pageNotFoundError(normalizedPagePath)
}
}

const pageUrl = ensureLeadingSlash(
removePagePathTail(normalizePathSep(pagePath), pageExtensions)
)

return {
bundlePath: posix.join('pages', normalizePagePath(pageUrl)),
absolutePagePath: join(pagesDir, pagePath),
page: posix.normalize(pageUrl),
function tryToNormalizePagePath(page: string) {
try {
return normalizePagePath(page)
} catch (err) {
console.error(err)
throw pageNotFoundError(page)
}
}

0 comments on commit b81564f

Please sign in to comment.