Warn build on duplicate pages (#8646)
* Fail build on duplicate pages This will fail the `next build` command when a duplicate page is found. In development, we'll emit a warning instead of crashing the dev server. * Add test for warning in development * Only issue a warning * Fix production test * Fix development test * Remove useless arg * Warn in development, too
This commit is contained in:
parent
594851111b
commit
55369051f5
6 changed files with 95 additions and 14 deletions
|
@ -1,8 +1,10 @@
|
|||
import { isTargetLikeServerless } from '../next-server/server/config'
|
||||
import chalk from 'chalk'
|
||||
import { join } from 'path'
|
||||
import { stringify } from 'querystring'
|
||||
|
||||
import { API_ROUTE, DOT_NEXT_ALIAS, PAGES_DIR_ALIAS } from '../lib/constants'
|
||||
import { isTargetLikeServerless } from '../next-server/server/config'
|
||||
import { warn } from './output/log'
|
||||
import { ServerlessLoaderQuery } from './webpack/loaders/next-serverless-loader'
|
||||
|
||||
type PagesMapping = {
|
||||
|
@ -13,6 +15,7 @@ export function createPagesMapping(
|
|||
pagePaths: string[],
|
||||
extensions: string[]
|
||||
): PagesMapping {
|
||||
const previousPages: PagesMapping = {}
|
||||
const pages: PagesMapping = pagePaths.reduce(
|
||||
(result: PagesMapping, pagePath): PagesMapping => {
|
||||
let page = `${pagePath
|
||||
|
@ -20,10 +23,20 @@ export function createPagesMapping(
|
|||
.replace(/\\/g, '/')}`.replace(/\/index$/, '')
|
||||
page = page === '/index' ? '/' : page
|
||||
|
||||
result[page === '' ? '/' : page] = join(
|
||||
PAGES_DIR_ALIAS,
|
||||
pagePath
|
||||
).replace(/\\/g, '/')
|
||||
const pageKey = page === '' ? '/' : page
|
||||
|
||||
if (pageKey in result) {
|
||||
warn(
|
||||
`Duplicate page detected. ${chalk.cyan(
|
||||
join('pages', previousPages[pageKey])
|
||||
)} and ${chalk.cyan(
|
||||
join('pages', pagePath)
|
||||
)} both resolve to ${chalk.cyan(pageKey)}.`
|
||||
)
|
||||
} else {
|
||||
previousPages[pageKey] = pagePath
|
||||
}
|
||||
result[pageKey] = join(PAGES_DIR_ALIAS, pagePath).replace(/\\/g, '/')
|
||||
return result
|
||||
},
|
||||
{}
|
||||
|
|
|
@ -94,11 +94,9 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
|
||||
// needed for static exporting since we want to replace with HTML
|
||||
// files
|
||||
const allPagePaths = [...pagePaths]
|
||||
const allStaticPages = new Set<string>()
|
||||
let allPageInfos = new Map<string, PageInfo>()
|
||||
|
||||
const allMappedPages = createPagesMapping(allPagePaths, config.pageExtensions)
|
||||
const mappedPages = createPagesMapping(pagePaths, config.pageExtensions)
|
||||
const entrypoints = createEntrypoints(mappedPages, target, buildId, config)
|
||||
const configs = await Promise.all([
|
||||
|
@ -202,7 +200,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
console.log(chalk.green('Compiled successfully.\n'))
|
||||
backgroundWork.push(
|
||||
recordBuildDuration({
|
||||
totalPageCount: allPagePaths.length,
|
||||
totalPageCount: pagePaths.length,
|
||||
durationInSeconds: webpackBuildEnd[0],
|
||||
})
|
||||
)
|
||||
|
@ -412,9 +410,9 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
backgroundWork.push(
|
||||
recordBuildOptimize({
|
||||
durationInSeconds: analysisEnd[0],
|
||||
totalPageCount: allPagePaths.length,
|
||||
totalPageCount: pagePaths.length,
|
||||
staticPageCount: staticPages.size,
|
||||
ssrPageCount: allPagePaths.length - staticPages.size,
|
||||
ssrPageCount: pagePaths.length - staticPages.size,
|
||||
})
|
||||
)
|
||||
|
||||
|
@ -431,7 +429,7 @@ export default async function build(dir: string, conf = null): Promise<void> {
|
|||
allPageInfos.set(key, info)
|
||||
})
|
||||
|
||||
printTreeView(Object.keys(allMappedPages), allPageInfos, isLikeServerless)
|
||||
printTreeView(Object.keys(mappedPages), allPageInfos, isLikeServerless)
|
||||
|
||||
if (tracer) {
|
||||
const parsedResults = await tracer.profiler.stopProfiling()
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
import { join } from 'path'
|
||||
import chalk from 'chalk'
|
||||
import { isWriteable } from '../../build/is-writeable'
|
||||
import { warn } from '../../build/output/log'
|
||||
|
||||
export async function findPageFile(
|
||||
rootDir: string,
|
||||
normalizedPagePath: string,
|
||||
pageExtensions: string[]
|
||||
): Promise<string | null> {
|
||||
let foundPagePaths: string[] = []
|
||||
|
||||
for (const extension of pageExtensions) {
|
||||
const relativePagePath = `${normalizedPagePath}.${extension}`
|
||||
const pagePath = join(rootDir, relativePagePath)
|
||||
|
||||
if (await isWriteable(pagePath)) {
|
||||
return relativePagePath
|
||||
foundPagePaths.push(relativePagePath)
|
||||
}
|
||||
|
||||
const relativePagePathWithIndex = join(
|
||||
|
@ -20,9 +24,23 @@ export async function findPageFile(
|
|||
)
|
||||
const pagePathWithIndex = join(rootDir, relativePagePathWithIndex)
|
||||
if (await isWriteable(pagePathWithIndex)) {
|
||||
return relativePagePathWithIndex
|
||||
foundPagePaths.push(relativePagePathWithIndex)
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
if (foundPagePaths.length < 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (foundPagePaths.length > 1) {
|
||||
warn(
|
||||
`Duplicate page detected. ${chalk.cyan(
|
||||
join('pages', foundPagePaths[0])
|
||||
)} and ${chalk.cyan(
|
||||
join('pages', foundPagePaths[1])
|
||||
)} both resolve to ${chalk.cyan(normalizedPagePath)}.`
|
||||
)
|
||||
}
|
||||
|
||||
return foundPagePaths[0]
|
||||
}
|
||||
|
|
5
test/integration/duplicate-pages/pages/hello.js
Normal file
5
test/integration/duplicate-pages/pages/hello.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default () => (
|
||||
<>
|
||||
<h3>Hi 👋</h3>
|
||||
</>
|
||||
)
|
5
test/integration/duplicate-pages/pages/hello/index.js
Normal file
5
test/integration/duplicate-pages/pages/hello/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
export default () => (
|
||||
<>
|
||||
<h3>Hi 👋... again</h3>
|
||||
</>
|
||||
)
|
42
test/integration/duplicate-pages/test/index.test.js
Normal file
42
test/integration/duplicate-pages/test/index.test.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
/* eslint-env jest */
|
||||
/* global jasmine */
|
||||
import path from 'path'
|
||||
|
||||
import {
|
||||
nextBuild,
|
||||
findPort,
|
||||
launchApp,
|
||||
renderViaHTTP,
|
||||
killApp,
|
||||
waitFor
|
||||
} from 'next-test-utils'
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000 * 60 * 1
|
||||
const appDir = path.join(__dirname, '..')
|
||||
|
||||
describe('Handles Duplicate Pages', () => {
|
||||
describe('production', () => {
|
||||
it('Throws an error during build', async () => {
|
||||
const { stdout } = await nextBuild(appDir, [], { stdout: true })
|
||||
expect(stdout).toContain('Duplicate page detected')
|
||||
})
|
||||
})
|
||||
|
||||
describe('dev mode', () => {
|
||||
it('Shows warning in development', async () => {
|
||||
let output
|
||||
const handleOutput = msg => {
|
||||
output += msg
|
||||
}
|
||||
const appPort = await findPort()
|
||||
const app = await launchApp(appDir, appPort, {
|
||||
onStdout: handleOutput,
|
||||
onStderr: handleOutput
|
||||
})
|
||||
await renderViaHTTP(appPort, '/hello')
|
||||
await waitFor(3000)
|
||||
await killApp(app)
|
||||
expect(output).toMatch(/Duplicate page detected/)
|
||||
})
|
||||
})
|
||||
})
|
Loading…
Reference in a new issue