Skip to content

Commit

Permalink
Merge pull request #257 from zto-sbenning/issue-248
Browse files Browse the repository at this point in the history
fix(router): fix the sort function of the router
  • Loading branch information
svobik7 authored Jul 24, 2024
2 parents cc78b1c + 3a16415 commit bdf03f9
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 15 deletions.
8 changes: 8 additions & 0 deletions src/router/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ const inputSchema: RouterSchema = {
name: '/blog/articles/[articleId]',
href: '/es/blog/articulos/:articleId',
},
{
name: '/[...catchAll]',
href: '/es/:catchAll+',
},
{
name: '/[slug]',
href: '/es/:slug',
Expand All @@ -41,6 +45,10 @@ const inputSchema: RouterSchema = {
name: '/blog/articles/[articleId]',
href: '/cs/blog/clanky/:articleId',
},
{
name: '/[...catchAll]',
href: '/cs/:catchAll+',
},
{
name: '/[slug]',
href: '/cs/:slug',
Expand Down
18 changes: 4 additions & 14 deletions src/router/router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { compile, match } from 'path-to-regexp'
import type { Route, RouterSchema } from '~/types'
import { getLocaleFactory } from '~/utils/locale-utils'
import { sanitizeSchema } from '~/utils/schema-utils'
import { StaticRouter } from './static-router'

/**
Expand All @@ -16,7 +17,7 @@ export class Router extends StaticRouter {

constructor(schema: RouterSchema) {
super()
this.schema = schema
this.schema = sanitizeSchema(schema)
}

/**
Expand Down Expand Up @@ -68,24 +69,13 @@ export class Router extends StaticRouter {
}

/**
* Gets all routes for a given locale, sorted by their dynamic nature
* Gets all routes for a given locale
* @param {string} locale - The locale for which to get routes
* @returns {Route[]} - The sorted array of routes
*/

private getLocalizedRoutes(locale: string) {
return (
this.schema.routes[locale]?.sort((a, b) => {
const dynamicIndexA = a.name.indexOf('[')
const dynamicIndexB = b.name.indexOf('[')
const isDynamicA = dynamicIndexA !== -1
const isDynamicB = dynamicIndexB !== -1
if (isDynamicA && isDynamicB) {
return dynamicIndexB - dynamicIndexA
}
return isDynamicA ? 1 : -1
}) || []
)
return this.schema.routes[locale] || []
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ export type RouterSchema = {
routes: Record<string, Route[]>
locales: string[]
defaultLocale: string
}
}

export type RoutesPrioritiesMap = Record<`/${string}`, number>
83 changes: 83 additions & 0 deletions src/utils/route-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Checks if given route segment is static
* @param {string} segment - The route segment
* @returns {boolean} - Whether the segment is static
*/

import type { Route, RouterSchema, RoutesPrioritiesMap } from '~/types'

export function isStaticRouteSegment(segment: string): boolean {
return !segment.includes('[')
}

/**
* Checks if given route segment is catch-all
* @param {string} segment - The route segment
* @returns {boolean} - Whether the segment is catch-all
*/

export function isCatchAllRouteSegment(segment: string): boolean {
return segment.includes('...')
}

/**
* Checks if given route segment is dynamic
* @param {string} segment - The route segment
* @returns {boolean} - Whether the segment is dynamic
*/

export function isDynamicRouteSegment(segment: string): boolean {
return segment.includes('[') && !isCatchAllRouteSegment(segment)
}

/**
* Gets priority of a route segment based on its nature
* @param {string} segment - The route segment
* @returns {number} - The priority of the segment
*/

export function getRouteSegmentPriority(segment: string): number {
if (isStaticRouteSegment(segment)) {
return 1
} else if (isDynamicRouteSegment(segment)) {
return 2
} else if (isCatchAllRouteSegment(segment)) {
return 3
}
return 0
}

/**
* Computes priority of a route based on its segments nature
* @param {Route} route - The route to compute priority for
* @returns {number} - The priority of the route
*/

export function computeRoutePriority(route: Route): number {
const segments = route.name.split('/').filter((segment) => segment.length > 0) // filter out empty segments
let priority = '0.'
for (const segment of segments) {
const segmentPriority = getRouteSegmentPriority(segment)
priority += segmentPriority
}
return parseFloat(priority)
}

/**
* Creates a map of route priorities based on the routing schema
* @param {RouterSchema} schema - The routing schema
* @returns {RoutesPrioritiesMap} - The map of route priorities
*/

export function createRoutesPrioritiesMap(
schema: RouterSchema
): RoutesPrioritiesMap {
const routesPrioritiesMap: RoutesPrioritiesMap = {}
const routes = schema.routes[schema.defaultLocale] || [] // No need to create a map for each locale as the names are the same

for (const route of routes) {
routesPrioritiesMap[route.name] = computeRoutePriority(route)
}

return routesPrioritiesMap
}
15 changes: 15 additions & 0 deletions src/utils/schema-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { RouterSchema } from '~/types'
import { createRoutesPrioritiesMap } from './route-utils'

export function sanitizeSchema(schema: RouterSchema) {
const routesPrioritiesMap = createRoutesPrioritiesMap(schema)
Object.keys(schema.routes).forEach((locale) => {
schema.routes[locale] = schema.routes[locale].sort((a, b) => {
const priorityA = routesPrioritiesMap[a.name]
const priorityB = routesPrioritiesMap[b.name]
return priorityA - priorityB
})
})

return schema
}
146 changes: 146 additions & 0 deletions src/utils/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import type { RouterSchema } from '~/types'
import { trimLeadingSlash } from './path-utils'
import { isLayout, isNotFound, isPage } from './rewrite-utils'
import {
computeRoutePriority,
createRoutesPrioritiesMap,
getRouteSegmentPriority,
isCatchAllRouteSegment,
isDynamicRouteSegment,
isStaticRouteSegment,
} from './route-utils'
import { sanitizeSchema } from './schema-utils'

describe('trimLeadingSlash', () => {
const testCases = [
Expand Down Expand Up @@ -65,3 +75,139 @@ describe('rewrite-utils', () => {
})
})
})

describe('route-utils', () => {
describe('isStaticRouteSegment', () => {
const testCases = [
['static', true],
['[dynamic]', false],
['[...catchAll]', false],
['[[...optionalCatchAll]]', false],
] as const

test.each(testCases)('given %o, returns %s', (input, expectedResult) => {
expect(isStaticRouteSegment(input)).toEqual(expectedResult)
})
})

describe('isDynamicRouteSegment', () => {
const testCases = [
['static', false],
['[dynamic]', true],
['[...catchAll]', false],
['[[...optionalCatchAll]]', false],
] as const

test.each(testCases)('given %o, returns %s', (input, expectedResult) => {
expect(isDynamicRouteSegment(input)).toEqual(expectedResult)
})
})

describe('isCatchAllRouteSegment', () => {
const testCases = [
['static', false],
['[dynamic]', false],
['[...catchAll]', true],
['[[...optionalCatchAll]]', true],
] as const

test.each(testCases)('given %o, returns %s', (input, expectedResult) => {
expect(isCatchAllRouteSegment(input)).toEqual(expectedResult)
})
})

describe('getRouteSegmentPriority', () => {
const testCases = [
['static', 1],
['[dynamic]', 2],
['[...catchAll]', 3],
['[[...optionalCatchAll]]', 3],
] as const

test.each(testCases)('given %o, returns %s', (input, expectedResult) => {
expect(getRouteSegmentPriority(input)).toEqual(expectedResult)
})
})

describe('computeRoutePriority', () => {
const testCases = [
[{ name: '/only-static/nested-path' }, 0.11],
[{ name: '/static/[withDynamic]' }, 0.12],
[{ name: '/static/[...withCatchAll]' }, 0.13],
[{ name: '/[dynamic]/with-static' }, 0.21],
[{ name: '/[dynamic]/[withDynamic]' }, 0.22],
[{ name: '/[dynamic]/[...catchAll]' }, 0.23],
[{ name: '/[...withCatchAll]' }, 0.3],
] as const

test.each(testCases)('given %o, returns %s', (input, expectedResult) => {
expect(computeRoutePriority({ name: input.name, href: '/' })).toEqual(
expectedResult
)
})
})

test('createRoutesPrioritiesMap', () => {
const inputSchema: RouterSchema = {
defaultLocale: 'en',
locales: ['en'],
routes: {
en: [
{ name: '/static', href: '/static' },
{ name: '/static/[dynamic]', href: '/static/:dynamic' },
{ name: '/static/[...catchAll]', href: '/static/:catchAll+' },
{ name: '/[dynamic]', href: '/:dynamic' },
{ name: '/[...catchAll]', href: '/:catchAll+' },
{ name: '/[[...optionalCatchAll]]', href: '/:optionalCatchAll*' },
],
},
}

const expectedResult = {
'/static': 0.1,
'/static/[dynamic]': 0.12,
'/static/[...catchAll]': 0.13,
'/[dynamic]': 0.2,
'/[...catchAll]': 0.3,
'/[[...optionalCatchAll]]': 0.3,
}

expect(createRoutesPrioritiesMap(inputSchema)).toEqual(expectedResult)
})
})

describe('schema-utils', () => {
test('sanitizeSchema', () => {
const inputSchema: RouterSchema = {
defaultLocale: 'en',
locales: ['en'],
routes: {
en: [
{ name: '/[dynamic]', href: '/:dynamic' },
{ name: '/[...catchAll]', href: '/:catchAll+' },
{ name: '/static', href: '/static' },
{ name: '/[[...optionalCatchAll]]', href: '/:optionalCatchAll*' },
{ name: '/static/[...catchAll]', href: '/static/:catchAll+' },
{ name: '/static/[dynamic]', href: '/static/:dynamic' },
],
},
}

const expectedResult: RouterSchema = {
defaultLocale: 'en',
locales: ['en'],
routes: {
en: [
{ name: '/static', href: '/static' },
{ name: '/static/[dynamic]', href: '/static/:dynamic' },
{ name: '/static/[...catchAll]', href: '/static/:catchAll+' },
{ name: '/[dynamic]', href: '/:dynamic' },
{ name: '/[...catchAll]', href: '/:catchAll+' },
{ name: '/[[...optionalCatchAll]]', href: '/:optionalCatchAll*' },
],
},
}

expect(sanitizeSchema(inputSchema)).toEqual(expectedResult)
})
})

0 comments on commit bdf03f9

Please sign in to comment.