Dynamic Routes: Change impl from $param to [param] (#7623)
* Dynamic Routes: Change impl from $param to [param] * Update expected test snapshot * Update test to use new syntax * Update test file * Test more behavior * Update route sorter for new param syntax * Update dynamic routing tests * Update danging test file * Tweak test * Fix dev and update tests
This commit is contained in:
parent
eddc852de3
commit
6c625703ed
15 changed files with 58 additions and 48 deletions
|
@ -1,3 +1,6 @@
|
||||||
|
// Identify /[param]/ in route string
|
||||||
|
const TEST_ROUTE = /\/\[[^\/]+?\](?=\/|$)/
|
||||||
|
|
||||||
export function isDynamicRoute(route: string): boolean {
|
export function isDynamicRoute(route: string): boolean {
|
||||||
return route.indexOf('/$') !== -1
|
return TEST_ROUTE.test(route)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export function getRouteRegex(
|
export function getRouteRegex(
|
||||||
normalizedRoute: string
|
normalizedRoute: string
|
||||||
): { re: RegExp; groups: { [groupName: string]: number } } {
|
): { re: RegExp; groups: { [groupName: string]: number } } {
|
||||||
|
// Escape all characters that could be considered RegEx
|
||||||
const escapedRoute = (normalizedRoute.replace(/\/$/, '') || '/').replace(
|
const escapedRoute = (normalizedRoute.replace(/\/$/, '') || '/').replace(
|
||||||
/[|\\{}()[\]^$+*?.-]/g,
|
/[|\\{}()[\]^$+*?.-]/g,
|
||||||
'\\$&'
|
'\\$&'
|
||||||
|
@ -10,20 +11,13 @@ export function getRouteRegex(
|
||||||
let groupIndex = 1
|
let groupIndex = 1
|
||||||
|
|
||||||
const parameterizedRoute = escapedRoute.replace(
|
const parameterizedRoute = escapedRoute.replace(
|
||||||
/\/\\\$([^\/]+?)(?=\/|$)/g,
|
/\/\\\[([^\/]+?)\\\](?=\/|$)/g,
|
||||||
(_, $1) => (
|
(_, $1) => (
|
||||||
(groups[
|
(groups[
|
||||||
$1
|
$1
|
||||||
// // Remove optional parameter marker
|
|
||||||
// .replace(/\\\$$/, '')
|
|
||||||
// Un-escape key
|
// Un-escape key
|
||||||
.replace(/\\([|\\{}()[\]^$+*?.-])/g, '$1')
|
.replace(/\\([|\\{}()[\]^$+*?.-])/g, '$1')
|
||||||
] = groupIndex++),
|
] = groupIndex++),
|
||||||
// // Test the route for an optional parameter
|
|
||||||
// $1.lastIndexOf('$') === $1.length - 1
|
|
||||||
// ? // Optional: match a param
|
|
||||||
// '(?:/([^/]+?))?'
|
|
||||||
// : // Required: match a param
|
|
||||||
'/([^/]+?)'
|
'/([^/]+?)'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,7 +18,7 @@ class UrlNode {
|
||||||
private _smoosh(prefix: string = '/'): string[] {
|
private _smoosh(prefix: string = '/'): string[] {
|
||||||
const childrenPaths = [...this.children.keys()].sort()
|
const childrenPaths = [...this.children.keys()].sort()
|
||||||
if (this.hasSlug) {
|
if (this.hasSlug) {
|
||||||
childrenPaths.splice(childrenPaths.indexOf('$'), 1)
|
childrenPaths.splice(childrenPaths.indexOf('[]'), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
const routes = childrenPaths
|
const routes = childrenPaths
|
||||||
|
@ -27,7 +27,7 @@ class UrlNode {
|
||||||
|
|
||||||
if (this.hasSlug) {
|
if (this.hasSlug) {
|
||||||
routes.push(
|
routes.push(
|
||||||
...this.children.get('$')!._smoosh(`${prefix}$${this.slugName}/`)
|
...this.children.get('[]')!._smoosh(`${prefix}[${this.slugName}]/`)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,8 +45,8 @@ class UrlNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
let [nextSegment] = urlPaths
|
let [nextSegment] = urlPaths
|
||||||
if (nextSegment.startsWith('$')) {
|
if (nextSegment.startsWith('[') && nextSegment.endsWith(']')) {
|
||||||
const slugName = nextSegment.substring(1)
|
const slugName = nextSegment.slice(1, -1)
|
||||||
if (this.hasSlug && slugName !== this.slugName) {
|
if (this.hasSlug && slugName !== this.slugName) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'You cannot use different slug names for the same dynamic path.'
|
'You cannot use different slug names for the same dynamic path.'
|
||||||
|
@ -54,7 +54,7 @@ class UrlNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.slugName = slugName
|
this.slugName = slugName
|
||||||
nextSegment = '$'
|
nextSegment = '[]'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.children.has(nextSegment)) {
|
if (!this.children.has(nextSegment)) {
|
||||||
|
|
|
@ -81,7 +81,7 @@ class Container extends React.Component {
|
||||||
this.scrollToHash()
|
this.scrollToHash()
|
||||||
|
|
||||||
// If page was exported and has a querystring
|
// If page was exported and has a querystring
|
||||||
// If it's a dynamic route (/$ inside) or has a querystring
|
// If it's a dynamic route or has a querystring
|
||||||
if (
|
if (
|
||||||
data.nextExport &&
|
data.nextExport &&
|
||||||
(isDynamicRoute(router.pathname) || window.location.search)
|
(isDynamicRoute(router.pathname) || window.location.search)
|
||||||
|
|
|
@ -114,14 +114,16 @@ export default class DevServer extends Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
let pageName = '/' + relative(pagesDir, fileName).replace(/\\+/g, '/')
|
let pageName = '/' + relative(pagesDir, fileName).replace(/\\+/g, '/')
|
||||||
if (!isDynamicRoute(pageName)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageExt = extname(pageName)
|
const pageExt = extname(pageName)
|
||||||
pageName = pageName.slice(0, -pageExt.length)
|
pageName = pageName.slice(0, -pageExt.length)
|
||||||
|
|
||||||
pageName = pageName.replace(/\/index$/, '')
|
pageName = pageName.replace(/\/index$/, '') || '/'
|
||||||
|
|
||||||
|
if (!isDynamicRoute(pageName)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
dynamicRoutedPages.push(pageName)
|
dynamicRoutedPages.push(pageName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,27 +3,23 @@ import Link from 'next/link'
|
||||||
const Page = () => (
|
const Page = () => (
|
||||||
<div>
|
<div>
|
||||||
<h3>My blog</h3>
|
<h3>My blog</h3>
|
||||||
<Link href='/$post' as='/post-1'>
|
<Link href='/[post]' as='/post-1'>
|
||||||
<a id='view-post-1'>View post 1</a>
|
<a id='view-post-1'>View post 1</a>
|
||||||
</Link>
|
</Link>
|
||||||
<br />
|
<br />
|
||||||
<Link href='/$post/comments' as='/post-1/comments'>
|
<Link href='/[post]/comments' as='/post-1/comments'>
|
||||||
<a id='view-post-1-comments'>View post 1 comments</a>
|
<a id='view-post-1-comments'>View post 1 comments</a>
|
||||||
</Link>
|
</Link>
|
||||||
<br />
|
<br />
|
||||||
<Link href='/$post/$comment' as='/post-1/comment-1'>
|
<Link href='/[post]/[comment]' as='/post-1/comment-1'>
|
||||||
<a id='view-post-1-comment-1'>View comment 1 on post 1</a>
|
<a id='view-post-1-comment-1'>View comment 1 on post 1</a>
|
||||||
</Link>
|
</Link>
|
||||||
{/* <br />
|
|
||||||
<Link href='/blog/$post/comment/$id$' as='/blog/543/comment'>
|
|
||||||
<a id='view-blog-post-1-comments'>View all comments on blog post 543</a>
|
|
||||||
</Link> */}
|
|
||||||
<br />
|
<br />
|
||||||
<Link href='/blog/$post/comment/$id' as='/blog/321/comment/123'>
|
<Link href='/blog/[post]/comment/[id]' as='/blog/321/comment/123'>
|
||||||
<a id='view-nested-dynamic-cmnt'>View comment 123 on blog post 321</a>
|
<a id='view-nested-dynamic-cmnt'>View comment 123 on blog post 321</a>
|
||||||
</Link>
|
</Link>
|
||||||
<br />
|
<br />
|
||||||
<Link href='/$post?fromHome=true' as='/post-1?fromHome=true'>
|
<Link href='/[post]?fromHome=true' as='/post-1?fromHome=true'>
|
||||||
<a id='view-post-1-with-query'>View post 1 with query</a>
|
<a id='view-post-1-with-query'>View post 1 with query</a>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,5 +3,5 @@ import { useRouter } from 'next/router'
|
||||||
export default () => {
|
export default () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { query } = router
|
const { query } = router
|
||||||
return <p>post: {query.post}</p>
|
return <p>onmpost: {query.post || 'pending'}</p>
|
||||||
}
|
}
|
|
@ -142,10 +142,13 @@ function runTests () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should update dynamic values on mount', async () => {
|
it('should update dynamic values on mount', async () => {
|
||||||
|
const html = await renderViaHTTP(appPort, '/on-mount/post-1')
|
||||||
|
expect(html).toMatch(/onmpost:.*pending/)
|
||||||
|
|
||||||
const browser = await webdriver(appPort, '/on-mount/post-1')
|
const browser = await webdriver(appPort, '/on-mount/post-1')
|
||||||
await waitFor(1000)
|
await waitFor(1000)
|
||||||
const text = await browser.eval(`document.body.innerHTML`)
|
const text = await browser.eval(`document.body.innerHTML`)
|
||||||
expect(text).toMatch(/post:.*post-1/)
|
expect(text).toMatch(/onmpost:.*post-1/)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@
|
||||||
exports[`getSortedRoutes correctly sorts required slugs 1`] = `
|
exports[`getSortedRoutes correctly sorts required slugs 1`] = `
|
||||||
Array [
|
Array [
|
||||||
"/",
|
"/",
|
||||||
"/apples/$ab/$cd/ef",
|
"/apples/[ab]/[cd]/ef",
|
||||||
"/blog/$id",
|
"/blog/[id]",
|
||||||
"/blog/$id/comments/$cid",
|
"/blog/[id]/comments/[cid]",
|
||||||
"/foo/$d/bar/baz/$f",
|
"/foo/[d]/bar/baz/[f]",
|
||||||
"/posts",
|
"/posts",
|
||||||
"/posts/$id",
|
"/posts/[id]",
|
||||||
"/$root-slug",
|
"/[root-slug]",
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -5,14 +5,14 @@ describe('getSortedRoutes', () => {
|
||||||
it('does not add extra routes', () => {
|
it('does not add extra routes', () => {
|
||||||
expect(getSortedRoutes(['/posts'])).toEqual(['/posts'])
|
expect(getSortedRoutes(['/posts'])).toEqual(['/posts'])
|
||||||
|
|
||||||
expect(getSortedRoutes(['/posts/$id'])).toEqual(['/posts/$id'])
|
expect(getSortedRoutes(['/posts/[id]'])).toEqual(['/posts/[id]'])
|
||||||
expect(getSortedRoutes(['/posts/$id/foo'])).toEqual(['/posts/$id/foo'])
|
expect(getSortedRoutes(['/posts/[id]/foo'])).toEqual(['/posts/[id]/foo'])
|
||||||
|
|
||||||
expect(getSortedRoutes(['/posts/$id/$foo/bar'])).toEqual([
|
expect(getSortedRoutes(['/posts/[id]/[foo]/bar'])).toEqual([
|
||||||
'/posts/$id/$foo/bar'
|
'/posts/[id]/[foo]/bar'
|
||||||
])
|
])
|
||||||
expect(getSortedRoutes(['/posts/$id/baz/$foo/bar'])).toEqual([
|
expect(getSortedRoutes(['/posts/[id]/baz/[foo]/bar'])).toEqual([
|
||||||
'/posts/$id/baz/$foo/bar'
|
'/posts/[id]/baz/[foo]/bar'
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -20,14 +20,26 @@ describe('getSortedRoutes', () => {
|
||||||
expect(
|
expect(
|
||||||
getSortedRoutes([
|
getSortedRoutes([
|
||||||
'/posts',
|
'/posts',
|
||||||
'/$root-slug',
|
'/[root-slug]',
|
||||||
'/',
|
'/',
|
||||||
'/posts/$id',
|
'/posts/[id]',
|
||||||
'/blog/$id/comments/$cid',
|
'/blog/[id]/comments/[cid]',
|
||||||
'/blog/$id',
|
'/blog/[id]',
|
||||||
'/foo/$d/bar/baz/$f',
|
'/foo/[d]/bar/baz/[f]',
|
||||||
'/apples/$ab/$cd/ef'
|
'/apples/[ab]/[cd]/ef'
|
||||||
])
|
])
|
||||||
).toMatchSnapshot()
|
).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('catches mismatched param names', () => {
|
||||||
|
expect(() =>
|
||||||
|
getSortedRoutes([
|
||||||
|
'/',
|
||||||
|
'/blog',
|
||||||
|
'/blog/[id]',
|
||||||
|
'/blog/[id]/comments/[cid]',
|
||||||
|
'/blog/[cid]'
|
||||||
|
])
|
||||||
|
).toThrowError(/different slug names/)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue