diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 4e85f9b13df83bb..0a5c5bc90c598a6 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -413,6 +413,28 @@ export default async function getBaseWebpackConfig( resolvedBaseUrl = path.resolve(dir, jsConfig.compilerOptions.baseUrl) } + let customAppFile: string | null = await findPageFile( + pagesDir, + '/_app', + config.pageExtensions + ) + let customAppFileExt = customAppFile ? path.extname(customAppFile) : null + if (customAppFile) { + customAppFile = path.resolve(path.join(pagesDir, customAppFile)) + } + + let customDocumentFile: string | null = await findPageFile( + pagesDir, + '/_document', + config.pageExtensions + ) + let customDocumentFileExt = customDocumentFile + ? path.extname(customDocumentFile) + : null + if (customDocumentFile) { + customDocumentFile = path.resolve(path.join(pagesDir, customDocumentFile)) + } + function getReactProfilingInProduction() { if (reactProductionProfiling) { return { @@ -454,6 +476,27 @@ export default async function getBaseWebpackConfig( ], alias: { next: NEXT_PROJECT_ROOT, + + // fallback to default _app when custom is removed + ...(dev && customAppFileExt && isWebpack5 + ? { + [`${PAGES_DIR_ALIAS}/_app${customAppFileExt}`]: [ + path.join(pagesDir, `_app${customAppFileExt}`), + 'next/dist/pages/_app.js', + ], + } + : {}), + + // fallback to default _document when custom is removed + ...(dev && customDocumentFileExt && isWebpack5 + ? { + [`${PAGES_DIR_ALIAS}/_document${customDocumentFileExt}`]: [ + path.join(pagesDir, `_document${customDocumentFileExt}`), + 'next/dist/pages/_document.js', + ], + } + : {}), + [PAGES_DIR_ALIAS]: pagesDir, [DOT_NEXT_ALIAS]: distDir, ...getOptimizedAliases(isServer), @@ -647,15 +690,6 @@ export default async function getBaseWebpackConfig( const crossOrigin = config.crossOrigin - let customAppFile: string | null = await findPageFile( - pagesDir, - '/_app', - config.pageExtensions - ) - if (customAppFile) { - customAppFile = path.resolve(path.join(pagesDir, customAppFile)) - } - const conformanceConfig = Object.assign( { ReactSyncScriptsConformanceCheck: { diff --git a/test/integration/app-document-remove-hmr/pages/_app.js b/test/integration/app-document-remove-hmr/pages/_app.js new file mode 100644 index 000000000000000..11e56041b984b4e --- /dev/null +++ b/test/integration/app-document-remove-hmr/pages/_app.js @@ -0,0 +1,8 @@ +export default function MyApp({ Component, pageProps }) { + return ( + <> +

custom _app

+ + + ) +} diff --git a/test/integration/app-document-remove-hmr/pages/_document.js b/test/integration/app-document-remove-hmr/pages/_document.js new file mode 100644 index 000000000000000..8a14e8e67143036 --- /dev/null +++ b/test/integration/app-document-remove-hmr/pages/_document.js @@ -0,0 +1,23 @@ +import Document, { Html, Head, Main, NextScript } from 'next/document' + +class MyDocument extends Document { + static async getInitialProps(ctx) { + const initialProps = await Document.getInitialProps(ctx) + return { ...initialProps } + } + + render() { + return ( + + + +

custom _document

+
+ + + + ) + } +} + +export default MyDocument diff --git a/test/integration/app-document-remove-hmr/pages/index.js b/test/integration/app-document-remove-hmr/pages/index.js new file mode 100644 index 000000000000000..08263e34c35fd22 --- /dev/null +++ b/test/integration/app-document-remove-hmr/pages/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return

index page

+} diff --git a/test/integration/app-document-remove-hmr/test/index.test.js b/test/integration/app-document-remove-hmr/test/index.test.js new file mode 100644 index 000000000000000..dd5b850ede58673 --- /dev/null +++ b/test/integration/app-document-remove-hmr/test/index.test.js @@ -0,0 +1,128 @@ +/* eslint-env jest */ + +import fs from 'fs-extra' +import { join } from 'path' +import webdriver from 'next-webdriver' +import { killApp, findPort, launchApp, check } from 'next-test-utils' + +jest.setTimeout(1000 * 60 * 2) + +const appDir = join(__dirname, '../') +const appPage = join(appDir, 'pages/_app.js') +const indexPage = join(appDir, 'pages/index.js') +const documentPage = join(appDir, 'pages/_document.js') + +let appPort +let app + +describe('_app removal HMR', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + }) + afterAll(() => killApp(app)) + + it('should HMR when _app is removed', async () => { + let indexContent = await fs.readFile(indexPage) + try { + const browser = await webdriver(appPort, '/') + + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('custom _app') + + await fs.rename(appPage, appPage + '.bak') + + await check(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + return html.includes('index page') && !html.includes('custom _app') + ? 'success' + : html + }, 'success') + + await fs.writeFile( + indexPage, + ` + export default function Page() { + return

index page updated

+ } + ` + ) + + await check(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + return html.indexOf('index page updated') && + !html.includes('custom _app') + ? 'success' + : html + }, 'success') + + await fs.rename(appPage + '.bak', appPage) + + await check(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + return html.includes('index page updated') && + html.includes('custom _app') + ? 'success' + : html + }, 'success') + } finally { + await fs.writeFile(indexPage, indexContent) + + if (await fs.pathExists(appPage + '.bak')) { + await fs.rename(appPage + '.bak', appPage) + } + } + }) + + it('should HMR when _document is removed', async () => { + let indexContent = await fs.readFile(indexPage) + try { + const browser = await webdriver(appPort, '/') + + const html = await browser.eval('document.documentElement.innerHTML') + expect(html).toContain('custom _document') + + await fs.rename(documentPage, documentPage + '.bak') + + await check(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + return html.includes('index page') && !html.includes('custom _document') + ? 'success' + : html + }, 'success') + + await fs.writeFile( + indexPage, + ` + export default function Page() { + return

index page updated

+ } + ` + ) + + await check(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + return html.indexOf('index page updated') && + !html.includes('custom _document') + ? 'success' + : html + }, 'success') + + await fs.rename(documentPage + '.bak', documentPage) + + await check(async () => { + const html = await browser.eval('document.documentElement.innerHTML') + return html.includes('index page updated') && + html.includes('custom _document') + ? 'success' + : html + }, 'success') + } finally { + await fs.writeFile(indexPage, indexContent) + + if (await fs.pathExists(documentPage + '.bak')) { + await fs.rename(documentPage + '.bak', documentPage) + } + } + }) +})