make client and server independent for on-demand-entries (#29518)
allow to dispose server while client is making changes allow to dispose other entries while making changes avoid recompiling when disposing entries ## Bug - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Errors have helpful link attached, see `contributing.md` ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation / Examples - [ ] Make sure the linting passes
This commit is contained in:
parent
5778f9ffb3
commit
59b6967e00
4 changed files with 119 additions and 72 deletions
|
@ -38,6 +38,8 @@ function hasStoreChanged(nextStore: OutputState) {
|
|||
return true
|
||||
}
|
||||
|
||||
let startTime = 0
|
||||
|
||||
store.subscribe((state) => {
|
||||
if (!hasStoreChanged(state)) {
|
||||
return
|
||||
|
@ -52,6 +54,7 @@ store.subscribe((state) => {
|
|||
|
||||
if (state.loading) {
|
||||
Log.wait('compiling...')
|
||||
if (startTime === 0) startTime = Date.now()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -77,6 +80,15 @@ store.subscribe((state) => {
|
|||
return
|
||||
}
|
||||
|
||||
let timeMessage = ''
|
||||
if (startTime) {
|
||||
const time = Date.now() - startTime
|
||||
startTime = 0
|
||||
|
||||
timeMessage =
|
||||
time > 2000 ? ` in ${Math.round(time / 100) / 10} s` : ` in ${time} ms`
|
||||
}
|
||||
|
||||
if (state.warnings) {
|
||||
Log.warn(state.warnings.join('\n\n'))
|
||||
// Ensure traces are flushed after each compile in development mode
|
||||
|
@ -85,11 +97,13 @@ store.subscribe((state) => {
|
|||
}
|
||||
|
||||
if (state.typeChecking) {
|
||||
Log.info('bundled successfully, waiting for typecheck results...')
|
||||
Log.info(
|
||||
`bundled successfully${timeMessage}, waiting for typecheck results...`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
Log.event('compiled successfully')
|
||||
Log.event(`compiled successfully${timeMessage}`)
|
||||
// Ensure traces are flushed after each compile in development mode
|
||||
flushAllTraces()
|
||||
})
|
||||
|
|
|
@ -1414,11 +1414,6 @@ export default async function getBaseWebpackConfig(
|
|||
},
|
||||
}
|
||||
|
||||
if (isServer && dev) {
|
||||
// Enable building of client compilation before server compilation in development
|
||||
webpack5Config.dependencies = ['client']
|
||||
}
|
||||
|
||||
if (dev) {
|
||||
// @ts-ignore unsafeCache exists
|
||||
webpack5Config.module.unsafeCache = (module) =>
|
||||
|
|
|
@ -230,7 +230,7 @@ export default class HotReloader {
|
|||
|
||||
if (page === '/_error' || BLOCKED_PAGES.indexOf(page) === -1) {
|
||||
try {
|
||||
await this.ensurePage(page)
|
||||
await this.ensurePage(page, true)
|
||||
} catch (error) {
|
||||
await renderScriptError(
|
||||
pageBundleRes,
|
||||
|
@ -403,30 +403,31 @@ export default class HotReloader {
|
|||
const isClientCompilation = config.name === 'client'
|
||||
|
||||
await Promise.all(
|
||||
Object.keys(entries).map(async (page) => {
|
||||
Object.keys(entries).map(async (pageKey) => {
|
||||
const isClientKey = pageKey.startsWith('client')
|
||||
if (isClientKey !== isClientCompilation) return
|
||||
const page = pageKey.slice(
|
||||
isClientKey ? 'client'.length : 'server'.length
|
||||
)
|
||||
if (isClientCompilation && page.match(API_ROUTE)) {
|
||||
return
|
||||
}
|
||||
const { serverBundlePath, clientBundlePath, absolutePagePath } =
|
||||
entries[page]
|
||||
const { bundlePath, absolutePagePath } = entries[pageKey]
|
||||
const pageExists = await isWriteable(absolutePagePath)
|
||||
if (!pageExists) {
|
||||
// page was removed
|
||||
delete entries[page]
|
||||
delete entries[pageKey]
|
||||
return
|
||||
}
|
||||
|
||||
entries[page].status = BUILDING
|
||||
entries[pageKey].status = BUILDING
|
||||
const pageLoaderOpts: ClientPagesLoaderOptions = {
|
||||
page,
|
||||
absolutePagePath,
|
||||
}
|
||||
|
||||
const name = isClientCompilation
|
||||
? clientBundlePath
|
||||
: serverBundlePath
|
||||
entrypoints[name] = finalizeEntrypoint(
|
||||
name,
|
||||
entrypoints[bundlePath] = finalizeEntrypoint(
|
||||
bundlePath,
|
||||
isClientCompilation
|
||||
? `next-client-pages-loader?${stringify(pageLoaderOpts)}!`
|
||||
: absolutePagePath,
|
||||
|
@ -440,6 +441,10 @@ export default class HotReloader {
|
|||
}
|
||||
}
|
||||
|
||||
// Enable building of client compilation before server compilation in development
|
||||
// @ts-ignore webpack 5
|
||||
configs.parallelism = 1
|
||||
|
||||
const multiCompiler = webpack(configs)
|
||||
|
||||
watchCompilers(multiCompiler.compilers[0], multiCompiler.compilers[1])
|
||||
|
@ -682,15 +687,18 @@ export default class HotReloader {
|
|||
)
|
||||
}
|
||||
|
||||
public async ensurePage(page: string) {
|
||||
public async ensurePage(page: string, clientOnly: boolean = false) {
|
||||
// Make sure we don't re-build or dispose prebuilt pages
|
||||
if (page !== '/_error' && BLOCKED_PAGES.indexOf(page) !== -1) {
|
||||
return
|
||||
}
|
||||
if (this.serverError || this.clientError) {
|
||||
return Promise.reject(this.serverError || this.clientError)
|
||||
const error = clientOnly
|
||||
? this.clientError
|
||||
: this.serverError || this.clientError
|
||||
if (error) {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
return this.onDemandEntries.ensurePage(page)
|
||||
return this.onDemandEntries.ensurePage(page, clientOnly)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ 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'
|
||||
import { API_ROUTE } from '../../lib/constants'
|
||||
|
||||
export const ADDED = Symbol('added')
|
||||
export const BUILDING = Symbol('building')
|
||||
|
@ -15,8 +16,7 @@ export const BUILT = Symbol('built')
|
|||
|
||||
export let entries: {
|
||||
[page: string]: {
|
||||
serverBundlePath: string
|
||||
clientBundlePath: string
|
||||
bundlePath: string
|
||||
absolutePagePath: string
|
||||
status?: typeof ADDED | typeof BUILDING | typeof BUILT
|
||||
lastActiveTime?: number
|
||||
|
@ -41,7 +41,7 @@ export default function onDemandEntryHandler(
|
|||
const { compilers } = multiCompiler
|
||||
const invalidator = new Invalidator(watcher, multiCompiler)
|
||||
|
||||
let lastAccessPages = ['']
|
||||
let lastClientAccessPages = ['']
|
||||
let doneCallbacks: EventEmitter | null = new EventEmitter()
|
||||
|
||||
for (const compiler of compilers) {
|
||||
|
@ -53,12 +53,15 @@ export default function onDemandEntryHandler(
|
|||
)
|
||||
}
|
||||
|
||||
function getPagePathsFromEntrypoints(entrypoints: any): string[] {
|
||||
function getPagePathsFromEntrypoints(
|
||||
type: string,
|
||||
entrypoints: any
|
||||
): string[] {
|
||||
const pagePaths = []
|
||||
for (const entrypoint of entrypoints.values()) {
|
||||
const page = getRouteFromEntrypoint(entrypoint.name)
|
||||
if (page) {
|
||||
pagePaths.push(page)
|
||||
pagePaths.push(`${type}${page}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,10 +73,16 @@ export default function onDemandEntryHandler(
|
|||
return invalidator.doneBuilding()
|
||||
}
|
||||
const [clientStats, serverStats] = multiStats.stats
|
||||
const pagePaths = new Set([
|
||||
...getPagePathsFromEntrypoints(clientStats.compilation.entrypoints),
|
||||
...getPagePathsFromEntrypoints(serverStats.compilation.entrypoints),
|
||||
])
|
||||
const pagePaths = [
|
||||
...getPagePathsFromEntrypoints(
|
||||
'client',
|
||||
clientStats.compilation.entrypoints
|
||||
),
|
||||
...getPagePathsFromEntrypoints(
|
||||
'server',
|
||||
serverStats.compilation.entrypoints
|
||||
),
|
||||
]
|
||||
|
||||
for (const page of pagePaths) {
|
||||
const entry = entries[page]
|
||||
|
@ -86,7 +95,6 @@ export default function onDemandEntryHandler(
|
|||
}
|
||||
|
||||
entry.status = BUILT
|
||||
entry.lastActiveTime = Date.now()
|
||||
doneCallbacks!.emit(page)
|
||||
}
|
||||
|
||||
|
@ -94,14 +102,15 @@ export default function onDemandEntryHandler(
|
|||
})
|
||||
|
||||
const disposeHandler = setInterval(function () {
|
||||
disposeInactiveEntries(watcher, lastAccessPages, maxInactiveAge)
|
||||
disposeInactiveEntries(watcher, lastClientAccessPages, maxInactiveAge)
|
||||
}, 5000)
|
||||
|
||||
disposeHandler.unref()
|
||||
|
||||
function handlePing(pg: string) {
|
||||
const page = normalizePathSep(pg)
|
||||
const entryInfo = entries[page]
|
||||
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.
|
||||
|
@ -121,12 +130,12 @@ export default function onDemandEntryHandler(
|
|||
if (entryInfo.status !== BUILT) return
|
||||
|
||||
// If there's an entryInfo
|
||||
if (!lastAccessPages.includes(page)) {
|
||||
lastAccessPages.unshift(page)
|
||||
if (!lastClientAccessPages.includes(pageKey)) {
|
||||
lastClientAccessPages.unshift(pageKey)
|
||||
|
||||
// Maintain the buffer max length
|
||||
if (lastAccessPages.length > pagesBufferLength) {
|
||||
lastAccessPages.pop()
|
||||
if (lastClientAccessPages.length > pagesBufferLength) {
|
||||
lastClientAccessPages.pop()
|
||||
}
|
||||
}
|
||||
entryInfo.lastActiveTime = Date.now()
|
||||
|
@ -134,7 +143,7 @@ export default function onDemandEntryHandler(
|
|||
}
|
||||
|
||||
return {
|
||||
async ensurePage(page: string) {
|
||||
async ensurePage(page: string, clientOnly: boolean) {
|
||||
let normalizedPagePath: string
|
||||
try {
|
||||
normalizedPagePath = normalizePagePath(page)
|
||||
|
@ -167,48 +176,69 @@ export default function onDemandEntryHandler(
|
|||
pageUrl = pageUrl === '' ? '/' : pageUrl
|
||||
|
||||
const bundleFile = normalizePagePath(pageUrl)
|
||||
const serverBundlePath = posix.join('pages', bundleFile)
|
||||
const clientBundlePath = posix.join('pages', bundleFile)
|
||||
const bundlePath = posix.join('pages', bundleFile)
|
||||
const absolutePagePath = pagePath.startsWith('next/dist/pages')
|
||||
? require.resolve(pagePath)
|
||||
: join(pagesDir, pagePath)
|
||||
|
||||
page = posix.normalize(pageUrl)
|
||||
const normalizedPage = normalizePathSep(page)
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// Makes sure the page that is being kept in on-demand-entries matches the webpack output
|
||||
const normalizedPage = normalizePathSep(page)
|
||||
const entryInfo = entries[normalizedPage]
|
||||
const isApiRoute = normalizedPage.match(API_ROUTE)
|
||||
|
||||
if (entryInfo) {
|
||||
if (entryInfo.status === BUILT) {
|
||||
let entriesChanged = false
|
||||
const addPageEntry = (type: 'client' | '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}${normalizedPage}`
|
||||
const entryInfo = entries[pageKey]
|
||||
|
||||
if (entryInfo) {
|
||||
entryInfo.lastActiveTime = Date.now()
|
||||
if (entryInfo.status === BUILT) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
doneCallbacks!.once(pageKey, handleCallback)
|
||||
return
|
||||
}
|
||||
|
||||
entriesChanged = true
|
||||
|
||||
entries[pageKey] = {
|
||||
bundlePath,
|
||||
absolutePagePath,
|
||||
status: ADDED,
|
||||
lastActiveTime: Date.now(),
|
||||
}
|
||||
doneCallbacks!.once(pageKey, handleCallback)
|
||||
|
||||
function handleCallback(err: Error) {
|
||||
if (err) return reject(err)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (entryInfo.status === BUILDING) {
|
||||
doneCallbacks!.once(normalizedPage, handleCallback)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Log.event(`build page: ${normalizedPage}`)
|
||||
|
||||
entries[normalizedPage] = {
|
||||
serverBundlePath,
|
||||
clientBundlePath,
|
||||
absolutePagePath,
|
||||
status: ADDED,
|
||||
}
|
||||
doneCallbacks!.once(normalizedPage, handleCallback)
|
||||
const promise = isApiRoute
|
||||
? addPageEntry('server')
|
||||
: clientOnly
|
||||
? addPageEntry('client')
|
||||
: Promise.all([addPageEntry('client'), addPageEntry('server')])
|
||||
|
||||
if (entriesChanged) {
|
||||
Log.event(
|
||||
isApiRoute
|
||||
? `build page: ${normalizedPage} (server only)`
|
||||
: clientOnly
|
||||
? `build page: ${normalizedPage} (client only)`
|
||||
: `build page: ${normalizedPage}`
|
||||
)
|
||||
invalidator.invalidate()
|
||||
}
|
||||
|
||||
function handleCallback(err: Error) {
|
||||
if (err) return reject(err)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
return promise
|
||||
},
|
||||
|
||||
middleware(req: IncomingMessage, res: ServerResponse, next: Function) {
|
||||
|
@ -234,8 +264,8 @@ export default function onDemandEntryHandler(
|
|||
}
|
||||
|
||||
function disposeInactiveEntries(
|
||||
watcher: any,
|
||||
lastAccessPages: any,
|
||||
_watcher: any,
|
||||
lastClientAccessPages: any,
|
||||
maxInactiveAge: number
|
||||
) {
|
||||
const disposingPages: any = []
|
||||
|
@ -250,7 +280,7 @@ function disposeInactiveEntries(
|
|||
// 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 (lastAccessPages.includes(page)) return
|
||||
if (lastClientAccessPages.includes(page)) return
|
||||
|
||||
if (lastActiveTime && Date.now() - lastActiveTime > maxInactiveAge) {
|
||||
disposingPages.push(page)
|
||||
|
@ -262,7 +292,7 @@ function disposeInactiveEntries(
|
|||
delete entries[page]
|
||||
})
|
||||
// disposing inactive page(s)
|
||||
watcher.invalidate()
|
||||
// watcher.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue