Skip to content

Commit

Permalink
Add proper error when conflicting paths are detected (#20918)
Browse files Browse the repository at this point in the history
This helps catch conflicting paths returned from `getStaticPaths` with a friendly error

<details>

<summary>
Preview of error
</summary>


<img width="962" alt="Screen Shot 2021-01-08 at 5 03 04 PM" src="https://user-images.githubusercontent.com/22380829/104074719-6e481100-51d6-11eb-9397-938aee3ae30b.png">
<img width="962" alt="Screen Shot 2021-01-08 at 5 03 41 PM" src="https://user-images.githubusercontent.com/22380829/104074722-6f793e00-51d6-11eb-90f6-7cdf9882bf00.png">

</details>




Closes: #19527
  • Loading branch information
ijjk committed Jan 11, 2021
1 parent e0a44d9 commit 9caca27
Show file tree
Hide file tree
Showing 4 changed files with 338 additions and 0 deletions.
68 changes: 68 additions & 0 deletions errors/conflicting-ssg-paths.md
@@ -0,0 +1,68 @@
# Conflicting SSG Paths

#### Why This Error Occurred

In your `getStaticPaths` function for one of your pages you returned conflicting paths. All page paths must be unique and duplicates are not allowed.

#### Possible Ways to Fix It

Remove any conflicting paths shown in the error message and only return them from one `getStaticPaths`.

Example conflicting paths:

```jsx
// pages/hello/world.js
export default function Hello() {
return 'hello world!'
}

// pages/[...catchAll].js
export const getStaticProps = () => ({ props: {} })

export const getStaticPaths = () => ({
paths: [
// this conflicts with the /hello/world.js page, remove to resolve error
'/hello/world',
'/another',
],
fallback: false,
})

export default function CatchAll() {
return 'Catch-all page'
}
```

Example conflicting paths:

```jsx
// pages/blog/[slug].js
export const getStaticPaths = () => ({
paths: ['/blog/conflicting', '/blog/another'],
fallback: false,
})

export default function Blog() {
return 'Blog!'
}

// pages/[...catchAll].js
export const getStaticProps = () => ({ props: {} })

export const getStaticPaths = () => ({
paths: [
// this conflicts with the /blog/conflicting path above, remove to resolve error
'/blog/conflicting',
'/another',
],
fallback: false,
})

export default function CatchAll() {
return 'Catch-all page'
}
```

### Useful Links

- [`getStaticPaths` Documentation](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation)
10 changes: 10 additions & 0 deletions packages/next/build/index.ts
Expand Up @@ -72,6 +72,7 @@ import createSpinner from './spinner'
import { traceAsyncFn, traceFn, tracer } from './tracer'
import {
collectPages,
detectConflictingPaths,
getJsPageSizeInKb,
getNamedExports,
hasCustomGetInitialProps,
Expand Down Expand Up @@ -870,6 +871,15 @@ export default async function build(
await traceAsyncFn(tracer.startSpan('static-generation'), async () => {
if (staticPages.size > 0 || ssgPages.size > 0 || useStatic404) {
const combinedPages = [...staticPages, ...ssgPages]

detectConflictingPaths(
[
...combinedPages,
...pageKeys.filter((page) => !combinedPages.includes(page)),
],
ssgPages,
additionalSsgPaths
)
const exportApp = require('../export').default
const exportOptions = {
silent: false,
Expand Down
77 changes: 77 additions & 0 deletions packages/next/build/utils.ts
Expand Up @@ -28,6 +28,7 @@ import { BuildManifest } from '../next-server/server/get-page-files'
import { removePathTrailingSlash } from '../client/normalize-trailing-slash'
import { UnwrapPromise } from '../lib/coalesced-function'
import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'
import * as Log from './output/log'
import opentelemetryApi from '@opentelemetry/api'
import { tracer, traceAsyncFn } from './tracer'

Expand Down Expand Up @@ -879,3 +880,79 @@ export function getNamedExports(
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
return Object.keys(require(bundle))
}

export function detectConflictingPaths(
combinedPages: string[],
ssgPages: Set<string>,
additionalSsgPaths: Map<string, string[]>
) {
const conflictingPaths = new Map<
string,
Array<{
path: string
page: string
}>
>()

const dynamicSsgPages = [...ssgPages].filter((page) => isDynamicRoute(page))

additionalSsgPaths.forEach((paths, pathsPage) => {
paths.forEach((curPath) => {
const lowerPath = curPath.toLowerCase()
let conflictingPage = combinedPages.find(
(page) => page.toLowerCase() === lowerPath
)

if (conflictingPage) {
conflictingPaths.set(lowerPath, [
{ path: curPath, page: pathsPage },
{ path: conflictingPage, page: conflictingPage },
])
} else {
let conflictingPath: string | undefined

conflictingPage = dynamicSsgPages.find((page) => {
if (page === pathsPage) return false

conflictingPath = additionalSsgPaths
.get(page)
?.find((compPath) => compPath.toLowerCase() === lowerPath)
return conflictingPath
})

if (conflictingPage && conflictingPath) {
conflictingPaths.set(lowerPath, [
{ path: curPath, page: pathsPage },
{ path: conflictingPath, page: conflictingPage },
])
}
}
})
})

if (conflictingPaths.size > 0) {
let conflictingPathsOutput = ''

conflictingPaths.forEach((pathItems) => {
pathItems.forEach((pathItem, idx) => {
const isDynamic = pathItem.page !== pathItem.path

if (idx > 0) {
conflictingPathsOutput += 'conflicts with '
}

conflictingPathsOutput += `path: "${pathItem.path}"${
isDynamic ? ` from page: "${pathItem.page}" ` : ' '
}`
})
conflictingPathsOutput += '\n'
})

Log.error(
'Conflicting paths returned from getStaticPaths, paths must unique per page.\n' +
'See more info here: https://err.sh/next.js/conflicting-ssg-paths\n\n' +
conflictingPathsOutput
)
process.exit(1)
}
}
183 changes: 183 additions & 0 deletions test/integration/conflicting-ssg-paths/test/index.test.js
@@ -0,0 +1,183 @@
/* eslint-env jest */

import fs from 'fs-extra'
import { join } from 'path'
import { nextBuild } from 'next-test-utils'

jest.setTimeout(1000 * 60 * 1)

const appDir = join(__dirname, '../')
const pagesDir = join(appDir, 'pages')

describe('Conflicting SSG paths', () => {
afterEach(() => fs.remove(pagesDir))

it('should show proper error when two dynamic SSG routes have conflicting paths', async () => {
await fs.ensureDir(join(pagesDir, 'blog'))
await fs.writeFile(
join(pagesDir, 'blog/[slug].js'),
`
export const getStaticProps = () => {
return {
props: {}
}
}
export const getStaticPaths = () => {
return {
paths: [
'/blog/conflicting',
'/blog/first'
],
fallback: false
}
}
export default function Page() {
return '/blog/[slug]'
}
`
)

await fs.writeFile(
join(pagesDir, '[...catchAll].js'),
`
export const getStaticProps = () => {
return {
props: {}
}
}
export const getStaticPaths = () => {
return {
paths: [
'/blog/conflicting',
'/hello/world'
],
fallback: false
}
}
export default function Page() {
return '/[catchAll]'
}
`
)

const result = await nextBuild(appDir, undefined, {
stdout: true,
stderr: true,
})
const output = result.stdout + result.stderr
expect(output).toContain(
'Conflicting paths returned from getStaticPaths, paths must unique per page'
)
expect(output).toContain('err.sh/next.js/conflicting-ssg-paths')
expect(output).toContain(
`path: "/blog/conflicting" from page: "/[...catchAll]"`
)
expect(output).toContain(`conflicts with path: "/blog/conflicting"`)
})

it('should show proper error when a dynamic SSG route conflicts with normal route', async () => {
await fs.ensureDir(join(pagesDir, 'hello'))
await fs.writeFile(
join(pagesDir, 'hello/world.js'),
`
export default function Page() {
return '/hello/world'
}
`
)

await fs.writeFile(
join(pagesDir, '[...catchAll].js'),
`
export const getStaticProps = () => {
return {
props: {}
}
}
export const getStaticPaths = () => {
return {
paths: [
'/hello',
'/hellO/world'
],
fallback: false
}
}
export default function Page() {
return '/[catchAll]'
}
`
)

const result = await nextBuild(appDir, undefined, {
stdout: true,
stderr: true,
})
const output = result.stdout + result.stderr
expect(output).toContain(
'Conflicting paths returned from getStaticPaths, paths must unique per page'
)
expect(output).toContain('err.sh/next.js/conflicting-ssg-paths')
expect(output).toContain(
`path: "/hellO/world" from page: "/[...catchAll]" conflicts with path: "/hello/world"`
)
})

it('should show proper error when a dynamic SSG route conflicts with SSR route', async () => {
await fs.ensureDir(join(pagesDir, 'hello'))
await fs.writeFile(
join(pagesDir, 'hello/world.js'),
`
export const getServerSideProps = () => ({ props: {} })
export default function Page() {
return '/hello/world'
}
`
)

await fs.writeFile(
join(pagesDir, '[...catchAll].js'),
`
export const getStaticProps = () => {
return {
props: {}
}
}
export const getStaticPaths = () => {
return {
paths: [
'/hello',
'/hellO/world'
],
fallback: false
}
}
export default function Page() {
return '/[catchAll]'
}
`
)

const result = await nextBuild(appDir, undefined, {
stdout: true,
stderr: true,
})
const output = result.stdout + result.stderr
expect(output).toContain(
'Conflicting paths returned from getStaticPaths, paths must unique per page'
)
expect(output).toContain('err.sh/next.js/conflicting-ssg-paths')
expect(output).toContain(
`path: "/hellO/world" from page: "/[...catchAll]" conflicts with path: "/hello/world"`
)
})
})

0 comments on commit 9caca27

Please sign in to comment.