Skip to content

Commit

Permalink
Merge branch 'canary' into sam/github-workflow/popular-linear-next
Browse files Browse the repository at this point in the history
  • Loading branch information
samcx authored May 2, 2024
2 parents 3bdd8de + 2a2a4e7 commit 551a9ef
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 13 deletions.
3 changes: 1 addition & 2 deletions packages/next/src/server/app-render/app-render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,8 @@ function makeGetDynamicParamFromSegment(
// remove the first empty string
.slice(1)
// replace any dynamic params with the actual values
.map((pathSegment) => {
.flatMap((pathSegment) => {
const param = parseParameter(pathSegment)

// if the segment matches a param, return the param value
// otherwise, it's a static segment, so just return that
return params[param.key] ?? param.key
Expand Down
38 changes: 38 additions & 0 deletions packages/next/src/shared/lib/router/utils/route-regex.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getNamedRouteRegex } from './route-regex'
import { parseParameter } from './route-regex'

describe('getNamedRouteRegex', () => {
it('should handle interception markers adjacent to dynamic path segments', () => {
Expand Down Expand Up @@ -109,3 +110,40 @@ describe('getNamedRouteRegex', () => {
expect(regex.re.test('/photos')).toBe(true)
})
})

describe('parseParameter', () => {
it('should parse a optional catchall parameter', () => {
const param = '[[...slug]]'
const expected = { key: 'slug', repeat: true, optional: true }
const result = parseParameter(param)
expect(result).toEqual(expected)
})

it('should parse a catchall parameter', () => {
const param = '[...slug]'
const expected = { key: 'slug', repeat: true, optional: false }
const result = parseParameter(param)
expect(result).toEqual(expected)
})

it('should parse a optional parameter', () => {
const param = '[[foo]]'
const expected = { key: 'foo', repeat: false, optional: true }
const result = parseParameter(param)
expect(result).toEqual(expected)
})

it('should parse a dynamic parameter', () => {
const param = '[bar]'
const expected = { key: 'bar', repeat: false, optional: false }
const result = parseParameter(param)
expect(result).toEqual(expected)
})

it('should parse non-dynamic parameter', () => {
const param = 'fizz'
const expected = { key: 'fizz', repeat: false, optional: false }
const result = parseParameter(param)
expect(result).toEqual(expected)
})
})
52 changes: 46 additions & 6 deletions packages/next/src/shared/lib/router/utils/route-regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,51 @@ export interface RouteRegex {
re: RegExp
}

/**
* Regular expression pattern used to match route parameters.
* Matches both single parameters and parameter groups.
* Examples:
* - `[[...slug]]` matches parameter group with key 'slug', repeat: true, optional: true
* - `[...slug]` matches parameter group with key 'slug', repeat: true, optional: false
* - `[[foo]]` matches parameter with key 'foo', repeat: false, optional: true
* - `[bar]` matches parameter with key 'bar', repeat: false, optional: false
*/
const PARAMETER_PATTERN = /\[((?:\[.*\])|.+)\]/

/**
* Parses a given parameter from a route to a data structure that can be used
* to generate the parametrized route. Examples:
* to generate the parametrized route.
* Examples:
* - `[[...slug]]` -> `{ key: 'slug', repeat: true, optional: true }`
* - `[...slug]` -> `{ key: 'slug', repeat: true, optional: false }`
* - `[[foo]]` -> `{ key: 'foo', repeat: false, optional: true }`
* - `[bar]` -> `{ key: 'bar', repeat: false, optional: false }`
* - `fizz` -> `{ key: 'fizz', repeat: false, optional: false }`
* @param param - The parameter to parse.
* @returns The parsed parameter as a data structure.
*/
export function parseParameter(param: string) {
const match = param.match(PARAMETER_PATTERN)

if (!match) {
return parseMatchedParameter(param)
}

return parseMatchedParameter(match[1])
}

/**
* Parses a matched parameter from the PARAMETER_PATTERN regex to a data structure that can be used
* to generate the parametrized route.
* Examples:
* - `[...slug]` -> `{ key: 'slug', repeat: true, optional: true }`
* - `...slug` -> `{ key: 'slug', repeat: true, optional: false }`
* - `[foo]` -> `{ key: 'foo', repeat: false, optional: true }`
* - `bar` -> `{ key: 'bar', repeat: false, optional: false }`
* @param param - The matched parameter to parse.
* @returns The parsed parameter as a data structure.
*/
export function parseParameter(param: string) {
function parseMatchedParameter(param: string) {
const optional = param.startsWith('[') && param.endsWith(']')
if (optional) {
param = param.slice(1, -1)
Expand All @@ -47,14 +83,18 @@ function getParametrizedRoute(route: string) {
const markerMatch = INTERCEPTION_ROUTE_MARKERS.find((m) =>
segment.startsWith(m)
)
const paramMatches = segment.match(/\[((?:\[.*\])|.+)\]/) // Check for parameters
const paramMatches = segment.match(PARAMETER_PATTERN) // Check for parameters

if (markerMatch && paramMatches) {
const { key, optional, repeat } = parseParameter(paramMatches[1])
const { key, optional, repeat } = parseMatchedParameter(
paramMatches[1]
)
groups[key] = { pos: groupIndex++, repeat, optional }
return `/${escapeStringRegexp(markerMatch)}([^/]+?)`
} else if (paramMatches) {
const { key, repeat, optional } = parseParameter(paramMatches[1])
const { key, repeat, optional } = parseMatchedParameter(
paramMatches[1]
)
groups[key] = { pos: groupIndex++, repeat, optional }
return repeat ? (optional ? '(?:/(.+?))?' : '/(.+?)') : '/([^/]+?)'
} else {
Expand Down Expand Up @@ -110,7 +150,7 @@ function getSafeKeyFromSegment({
routeKeys: Record<string, string>
keyPrefix?: string
}) {
const { key, optional, repeat } = parseParameter(segment)
const { key, optional, repeat } = parseMatchedParameter(segment)

// replace any non-word characters since they can break
// the named regex
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return (
<div>
<h2>/buzz/[[...fizz]] Page!</h2>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Page() {
return (
<div>
<h2>/fizz/[...buzz] Page!</h2>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default function StaticPage() {
export default function Page() {
return (
<div>
<h2>/foo/[lang]/bar Page!</h2>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,34 @@ describe('parallel-routes-breadcrumbs', () => {
expect(await slot.text()).toContain('Album: en')
expect(await slot.text()).toContain('Track: bar')
})

it('should render the breadcrumbs correctly with catchall route segments', async () => {
const browser = await next.browser('/fizz/a/b')
const slot = await browser.waitForElementByCss('#slot')

expect(await browser.elementByCss('h1').text()).toBe('Parallel Route!')
expect(await browser.elementByCss('h2').text()).toBe(
'/fizz/[...buzz] Page!'
)

// verify slot is rendering the params
expect(await slot.text()).toContain('Artist: fizz')
expect(await slot.text()).toContain('Album: a')
expect(await slot.text()).toContain('Track: b')
})

it('should render the breadcrumbs correctly with optional catchall route segments', async () => {
const browser = await next.browser('/buzz/a/b')
const slot = await browser.waitForElementByCss('#slot')

expect(await browser.elementByCss('h1').text()).toBe('Parallel Route!')
expect(await browser.elementByCss('h2').text()).toBe(
'/buzz/[[...fizz]] Page!'
)

// verify slot is rendering the params
expect(await slot.text()).toContain('Artist: buzz')
expect(await slot.text()).toContain('Album: a')
expect(await slot.text()).toContain('Track: b')
})
})
4 changes: 0 additions & 4 deletions test/unit/next-image-new.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@ import ReactDOMServer from 'react-dom/server'
import Image from 'next/image'
import cheerio from 'cheerio'

// Since this unit test doesn't check the result of
// ReactDOM.preload(), we can turn it into a noop.
jest.mock('react-dom', () => ({ preload: () => null }))

describe('Image rendering', () => {
it('should render Image on its own', async () => {
const element = React.createElement(Image, {
Expand Down

0 comments on commit 551a9ef

Please sign in to comment.