diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 24d92156f690941..a63780edc904442 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -64,7 +64,7 @@ import createSpinner from './spinner' import { collectPages, getPageSizeInKb, - hasCustomAppGetInitialProps, + hasCustomGetInitialProps, isPageStatic, PageInfo, printCustomRoutes, @@ -229,6 +229,7 @@ export default async function build(dir: string, conf = null): Promise { const hasPages404 = Boolean( mappedPages['/404'] && mappedPages['/404'].startsWith('private-next-pages') ) + let hasNonStaticErrorPage: boolean if (hasPublicDir) { try { @@ -456,6 +457,24 @@ export default async function build(dir: string, conf = null): Promise { staticCheckWorkers.getStdout().pipe(process.stdout) staticCheckWorkers.getStderr().pipe(process.stderr) + const runtimeEnvConfig = { + publicRuntimeConfig: config.publicRuntimeConfig, + serverRuntimeConfig: config.serverRuntimeConfig, + } + + hasNonStaticErrorPage = + hasCustomErrorPage && + (await hasCustomGetInitialProps( + path.join( + distDir, + ...(isLikeServerless + ? ['serverless', 'pages'] + : ['server', 'static', buildId, 'pages']), + '_error.js' + ), + runtimeEnvConfig + )) + const analysisBegin = process.hrtime() await Promise.all( pageKeys.map(async page => { @@ -485,14 +504,10 @@ export default async function build(dir: string, conf = null): Promise { pagesManifest[page] = bundleRelative.replace(/\\/g, '/') - const runtimeEnvConfig = { - publicRuntimeConfig: config.publicRuntimeConfig, - serverRuntimeConfig: config.serverRuntimeConfig, - } const nonReservedPage = !page.match(/^\/(_app|_error|_document|api)/) if (nonReservedPage && customAppGetInitialProps === undefined) { - customAppGetInitialProps = hasCustomAppGetInitialProps( + customAppGetInitialProps = hasCustomGetInitialProps( isLikeServerless ? serverBundle : path.join( @@ -618,7 +633,7 @@ export default async function build(dir: string, conf = null): Promise { // Since custom _app.js can wrap the 404 page we have to opt-out of static optimization if it has getInitialProps // Only export the static 404 when there is no /_error present const useStatic404 = - !customAppGetInitialProps && (!hasCustomErrorPage || hasPages404) + !customAppGetInitialProps && (!hasNonStaticErrorPage || hasPages404) if (invalidPages.size > 0) { throw new Error( @@ -907,6 +922,7 @@ export default async function build(dir: string, conf = null): Promise { distPath: distDir, buildId: buildId, pagesDir, + useStatic404, pageExtensions: config.pageExtensions, buildManifest, isModern: config.experimental.modern, diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index e0e9ae3d605ffa7..999f29c56ad1eee 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -57,6 +57,7 @@ export async function printTreeView( pageExtensions, buildManifest, isModern, + useStatic404, }: { distPath: string buildId: string @@ -64,6 +65,7 @@ export async function printTreeView( pageExtensions: string[] buildManifest: BuildManifestShape isModern: boolean + useStatic404: boolean } ) { const getPrettySize = (_size: number): string => { @@ -87,6 +89,14 @@ export async function printTreeView( const hasCustomApp = await findPageFile(pagesDir, '/_app', pageExtensions) const hasCustomError = await findPageFile(pagesDir, '/_error', pageExtensions) + if (useStatic404) { + pageInfos.set('/404', { + ...(pageInfos.get('/404') || pageInfos.get('/_error')), + static: true, + } as any) + list = [...list, '/404'] + } + const pageList = list .slice() .filter( @@ -720,14 +730,14 @@ export async function isPageStatic( } } -export function hasCustomAppGetInitialProps( - _appBundle: string, +export function hasCustomGetInitialProps( + bundle: string, runtimeEnvConfig: any ): boolean { require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig) - let mod = require(_appBundle) + let mod = require(bundle) - if (_appBundle.endsWith('_app.js')) { + if (bundle.endsWith('_app.js') || bundle.endsWith('_error.js')) { mod = mod.default || mod } else { // since we don't output _app in serverless mode get it from a page diff --git a/packages/next/pages/_error.tsx b/packages/next/pages/_error.tsx index e52e207c19ebd25..1a9718984210754 100644 --- a/packages/next/pages/_error.tsx +++ b/packages/next/pages/_error.tsx @@ -14,20 +14,23 @@ export type ErrorProps = { title?: string } +function _getInitialProps({ + res, + err, +}: NextPageContext): Promise | ErrorProps { + const statusCode = + res && res.statusCode ? res.statusCode : err ? err.statusCode! : 404 + return { statusCode } +} + /** * `Error` component used for handling errors. */ export default class Error

extends React.Component

{ static displayName = 'ErrorPage' - static getInitialProps({ - res, - err, - }: NextPageContext): Promise | ErrorProps { - const statusCode = - res && res.statusCode ? res.statusCode : err ? err.statusCode! : 404 - return { statusCode } - } + static getInitialProps = _getInitialProps + static origGetInitialProps = _getInitialProps render() { const { statusCode } = this.props diff --git a/test/integration/static-404/pages/_error.js b/test/integration/404-page-custom-error/pages/_error.js similarity index 100% rename from test/integration/static-404/pages/_error.js rename to test/integration/404-page-custom-error/pages/_error.js diff --git a/test/integration/404-page-custom-error/pages/err.js b/test/integration/404-page-custom-error/pages/err.js new file mode 100644 index 000000000000000..6d0f2c17817a3a7 --- /dev/null +++ b/test/integration/404-page-custom-error/pages/err.js @@ -0,0 +1,5 @@ +const page = () => 'err page' +page.getInitialProps = () => { + throw new Error('oops') +} +export default page diff --git a/test/integration/404-page-custom-error/pages/index.js b/test/integration/404-page-custom-error/pages/index.js new file mode 100644 index 000000000000000..f6c15d1f66e8a6d --- /dev/null +++ b/test/integration/404-page-custom-error/pages/index.js @@ -0,0 +1 @@ +export default () => 'hello from index' diff --git a/test/integration/404-page-custom-error/test/index.test.js b/test/integration/404-page-custom-error/test/index.test.js new file mode 100644 index 000000000000000..3caf5cd042de74f --- /dev/null +++ b/test/integration/404-page-custom-error/test/index.test.js @@ -0,0 +1,127 @@ +/* eslint-env jest */ +/* global jasmine */ +import fs from 'fs-extra' +import { join } from 'path' +import { + killApp, + findPort, + launchApp, + nextStart, + nextBuild, + renderViaHTTP, + fetchViaHTTP, +} from 'next-test-utils' + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 2 + +const appDir = join(__dirname, '../') +const nextConfig = join(appDir, 'next.config.js') + +let appPort +let buildId +let app + +const runTests = mode => { + const isDev = mode === 'dev' + + it('should respond to 404 correctly', async () => { + const res = await fetchViaHTTP(appPort, '/404') + expect(res.status).toBe(404) + expect(await res.text()).toContain('This page could not be found') + }) + + it('should render error correctly', async () => { + const text = await renderViaHTTP(appPort, '/err') + expect(text).toContain(isDev ? 'oops' : 'Internal Server Error') + }) + + it('should render index page normal', async () => { + const html = await renderViaHTTP(appPort, '/') + expect(html).toContain('hello from index') + }) + + if (!isDev) { + it('should set pages404 in routes-manifest correctly', async () => { + const data = await fs.readJSON(join(appDir, '.next/routes-manifest.json')) + expect(data.pages404).toBe(true) + }) + + it('should have output 404.html', async () => { + expect( + await fs + .access( + join( + appDir, + '.next', + ...(mode === 'server' + ? ['server', 'static', buildId, 'pages'] + : ['serverless', 'pages']), + '404.html' + ) + ) + .then(() => true) + .catch(() => false) + ) + }) + } +} + +describe('Default 404 Page with custom _error', () => { + describe('server mode', () => { + afterAll(() => killApp(app)) + + it('should build successfully', async () => { + const { code } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + + appPort = await findPort() + + app = await nextStart(appDir, appPort) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + runTests('server') + }) + + describe('serverless mode', () => { + afterAll(async () => { + await fs.remove(nextConfig) + await killApp(app) + }) + + it('should build successfully', async () => { + await fs.writeFile( + nextConfig, + ` + module.exports = { target: 'experimental-serverless-trace' } + ` + ) + const { code } = await nextBuild(appDir, [], { + stderr: true, + stdout: true, + }) + + expect(code).toBe(0) + + appPort = await findPort() + app = await nextStart(appDir, appPort) + buildId = await fs.readFile(join(appDir, '.next/BUILD_ID'), 'utf8') + }) + + runTests('serverless') + }) + + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + runTests('dev') + }) +}) diff --git a/test/integration/static-404/next.config.js b/test/integration/static-404/next.config.js deleted file mode 100644 index 5e70488e69d7d4f..000000000000000 --- a/test/integration/static-404/next.config.js +++ /dev/null @@ -1,4 +0,0 @@ -module.exports = { - generateBuildId: () => 'test-id', - experimental: { static404: true }, -} diff --git a/test/integration/static-404/test/index.test.js b/test/integration/static-404/test/index.test.js index 2d100d3cdec8a60..ad612bdd5c1d3da 100644 --- a/test/integration/static-404/test/index.test.js +++ b/test/integration/static-404/test/index.test.js @@ -63,8 +63,20 @@ describe('Static 404 page', () => { ).toBe(true) }) - it('should not export 404 page with custom _error', async () => { - await fs.writeFile(errorPage, `export { default } from 'next/error'`) + it('should not export 404 page with custom _error GIP', async () => { + await fs.writeFile( + errorPage, + ` + import Error from 'next/error' + export default class MyError extends Error { + static getInitialProps() { + return { + statusCode: 404 + } + } + } + ` + ) await nextBuild(appDir) await fs.remove(errorPage) expect(await fs.exists(static404)).toBe(false)