Skip to content

Commit

Permalink
Fix HMR when custom _app or _document is removed (#28227)
Browse files Browse the repository at this point in the history
This adds the fallback webpack alias handling to handle a custom `_app` or `_document` being removed in development gracefully. 
 
## Bug

- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Errors have helpful link attached, see `contributing.md`

Fixes: #27888
  • Loading branch information
ijjk committed Aug 18, 2021
1 parent 51559f5 commit 17d7e59
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 9 deletions.
52 changes: 43 additions & 9 deletions packages/next/build/webpack-config.ts
Expand Up @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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: {
Expand Down
8 changes: 8 additions & 0 deletions test/integration/app-document-remove-hmr/pages/_app.js
@@ -0,0 +1,8 @@
export default function MyApp({ Component, pageProps }) {
return (
<>
<p>custom _app</p>
<Component {...pageProps} />
</>
)
}
23 changes: 23 additions & 0 deletions 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 (
<Html>
<Head />
<body>
<p>custom _document</p>
<Main />
<NextScript />
</body>
</Html>
)
}
}

export default MyDocument
3 changes: 3 additions & 0 deletions test/integration/app-document-remove-hmr/pages/index.js
@@ -0,0 +1,3 @@
export default function Page() {
return <p>index page</p>
}
128 changes: 128 additions & 0 deletions 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 <p>index page updated</p>
}
`
)

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 <p>index page updated</p>
}
`
)

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

0 comments on commit 17d7e59

Please sign in to comment.