| | import type ws from 'next/dist/compiled/ws' |
| | import type { webpack } from 'next/dist/compiled/webpack/webpack' |
| | import type { NextConfigComplete } from '../config-shared' |
| | import type { |
| | DynamicParamTypesShort, |
| | FlightRouterState, |
| | FlightSegmentPath, |
| | } from '../../shared/lib/app-router-types' |
| | import type { CompilerNameValues } from '../../shared/lib/constants' |
| | import type { RouteDefinition } from '../route-definitions/route-definition' |
| |
|
| | import createDebug from 'next/dist/compiled/debug' |
| | import { EventEmitter } from 'events' |
| | import { findPageFile } from '../lib/find-page-file' |
| | import { runDependingOnPageType } from '../../build/entries' |
| | import { getStaticInfoIncludingLayouts } from '../../build/get-static-info-including-layouts' |
| | import { join, posix } from 'path' |
| | 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' |
| | import { reportTrigger } from '../../build/output' |
| | import getRouteFromEntrypoint from '../get-route-from-entrypoint' |
| | import { |
| | isInstrumentationHookFile, |
| | isInstrumentationHookFilename, |
| | isMiddlewareFile, |
| | isMiddlewareFilename, |
| | } from '../../build/utils' |
| | import { PageNotFoundError, stringifyError } from '../../shared/lib/utils' |
| | import { |
| | COMPILER_INDEXES, |
| | COMPILER_NAMES, |
| | RSC_MODULE_TYPES, |
| | UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, |
| | } from '../../shared/lib/constants' |
| | import { PAGE_SEGMENT_KEY } from '../../shared/lib/segment' |
| | import { |
| | HMR_MESSAGE_SENT_TO_BROWSER, |
| | HMR_MESSAGE_SENT_TO_SERVER, |
| | type NextJsHotReloaderInterface, |
| | } from './hot-reloader-types' |
| | import { isAppPageRouteDefinition } from '../route-definitions/app-page-route-definition' |
| | import { scheduleOnNextTick } from '../../lib/scheduler' |
| | import { Batcher } from '../../lib/batcher' |
| | import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' |
| | import { PAGE_TYPES } from '../../lib/page-types' |
| | import { getNextFlightSegmentPath } from '../../client/flight-data-helpers' |
| | import { handleErrorStateResponse } from '../mcp/tools/get-errors' |
| | import { handlePageMetadataResponse } from '../mcp/tools/get-page-metadata' |
| |
|
| | const debug = createDebug('next:on-demand-entry-handler') |
| |
|
| | |
| | |
| | |
| | const keys = Object.keys as <T>(o: T) => Extract<keyof T, string>[] |
| |
|
| | const COMPILER_KEYS = keys(COMPILER_INDEXES) |
| |
|
| | function treePathToEntrypoint( |
| | segmentPath: FlightSegmentPath, |
| | parentPath?: string |
| | ): string { |
| | const [parallelRouteKey, segment] = segmentPath |
| |
|
| | |
| | const path = |
| | (parentPath ? parentPath + '/' : '') + |
| | (parallelRouteKey !== 'children' && !segment.startsWith('@') |
| | ? `@${parallelRouteKey}/` |
| | : '') + |
| | (segment === '' ? 'page' : segment) |
| |
|
| | |
| | if (segmentPath.length === 2) { |
| | return path |
| | } |
| |
|
| | const childSegmentPath = getNextFlightSegmentPath(segmentPath) |
| | return treePathToEntrypoint(childSegmentPath, path) |
| | } |
| |
|
| | function convertDynamicParamTypeToSyntax( |
| | dynamicParamTypeShort: DynamicParamTypesShort, |
| | param: string |
| | ) { |
| | switch (dynamicParamTypeShort) { |
| | case 'c': |
| | case 'ci(..)(..)': |
| | case 'ci(.)': |
| | case 'ci(..)': |
| | case 'ci(...)': |
| | return `[...${param}]` |
| | case 'oc': |
| | return `[[...${param}]]` |
| | case 'd': |
| | case 'di(..)(..)': |
| | case 'di(.)': |
| | case 'di(..)': |
| | case 'di(...)': |
| | return `[${param}]` |
| | default: |
| | throw new Error('Unknown dynamic param type') |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | export function getEntryKey( |
| | compilerType: CompilerNameValues, |
| | pageBundleType: PAGE_TYPES, |
| | page: string |
| | ) { |
| | |
| | |
| | const pageKey = page.replace(/(@[^/]+)\/children/g, '$1') |
| | return `${compilerType}@${pageBundleType}@${pageKey}` |
| | } |
| |
|
| | function getPageBundleType(pageBundlePath: string): PAGE_TYPES { |
| | |
| | if (pageBundlePath === '/_error') return PAGE_TYPES.PAGES |
| | if (isMiddlewareFilename(pageBundlePath)) return PAGE_TYPES.ROOT |
| | return pageBundlePath.startsWith('pages/') |
| | ? PAGE_TYPES.PAGES |
| | : pageBundlePath.startsWith('app/') |
| | ? PAGE_TYPES.APP |
| | : PAGE_TYPES.ROOT |
| | } |
| |
|
| | function getEntrypointsFromTree( |
| | tree: FlightRouterState, |
| | isFirst: boolean, |
| | parentPath: string[] = [] |
| | ) { |
| | const [segment, parallelRoutes] = tree |
| |
|
| | const currentSegment = Array.isArray(segment) |
| | ? convertDynamicParamTypeToSyntax(segment[2], segment[0]) |
| | : segment |
| |
|
| | const isPageSegment = currentSegment.startsWith(PAGE_SEGMENT_KEY) |
| |
|
| | const currentPath = [...parentPath, isPageSegment ? '' : currentSegment] |
| |
|
| | if (!isFirst && isPageSegment) { |
| | |
| | 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] |
| | }, |
| | [] |
| | ) |
| | } |
| |
|
| | export const ADDED = Symbol('added') |
| | export const BUILDING = Symbol('building') |
| | export const BUILT = Symbol('built') |
| |
|
| | interface EntryType { |
| | |
| | |
| | |
| | dispose?: boolean |
| | |
| | |
| | |
| | lastActiveTime?: number |
| | |
| | |
| | |
| | status?: typeof ADDED | typeof BUILDING | typeof BUILT |
| |
|
| | |
| | |
| | |
| | |
| | bundlePath: string |
| |
|
| | |
| | |
| | |
| | request: string |
| | } |
| |
|
| | |
| | export const enum EntryTypes { |
| | ENTRY, |
| | CHILD_ENTRY, |
| | } |
| | interface Entry extends EntryType { |
| | type: EntryTypes.ENTRY |
| | |
| | |
| | |
| | |
| | absolutePagePath: string |
| | |
| | |
| | |
| | |
| | appPaths: ReadonlyArray<string> | null |
| | } |
| |
|
| | interface ChildEntry extends EntryType { |
| | type: EntryTypes.CHILD_ENTRY |
| | |
| | |
| | |
| | parentEntries: Set<string> |
| | |
| | |
| | |
| | |
| | absoluteEntryFilePath?: string |
| | } |
| |
|
| | const entriesMap: Map< |
| | string, |
| | { |
| | |
| | |
| | |
| | |
| | [entryName: string]: Entry | ChildEntry |
| | } |
| | > = new Map() |
| |
|
| | |
| | const normalizeOutputPath = (dir: string) => dir.replace(/[/\\]server$/, '') |
| |
|
| | export const getEntries = ( |
| | dir: string |
| | ): NonNullable<ReturnType<(typeof entriesMap)['get']>> => { |
| | dir = normalizeOutputPath(dir) |
| | const entries = entriesMap.get(dir) || {} |
| | entriesMap.set(dir, entries) |
| | return entries |
| | } |
| |
|
| | const invalidators: Map<string, Invalidator> = new Map() |
| |
|
| | export const getInvalidator = (dir: string) => { |
| | dir = normalizeOutputPath(dir) |
| | return invalidators.get(dir) |
| | } |
| |
|
| | const doneCallbacks: EventEmitter = new EventEmitter() |
| | const lastClientAccessPages = [''] |
| | const lastServerAccessPagesForAppDir = [''] |
| |
|
| | type BuildingTracker = Set<CompilerNameValues> |
| | type RebuildTracker = Set<CompilerNameValues> |
| |
|
| | |
| | |
| | class Invalidator { |
| | private multiCompiler: webpack.MultiCompiler |
| |
|
| | private building: BuildingTracker = new Set() |
| | private rebuildAgain: RebuildTracker = new Set() |
| |
|
| | constructor(multiCompiler: webpack.MultiCompiler) { |
| | this.multiCompiler = multiCompiler |
| | } |
| |
|
| | public shouldRebuildAll() { |
| | return this.rebuildAgain.size > 0 |
| | } |
| |
|
| | invalidate(compilerKeys: typeof COMPILER_KEYS = COMPILER_KEYS): void { |
| | for (const key of compilerKeys) { |
| | |
| | |
| | |
| | |
| |
|
| | if (this.building.has(key)) { |
| | this.rebuildAgain.add(key) |
| | continue |
| | } |
| |
|
| | this.building.add(key) |
| | this.multiCompiler.compilers[COMPILER_INDEXES[key]].watching?.invalidate() |
| | } |
| | } |
| |
|
| | public startBuilding(compilerKey: keyof typeof COMPILER_INDEXES) { |
| | this.building.add(compilerKey) |
| | } |
| |
|
| | public doneBuilding(compilerKeys: typeof COMPILER_KEYS = []) { |
| | const rebuild: typeof COMPILER_KEYS = [] |
| | for (const key of compilerKeys) { |
| | this.building.delete(key) |
| |
|
| | if (this.rebuildAgain.has(key)) { |
| | rebuild.push(key) |
| | this.rebuildAgain.delete(key) |
| | } |
| | } |
| |
|
| | if (rebuild.length > 0) { |
| | this.invalidate(rebuild) |
| | } |
| | } |
| |
|
| | public willRebuild(compilerKey: keyof typeof COMPILER_INDEXES) { |
| | return this.rebuildAgain.has(compilerKey) |
| | } |
| | } |
| |
|
| | function disposeInactiveEntries( |
| | entries: NonNullable<ReturnType<(typeof entriesMap)['get']>>, |
| | maxInactiveAge: number |
| | ) { |
| | Object.keys(entries).forEach((entryKey) => { |
| | const entryData = entries[entryKey] |
| | const { lastActiveTime, status, dispose, bundlePath } = entryData |
| |
|
| | |
| | if (entryData.type === EntryTypes.CHILD_ENTRY) { |
| | return |
| | } |
| |
|
| | |
| | |
| | if ( |
| | isMiddlewareFilename(bundlePath) || |
| | isInstrumentationHookFilename(bundlePath) |
| | ) { |
| | return |
| | } |
| |
|
| | if (dispose) |
| | |
| | return |
| |
|
| | |
| | |
| | if (status !== BUILT) return |
| |
|
| | |
| | |
| | |
| | if ( |
| | lastClientAccessPages.includes(entryKey) || |
| | lastServerAccessPagesForAppDir.includes(entryKey) |
| | ) |
| | return |
| |
|
| | if (lastActiveTime && Date.now() - lastActiveTime > maxInactiveAge) { |
| | entries[entryKey].dispose = true |
| | } |
| | }) |
| | } |
| |
|
| | |
| | function tryToNormalizePagePath(page: string) { |
| | try { |
| | return normalizePagePath(page) |
| | } catch (err) { |
| | console.error(err) |
| | throw new PageNotFoundError(page) |
| | } |
| | } |
| |
|
| | interface PagePathData { |
| | filename: string |
| | bundlePath: string |
| | page: string |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export async function findPagePathData( |
| | rootDir: string, |
| | page: string, |
| | extensions: string[], |
| | pagesDir: string | undefined, |
| | appDir: string | undefined, |
| | isGlobalNotFoundEnabled: boolean |
| | ): Promise<PagePathData> { |
| | const normalizedPagePath = tryToNormalizePagePath(page) |
| | let pagePath: string | null = null |
| |
|
| | const isInstrumentation = isInstrumentationHookFile(normalizedPagePath) |
| | if (isMiddlewareFile(normalizedPagePath) || isInstrumentation) { |
| | pagePath = await findPageFile( |
| | rootDir, |
| | normalizedPagePath, |
| | extensions, |
| | false |
| | ) |
| |
|
| | if (!pagePath) { |
| | throw new PageNotFoundError(normalizedPagePath) |
| | } |
| |
|
| | const pageUrl = ensureLeadingSlash( |
| | removePagePathTail(normalizePathSep(pagePath), { |
| | extensions, |
| | }) |
| | ) |
| |
|
| | let bundlePath = normalizedPagePath |
| | let pageKey = posix.normalize(pageUrl) |
| |
|
| | if (isInstrumentation || isMiddlewareFile(normalizedPagePath)) { |
| | bundlePath = bundlePath.replace('/src', '') |
| | pageKey = page.replace('/src', '') |
| | } |
| |
|
| | return { |
| | filename: join(rootDir, pagePath), |
| | bundlePath: bundlePath.slice(1), |
| | page: pageKey, |
| | } |
| | } |
| |
|
| | |
| | if (appDir) { |
| | if (page === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY) { |
| | |
| | |
| | if (isGlobalNotFoundEnabled) { |
| | const globalNotFoundPath = await findPageFile( |
| | appDir, |
| | 'global-not-found', |
| | extensions, |
| | true |
| | ) |
| | if (globalNotFoundPath) { |
| | return { |
| | filename: join(appDir, globalNotFoundPath), |
| | bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, |
| | page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, |
| | } |
| | } |
| | } else { |
| | |
| | const notFoundPath = await findPageFile( |
| | appDir, |
| | 'not-found', |
| | extensions, |
| | true |
| | ) |
| | if (notFoundPath) { |
| | return { |
| | filename: join(appDir, notFoundPath), |
| | bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, |
| | page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, |
| | } |
| | } |
| | } |
| |
|
| | |
| | return { |
| | filename: require.resolve( |
| | 'next/dist/client/components/builtin/global-not-found' |
| | ), |
| | bundlePath: `app${UNDERSCORE_NOT_FOUND_ROUTE_ENTRY}`, |
| | page: UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, |
| | } |
| | } |
| | pagePath = await findPageFile(appDir, normalizedPagePath, extensions, true) |
| | if (pagePath) { |
| | const pageUrl = ensureLeadingSlash( |
| | removePagePathTail(normalizePathSep(pagePath), { |
| | keepIndex: true, |
| | extensions, |
| | }) |
| | ) |
| |
|
| | return { |
| | filename: join(appDir, pagePath), |
| | bundlePath: posix.join('app', pageUrl), |
| | page: posix.normalize(pageUrl), |
| | } |
| | } |
| | } |
| |
|
| | if (!pagePath && pagesDir) { |
| | pagePath = await findPageFile( |
| | pagesDir, |
| | normalizedPagePath, |
| | extensions, |
| | false |
| | ) |
| | } |
| |
|
| | if (pagePath !== null && pagesDir) { |
| | const pageUrl = ensureLeadingSlash( |
| | removePagePathTail(normalizePathSep(pagePath), { |
| | extensions, |
| | }) |
| | ) |
| |
|
| | return { |
| | filename: join(pagesDir, pagePath), |
| | bundlePath: posix.join('pages', normalizePagePath(pageUrl)), |
| | page: posix.normalize(pageUrl), |
| | } |
| | } |
| |
|
| | if (page === '/_error') { |
| | return { |
| | filename: require.resolve('next/dist/pages/_error'), |
| | bundlePath: page, |
| | page: normalizePathSep(page), |
| | } |
| | } else { |
| | throw new PageNotFoundError(normalizedPagePath) |
| | } |
| | } |
| |
|
| | export function onDemandEntryHandler({ |
| | hotReloader, |
| | maxInactiveAge, |
| | multiCompiler, |
| | nextConfig, |
| | pagesBufferLength, |
| | pagesDir, |
| | rootDir, |
| | appDir, |
| | }: { |
| | hotReloader: NextJsHotReloaderInterface |
| | maxInactiveAge: number |
| | multiCompiler: webpack.MultiCompiler |
| | nextConfig: NextConfigComplete |
| | pagesBufferLength: number |
| | pagesDir?: string |
| | rootDir: string |
| | appDir?: string |
| | }) { |
| | const hasAppDir = !!appDir |
| | let curInvalidator: Invalidator = getInvalidator( |
| | multiCompiler.outputPath |
| | ) as any |
| | const curEntries = getEntries(multiCompiler.outputPath) as any |
| |
|
| | if (!curInvalidator) { |
| | curInvalidator = new Invalidator(multiCompiler) |
| | invalidators.set(multiCompiler.outputPath, curInvalidator) |
| | } |
| |
|
| | const startBuilding = (compilation: webpack.Compilation) => { |
| | const compilationName = compilation.name as any as CompilerNameValues |
| | curInvalidator.startBuilding(compilationName) |
| | } |
| | for (const compiler of multiCompiler.compilers) { |
| | compiler.hooks.make.tap('NextJsOnDemandEntries', startBuilding) |
| | } |
| |
|
| | function getPagePathsFromEntrypoints( |
| | type: CompilerNameValues, |
| | entrypoints: Map<string, { name?: string | null }> |
| | ) { |
| | const pagePaths: string[] = [] |
| | for (const entrypoint of entrypoints.values()) { |
| | const page = getRouteFromEntrypoint(entrypoint.name!, hasAppDir) |
| |
|
| | if (page) { |
| | const pageBundleType = entrypoint.name?.startsWith('app/') |
| | ? PAGE_TYPES.APP |
| | : PAGE_TYPES.PAGES |
| | pagePaths.push(getEntryKey(type, pageBundleType, page)) |
| | } else if ( |
| | isMiddlewareFilename(entrypoint.name) || |
| | isInstrumentationHookFilename(entrypoint.name) |
| | ) { |
| | pagePaths.push( |
| | getEntryKey(type, PAGE_TYPES.ROOT, `/${entrypoint.name}`) |
| | ) |
| | } |
| | } |
| | return pagePaths |
| | } |
| |
|
| | for (const compiler of multiCompiler.compilers) { |
| | compiler.hooks.done.tap('NextJsOnDemandEntries', () => |
| | getInvalidator(compiler.outputPath)?.doneBuilding([ |
| | compiler.name as keyof typeof COMPILER_INDEXES, |
| | ]) |
| | ) |
| | } |
| |
|
| | multiCompiler.hooks.done.tap('NextJsOnDemandEntries', (multiStats) => { |
| | const [clientStats, serverStats, edgeServerStats] = multiStats.stats |
| | const entryNames = [ |
| | ...getPagePathsFromEntrypoints( |
| | COMPILER_NAMES.client, |
| | clientStats.compilation.entrypoints |
| | ), |
| | ...getPagePathsFromEntrypoints( |
| | COMPILER_NAMES.server, |
| | serverStats.compilation.entrypoints |
| | ), |
| | ...(edgeServerStats |
| | ? getPagePathsFromEntrypoints( |
| | COMPILER_NAMES.edgeServer, |
| | edgeServerStats.compilation.entrypoints |
| | ) |
| | : []), |
| | ] |
| |
|
| | for (const name of entryNames) { |
| | const entry = curEntries[name] |
| | if (!entry) { |
| | continue |
| | } |
| |
|
| | if (entry.status !== BUILDING) { |
| | continue |
| | } |
| |
|
| | entry.status = BUILT |
| | doneCallbacks.emit(name) |
| | } |
| |
|
| | getInvalidator(multiCompiler.outputPath)?.doneBuilding([...COMPILER_KEYS]) |
| | }) |
| |
|
| | const pingIntervalTime = Math.max(1000, Math.min(5000, maxInactiveAge)) |
| |
|
| | setInterval(function () { |
| | disposeInactiveEntries(curEntries, maxInactiveAge) |
| | }, pingIntervalTime + 1000).unref() |
| |
|
| | function handleAppDirPing(tree: FlightRouterState): void { |
| | const pages = getEntrypointsFromTree(tree, true) |
| |
|
| | for (const page of pages) { |
| | for (const compilerType of [ |
| | COMPILER_NAMES.client, |
| | COMPILER_NAMES.server, |
| | COMPILER_NAMES.edgeServer, |
| | ]) { |
| | const entryKey = getEntryKey(compilerType, PAGE_TYPES.APP, `/${page}`) |
| | const entryInfo = curEntries[entryKey] |
| |
|
| | |
| | if (!entryInfo) { |
| | |
| | continue |
| | } |
| |
|
| | |
| | if (entryInfo.status !== BUILT) continue |
| |
|
| | |
| | if (!lastServerAccessPagesForAppDir.includes(entryKey)) { |
| | lastServerAccessPagesForAppDir.unshift(entryKey) |
| |
|
| | |
| | |
| | if (lastServerAccessPagesForAppDir.length > pagesBufferLength) { |
| | lastServerAccessPagesForAppDir.pop() |
| | } |
| | } |
| | entryInfo.lastActiveTime = Date.now() |
| | entryInfo.dispose = false |
| | } |
| | } |
| | } |
| |
|
| | function handlePing(pg: string): void { |
| | const page = normalizePathSep(pg) |
| | for (const compilerType of [ |
| | COMPILER_NAMES.client, |
| | COMPILER_NAMES.server, |
| | COMPILER_NAMES.edgeServer, |
| | ]) { |
| | const entryKey = getEntryKey(compilerType, PAGE_TYPES.PAGES, page) |
| | const entryInfo = curEntries[entryKey] |
| |
|
| | |
| | if (!entryInfo) { |
| | |
| | if (compilerType === COMPILER_NAMES.client) { |
| | return |
| | } |
| | continue |
| | } |
| |
|
| | |
| | if (entryInfo.status !== BUILT) continue |
| |
|
| | |
| | if (!lastClientAccessPages.includes(entryKey)) { |
| | lastClientAccessPages.unshift(entryKey) |
| |
|
| | |
| | if (lastClientAccessPages.length > pagesBufferLength) { |
| | lastClientAccessPages.pop() |
| | } |
| | } |
| | entryInfo.lastActiveTime = Date.now() |
| | entryInfo.dispose = false |
| | } |
| | return |
| | } |
| |
|
| | async function ensurePageImpl({ |
| | page, |
| | appPaths, |
| | definition, |
| | isApp, |
| | url, |
| | }: { |
| | page: string |
| | appPaths: ReadonlyArray<string> | null |
| | definition: RouteDefinition | undefined |
| | isApp: boolean | undefined |
| | url?: string |
| | }): Promise<void> { |
| | const stalledTime = 60 |
| | const stalledEnsureTimeout = setTimeout(() => { |
| | debug( |
| | `Ensuring ${page} has taken longer than ${stalledTime}s, if this continues to stall this may be a bug` |
| | ) |
| | }, stalledTime * 1000) |
| |
|
| | try { |
| | let route: Pick<RouteDefinition, 'filename' | 'bundlePath' | 'page'> |
| | if (definition) { |
| | route = definition |
| | } else { |
| | route = await findPagePathData( |
| | rootDir, |
| | page, |
| | nextConfig.pageExtensions, |
| | pagesDir, |
| | appDir, |
| | !!nextConfig.experimental.globalNotFound |
| | ) |
| | } |
| |
|
| | const isInsideAppDir = !!appDir && route.filename.startsWith(appDir) |
| |
|
| | if (typeof isApp === 'boolean' && isApp !== isInsideAppDir) { |
| | Error.stackTraceLimit = 15 |
| | throw new Error( |
| | `Ensure bailed, found path "${ |
| | route.page |
| | }" does not match ensure type (${isApp ? 'app' : 'pages'})` |
| | ) |
| | } |
| |
|
| | const pageBundleType = getPageBundleType(route.bundlePath) |
| | const addEntry = ( |
| | compilerType: CompilerNameValues |
| | ): { |
| | entryKey: string |
| | newEntry: boolean |
| | shouldInvalidate: boolean |
| | } => { |
| | const entryKey = getEntryKey(compilerType, pageBundleType, route.page) |
| | if ( |
| | curEntries[entryKey] && |
| | |
| | |
| | |
| | !isInstrumentationHookFilename(curEntries[entryKey].bundlePath) |
| | ) { |
| | curEntries[entryKey].dispose = false |
| | curEntries[entryKey].lastActiveTime = Date.now() |
| | if (curEntries[entryKey].status === BUILT) { |
| | return { |
| | entryKey, |
| | newEntry: false, |
| | shouldInvalidate: false, |
| | } |
| | } |
| |
|
| | return { |
| | entryKey, |
| | newEntry: false, |
| | shouldInvalidate: true, |
| | } |
| | } |
| |
|
| | curEntries[entryKey] = { |
| | type: EntryTypes.ENTRY, |
| | appPaths, |
| | absolutePagePath: route.filename, |
| | request: route.filename, |
| | bundlePath: route.bundlePath, |
| | dispose: false, |
| | lastActiveTime: Date.now(), |
| | status: ADDED, |
| | } |
| | return { |
| | entryKey: entryKey, |
| | newEntry: true, |
| | shouldInvalidate: true, |
| | } |
| | } |
| |
|
| | const staticInfo = await getStaticInfoIncludingLayouts({ |
| | page, |
| | pageFilePath: route.filename, |
| | isInsideAppDir, |
| | pageExtensions: nextConfig.pageExtensions, |
| | isDev: true, |
| | config: nextConfig, |
| | appDir, |
| | }) |
| |
|
| | const added = new Map<CompilerNameValues, ReturnType<typeof addEntry>>() |
| | const isServerComponent = |
| | isInsideAppDir && staticInfo.rsc !== RSC_MODULE_TYPES.client |
| |
|
| | let pageRuntime = staticInfo.runtime |
| |
|
| | runDependingOnPageType({ |
| | page: route.page, |
| | pageRuntime, |
| | pageType: pageBundleType, |
| | onClient: () => { |
| | |
| | if (isServerComponent || isInsideAppDir) { |
| | return |
| | } |
| | added.set(COMPILER_NAMES.client, addEntry(COMPILER_NAMES.client)) |
| | }, |
| | onServer: () => { |
| | added.set(COMPILER_NAMES.server, addEntry(COMPILER_NAMES.server)) |
| | const edgeServerEntry = getEntryKey( |
| | COMPILER_NAMES.edgeServer, |
| | pageBundleType, |
| | route.page |
| | ) |
| | if ( |
| | curEntries[edgeServerEntry] && |
| | !isInstrumentationHookFile(route.page) |
| | ) { |
| | |
| | delete curEntries[edgeServerEntry] |
| | } |
| | }, |
| | onEdgeServer: () => { |
| | added.set( |
| | COMPILER_NAMES.edgeServer, |
| | addEntry(COMPILER_NAMES.edgeServer) |
| | ) |
| | const serverEntry = getEntryKey( |
| | COMPILER_NAMES.server, |
| | pageBundleType, |
| | route.page |
| | ) |
| | if ( |
| | curEntries[serverEntry] && |
| | !isInstrumentationHookFile(route.page) |
| | ) { |
| | |
| | delete curEntries[serverEntry] |
| | } |
| | }, |
| | }) |
| |
|
| | const addedValues = [...added.values()] |
| | const entriesThatShouldBeInvalidated = [...added.entries()].filter( |
| | ([, entry]) => entry.shouldInvalidate |
| | ) |
| | const hasNewEntry = addedValues.some((entry) => entry.newEntry) |
| |
|
| | if (hasNewEntry) { |
| | const routePage = isApp ? route.page : normalizeAppPath(route.page) |
| | |
| | reportTrigger( |
| | isMiddlewareFile(routePage) ? routePage.slice(1) : routePage, |
| | url |
| | ) |
| | } |
| |
|
| | if (entriesThatShouldBeInvalidated.length > 0) { |
| | const invalidatePromise = Promise.all( |
| | entriesThatShouldBeInvalidated.map(([compilerKey, { entryKey }]) => { |
| | return new Promise<void>((resolve, reject) => { |
| | doneCallbacks.once(entryKey, (err: Error) => { |
| | if (err) { |
| | return reject(err) |
| | } |
| |
|
| | |
| | |
| | const needsRebuild = curInvalidator.willRebuild(compilerKey) |
| | if (needsRebuild) { |
| | doneCallbacks.once(entryKey, (rebuildErr: Error) => { |
| | if (rebuildErr) { |
| | return reject(rebuildErr) |
| | } |
| | resolve() |
| | }) |
| | } else { |
| | resolve() |
| | } |
| | }) |
| | }) |
| | }) |
| | ) |
| |
|
| | curInvalidator.invalidate([...added.keys()]) |
| | await invalidatePromise |
| | } |
| | } finally { |
| | clearTimeout(stalledEnsureTimeout) |
| | } |
| | } |
| |
|
| | type EnsurePageOptions = { |
| | page: string |
| | appPaths?: ReadonlyArray<string> | null |
| | definition?: RouteDefinition |
| | isApp?: boolean |
| | url?: string |
| | } |
| |
|
| | |
| | const batcher = Batcher.create<EnsurePageOptions, void, string>({ |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | cacheKeyFn: (options) => JSON.stringify(options), |
| | |
| | schedulerFn: scheduleOnNextTick, |
| | }) |
| |
|
| | return { |
| | async ensurePage({ |
| | page, |
| | appPaths = null, |
| | definition, |
| | isApp, |
| | url, |
| | }: EnsurePageOptions) { |
| | |
| | |
| | if (!appPaths && definition && isAppPageRouteDefinition(definition)) { |
| | appPaths = definition.appPaths |
| | } |
| |
|
| | |
| | |
| | |
| | return batcher.batch({ page, appPaths, definition, isApp }, async () => { |
| | await ensurePageImpl({ |
| | page, |
| | appPaths, |
| | definition, |
| | isApp, |
| | url, |
| | }) |
| | }) |
| | }, |
| | onHMR(client: ws, getHmrServerError: () => Error | null) { |
| | let bufferedHmrServerError: Error | null = null |
| |
|
| | client.addEventListener('close', () => { |
| | bufferedHmrServerError = null |
| | }) |
| | client.addEventListener('message', ({ data }) => { |
| | try { |
| | const error = getHmrServerError() |
| |
|
| | |
| | if (!bufferedHmrServerError && error) { |
| | hotReloader.send({ |
| | type: HMR_MESSAGE_SENT_TO_BROWSER.SERVER_ERROR, |
| | errorJSON: stringifyError(error), |
| | }) |
| | bufferedHmrServerError = null |
| | } |
| |
|
| | const parsedData = JSON.parse( |
| | typeof data !== 'string' ? data.toString() : data |
| | ) |
| |
|
| | if (parsedData.event === HMR_MESSAGE_SENT_TO_SERVER.PING) { |
| | if (parsedData.appDirRoute) { |
| | handleAppDirPing(parsedData.tree) |
| | } else { |
| | handlePing(parsedData.page) |
| | } |
| | } else if ( |
| | parsedData.event === |
| | HMR_MESSAGE_SENT_TO_SERVER.MCP_ERROR_STATE_RESPONSE |
| | ) { |
| | handleErrorStateResponse( |
| | parsedData.requestId, |
| | parsedData.errorState, |
| | parsedData.url |
| | ) |
| | } else if ( |
| | parsedData.event === |
| | HMR_MESSAGE_SENT_TO_SERVER.MCP_PAGE_METADATA_RESPONSE |
| | ) { |
| | handlePageMetadataResponse( |
| | parsedData.requestId, |
| | parsedData.segmentTrieData, |
| | parsedData.url |
| | ) |
| | } |
| | } catch {} |
| | }) |
| | }, |
| | } |
| | } |
| |
|