Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure dev overlay is triggered for more _app/_document errors #24328

Merged
merged 7 commits into from
Apr 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/next/build/webpack-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export default async function getBaseWebpackConfig(
reactProductionProfiling = false,
entrypoints,
rewrites,
isDevFallback = false,
}: {
buildId: string
config: NextConfig
Expand All @@ -199,6 +200,7 @@ export default async function getBaseWebpackConfig(
reactProductionProfiling?: boolean
entrypoints: WebpackEntrypoints
rewrites: CustomRoutes['rewrites']
isDevFallback?: boolean
}
): Promise<webpack.Configuration> {
let plugins: PluginMetaData[] = []
Expand Down Expand Up @@ -916,7 +918,9 @@ export default async function getBaseWebpackConfig(
? isWebpack5 && !dev
? '../[name].js'
: '[name].js'
: `static/chunks/[name]${dev ? '' : '-[chunkhash]'}.js`,
: `static/chunks/${isDevFallback ? 'fallback/' : ''}[name]${
dev ? '' : '-[chunkhash]'
}.js`,
library: isServer ? undefined : '_N_E',
libraryTarget: isServer ? 'commonjs2' : 'assign',
hotUpdateChunkFilename: isWebpack5
Expand All @@ -928,7 +932,9 @@ export default async function getBaseWebpackConfig(
// This saves chunks with the name given via `import()`
chunkFilename: isServer
? `${dev ? '[name]' : '[name].[contenthash]'}.js`
: `static/chunks/${dev ? '[name]' : '[name].[contenthash]'}.js`,
: `static/chunks/${isDevFallback ? 'fallback/' : ''}${
dev ? '[name]' : '[name].[contenthash]'
}.js`,
strictModuleExceptionHandling: true,
crossOriginLoading: crossOrigin,
futureEmitAssets: !dev,
Expand Down Expand Up @@ -1188,6 +1194,7 @@ export default async function getBaseWebpackConfig(
new BuildManifestPlugin({
buildId,
rewrites,
isDevFallback,
}),
!dev &&
!isServer &&
Expand Down
62 changes: 36 additions & 26 deletions packages/next/build/webpack/plugins/build-manifest-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,13 +95,15 @@ const processRoute = (r: Rewrite) => {
export default class BuildManifestPlugin {
private buildId: string
private rewrites: CustomRoutes['rewrites']
private isDevFallback: boolean

constructor(options: {
buildId: string
rewrites: CustomRoutes['rewrites']
isDevFallback?: boolean
}) {
this.buildId = options.buildId

this.isDevFallback = !!options.isDevFallback
this.rewrites = {
beforeFiles: [],
afterFiles: [],
Expand Down Expand Up @@ -165,7 +167,6 @@ export default class BuildManifestPlugin {

for (const entrypoint of compilation.entrypoints.values()) {
if (systemEntrypoints.has(entrypoint.name)) continue

const pagePath = getRouteFromEntrypoint(entrypoint.name)

if (!pagePath) {
Expand All @@ -177,40 +178,49 @@ export default class BuildManifestPlugin {
assetMap.pages[pagePath] = [...new Set([...mainFiles, ...filesForPage])]
}

// Add the runtime build manifest file (generated later in this file)
// as a dependency for the app. If the flag is false, the file won't be
// downloaded by the client.
assetMap.lowPriorityFiles.push(
`${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
)

// Add the runtime ssg manifest file as a lazy-loaded file dependency.
// We also stub this file out for development mode (when it is not
// generated).
const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`

const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js`
assetMap.lowPriorityFiles.push(ssgManifestPath)
assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest)
if (!this.isDevFallback) {
// Add the runtime build manifest file (generated later in this file)
// as a dependency for the app. If the flag is false, the file won't be
// downloaded by the client.
assetMap.lowPriorityFiles.push(
`${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
)
// Add the runtime ssg manifest file as a lazy-loaded file dependency.
// We also stub this file out for development mode (when it is not
// generated).
const srcEmptySsgManifest = `self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`

const ssgManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_ssgManifest.js`
assetMap.lowPriorityFiles.push(ssgManifestPath)
assets[ssgManifestPath] = new sources.RawSource(srcEmptySsgManifest)
}

assetMap.pages = Object.keys(assetMap.pages)
.sort()
// eslint-disable-next-line
.reduce((a, c) => ((a[c] = assetMap.pages[c]), a), {} as any)

assets[BUILD_MANIFEST] = new sources.RawSource(
let buildManifestName = BUILD_MANIFEST

if (this.isDevFallback) {
buildManifestName = `fallback-${BUILD_MANIFEST}`
}

assets[buildManifestName] = new sources.RawSource(
JSON.stringify(assetMap, null, 2)
)

const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`
if (!this.isDevFallback) {
const clientManifestPath = `${CLIENT_STATIC_FILES_PATH}/${this.buildId}/_buildManifest.js`

assets[clientManifestPath] = new sources.RawSource(
`self.__BUILD_MANIFEST = ${generateClientManifest(
compiler,
assetMap,
this.rewrites
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
)
assets[clientManifestPath] = new sources.RawSource(
`self.__BUILD_MANIFEST = ${generateClientManifest(
compiler,
assetMap,
this.rewrites
)};self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()`
)
}

return assets
})
Expand Down
21 changes: 16 additions & 5 deletions packages/next/next-server/server/load-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ export async function loadDefaultErrorComponents(distDir: string) {
App,
Document,
Component,
buildManifest: require(join(distDir, BUILD_MANIFEST)),
reactLoadableManifest: require(join(distDir, REACT_LOADABLE_MANIFEST)),
buildManifest: require(join(distDir, `fallback-${BUILD_MANIFEST}`)),
reactLoadableManifest: {},
ComponentMod,
}
}
Expand Down Expand Up @@ -74,9 +74,20 @@ export async function loadComponents(
} as LoadComponentsReturnType
}

const DocumentMod = await requirePage('/_document', distDir, serverless)
const AppMod = await requirePage('/_app', distDir, serverless)
const ComponentMod = await requirePage(pathname, distDir, serverless)
let DocumentMod
let AppMod
let ComponentMod

try {
DocumentMod = await requirePage('/_document', distDir, serverless)
AppMod = await requirePage('/_app', distDir, serverless)
ComponentMod = await requirePage(pathname, distDir, serverless)
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
throw new Error(`Failed to load ${pathname}`)
}
throw err
}

const [
buildManifest,
Expand Down
3 changes: 3 additions & 0 deletions packages/next/next-server/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import cookie from 'next/dist/compiled/cookie'
import escapePathDelimiters from '../lib/router/utils/escape-path-delimiters'
import { getUtils } from '../../build/webpack/loaders/next-serverless-loader/utils'
import { PreviewData } from 'next/types'
import HotReloader from '../../server/hot-reloader'

const getCustomRouteMatcher = pathMatch(true)

Expand Down Expand Up @@ -2051,6 +2052,8 @@ export default class Server {
res.statusCode = 500

if (this.renderOpts.dev) {
await ((this as any).hotReloader as HotReloader).buildFallbackError()

const fallbackResult = await loadDefaultErrorComponents(this.distDir)
return this.renderToHTMLWithComponents(
req,
Expand Down
49 changes: 43 additions & 6 deletions packages/next/server/hot-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,62 @@ import http from 'http'
export class WebpackHotMiddleware {
eventStream: EventStream
latestStats: webpack.Stats | null
clientLatestStats: webpack.Stats | null
closed: boolean
serverError: boolean

constructor(compiler: webpack.Compiler) {
constructor(compilers: webpack.Compiler[]) {
this.eventStream = new EventStream()
this.latestStats = null
this.clientLatestStats = null
this.serverError = false
this.closed = false

compiler.hooks.invalid.tap('webpack-hot-middleware', this.onInvalid)
compiler.hooks.done.tap('webpack-hot-middleware', this.onDone)
compilers[0].hooks.invalid.tap(
'webpack-hot-middleware',
this.onClientInvalid
)
compilers[0].hooks.done.tap('webpack-hot-middleware', this.onClientDone)

compilers[1].hooks.invalid.tap(
'webpack-hot-middleware',
this.onServerInvalid
)
compilers[1].hooks.done.tap('webpack-hot-middleware', this.onServerDone)
}

onInvalid = () => {
if (this.closed) return
onServerInvalid = () => {
if (!this.serverError) return

this.serverError = false

if (this.clientLatestStats) {
this.latestStats = this.clientLatestStats
this.publishStats('built', this.latestStats)
}
}
onClientInvalid = () => {
if (this.closed || this.serverError) return
this.latestStats = null
this.eventStream.publish({ action: 'building' })
}
onDone = (statsResult: webpack.Stats) => {
onServerDone = (statsResult: webpack.Stats) => {
if (this.closed) return
// Keep hold of latest stats so they can be propagated to new clients
// this.latestStats = statsResult
// this.publishStats('built', this.latestStats)
this.serverError = statsResult.hasErrors()

if (this.serverError) {
this.latestStats = statsResult
this.publishStats('built', this.latestStats)
}
}
onClientDone = (statsResult: webpack.Stats) => {
this.clientLatestStats = statsResult

if (this.closed || this.serverError) return
// Keep hold of latest stats so they can be propagated to new clients
this.latestStats = statsResult
this.publishStats('built', this.latestStats)
}
Expand Down
60 changes: 57 additions & 3 deletions packages/next/server/hot-reloader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export default class HotReloader {
private previewProps: __ApiPreviewProps
private watcher: any
private rewrites: CustomRoutes['rewrites']
private fallbackWatcher: any
public isWebpack5: any

constructor(
Expand Down Expand Up @@ -300,6 +301,51 @@ export default class HotReloader {
])
}

public async buildFallbackError(): Promise<void> {
if (this.fallbackWatcher) return

const fallbackConfig = await getBaseWebpackConfig(this.dir, {
dev: true,
isServer: false,
config: this.config,
buildId: this.buildId,
pagesDir: this.pagesDir,
rewrites: {
beforeFiles: [],
afterFiles: [],
fallback: [],
},
isDevFallback: true,
entrypoints: createEntrypoints(
{
'/_app': 'next/dist/pages/_app',
'/_error': 'next/dist/pages/_error',
},
'server',
this.buildId,
this.previewProps,
this.config,
[]
).client,
})
const fallbackCompiler = webpack(fallbackConfig)

this.fallbackWatcher = await new Promise((resolve) => {
let bootedFallbackCompiler = false
fallbackCompiler.watch(
// @ts-ignore webpack supports an array of watchOptions when using a multiCompiler
fallbackConfig.watchOptions,
// Errors are handled separately
(_err: any) => {
if (!bootedFallbackCompiler) {
bootedFallbackCompiler = true
resolve(true)
}
}
)
})
}

public async start(): Promise<void> {
await this.clean()

Expand Down Expand Up @@ -492,7 +538,7 @@ export default class HotReloader {
)

this.webpackHotMiddleware = new WebpackHotMiddleware(
multiCompiler.compilers[0]
multiCompiler.compilers
)

let booted = false
Expand Down Expand Up @@ -534,9 +580,17 @@ export default class HotReloader {
}

public async stop(): Promise<void> {
return new Promise((resolve, reject) => {
this.watcher.close((err: any) => (err ? reject(err) : resolve()))
await new Promise((resolve, reject) => {
this.watcher.close((err: any) => (err ? reject(err) : resolve(true)))
})

if (this.fallbackWatcher) {
await new Promise((resolve, reject) => {
this.fallbackWatcher.close((err: any) =>
err ? reject(err) : resolve(true)
)
})
}
}

public async getCompilationErrors(page: string) {
Expand Down
Loading