Skip to content

Commit 9caca27

Browse files
authored
Add proper error when conflicting paths are detected (vercel#20918)
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: vercel#19527
1 parent e0a44d9 commit 9caca27

File tree

4 files changed

+338
-0
lines changed

4 files changed

+338
-0
lines changed

errors/conflicting-ssg-paths.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# Conflicting SSG Paths
2+
3+
#### Why This Error Occurred
4+
5+
In your `getStaticPaths` function for one of your pages you returned conflicting paths. All page paths must be unique and duplicates are not allowed.
6+
7+
#### Possible Ways to Fix It
8+
9+
Remove any conflicting paths shown in the error message and only return them from one `getStaticPaths`.
10+
11+
Example conflicting paths:
12+
13+
```jsx
14+
// pages/hello/world.js
15+
export default function Hello() {
16+
return 'hello world!'
17+
}
18+
19+
// pages/[...catchAll].js
20+
export const getStaticProps = () => ({ props: {} })
21+
22+
export const getStaticPaths = () => ({
23+
paths: [
24+
// this conflicts with the /hello/world.js page, remove to resolve error
25+
'/hello/world',
26+
'/another',
27+
],
28+
fallback: false,
29+
})
30+
31+
export default function CatchAll() {
32+
return 'Catch-all page'
33+
}
34+
```
35+
36+
Example conflicting paths:
37+
38+
```jsx
39+
// pages/blog/[slug].js
40+
export const getStaticPaths = () => ({
41+
paths: ['/blog/conflicting', '/blog/another'],
42+
fallback: false,
43+
})
44+
45+
export default function Blog() {
46+
return 'Blog!'
47+
}
48+
49+
// pages/[...catchAll].js
50+
export const getStaticProps = () => ({ props: {} })
51+
52+
export const getStaticPaths = () => ({
53+
paths: [
54+
// this conflicts with the /blog/conflicting path above, remove to resolve error
55+
'/blog/conflicting',
56+
'/another',
57+
],
58+
fallback: false,
59+
})
60+
61+
export default function CatchAll() {
62+
return 'Catch-all page'
63+
}
64+
```
65+
66+
### Useful Links
67+
68+
- [`getStaticPaths` Documentation](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation)

packages/next/build/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import createSpinner from './spinner'
7272
import { traceAsyncFn, traceFn, tracer } from './tracer'
7373
import {
7474
collectPages,
75+
detectConflictingPaths,
7576
getJsPageSizeInKb,
7677
getNamedExports,
7778
hasCustomGetInitialProps,
@@ -870,6 +871,15 @@ export default async function build(
870871
await traceAsyncFn(tracer.startSpan('static-generation'), async () => {
871872
if (staticPages.size > 0 || ssgPages.size > 0 || useStatic404) {
872873
const combinedPages = [...staticPages, ...ssgPages]
874+
875+
detectConflictingPaths(
876+
[
877+
...combinedPages,
878+
...pageKeys.filter((page) => !combinedPages.includes(page)),
879+
],
880+
ssgPages,
881+
additionalSsgPaths
882+
)
873883
const exportApp = require('../export').default
874884
const exportOptions = {
875885
silent: false,

packages/next/build/utils.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { BuildManifest } from '../next-server/server/get-page-files'
2828
import { removePathTrailingSlash } from '../client/normalize-trailing-slash'
2929
import { UnwrapPromise } from '../lib/coalesced-function'
3030
import { normalizeLocalePath } from '../next-server/lib/i18n/normalize-locale-path'
31+
import * as Log from './output/log'
3132
import opentelemetryApi from '@opentelemetry/api'
3233
import { tracer, traceAsyncFn } from './tracer'
3334

@@ -879,3 +880,79 @@ export function getNamedExports(
879880
require('../next-server/lib/runtime-config').setConfig(runtimeEnvConfig)
880881
return Object.keys(require(bundle))
881882
}
883+
884+
export function detectConflictingPaths(
885+
combinedPages: string[],
886+
ssgPages: Set<string>,
887+
additionalSsgPaths: Map<string, string[]>
888+
) {
889+
const conflictingPaths = new Map<
890+
string,
891+
Array<{
892+
path: string
893+
page: string
894+
}>
895+
>()
896+
897+
const dynamicSsgPages = [...ssgPages].filter((page) => isDynamicRoute(page))
898+
899+
additionalSsgPaths.forEach((paths, pathsPage) => {
900+
paths.forEach((curPath) => {
901+
const lowerPath = curPath.toLowerCase()
902+
let conflictingPage = combinedPages.find(
903+
(page) => page.toLowerCase() === lowerPath
904+
)
905+
906+
if (conflictingPage) {
907+
conflictingPaths.set(lowerPath, [
908+
{ path: curPath, page: pathsPage },
909+
{ path: conflictingPage, page: conflictingPage },
910+
])
911+
} else {
912+
let conflictingPath: string | undefined
913+
914+
conflictingPage = dynamicSsgPages.find((page) => {
915+
if (page === pathsPage) return false
916+
917+
conflictingPath = additionalSsgPaths
918+
.get(page)
919+
?.find((compPath) => compPath.toLowerCase() === lowerPath)
920+
return conflictingPath
921+
})
922+
923+
if (conflictingPage && conflictingPath) {
924+
conflictingPaths.set(lowerPath, [
925+
{ path: curPath, page: pathsPage },
926+
{ path: conflictingPath, page: conflictingPage },
927+
])
928+
}
929+
}
930+
})
931+
})
932+
933+
if (conflictingPaths.size > 0) {
934+
let conflictingPathsOutput = ''
935+
936+
conflictingPaths.forEach((pathItems) => {
937+
pathItems.forEach((pathItem, idx) => {
938+
const isDynamic = pathItem.page !== pathItem.path
939+
940+
if (idx > 0) {
941+
conflictingPathsOutput += 'conflicts with '
942+
}
943+
944+
conflictingPathsOutput += `path: "${pathItem.path}"${
945+
isDynamic ? ` from page: "${pathItem.page}" ` : ' '
946+
}`
947+
})
948+
conflictingPathsOutput += '\n'
949+
})
950+
951+
Log.error(
952+
'Conflicting paths returned from getStaticPaths, paths must unique per page.\n' +
953+
'See more info here: https://err.sh/next.js/conflicting-ssg-paths\n\n' +
954+
conflictingPathsOutput
955+
)
956+
process.exit(1)
957+
}
958+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/* eslint-env jest */
2+
3+
import fs from 'fs-extra'
4+
import { join } from 'path'
5+
import { nextBuild } from 'next-test-utils'
6+
7+
jest.setTimeout(1000 * 60 * 1)
8+
9+
const appDir = join(__dirname, '../')
10+
const pagesDir = join(appDir, 'pages')
11+
12+
describe('Conflicting SSG paths', () => {
13+
afterEach(() => fs.remove(pagesDir))
14+
15+
it('should show proper error when two dynamic SSG routes have conflicting paths', async () => {
16+
await fs.ensureDir(join(pagesDir, 'blog'))
17+
await fs.writeFile(
18+
join(pagesDir, 'blog/[slug].js'),
19+
`
20+
export const getStaticProps = () => {
21+
return {
22+
props: {}
23+
}
24+
}
25+
26+
export const getStaticPaths = () => {
27+
return {
28+
paths: [
29+
'/blog/conflicting',
30+
'/blog/first'
31+
],
32+
fallback: false
33+
}
34+
}
35+
36+
export default function Page() {
37+
return '/blog/[slug]'
38+
}
39+
`
40+
)
41+
42+
await fs.writeFile(
43+
join(pagesDir, '[...catchAll].js'),
44+
`
45+
export const getStaticProps = () => {
46+
return {
47+
props: {}
48+
}
49+
}
50+
51+
export const getStaticPaths = () => {
52+
return {
53+
paths: [
54+
'/blog/conflicting',
55+
'/hello/world'
56+
],
57+
fallback: false
58+
}
59+
}
60+
61+
export default function Page() {
62+
return '/[catchAll]'
63+
}
64+
`
65+
)
66+
67+
const result = await nextBuild(appDir, undefined, {
68+
stdout: true,
69+
stderr: true,
70+
})
71+
const output = result.stdout + result.stderr
72+
expect(output).toContain(
73+
'Conflicting paths returned from getStaticPaths, paths must unique per page'
74+
)
75+
expect(output).toContain('err.sh/next.js/conflicting-ssg-paths')
76+
expect(output).toContain(
77+
`path: "/blog/conflicting" from page: "/[...catchAll]"`
78+
)
79+
expect(output).toContain(`conflicts with path: "/blog/conflicting"`)
80+
})
81+
82+
it('should show proper error when a dynamic SSG route conflicts with normal route', async () => {
83+
await fs.ensureDir(join(pagesDir, 'hello'))
84+
await fs.writeFile(
85+
join(pagesDir, 'hello/world.js'),
86+
`
87+
export default function Page() {
88+
return '/hello/world'
89+
}
90+
`
91+
)
92+
93+
await fs.writeFile(
94+
join(pagesDir, '[...catchAll].js'),
95+
`
96+
export const getStaticProps = () => {
97+
return {
98+
props: {}
99+
}
100+
}
101+
102+
export const getStaticPaths = () => {
103+
return {
104+
paths: [
105+
'/hello',
106+
'/hellO/world'
107+
],
108+
fallback: false
109+
}
110+
}
111+
112+
export default function Page() {
113+
return '/[catchAll]'
114+
}
115+
`
116+
)
117+
118+
const result = await nextBuild(appDir, undefined, {
119+
stdout: true,
120+
stderr: true,
121+
})
122+
const output = result.stdout + result.stderr
123+
expect(output).toContain(
124+
'Conflicting paths returned from getStaticPaths, paths must unique per page'
125+
)
126+
expect(output).toContain('err.sh/next.js/conflicting-ssg-paths')
127+
expect(output).toContain(
128+
`path: "/hellO/world" from page: "/[...catchAll]" conflicts with path: "/hello/world"`
129+
)
130+
})
131+
132+
it('should show proper error when a dynamic SSG route conflicts with SSR route', async () => {
133+
await fs.ensureDir(join(pagesDir, 'hello'))
134+
await fs.writeFile(
135+
join(pagesDir, 'hello/world.js'),
136+
`
137+
export const getServerSideProps = () => ({ props: {} })
138+
139+
export default function Page() {
140+
return '/hello/world'
141+
}
142+
`
143+
)
144+
145+
await fs.writeFile(
146+
join(pagesDir, '[...catchAll].js'),
147+
`
148+
export const getStaticProps = () => {
149+
return {
150+
props: {}
151+
}
152+
}
153+
154+
export const getStaticPaths = () => {
155+
return {
156+
paths: [
157+
'/hello',
158+
'/hellO/world'
159+
],
160+
fallback: false
161+
}
162+
}
163+
164+
export default function Page() {
165+
return '/[catchAll]'
166+
}
167+
`
168+
)
169+
170+
const result = await nextBuild(appDir, undefined, {
171+
stdout: true,
172+
stderr: true,
173+
})
174+
const output = result.stdout + result.stderr
175+
expect(output).toContain(
176+
'Conflicting paths returned from getStaticPaths, paths must unique per page'
177+
)
178+
expect(output).toContain('err.sh/next.js/conflicting-ssg-paths')
179+
expect(output).toContain(
180+
`path: "/hellO/world" from page: "/[...catchAll]" conflicts with path: "/hello/world"`
181+
)
182+
})
183+
})

0 commit comments

Comments
 (0)