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:
Tobias Koppers 2021-09-30 15:52:26 +02:00 committed by GitHub
parent 5778f9ffb3
commit 59b6967e00
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 119 additions and 72 deletions

View file

@ -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()
})

View file

@ -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) =>

View file

@ -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)
}
}

View file

@ -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()
}
}